@@ -97,6 +97,11 @@ import { WorkspaceFileCollector } from './agent/WorkspaceFileCollector.js';
9797import { ProviderConfigManager } from './agent/ProviderConfigManager.js' ;
9898import { AutoReportManager } from '../reporting/AutoReportManager.js' ;
9999import { isLikelyFilePathSlashInput } from './slashInputDetection.js' ;
100+ import {
101+ buildMcpStartupSummaryRows ,
102+ getAutoConnectMcpServerNames ,
103+ truncateMcpStartupError ,
104+ } from './mcpStartupHistory.js' ;
100105
101106export class AutohandAgent {
102107 private mentionContexts : { path : string ; contents : string } [ ] = [ ] ;
@@ -646,6 +651,9 @@ export class AutohandAgent {
646651 /** Promise that resolves when background init is complete */
647652 private initReady : Promise < void > | null = null ;
648653 private initDone = false ;
654+ private mcpStartupAutoConnectServers : string [ ] = [ ] ;
655+ private mcpStartupConnectStartedAt : number | null = null ;
656+ private mcpStartupSummaryPrinted = false ;
649657
650658 async runInteractive ( ) : Promise < void > {
651659 // Bail out early if stdin is not a TTY - interactive mode requires a terminal
@@ -655,6 +663,16 @@ export class AutohandAgent {
655663 return ;
656664 }
657665
666+ // Prepare startup visibility for async MCP connections.
667+ this . mcpStartupAutoConnectServers = getAutoConnectMcpServerNames ( this . runtime . config . mcp ?. servers ) ;
668+ this . mcpStartupConnectStartedAt = null ;
669+ this . mcpStartupSummaryPrinted = false ;
670+ if ( this . runtime . config . mcp ?. enabled !== false && this . mcpStartupAutoConnectServers . length > 0 ) {
671+ const count = this . mcpStartupAutoConnectServers . length ;
672+ const label = count === 1 ? 'server' : 'servers' ;
673+ console . log ( chalk . gray ( `MCP startup: connecting ${ count } ${ label } in background...` ) ) ;
674+ }
675+
658676 // Start ALL initialization in background so prompt appears instantly.
659677 // The user can start typing while managers initialize.
660678 // When they submit, we await initReady before processing.
@@ -685,6 +703,7 @@ export class AutohandAgent {
685703 // Servers connect asynchronously; tools become available once ready.
686704 // Does NOT block the main init pipeline or user prompt.
687705 if ( this . runtime . config . mcp ?. enabled !== false ) {
706+ this . mcpStartupConnectStartedAt = Date . now ( ) ;
688707 this . mcpReady = this . mcpManager
689708 . connectAll ( this . runtime . config . mcp ?. servers ?? [ ] )
690709 . then ( ( ) => { this . syncMcpTools ( ) ; } )
@@ -732,6 +751,7 @@ export class AutohandAgent {
732751 if ( this . mcpReady ) {
733752 await this . mcpReady ;
734753 }
754+ this . printMcpStartupSummaryIfNeeded ( ) ;
735755
736756 // Fire session-start hook now that the prompt is closed and stdout is clean
737757 const session = this . sessionManager . getCurrentSession ( ) ;
@@ -1213,33 +1233,32 @@ If lint or tests fail, report the issues but do NOT commit.`;
12131233 return null ;
12141234 }
12151235
1216- if ( normalized . startsWith ( '/' ) && ! isLikelyFilePathSlashInput ( normalized ) ) {
1217- // Parse command and arguments from input
1218- const parts = normalized . split ( / \s + / ) ;
1219- let command = parts [ 0 ] ;
1220- let args = parts . slice ( 1 ) ;
1221-
1222- // Handle multi-word commands like "/skills install", "/mcp install"
1223- const twoWordCommands = [ '/skills install' , '/skills new' , '/skills use' , '/agents new' , '/mcp install' ] ;
1224- const potentialTwoWord = `${ parts [ 0 ] } ${ parts [ 1 ] || '' } ` . trim ( ) ;
1225- if ( twoWordCommands . includes ( potentialTwoWord ) ) {
1226- command = potentialTwoWord ;
1227- args = parts . slice ( 2 ) ;
1228- }
1229-
1230- // /quit and /exit return themselves as pass-through instructions
1231- // so the interactive loop's special exit handler (line 963) can catch them.
1232- // Skip the slash handler for these — they're control-flow, not commands.
1233- if ( command === '/quit' || command === '/exit' ) {
1234- return command ;
1235- }
1236+ if ( normalized . startsWith ( '/' ) ) {
1237+ // Always prioritize known slash commands, even when args contain '/'
1238+ // (e.g. package specs like "@playwright/mcp@latest").
1239+ const parsed = this . parseSlashCommand ( normalized ) ;
1240+ const isKnownSlashCommand = this . isSlashCommandSupported ( parsed . command ) ;
1241+ if ( ! isKnownSlashCommand && isLikelyFilePathSlashInput ( normalized ) ) {
1242+ // Looks like an absolute file path, not a command.
1243+ // Fall through to normal prompt handling below.
1244+ } else {
1245+ const command = parsed . command ;
1246+ const args = parsed . args ;
1247+
1248+ // /quit and /exit return themselves as pass-through instructions
1249+ // so the interactive loop's special exit handler (line 963) can catch them.
1250+ // Skip the slash handler for these — they're control-flow, not commands.
1251+ if ( command === '/quit' || command === '/exit' ) {
1252+ return command ;
1253+ }
12361254
1237- const handled = await this . handleSlashCommand ( command , args ) ;
1238- if ( handled !== null ) {
1239- // Slash command returned display output — print it, don't send to LLM
1240- console . log ( handled ) ;
1255+ const handled = await this . handleSlashCommand ( command , args ) ;
1256+ if ( handled !== null ) {
1257+ // Slash command returned display output — print it, don't send to LLM
1258+ console . log ( handled ) ;
1259+ }
1260+ return null ;
12411261 }
1242- return null ;
12431262 }
12441263
12451264 // Handle # trigger for storing memories
@@ -4013,6 +4032,12 @@ If lint or tests fail, report the issues but do NOT commit.`;
40134032 * Returns the command output or null if the command doesn't exist
40144033 */
40154034 async handleSlashCommand ( command : string , args : string [ ] = [ ] ) : Promise < string | null > {
4035+ // /mcp depends on background startup state (notably MCP auto-connect).
4036+ // Ensure startup init is settled before rendering server status/actions.
4037+ if ( command === '/mcp' || command === '/mcp install' ) {
4038+ await this . ensureInitComplete ( ) ;
4039+ }
4040+
40164041 const result = await this . slashHandler . handle ( command , args ) ;
40174042 if ( command === '/mcp' || command === '/mcp install' ) {
40184043 this . syncMcpTools ( ) ;
@@ -4264,6 +4289,64 @@ If lint or tests fail, report the issues but do NOT commit.`;
42644289 return `${ planIndicator } ${ percent } % context left · ${ t ( 'ui.commandHint' ) } ${ queueStatus } ` ;
42654290 }
42664291
4292+ private printMcpStartupSummaryIfNeeded ( ) : void {
4293+ if ( this . mcpStartupSummaryPrinted ) {
4294+ return ;
4295+ }
4296+ if ( this . runtime . config . mcp ?. enabled === false ) {
4297+ this . mcpStartupSummaryPrinted = true ;
4298+ return ;
4299+ }
4300+ if ( this . mcpStartupAutoConnectServers . length === 0 ) {
4301+ this . mcpStartupSummaryPrinted = true ;
4302+ return ;
4303+ }
4304+
4305+ this . mcpStartupSummaryPrinted = true ;
4306+
4307+ const rows = buildMcpStartupSummaryRows (
4308+ this . mcpStartupAutoConnectServers ,
4309+ this . mcpManager . listServers ( )
4310+ ) ;
4311+
4312+ const elapsed = this . mcpStartupConnectStartedAt
4313+ ? formatElapsedTime ( this . mcpStartupConnectStartedAt )
4314+ : null ;
4315+
4316+ const connected = rows . filter ( ( row ) => row . status === 'connected' ) . length ;
4317+ const failed = rows . filter ( ( row ) => row . status === 'error' ) . length ;
4318+ const disconnected = rows . filter ( ( row ) => row . status === 'disconnected' ) . length ;
4319+ const summaryParts = [
4320+ `${ connected } connected` ,
4321+ failed > 0 ? `${ failed } failed` : null ,
4322+ disconnected > 0 ? `${ disconnected } disconnected` : null ,
4323+ ] . filter ( Boolean ) . join ( ', ' ) ;
4324+ const elapsedSuffix = elapsed ? ` in ${ elapsed } ` : '' ;
4325+
4326+ console . log ( chalk . bold ( '\n* MCP startup' ) ) ;
4327+ console . log ( chalk . gray ( ` Async connection phase complete${ elapsedSuffix } (${ summaryParts } )` ) ) ;
4328+
4329+ for ( const row of rows ) {
4330+ if ( row . status === 'connected' ) {
4331+ const toolLabel = row . toolCount === 1 ? 'tool' : 'tools' ;
4332+ console . log ( ` ${ chalk . green ( '✓' ) } ${ row . name } connected (${ row . toolCount } ${ toolLabel } )` ) ;
4333+ continue ;
4334+ }
4335+
4336+ if ( row . status === 'error' ) {
4337+ const errorSuffix = row . error
4338+ ? `: ${ truncateMcpStartupError ( row . error ) } `
4339+ : '' ;
4340+ console . log ( ` ${ chalk . red ( '✖' ) } ${ row . name } failed${ errorSuffix } ` ) ;
4341+ continue ;
4342+ }
4343+
4344+ console . log ( ` ${ chalk . yellow ( '○' ) } ${ row . name } not connected` ) ;
4345+ }
4346+
4347+ console . log ( ) ;
4348+ }
4349+
42674350 private async resetConversationContext ( ) : Promise < void > {
42684351 const systemPrompt = await this . buildSystemPrompt ( ) ;
42694352 this . conversation . reset ( systemPrompt ) ;
0 commit comments