@@ -762,6 +762,16 @@ When creating pull requests, add the following footer at the end of the PR descr
762762 } ) ) ,
763763 ) ;
764764
765+ // codex-acp connects to every MCP server eagerly during session creation
766+ // and treats an unreachable one as fatal, which kills the session
767+ // ("ACP connection closed") and makes the host silently fall back to a
768+ // Claude/Opus session. Claude connects lazily and is unaffected, so only
769+ // the Codex server list is pruned to the reachable ones.
770+ const sessionMcpServers =
771+ adapter === "codex"
772+ ? await this . filterReachableMcpServers ( mcpServers , taskRunId )
773+ : mcpServers ;
774+
765775 let externalPlugins : Awaited < ReturnType < typeof discoverExternalPlugins > > =
766776 [ ] ;
767777 try {
@@ -833,7 +843,7 @@ When creating pull requests, add the following footer at the end of the PR descr
833843 const resumeResponse = await connection . resumeSession ( {
834844 sessionId : existingSessionId ,
835845 cwd : repoPath ,
836- mcpServers,
846+ mcpServers : sessionMcpServers ,
837847 _meta : {
838848 ...( logUrl && {
839849 persistence : { taskId, runId : taskRunId , logUrl } ,
@@ -862,7 +872,7 @@ When creating pull requests, add the following footer at the end of the PR descr
862872 }
863873 const newSessionResponse = await connection . newSession ( {
864874 cwd : repoPath ,
865- mcpServers,
875+ mcpServers : sessionMcpServers ,
866876 _meta : {
867877 taskRunId,
868878 environment : "local" ,
@@ -952,6 +962,75 @@ When creating pull requests, add the following footer at the end of the PR descr
952962 }
953963 }
954964
965+ private async filterReachableMcpServers <
966+ T extends {
967+ name : string ;
968+ url : string ;
969+ headers : Array < { name : string ; value : string } > ;
970+ } ,
971+ > ( servers : T [ ] , taskRunId : string ) : Promise < T [ ] > {
972+ const probed = await Promise . all (
973+ servers . map ( async ( server ) => ( {
974+ server,
975+ reachable : await this . isMcpServerReachable ( server ) ,
976+ } ) ) ,
977+ ) ;
978+ const reachable : T [ ] = [ ] ;
979+ for ( const { server, reachable : ok } of probed ) {
980+ if ( ok ) {
981+ reachable . push ( server ) ;
982+ } else {
983+ this . log . warn (
984+ "Dropping unreachable MCP server from Codex session; codex-acp treats an unreachable server as a fatal startup error" ,
985+ { taskRunId, server : server . name , url : server . url } ,
986+ ) ;
987+ }
988+ }
989+ return reachable ;
990+ }
991+
992+ private async isMcpServerReachable ( server : {
993+ url : string ;
994+ headers : Array < { name : string ; value : string } > ;
995+ } ) : Promise < boolean > {
996+ const PROBE_TIMEOUT_MS = 4_000 ;
997+ try {
998+ const headers : Record < string , string > = {
999+ "content-type" : "application/json" ,
1000+ accept : "application/json, text/event-stream" ,
1001+ } ;
1002+ for ( const header of server . headers ) {
1003+ headers [ header . name ] = header . value ;
1004+ }
1005+ const response = await fetch ( server . url , {
1006+ method : "POST" ,
1007+ headers,
1008+ body : JSON . stringify ( {
1009+ jsonrpc : "2.0" ,
1010+ id : 0 ,
1011+ method : "initialize" ,
1012+ params : {
1013+ protocolVersion : "2025-06-18" ,
1014+ capabilities : { } ,
1015+ clientInfo : { name : "posthog-code" , version : "1.0.0" } ,
1016+ } ,
1017+ } ) ,
1018+ signal : AbortSignal . timeout ( PROBE_TIMEOUT_MS ) ,
1019+ } ) ;
1020+ await response . body ?. cancel ( ) ;
1021+ // Any HTTP response means the endpoint is reachable. codex-acp only treats
1022+ // transport failures (connection refused, DNS, timeout) as fatal; HTTP or
1023+ // JSON-RPC error responses are handled gracefully.
1024+ return true ;
1025+ } catch ( err ) {
1026+ this . log . debug ( "MCP server reachability probe failed" , {
1027+ url : server . url ,
1028+ error : err instanceof Error ? err . message : String ( err ) ,
1029+ } ) ;
1030+ return false ;
1031+ }
1032+ }
1033+
9551034 async prompt (
9561035 sessionId : string ,
9571036 prompt : ContentBlock [ ] ,
0 commit comments