11import { observable } from "@trpc/server/observable"
22import { eq } from "drizzle-orm"
3- import { app , safeStorage } from "electron"
3+ import { app , safeStorage , BrowserWindow } from "electron"
44import path from "path"
55import * as os from "os"
66import * as fs from "fs/promises"
@@ -18,23 +18,25 @@ import { publicProcedure, router } from "../index"
1818import { buildAgentsOption } from "./agent-utils"
1919
2020/**
21- * Parse @[agent:name] and @[skill :name] mentions from prompt text
22- * Returns the cleaned prompt and lists of mentioned agents/skills
21+ * Parse @[agent:name], @[skill:name], and @[tool :name] mentions from prompt text
22+ * Returns the cleaned prompt and lists of mentioned agents/skills/tools
2323 */
2424function parseMentions ( prompt : string ) : {
2525 cleanedPrompt : string
2626 agentMentions : string [ ]
2727 skillMentions : string [ ]
2828 fileMentions : string [ ]
2929 folderMentions : string [ ]
30+ toolMentions : string [ ]
3031} {
3132 const agentMentions : string [ ] = [ ]
3233 const skillMentions : string [ ] = [ ]
3334 const fileMentions : string [ ] = [ ]
3435 const folderMentions : string [ ] = [ ]
36+ const toolMentions : string [ ] = [ ]
3537
3638 // Match @[prefix:name] pattern
37- const mentionRegex = / @ \[ ( f i l e | f o l d e r | s k i l l | a g e n t ) : ( [ ^ \] ] + ) \] / g
39+ const mentionRegex = / @ \[ ( f i l e | f o l d e r | s k i l l | a g e n t | t o o l ) : ( [ ^ \] ] + ) \] / g
3840 let match
3941
4042 while ( ( match = mentionRegex . exec ( prompt ) ) !== null ) {
@@ -52,17 +54,34 @@ function parseMentions(prompt: string): {
5254 case "folder" :
5355 folderMentions . push ( name )
5456 break
57+ case "tool" :
58+ // Validate tool name format: only alphanumeric, underscore, hyphen allowed
59+ // This prevents prompt injection via malicious tool names
60+ if ( / ^ [ a - z A - Z 0 - 9 _ - ] + $ / . test ( name ) ) {
61+ toolMentions . push ( name )
62+ }
63+ break
5564 }
5665 }
5766
58- // Clean agent/skill mentions from prompt (they will be added as context)
67+ // Clean agent/skill/tool mentions from prompt (they will be added as context or hints )
5968 // Keep file/folder mentions as they are useful context
60- const cleanedPrompt = prompt
69+ let cleanedPrompt = prompt
6170 . replace ( / @ \[ a g e n t : [ ^ \] ] + \] / g, "" )
6271 . replace ( / @ \[ s k i l l : [ ^ \] ] + \] / g, "" )
72+ . replace ( / @ \[ t o o l : [ ^ \] ] + \] / g, "" )
6373 . trim ( )
6474
65- return { cleanedPrompt, agentMentions, skillMentions, fileMentions, folderMentions }
75+ // Add tool usage hints if tools were mentioned
76+ // Tool names are already validated to contain only safe characters
77+ if ( toolMentions . length > 0 ) {
78+ const toolHints = toolMentions
79+ . map ( ( t ) => `Use the ${ t } tool for this request.` )
80+ . join ( " " )
81+ cleanedPrompt = `${ toolHints } \n\n${ cleanedPrompt } `
82+ }
83+
84+ return { cleanedPrompt, agentMentions, skillMentions, fileMentions, folderMentions, toolMentions }
6685}
6786
6887/**
@@ -149,6 +168,7 @@ export const claudeRouter = router({
149168 chatId : z . string ( ) ,
150169 prompt : z . string ( ) ,
151170 cwd : z . string ( ) ,
171+ projectPath : z . string ( ) . optional ( ) , // Original project path for MCP config lookup
152172 mode : z . enum ( [ "plan" , "agent" ] ) . default ( "agent" ) ,
153173 sessionId : z . string ( ) . optional ( ) ,
154174 model : z . string ( ) . optional ( ) ,
@@ -379,6 +399,9 @@ export const claudeRouter = router({
379399 input . subChatId
380400 )
381401
402+ // MCP servers to pass to SDK (read from ~/.claude.json)
403+ let mcpServersForSdk : Record < string , any > | undefined
404+
382405 // Ensure isolated config dir exists and symlink skills/agents from ~/.claude/
383406 // This is needed because SDK looks for skills at $CLAUDE_CONFIG_DIR/skills/
384407 try {
@@ -413,6 +436,37 @@ export const claudeRouter = router({
413436 } catch ( symlinkErr ) {
414437 // Ignore symlink errors (might already exist or permission issues)
415438 }
439+
440+ // Read MCP servers from ~/.claude.json for the original project path
441+ // These will be passed directly to the SDK via options.mcpServers
442+ const claudeJsonSource = path . join ( os . homedir ( ) , ".claude.json" )
443+ try {
444+ const claudeJsonSourceExists = await fs . stat ( claudeJsonSource ) . then ( ( ) => true ) . catch ( ( ) => false )
445+
446+ if ( claudeJsonSourceExists ) {
447+ // Read original config
448+ const originalConfig = JSON . parse ( await fs . readFile ( claudeJsonSource , "utf-8" ) )
449+
450+ // Look for project-specific MCP config using original project path
451+ // Config structure: { "projects": { "/path/to/project": { "mcpServers": {...} } } }
452+ const lookupPath = input . projectPath || input . cwd
453+ const projectConfig = originalConfig . projects ?. [ lookupPath ]
454+
455+ // Debug logging
456+ console . log ( `[claude] MCP config lookup: lookupPath=${ lookupPath } , found=${ ! ! projectConfig ?. mcpServers } ` )
457+ if ( projectConfig ?. mcpServers ) {
458+ console . log ( `[claude] MCP servers found: ${ Object . keys ( projectConfig . mcpServers ) . join ( ", " ) } ` )
459+ // Store MCP servers to pass to SDK
460+ mcpServersForSdk = projectConfig . mcpServers
461+ } else {
462+ // Log available project paths in config for debugging
463+ const projectPaths = Object . keys ( originalConfig . projects || { } ) . filter ( k => originalConfig . projects [ k ] ?. mcpServers )
464+ console . log ( `[claude] No MCP servers for ${ lookupPath } . Config has MCP for: ${ projectPaths . join ( ", " ) || "(none)" } ` )
465+ }
466+ }
467+ } catch ( configErr ) {
468+ console . error ( `[claude] Failed to read MCP config:` , configErr )
469+ }
416470 } catch ( mkdirErr ) {
417471 console . error ( `[claude] Failed to setup isolated config dir:` , mkdirErr )
418472 }
@@ -423,14 +477,16 @@ export const claudeRouter = router({
423477 ...( claudeCodeToken && {
424478 CLAUDE_CODE_OAUTH_TOKEN : claudeCodeToken ,
425479 } ) ,
426- // Isolate Claude's config/session storage per subChat
480+ // Re-enable CLAUDE_CONFIG_DIR now that we properly map MCP configs
427481 CLAUDE_CONFIG_DIR : isolatedConfigDir ,
428482 }
429483
430484 // Get bundled Claude binary path
431485 const claudeBinaryPath = getBundledClaudeBinaryPath ( )
432486
433487 const resumeSessionId = input . sessionId || existingSessionId || undefined
488+ console . log ( `[SD] Query options - cwd: ${ input . cwd } , projectPath: ${ input . projectPath || "(not set)" } , mcpServers: ${ mcpServersForSdk ? Object . keys ( mcpServersForSdk ) . join ( ", " ) : "(none)" } ` )
489+
434490 const queryOptions = {
435491 prompt,
436492 options : {
@@ -442,6 +498,8 @@ export const claudeRouter = router({
442498 } ,
443499 // Register mentioned agents with SDK via options.agents
444500 ...( Object . keys ( agentsOption ) . length > 0 && { agents : agentsOption } ) ,
501+ // Pass MCP servers from original project config directly to SDK
502+ ...( mcpServersForSdk && { mcpServers : mcpServersForSdk } ) ,
445503 env : finalEnv ,
446504 permissionMode :
447505 input . mode === "plan"
@@ -657,6 +715,18 @@ export const claudeRouter = router({
657715 currentSessionId = msgAny . session_id // Share with cleanup
658716 }
659717
718+ // Debug: Log system messages from SDK
719+ if ( msgAny . type === "system" ) {
720+ // Full log to see all fields including MCP errors
721+ console . log ( `[SD] SYSTEM message: subtype=${ msgAny . subtype } ` , JSON . stringify ( {
722+ cwd : msgAny . cwd ,
723+ mcp_servers : msgAny . mcp_servers ,
724+ tools : msgAny . tools ,
725+ plugins : msgAny . plugins ,
726+ permissionMode : msgAny . permissionMode ,
727+ } , null , 2 ) )
728+ }
729+
660730 // Transform and emit + accumulate
661731 for ( const chunk of transform ( msg ) ) {
662732 chunkCount ++
@@ -707,6 +777,21 @@ export const claudeRouter = router({
707777 if ( toolPart ) {
708778 toolPart . result = chunk . output
709779 toolPart . state = "result"
780+
781+ // Notify renderer about file changes for Write/Edit tools
782+ if ( toolPart . type === "tool-Write" || toolPart . type === "tool-Edit" ) {
783+ const filePath = toolPart . input ?. file_path
784+ if ( filePath ) {
785+ const windows = BrowserWindow . getAllWindows ( )
786+ for ( const win of windows ) {
787+ win . webContents . send ( "file-changed" , {
788+ filePath,
789+ type : toolPart . type ,
790+ subChatId : input . subChatId
791+ } )
792+ }
793+ }
794+ }
710795 }
711796 // Stop streaming after ExitPlanMode completes in plan mode
712797 // Match by toolCallId since toolName is undefined in output chunks
@@ -724,6 +809,22 @@ export const claudeRouter = router({
724809 case "message-metadata" :
725810 metadata = { ...metadata , ...chunk . messageMetadata }
726811 break
812+ case "system-Compact" :
813+ // Add system-Compact to parts so it renders in the chat
814+ // Find existing part by toolCallId or add new one
815+ const existingCompact = parts . find (
816+ ( p ) => p . type === "system-Compact" && p . toolCallId === chunk . toolCallId
817+ )
818+ if ( existingCompact ) {
819+ existingCompact . state = chunk . state
820+ } else {
821+ parts . push ( {
822+ type : "system-Compact" ,
823+ toolCallId : chunk . toolCallId ,
824+ state : chunk . state ,
825+ } )
826+ }
827+ break
727828 }
728829 // Break from chunk loop if plan is done
729830 if ( planCompleted ) {
@@ -953,6 +1054,47 @@ export const claudeRouter = router({
9531054 } )
9541055 } ) ,
9551056
1057+ /**
1058+ * Get MCP servers configuration for a project
1059+ * This allows showing MCP servers in UI before starting a chat session
1060+ */
1061+ getMcpConfig : publicProcedure
1062+ . input ( z . object ( { projectPath : z . string ( ) } ) )
1063+ . query ( async ( { input } ) => {
1064+ const claudeJsonPath = path . join ( os . homedir ( ) , ".claude.json" )
1065+
1066+ try {
1067+ const exists = await fs . stat ( claudeJsonPath ) . then ( ( ) => true ) . catch ( ( ) => false )
1068+ if ( ! exists ) {
1069+ return { mcpServers : [ ] , projectPath : input . projectPath }
1070+ }
1071+
1072+ const configContent = await fs . readFile ( claudeJsonPath , "utf-8" )
1073+ const config = JSON . parse ( configContent )
1074+
1075+ // Look for project-specific MCP config
1076+ const projectConfig = config . projects ?. [ input . projectPath ]
1077+
1078+ if ( ! projectConfig ?. mcpServers ) {
1079+ return { mcpServers : [ ] , projectPath : input . projectPath }
1080+ }
1081+
1082+ // Convert to array format with names
1083+ const mcpServers = Object . entries ( projectConfig . mcpServers ) . map ( ( [ name , serverConfig ] ) => ( {
1084+ name,
1085+ // Status will be "pending" until SDK actually connects
1086+ status : "pending" as const ,
1087+ // Include config details for display (command, args, etc)
1088+ config : serverConfig as Record < string , unknown > ,
1089+ } ) )
1090+
1091+ return { mcpServers, projectPath : input . projectPath }
1092+ } catch ( error ) {
1093+ console . error ( "[getMcpConfig] Error reading config:" , error )
1094+ return { mcpServers : [ ] , projectPath : input . projectPath , error : String ( error ) }
1095+ }
1096+ } ) ,
1097+
9561098 /**
9571099 * Cancel active session
9581100 */
0 commit comments