@@ -1391,6 +1391,20 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
13911391 // scanner touches node:fs, so it stays behind a dynamic import (the
13921392 // stdio-connector pattern) and behind the stdio gate: with stdio off the
13931393 // presets could not be added anyway.
1394+ /** How long the probe waits for the plugin to answer. Deliberately
1395+ * under the MCP SDK's 60s default: a pending macOS consent prompt
1396+ * blocks the call indefinitely, and the person should be told to look
1397+ * for the prompt rather than watch "Checking…" for a minute. */
1398+ const PROBE_ANSWER_TIMEOUT_MS = 25_000 ;
1399+ /** The client SDK signals its request timeout as an `SdkError` with
1400+ * code `REQUEST_TIMEOUT`. Matched structurally: the SDK is loaded
1401+ * dynamically, so its error class is not importable here. */
1402+ const isMcpRequestTimeout = ( cause : unknown ) : boolean =>
1403+ typeof cause === "object" &&
1404+ cause !== null &&
1405+ "code" in cause &&
1406+ ( cause as { readonly code : unknown } ) . code === "REQUEST_TIMEOUT" ;
1407+
13941408 /** Ask a Codex plugin whether macOS will actually let it work.
13951409 *
13961410 * Runs the plugin's own read-only probe tool down the REAL path — the
@@ -1424,15 +1438,26 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
14241438 ...( plugin . appServer === undefined ? { } : { appServer : plugin . appServer } ) ,
14251439 } ) ;
14261440
1441+ // A probe that hangs is a real outcome, not an edge case: an Apple
1442+ // Event blocks for as long as macOS sits on the consent decision.
1443+ // Under the SDK's 60s default the card shows "Checking…" for a full
1444+ // minute and then misreports the hang as a failed start.
1445+ let timedOut = false ;
14271446 return yield * Effect . gen ( function * ( ) {
14281447 const connection = yield * connector ;
14291448 const result = yield * Effect . tryPromise ( {
1430- try : ( ) => connection . client . callTool ( { name : probe . name , arguments : probe . args } ) ,
1431- catch : ( ) =>
1432- new McpConnectionError ( {
1449+ try : ( ) =>
1450+ connection . client . callTool (
1451+ { name : probe . name , arguments : probe . args } ,
1452+ { timeout : PROBE_ANSWER_TIMEOUT_MS } ,
1453+ ) ,
1454+ catch : ( cause ) => {
1455+ timedOut = isMcpRequestTimeout ( cause ) ;
1456+ return new McpConnectionError ( {
14331457 transport : "appserver" ,
14341458 message : "The plugin did not answer." ,
1435- } ) ,
1459+ } ) ;
1460+ } ,
14361461 } ) . pipe ( Effect . ensuring ( Effect . promise ( ( ) => connection . close ( ) ) ) ) ;
14371462
14381463 const text = ( Array . isArray ( result . content ) ? result . content : [ ] )
@@ -1454,8 +1479,9 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
14541479 McpConnectionError : ( ) =>
14551480 Effect . succeed ( {
14561481 status : "blocked" as const ,
1457- message :
1458- "Could not start the plugin. Check that Codex is installed and signed in." ,
1482+ message : timedOut
1483+ ? "macOS has not answered yet. If a permission prompt is on screen, answer it, then check again."
1484+ : "Could not start the plugin. Check that Codex is installed and signed in." ,
14591485 } ) ,
14601486 McpOAuthReauthorizationRequired : ( ) =>
14611487 Effect . succeed ( {
0 commit comments