@@ -192,6 +192,16 @@ const CodexMcpServer = Schema.Struct({
192192const decodeCodexMcpServers = Schema . decodeUnknownOption (
193193 Schema . fromJsonString ( Schema . Array ( CodexMcpServer ) ) ,
194194) ;
195+ const CodexMcpServerUrl = Schema . Struct ( {
196+ transport : Schema . optional (
197+ Schema . Struct ( {
198+ url : Schema . optional ( Schema . String ) ,
199+ } ) ,
200+ ) ,
201+ } ) ;
202+ const decodeCodexMcpServerUrl = Schema . decodeUnknownOption (
203+ Schema . fromJsonString ( CodexMcpServerUrl ) ,
204+ ) ;
195205
196206function cleanedCliLine ( value : string ) : string {
197207 return (
@@ -210,6 +220,35 @@ function findHttpUrl(value: string): string | null {
210220 return match ?. replace ( / [ ) , . ; ] + $ / u, "" ) ?? null ;
211221}
212222
223+ export function isCodexMcpServerMissingError ( error : unknown ) : boolean {
224+ const seen = new Set < unknown > ( ) ;
225+ let current : unknown = error ;
226+ while ( current && ! seen . has ( current ) ) {
227+ seen . add ( current ) ;
228+ const message =
229+ current instanceof Error
230+ ? current . message
231+ : typeof current === "object" && current !== null && "message" in current
232+ ? String ( Reflect . get ( current , "message" ) )
233+ : String ( current ) ;
234+ if ( / n o m c p s e r v e r n a m e d / iu. test ( message ) ) return true ;
235+ current =
236+ current instanceof Error
237+ ? current . cause
238+ : typeof current === "object" && current !== null && "cause" in current
239+ ? Reflect . get ( current , "cause" )
240+ : undefined ;
241+ }
242+ return false ;
243+ }
244+
245+ export function codexMcpLoginArgs ( name : string , url ?: string | null ) : ReadonlyArray < string > {
246+ const trimmedUrl = url ?. trim ( ) ;
247+ return trimmedUrl
248+ ? [ "-c" , `mcp_servers.${ name } .url=${ JSON . stringify ( trimmedUrl ) } ` , "mcp" , "login" , name ]
249+ : [ "mcp" , "login" , name ] ;
250+ }
251+
213252export function findClaudeMcpAuthorizationUrl ( value : string ) : string | null {
214253 // eslint-disable-next-line no-control-regex -- URLs end before terminal control sequences.
215254 for ( const match of value . matchAll ( / h t t p s ? : \/ \/ [ ^ \s \u0007 \u001B ] + / gu) ) {
@@ -437,7 +476,7 @@ export const make = (options: McpOAuthRuntimeOptions = {}) =>
437476 return next ;
438477 } ) ;
439478
440- const runCodexSession = ( key : string , session : ActiveSession ) =>
479+ const runCodexAppServerLogin = ( session : ActiveSession ) =>
441480 Effect . scoped (
442481 Effect . gen ( function * ( ) {
443482 const configured = yield * commandFor ( "codex" , "codex" ) ;
@@ -484,6 +523,7 @@ export const make = (options: McpOAuthRuntimeOptions = {}) =>
484523 capabilities : { experimentalApi : true } ,
485524 } ) ;
486525 yield * client . notify ( "initialized" , undefined ) ;
526+ yield * client . request ( "config/mcpServer/reload" , undefined ) . pipe ( Effect . ignore ) ;
487527 const response = yield * client . request ( "mcpServer/oauth/login" , {
488528 name : session . name ,
489529 timeoutSecs : 600 ,
@@ -539,7 +579,87 @@ export const make = (options: McpOAuthRuntimeOptions = {}) =>
539579 } ) ;
540580 }
541581 } ) ,
542- ) . pipe (
582+ ) ;
583+
584+ const runCodexCliLogin = ( session : ActiveSession ) =>
585+ Effect . scoped (
586+ Effect . gen ( function * ( ) {
587+ const configured = yield * commandFor ( "codex" , "codex" ) ;
588+ if ( ! configured ) {
589+ return yield * new McpOAuthProviderUnavailableError ( {
590+ operation : "start" ,
591+ harness : "codex" ,
592+ serverName : session . name ,
593+ } ) ;
594+ }
595+ const listed = yield * processRunner
596+ . run ( {
597+ command : configured . command ,
598+ args : [ "mcp" , "get" , session . name , "--json" ] ,
599+ cwd,
600+ env : configured . env ,
601+ timeout : "30 seconds" ,
602+ maxOutputBytes : 512 * 1024 ,
603+ outputMode : "truncate" ,
604+ } )
605+ . pipe ( Effect . option ) ;
606+ const listedUrl =
607+ Option . isSome ( listed ) && listed . value . code === 0
608+ ? Option . match ( decodeCodexMcpServerUrl ( listed . value . stdout ) , {
609+ onNone : ( ) => null ,
610+ onSome : ( server ) => server . transport ?. url ?. trim ( ) || null ,
611+ } )
612+ : null ;
613+ const environment = configured . env ;
614+ const spawnCommand = yield * resolveSpawnCommand (
615+ configured . command ,
616+ [ ...codexMcpLoginArgs ( session . name , listedUrl ) ] ,
617+ {
618+ env : environment ,
619+ extendEnv : true ,
620+ } ,
621+ ) ;
622+ const child = yield * spawner . spawn (
623+ ChildProcess . make ( spawnCommand . command , spawnCommand . args , {
624+ cwd,
625+ env : environment ,
626+ extendEnv : true ,
627+ shell : spawnCommand . shell ,
628+ forceKillAfter : "2 seconds" ,
629+ } ) ,
630+ ) ;
631+ yield * Deferred . succeed ( session . started , {
632+ authorizationUrl : "" ,
633+ callbackRequired : false ,
634+ } ) ;
635+ const exitCode = yield * Effect . raceFirst (
636+ child . exitCode ,
637+ Deferred . await ( session . cancelled ) . pipe (
638+ Effect . flatMap ( ( ) =>
639+ Effect . fail (
640+ new McpOAuthAuthenticationCancelledError ( {
641+ operation : "start" ,
642+ harness : "codex" ,
643+ serverName : session . name ,
644+ } ) ,
645+ ) ,
646+ ) ,
647+ ) ,
648+ ) ;
649+ if ( exitCode !== 0 ) {
650+ return yield * new McpOAuthAuthenticationFailedError ( {
651+ operation : "start" ,
652+ harness : "codex" ,
653+ serverName : session . name ,
654+ exitCode,
655+ } ) ;
656+ }
657+ } ) ,
658+ ) ;
659+
660+ const runCodexSession = ( key : string , session : ActiveSession ) =>
661+ runCodexAppServerLogin ( session ) . pipe (
662+ Effect . catchIf ( isCodexMcpServerMissingError , ( ) => runCodexCliLogin ( session ) ) ,
543663 Effect . mapError ( ( cause ) =>
544664 isMcpOAuthRuntimeError ( cause )
545665 ? cause
0 commit comments