From 9d85fa81b35c240a2e385d0e56c4f8d928e1ac1a Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Sat, 28 Mar 2026 00:17:39 -0700 Subject: [PATCH 1/6] update --- codex-rs/core/src/mcp_connection_manager.rs | 2 +- codex-rs/core/src/mcp_connection_manager_tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index eac8485048a8..1c56a76602e5 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -93,7 +93,7 @@ const MCP_TOOL_NAME_DELIMITER: &str = "__"; const MAX_TOOL_NAME_LENGTH: usize = 64; /// Default timeout for initializing MCP server & initially listing tools. -pub const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(10); +pub const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); /// Default timeout for individual tool calls. const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120); diff --git a/codex-rs/core/src/mcp_connection_manager_tests.rs b/codex-rs/core/src/mcp_connection_manager_tests.rs index 2331d0c1fe00..a98434f3f724 100644 --- a/codex-rs/core/src/mcp_connection_manager_tests.rs +++ b/codex-rs/core/src/mcp_connection_manager_tests.rs @@ -612,7 +612,7 @@ fn mcp_init_error_display_includes_startup_timeout_hint() { let display = mcp_init_error_display(server_name, None, &err); assert_eq!( - "MCP client for `slow` timed out after 10 seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.slow]\nstartup_timeout_sec = XX", + "MCP client for `slow` timed out after 30 seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.slow]\nstartup_timeout_sec = XX", display ); } From c51c16fca9cdd4b6a9f10cb8d6661675a5d2c7b9 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Sat, 28 Mar 2026 00:24:08 -0700 Subject: [PATCH 2/6] update --- .../schema/json/ClientRequest.json | 39 +++++++ .../schema/json/ServerNotification.json | 1 + .../codex_app_server_protocol.schemas.json | 56 ++++++++++ .../codex_app_server_protocol.v2.schemas.json | 56 ++++++++++ .../json/v2/ItemCompletedNotification.json | 1 + .../json/v2/ItemStartedNotification.json | 1 + .../schema/json/v2/McpResourceReadParams.json | 17 +++ .../json/v2/McpResourceReadResponse.json | 14 +++ .../schema/json/v2/ReviewStartResponse.json | 1 + .../schema/json/v2/ThreadForkResponse.json | 1 + .../schema/json/v2/ThreadListResponse.json | 1 + .../json/v2/ThreadMetadataUpdateResponse.json | 1 + .../schema/json/v2/ThreadReadResponse.json | 1 + .../schema/json/v2/ThreadResumeResponse.json | 1 + .../json/v2/ThreadRollbackResponse.json | 1 + .../schema/json/v2/ThreadStartResponse.json | 1 + .../json/v2/ThreadStartedNotification.json | 1 + .../json/v2/ThreadUnarchiveResponse.json | 1 + .../json/v2/TurnCompletedNotification.json | 1 + .../schema/json/v2/TurnStartResponse.json | 1 + .../json/v2/TurnStartedNotification.json | 1 + .../schema/typescript/ClientRequest.ts | 3 +- .../typescript/v2/McpResourceReadParams.ts | 5 + .../typescript/v2/McpResourceReadResponse.ts | 6 ++ .../schema/typescript/v2/McpToolCallResult.ts | 2 +- .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 5 + .../src/protocol/thread_history.rs | 62 +++++++++++ .../app-server-protocol/src/protocol/v2.rs | 18 ++++ codex-rs/app-server/README.md | 1 + .../app-server/src/bespoke_event_handling.rs | 8 +- .../app-server/src/codex_message_processor.rs | 59 +++++++++++ codex-rs/core/src/mcp/mod.rs | 100 +++++++++++++++--- codex-rs/core/src/mcp/mod_tests.rs | 4 + 34 files changed, 453 insertions(+), 20 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/McpResourceReadResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 7c9441984425..f5b929696b64 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1208,6 +1208,21 @@ } ] }, + "McpResourceReadParams": { + "properties": { + "server": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "server", + "uri" + ], + "type": "object" + }, "McpServerOauthLoginParams": { "properties": { "name": { @@ -4420,6 +4435,30 @@ "title": "McpServerStatus/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpResource/read" + ], + "title": "McpResource/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpResourceReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpResource/readRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 8ca93137c60a..8dc62a30c2ad 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1553,6 +1553,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 91490ce26fe8..62d896dee46d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1201,6 +1201,30 @@ "title": "McpServerStatus/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "mcpResource/read" + ], + "title": "McpResource/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/McpResourceReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpResource/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -8755,6 +8779,37 @@ ], "type": "string" }, + "McpResourceReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "server": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "server", + "uri" + ], + "title": "McpResourceReadParams", + "type": "object" + }, + "McpResourceReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "contents": { + "items": true, + "type": "array" + } + }, + "required": [ + "contents" + ], + "title": "McpResourceReadResponse", + "type": "object" + }, "McpServerOauthLoginCompletedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -8931,6 +8986,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 0f31f5b3ec94..be70f2dc0b00 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1776,6 +1776,30 @@ "title": "McpServerStatus/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpResource/read" + ], + "title": "McpResource/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpResourceReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpResource/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -5569,6 +5593,37 @@ ], "type": "string" }, + "McpResourceReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "server": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "server", + "uri" + ], + "title": "McpResourceReadParams", + "type": "object" + }, + "McpResourceReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "contents": { + "items": true, + "type": "array" + } + }, + "required": [ + "contents" + ], + "title": "McpResourceReadResponse", + "type": "object" + }, "McpServerOauthLoginCompletedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -5745,6 +5800,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 39641078658f..2883670c88fb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -294,6 +294,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index abb8aee5dc81..c2e71ccba91e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -294,6 +294,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json new file mode 100644 index 000000000000..7ea633b58678 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "server": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "server", + "uri" + ], + "title": "McpResourceReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadResponse.json new file mode 100644 index 000000000000..fa69f5ad7583 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadResponse.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "contents": { + "items": true, + "type": "array" + } + }, + "required": [ + "contents" + ], + "title": "McpResourceReadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 2e0c3605e7d4..6ff4279226c0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -430,6 +430,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 5ae0c5a122b5..01325fd7dec3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -518,6 +518,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 126d78603a11..c1f867610831 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -456,6 +456,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index dfdab228df3f..fec0bda2e925 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -456,6 +456,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 8f48dee4b735..34cfa9be7652 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -456,6 +456,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index edccc337da29..c9c435c59284 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -518,6 +518,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index cc41aac27f51..7c9f3e2074ee 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -456,6 +456,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index c3b50fee3b01..03f17dfefe47 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -518,6 +518,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 2240150394df..759e2bd3e713 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -456,6 +456,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 41b0d2d409c3..8457dd2ad8c1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -456,6 +456,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 770cc920cfb8..7b5df0c7a771 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -430,6 +430,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 7f1c3e4948dd..16e7339fbb99 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -430,6 +430,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 761ddc9a624c..ce92bd5fddb2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -430,6 +430,7 @@ }, "McpToolCallResult": { "properties": { + "_meta": true, "content": { "items": true, "type": "array" diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index e33a9863596b..4d6569f98d91 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -33,6 +33,7 @@ import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; import type { LoginAccountParams } from "./v2/LoginAccountParams"; +import type { McpResourceReadParams } from "./v2/McpResourceReadParams"; import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { PluginInstallParams } from "./v2/PluginInstallParams"; @@ -64,4 +65,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpResource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts new file mode 100644 index 000000000000..c58bf8499a2c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpResourceReadParams = { server: string, uri: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadResponse.ts new file mode 100644 index 000000000000..1b6b0b040bb2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type McpResourceReadResponse = { contents: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts index f493a86094e4..916a5f5bb3fb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { JsonValue } from "../serde_json/JsonValue"; -export type McpToolCallResult = { content: Array, structuredContent: JsonValue | null, }; +export type McpToolCallResult = { content: Array, structuredContent: JsonValue | null, _meta: JsonValue | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index d0687e5f1dbf..47ce19733beb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -170,6 +170,8 @@ export type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitatio export type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems"; export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema"; export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema"; +export type { McpResourceReadParams } from "./McpResourceReadParams"; +export type { McpResourceReadResponse } from "./McpResourceReadResponse"; export type { McpServerElicitationAction } from "./McpServerElicitationAction"; export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams"; export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 30061c716e92..f3c36529b614 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -459,6 +459,11 @@ client_request_definitions! { response: v2::ListMcpServerStatusResponse, }, + McpResourceRead => "mcpResource/read" { + params: v2::McpResourceReadParams, + response: v2::McpResourceReadResponse, + }, + WindowsSandboxSetupStart => "windowsSandbox/setupStart" { params: v2::WindowsSandboxSetupStartParams, response: v2::WindowsSandboxSetupStartResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 48fa56d687c8..000252957da2 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -547,6 +547,7 @@ impl ThreadHistoryBuilder { Some(McpToolCallResult { content: value.content.clone(), structured_content: value.structured_content.clone(), + meta: value.meta.clone(), }), None, ), @@ -1205,6 +1206,7 @@ mod tests { use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::items::UserMessageItem as CoreUserMessageItem; use codex_protocol::items::build_hook_prompt_message; + use codex_protocol::mcp::CallToolResult; use codex_protocol::models::MessagePhase as CoreMessagePhase; use codex_protocol::models::WebSearchAction as CoreWebSearchAction; use codex_protocol::parse_command::ParsedCommand; @@ -1884,6 +1886,66 @@ mod tests { ); } + #[test] + fn reconstructs_mcp_tool_result_meta_from_persisted_completion_events() { + let events = vec![ + EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".into(), + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + EventMsg::McpToolCallEnd(McpToolCallEndEvent { + call_id: "mcp-1".into(), + invocation: McpInvocation { + server: "docs".into(), + tool: "lookup".into(), + arguments: Some(serde_json::json!({"id":"123"})), + }, + duration: Duration::from_millis(8), + result: Ok(CallToolResult { + content: vec![serde_json::json!({ + "type": "text", + "text": "result" + })], + structured_content: Some(serde_json::json!({"id":"123"})), + is_error: Some(false), + meta: Some(serde_json::json!({ + "ui/resourceUri": "ui://widget/lookup.html" + })), + }), + }), + ]; + + let items = events + .into_iter() + .map(RolloutItem::EventMsg) + .collect::>(); + let turns = build_turns_from_rollout_items(&items); + assert_eq!(turns.len(), 1); + assert_eq!( + turns[0].items[0], + ThreadItem::McpToolCall { + id: "mcp-1".into(), + server: "docs".into(), + tool: "lookup".into(), + status: McpToolCallStatus::Completed, + arguments: serde_json::json!({"id":"123"}), + result: Some(McpToolCallResult { + content: vec![serde_json::json!({ + "type": "text", + "text": "result" + })], + structured_content: Some(serde_json::json!({"id":"123"})), + meta: Some(serde_json::json!({ + "ui/resourceUri": "ui://widget/lookup.html" + })), + }), + error: None, + duration_ms: Some(8), + } + ); + } + #[test] fn reconstructs_dynamic_tool_items_from_request_and_response_events() { let events = vec![ diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 9373c852d84c..0f7c826cc4ae 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1968,6 +1968,21 @@ pub struct ListMcpServerStatusResponse { pub next_cursor: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpResourceReadParams { + pub server: String, + pub uri: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpResourceReadResponse { + pub contents: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -4744,6 +4759,9 @@ pub struct McpToolCallResult { // representations). Using `JsonValue` keeps the payload wire-shaped and easy to export. pub content: Vec, pub structured_content: Option, + #[serde(rename = "_meta")] + #[ts(rename = "_meta")] + pub meta: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 4365cb8e29aa..f8121553c002 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -187,6 +187,7 @@ Example with notification opt-out: - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination. +- `mcpResource/read` — read a resource from a configured MCP server by `server` and `uri`, returning the resource `contents`. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `config/read` — fetch the effective config on disk after resolving config layering. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 9a850c0b1ead..dadcc22191f3 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2818,6 +2818,7 @@ async fn construct_mcp_tool_call_end_notification( Some(McpToolCallResult { content: value.content.clone(), structured_content: value.structured_content.clone(), + meta: value.meta.clone(), }), None, ), @@ -3747,7 +3748,9 @@ mod tests { content: content.clone(), is_error: Some(false), structured_content: None, - meta: None, + meta: Some(serde_json::json!({ + "ui/resourceUri": "ui://widget/list-resources.html" + })), }; let end_event = McpToolCallEndEvent { @@ -3782,6 +3785,9 @@ mod tests { result: Some(McpToolCallResult { content, structured_content: None, + meta: Some(serde_json::json!({ + "ui/resourceUri": "ui://widget/list-resources.html" + })), }), error: None, duration_ms: Some(0), diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index f7e291674724..d499974c4681 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -73,6 +73,8 @@ use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::LoginApiKeyParams; use codex_app_server_protocol::LogoutAccountResponse; use codex_app_server_protocol::MarketplaceInterface; +use codex_app_server_protocol::McpResourceReadParams; +use codex_app_server_protocol::McpResourceReadResponse; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::McpServerOauthLoginParams; use codex_app_server_protocol::McpServerOauthLoginResponse; @@ -220,6 +222,7 @@ use codex_core::mcp::auth::discover_supported_scopes; use codex_core::mcp::auth::resolve_oauth_scopes; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; +use codex_core::mcp::read_mcp_resource; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::plugins::MarketplaceError; @@ -874,6 +877,10 @@ impl CodexMessageProcessor { self.list_mcp_server_status(to_connection_request_id(request_id), params) .await; } + ClientRequest::McpResourceRead { request_id, params } => { + self.read_mcp_resource(to_connection_request_id(request_id), params) + .await; + } ClientRequest::WindowsSandboxSetupStart { request_id, params } => { self.windows_sandbox_setup_start(to_connection_request_id(request_id), params) .await; @@ -5089,6 +5096,58 @@ impl CodexMessageProcessor { outgoing.send_response(request_id, response).await; } + async fn read_mcp_resource( + &self, + request_id: ConnectionRequestId, + params: McpResourceReadParams, + ) { + let outgoing = Arc::clone(&self.outgoing); + let config = match self.load_latest_config(/*fallback_cwd*/ None).await { + Ok(config) => config, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + tokio::spawn(async move { + let result = read_mcp_resource(&config, ¶ms.server, ¶ms.uri).await; + match result { + Ok(result) => match serde_json::from_value::(result) { + Ok(response) => { + outgoing.send_response(request_id, response).await; + } + Err(error) => { + outgoing + .send_error( + request_id, + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!( + "failed to deserialize MCP resource read response: {error}" + ), + data: None, + }, + ) + .await; + } + }, + Err(error) => { + outgoing + .send_error( + request_id, + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("{error:#}"), + data: None, + }, + ) + .await; + } + } + }); + } + async fn send_invalid_request_error(&self, request_id: ConnectionRequestId, message: String) { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index a9d2388f7041..72218242bb28 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -14,16 +14,20 @@ use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; +use rmcp::model::ReadResourceRequestParams; use serde_json::Value; +use tokio_util::sync::CancellationToken; use crate::AuthManager; use crate::CodexAuth; use crate::config::Config; use crate::config::types::McpServerConfig; use crate::config::types::McpServerTransportConfig; +use crate::mcp::auth::McpAuthStatusEntry; use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::SandboxState; +use crate::mcp_connection_manager::ToolInfo; use crate::mcp_connection_manager::codex_apps_tools_cache_key; use crate::plugins::PluginCapabilitySummary; use crate::plugins::PluginsManager; @@ -251,6 +255,52 @@ fn effective_mcp_servers( } pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent { + let (mcp_connection_manager, cancel_token, auth_status_entries) = + new_mcp_connection_manager(config).await; + if !mcp_connection_manager.has_servers() { + cancel_token.cancel(); + return McpListToolsResponseEvent { + tools: HashMap::new(), + resources: HashMap::new(), + resource_templates: HashMap::new(), + auth_statuses: HashMap::new(), + }; + } + + let snapshot = + collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; + + cancel_token.cancel(); + + snapshot +} + +pub async fn read_mcp_resource(config: &Config, server: &str, uri: &str) -> anyhow::Result { + let (mcp_connection_manager, cancel_token, _auth_status_entries) = + new_mcp_connection_manager(config).await; + + let result = mcp_connection_manager + .read_resource( + server, + ReadResourceRequestParams { + meta: None, + uri: uri.to_string(), + }, + ) + .await; + + cancel_token.cancel(); + + Ok(serde_json::to_value(result?)?) +} + +async fn new_mcp_connection_manager( + config: &Config, +) -> ( + McpConnectionManager, + CancellationToken, + HashMap, +) { let auth_manager = AuthManager::shared( config.codex_home.clone(), /*enable_codex_api_key_env*/ false, @@ -261,12 +311,13 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent let mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()); let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config); if mcp_servers.is_empty() { - return McpListToolsResponseEvent { - tools: HashMap::new(), - resources: HashMap::new(), - resource_templates: HashMap::new(), - auth_statuses: HashMap::new(), - }; + let cancel_token = CancellationToken::new(); + cancel_token.cancel(); + return ( + McpConnectionManager::new_uninitialized(&config.permissions.approval_policy), + cancel_token, + HashMap::new(), + ); } let auth_status_entries = @@ -275,7 +326,6 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent let (tx_event, rx_event) = unbounded(); drop(rx_event); - // Use ReadOnly sandbox policy for MCP snapshot collection (safest default) let sandbox_state = SandboxState { sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), @@ -296,12 +346,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent ) .await; - let snapshot = - collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; - - cancel_token.cancel(); - - snapshot + (mcp_connection_manager, cancel_token, auth_status_entries) } pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String)> { @@ -333,6 +378,21 @@ pub fn group_tools_by_server( grouped } +fn get_mcp_snapshot_tool_name(tool: &ToolInfo) -> String { + if tool.server_name != CODEX_APPS_MCP_SERVER_NAME { + format!( + "{}{}{}{}{}", + MCP_TOOL_NAME_PREFIX, + MCP_TOOL_NAME_DELIMITER, + tool.server_name, + MCP_TOOL_NAME_DELIMITER, + tool.tool_name + ) + } else { + format!("{}{}", tool.tool_namespace, tool.tool_name) + } +} + pub(crate) async fn collect_mcp_snapshot_from_manager( mcp_connection_manager: &McpConnectionManager, auth_status_entries: HashMap, @@ -350,16 +410,22 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( let tools = tools .into_iter() - .filter_map(|(name, tool)| match serde_json::to_value(tool.tool) { + .filter_map(|(_name, tool)| match serde_json::to_value(&tool.tool) { Ok(value) => match Tool::from_mcp_value(value) { - Ok(tool) => Some((name, tool)), + Ok(tool_value) => Some((get_mcp_snapshot_tool_name(&tool), tool_value)), Err(err) => { - tracing::warn!("Failed to convert MCP tool '{name}': {err}"); + tracing::warn!( + "Failed to convert MCP tool '{}': {err}", + get_mcp_snapshot_tool_name(&tool) + ); None } }, Err(err) => { - tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); + tracing::warn!( + "Failed to serialize MCP tool '{}': {err}", + get_mcp_snapshot_tool_name(&tool) + ); None } }) diff --git a/codex-rs/core/src/mcp/mod_tests.rs b/codex-rs/core/src/mcp/mod_tests.rs index 855e71e1f20c..1de57a7c7487 100644 --- a/codex-rs/core/src/mcp/mod_tests.rs +++ b/codex-rs/core/src/mcp/mod_tests.rs @@ -50,6 +50,10 @@ fn split_qualified_tool_name_returns_server_and_tool() { split_qualified_tool_name("mcp__alpha__do_thing"), Some(("alpha".to_string(), "do_thing".to_string())) ); + assert_eq!( + split_qualified_tool_name("mcp__basic-react__get-time"), + Some(("basic-react".to_string(), "get-time".to_string())) + ); } #[test] From 1c7bf9d35a5efdd91e7e7d6323344ecbe92e73a0 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 31 Mar 2026 23:17:03 -0400 Subject: [PATCH 3/6] update --- .../schema/json/ClientRequest.json | 4 + .../codex_app_server_protocol.schemas.json | 4 + .../codex_app_server_protocol.v2.schemas.json | 4 + .../schema/json/v2/McpResourceReadParams.json | 4 + .../typescript/v2/McpResourceReadParams.ts | 2 +- .../app-server-protocol/src/protocol/v2.rs | 1 + codex-rs/app-server/README.md | 2 +- .../app-server/src/codex_message_processor.rs | 7 +- codex-rs/core/src/codex_thread.rs | 21 ++++ codex-rs/core/src/mcp/mod.rs | 100 +++--------------- 10 files changed, 60 insertions(+), 89 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f5b929696b64..571498de0b69 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1213,12 +1213,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 73969a0cec85..7db434e5b663 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -8785,12 +8785,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "title": "McpResourceReadParams", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index e6b458373276..7f453e3b4338 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -5599,12 +5599,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "title": "McpResourceReadParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json index 7ea633b58678..0242d4148bcf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json @@ -4,12 +4,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "title": "McpResourceReadParams", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts index c58bf8499a2c..51d650d9bb0b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type McpResourceReadParams = { server: string, uri: string, }; +export type McpResourceReadParams = { threadId: string, server: string, uri: string, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 0f7c826cc4ae..d3cc159104e5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1972,6 +1972,7 @@ pub struct ListMcpServerStatusResponse { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct McpResourceReadParams { + pub thread_id: String, pub server: String, pub uri: String, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index f8121553c002..ce766705ad29 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -187,7 +187,7 @@ Example with notification opt-out: - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination. -- `mcpResource/read` — read a resource from a configured MCP server by `server` and `uri`, returning the resource `contents`. +- `mcpResource/read` — read a resource from a thread's configured MCP server by `threadId`, `server`, and `uri`, returning the resource `contents`. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `config/read` — fetch the effective config on disk after resolving config layering. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d74ad32ca13b..4bebe8a1bed4 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -224,7 +224,6 @@ use codex_core::mcp::auth::discover_supported_scopes; use codex_core::mcp::auth::resolve_oauth_scopes; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; -use codex_core::mcp::read_mcp_resource; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::plugins::MarketplaceError; @@ -5141,8 +5140,8 @@ impl CodexMessageProcessor { params: McpResourceReadParams, ) { let outgoing = Arc::clone(&self.outgoing); - let config = match self.load_latest_config(/*fallback_cwd*/ None).await { - Ok(config) => config, + let (_, thread) = match self.load_thread(¶ms.thread_id).await { + Ok(thread) => thread, Err(error) => { self.outgoing.send_error(request_id, error).await; return; @@ -5150,7 +5149,7 @@ impl CodexMessageProcessor { }; tokio::spawn(async move { - let result = read_mcp_resource(&config, ¶ms.server, ¶ms.uri).await; + let result = thread.read_mcp_resource(¶ms.server, ¶ms.uri).await; match result { Ok(result) => match serde_json::from_value::(result) { Ok(response) => { diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 8b467fc10b83..5ead090f2cf8 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -22,6 +22,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::UserInput; +use rmcp::model::ReadResourceRequestParams; use std::path::PathBuf; use tokio::sync::Mutex; use tokio::sync::watch; @@ -198,6 +199,26 @@ impl CodexThread { self.codex.thread_config_snapshot().await } + pub async fn read_mcp_resource( + &self, + server: &str, + uri: &str, + ) -> anyhow::Result { + let result = self + .codex + .session + .read_resource( + server, + ReadResourceRequestParams { + meta: None, + uri: uri.to_string(), + }, + ) + .await?; + + Ok(serde_json::to_value(result)?) + } + pub fn enabled(&self, feature: Feature) -> bool { self.codex.enabled(feature) } diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index c05c2fd853db..afeb5647ea94 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -14,20 +14,16 @@ use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; -use rmcp::model::ReadResourceRequestParams; use serde_json::Value; -use tokio_util::sync::CancellationToken; use crate::AuthManager; use crate::CodexAuth; use crate::config::Config; use crate::config::types::McpServerConfig; use crate::config::types::McpServerTransportConfig; -use crate::mcp::auth::McpAuthStatusEntry; use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::SandboxState; -use crate::mcp_connection_manager::ToolInfo; use crate::mcp_connection_manager::codex_apps_tools_cache_key; use crate::plugins::PluginCapabilitySummary; use crate::plugins::PluginsManager; @@ -281,52 +277,6 @@ fn effective_mcp_servers( } pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent { - let (mcp_connection_manager, cancel_token, auth_status_entries) = - new_mcp_connection_manager(config).await; - if !mcp_connection_manager.has_servers() { - cancel_token.cancel(); - return McpListToolsResponseEvent { - tools: HashMap::new(), - resources: HashMap::new(), - resource_templates: HashMap::new(), - auth_statuses: HashMap::new(), - }; - } - - let snapshot = - collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; - - cancel_token.cancel(); - - snapshot -} - -pub async fn read_mcp_resource(config: &Config, server: &str, uri: &str) -> anyhow::Result { - let (mcp_connection_manager, cancel_token, _auth_status_entries) = - new_mcp_connection_manager(config).await; - - let result = mcp_connection_manager - .read_resource( - server, - ReadResourceRequestParams { - meta: None, - uri: uri.to_string(), - }, - ) - .await; - - cancel_token.cancel(); - - Ok(serde_json::to_value(result?)?) -} - -async fn new_mcp_connection_manager( - config: &Config, -) -> ( - McpConnectionManager, - CancellationToken, - HashMap, -) { let auth_manager = AuthManager::shared( config.codex_home.clone(), /*enable_codex_api_key_env*/ false, @@ -337,13 +287,12 @@ async fn new_mcp_connection_manager( let mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()); let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config); if mcp_servers.is_empty() { - let cancel_token = CancellationToken::new(); - cancel_token.cancel(); - return ( - McpConnectionManager::new_uninitialized(&config.permissions.approval_policy), - cancel_token, - HashMap::new(), - ); + return McpListToolsResponseEvent { + tools: HashMap::new(), + resources: HashMap::new(), + resource_templates: HashMap::new(), + auth_statuses: HashMap::new(), + }; } let auth_status_entries = @@ -352,6 +301,7 @@ async fn new_mcp_connection_manager( let (tx_event, rx_event) = unbounded(); drop(rx_event); + // Use ReadOnly sandbox policy for MCP snapshot collection (safest default) let sandbox_state = SandboxState { sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), @@ -372,7 +322,12 @@ async fn new_mcp_connection_manager( ) .await; - (mcp_connection_manager, cancel_token, auth_status_entries) + let snapshot = + collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; + + cancel_token.cancel(); + + snapshot } pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String)> { @@ -404,21 +359,6 @@ pub fn group_tools_by_server( grouped } -fn get_mcp_snapshot_tool_name(tool: &ToolInfo) -> String { - if tool.server_name != CODEX_APPS_MCP_SERVER_NAME { - format!( - "{}{}{}{}{}", - MCP_TOOL_NAME_PREFIX, - MCP_TOOL_NAME_DELIMITER, - tool.server_name, - MCP_TOOL_NAME_DELIMITER, - tool.tool_name - ) - } else { - format!("{}{}", tool.tool_namespace, tool.tool_name) - } -} - pub(crate) async fn collect_mcp_snapshot_from_manager( mcp_connection_manager: &McpConnectionManager, auth_status_entries: HashMap, @@ -436,22 +376,16 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( let tools = tools .into_iter() - .filter_map(|(_name, tool)| match serde_json::to_value(&tool.tool) { + .filter_map(|(name, tool)| match serde_json::to_value(tool.tool) { Ok(value) => match Tool::from_mcp_value(value) { - Ok(tool_value) => Some((get_mcp_snapshot_tool_name(&tool), tool_value)), + Ok(tool) => Some((name, tool)), Err(err) => { - tracing::warn!( - "Failed to convert MCP tool '{}': {err}", - get_mcp_snapshot_tool_name(&tool) - ); + tracing::warn!("Failed to convert MCP tool '{name}': {err}"); None } }, Err(err) => { - tracing::warn!( - "Failed to serialize MCP tool '{}': {err}", - get_mcp_snapshot_tool_name(&tool) - ); + tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); None } }) From 41052984fd37c1ab72c7d0433099eacf224610d0 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 31 Mar 2026 23:27:16 -0400 Subject: [PATCH 4/6] update --- codex-rs/core/src/mcp/mod_tests.rs | 4 ---- codex-rs/exec/tests/event_processor_with_json_output.rs | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/mcp/mod_tests.rs b/codex-rs/core/src/mcp/mod_tests.rs index 4afc020b5dbc..6821fbce3223 100644 --- a/codex-rs/core/src/mcp/mod_tests.rs +++ b/codex-rs/core/src/mcp/mod_tests.rs @@ -50,10 +50,6 @@ fn split_qualified_tool_name_returns_server_and_tool() { split_qualified_tool_name("mcp__alpha__do_thing"), Some(("alpha".to_string(), "do_thing".to_string())) ); - assert_eq!( - split_qualified_tool_name("mcp__basic-react__get-time"), - Some(("basic-react".to_string(), "get-time".to_string())) - ); } #[test] diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 5491e895e2e4..3d5d73b43c82 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -479,6 +479,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { result: Some(McpToolCallResult { content: Vec::new(), structured_content: None, + meta: None, }), error: None, duration_ms: Some(1_000), @@ -610,6 +611,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { "text": "done", })], structured_content: Some(json!({ "status": "ok" })), + meta: None, }), error: None, duration_ms: Some(10), From 86f6eb8e450ed0323cfd9b0aaf73dd1ea3e1f344 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 1 Apr 2026 15:17:10 -0400 Subject: [PATCH 5/6] update --- .../app-server/src/codex_message_processor.rs | 9 +- codex-rs/core/src/mcp/mod.rs | 145 ++++++++++++------ codex-rs/core/src/mcp/mod_tests.rs | 82 +++++----- 3 files changed, 140 insertions(+), 96 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 4bebe8a1bed4..6a3a7ce18d8f 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -222,8 +222,7 @@ use codex_core::find_thread_names_by_ids; use codex_core::find_thread_path_by_id_str; use codex_core::mcp::auth::discover_supported_scopes; use codex_core::mcp::auth::resolve_oauth_scopes; -use codex_core::mcp::collect_mcp_snapshot; -use codex_core::mcp::group_tools_by_server; +use codex_core::mcp::collect_mcp_server_status_snapshot; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::plugins::MarketplaceError; @@ -5057,9 +5056,7 @@ impl CodexMessageProcessor { params: ListMcpServerStatusParams, config: Config, ) { - let snapshot = collect_mcp_snapshot(&config).await; - - let tools_by_server = group_tools_by_server(&snapshot.tools); + let snapshot = collect_mcp_server_status_snapshot(&config).await; let mut server_names: Vec = config .mcp_servers @@ -5107,7 +5104,7 @@ impl CodexMessageProcessor { .iter() .map(|name| McpServerStatus { name: name.clone(), - tools: tools_by_server.get(name).cloned().unwrap_or_default(), + tools: snapshot.tools_by_server.get(name).cloned().unwrap_or_default(), resources: snapshot.resources.get(name).cloned().unwrap_or_default(), resource_templates: snapshot .resource_templates diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index afeb5647ea94..d86653383807 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -12,6 +12,7 @@ use async_channel::unbounded; use codex_protocol::mcp::Resource; use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; +use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; use serde_json::Value; @@ -24,6 +25,7 @@ use crate::config::types::McpServerTransportConfig; use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::SandboxState; +use crate::mcp_connection_manager::ToolInfo; use crate::mcp_connection_manager::codex_apps_tools_cache_key; use crate::plugins::PluginCapabilitySummary; use crate::plugins::PluginsManager; @@ -250,6 +252,13 @@ impl McpManager { } } +pub struct McpServerStatusSnapshot { + pub tools_by_server: HashMap>, + pub resources: HashMap>, + pub resource_templates: HashMap>, + pub auth_statuses: HashMap, +} + fn configured_mcp_servers( config: &Config, plugins_manager: &PluginsManager, @@ -276,7 +285,7 @@ fn effective_mcp_servers( ) } -pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent { +pub async fn collect_mcp_server_status_snapshot(config: &Config) -> McpServerStatusSnapshot { let auth_manager = AuthManager::shared( config.codex_home.clone(), /*enable_codex_api_key_env*/ false, @@ -287,8 +296,8 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent let mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()); let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config); if mcp_servers.is_empty() { - return McpListToolsResponseEvent { - tools: HashMap::new(), + return McpServerStatusSnapshot { + tools_by_server: HashMap::new(), resources: HashMap::new(), resource_templates: HashMap::new(), auth_statuses: HashMap::new(), @@ -322,41 +331,48 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent ) .await; - let snapshot = - collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; + let snapshot = collect_mcp_server_status_snapshot_from_manager( + &mcp_connection_manager, + auth_status_entries, + ) + .await; cancel_token.cancel(); snapshot } -pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String)> { - let mut parts = qualified_name.split(MCP_TOOL_NAME_DELIMITER); - let prefix = parts.next()?; - if prefix != MCP_TOOL_NAME_PREFIX { - return None; - } - let server_name = parts.next()?; - let tool_name: String = parts.collect::>().join(MCP_TOOL_NAME_DELIMITER); - if tool_name.is_empty() { - return None; - } - Some((server_name.to_string(), tool_name)) -} +async fn collect_mcp_server_status_snapshot_from_manager( + mcp_connection_manager: &McpConnectionManager, + auth_status_entries: HashMap, +) -> McpServerStatusSnapshot { + let (tools, resources, resource_templates) = tokio::join!( + mcp_connection_manager.list_all_tools(), + mcp_connection_manager.list_all_resources(), + mcp_connection_manager.list_all_resource_templates(), + ); -pub fn group_tools_by_server( - tools: &HashMap, -) -> HashMap> { - let mut grouped = HashMap::new(); - for (qualified_name, tool) in tools { - if let Some((server_name, tool_name)) = split_qualified_tool_name(qualified_name) { - grouped + let mut tools_by_server = HashMap::new(); + for tool_info in tools.into_values() { + let tool_name = mcp_server_status_tool_name(&tool_info); + let server_name = tool_info.server_name.clone(); + if let Some(tool) = convert_mcp_tool(tool_info.tool) { + tools_by_server .entry(server_name) .or_insert_with(HashMap::new) - .insert(tool_name, tool.clone()); + .insert(tool_name, tool); } } - grouped + + McpServerStatusSnapshot { + tools_by_server, + resources: convert_mcp_resources(resources), + resource_templates: convert_mcp_resource_templates(resource_templates), + auth_statuses: auth_status_entries + .iter() + .map(|(name, entry)| (name.clone(), entry.auth_status)) + .collect(), + } } pub(crate) async fn collect_mcp_snapshot_from_manager( @@ -376,22 +392,54 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( let tools = tools .into_iter() - .filter_map(|(name, tool)| match serde_json::to_value(tool.tool) { - Ok(value) => match Tool::from_mcp_value(value) { - Ok(tool) => Some((name, tool)), - Err(err) => { - tracing::warn!("Failed to convert MCP tool '{name}': {err}"); - None - } - }, - Err(err) => { - tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); - None - } + .filter_map(|(name, tool_info)| { + convert_mcp_tool(tool_info.tool).map(|tool| (name, tool)) }) .collect(); - let resources = resources + McpListToolsResponseEvent { + tools, + resources: convert_mcp_resources(resources), + resource_templates: convert_mcp_resource_templates(resource_templates), + auth_statuses, + } +} + +fn mcp_server_status_tool_name(tool: &ToolInfo) -> String { + if tool.server_name != CODEX_APPS_MCP_SERVER_NAME { + return tool.tool_name.clone(); + } + + let codex_apps_prefix = qualified_mcp_tool_name_prefix(CODEX_APPS_MCP_SERVER_NAME); + if let Some(connector_namespace) = tool.tool_namespace.strip_prefix(&codex_apps_prefix) { + return format!("{}{}", connector_namespace, tool.tool_name); + } + + tool.tool_name.clone() +} + +fn convert_mcp_tool(tool: rmcp::model::Tool) -> Option { + let value = match serde_json::to_value(tool) { + Ok(value) => value, + Err(err) => { + tracing::warn!("Failed to serialize MCP tool: {err}"); + return None; + } + }; + + match Tool::from_mcp_value(value) { + Ok(tool) => Some(tool), + Err(err) => { + tracing::warn!("Failed to convert MCP tool: {err}"); + None + } + } +} + +fn convert_mcp_resources( + resources: HashMap>, +) -> HashMap> { + resources .into_iter() .map(|(name, resources)| { let resources = resources @@ -424,9 +472,13 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( .collect::>(); (name, resources) }) - .collect(); + .collect() +} - let resource_templates = resource_templates +fn convert_mcp_resource_templates( + resource_templates: HashMap>, +) -> HashMap> { + resource_templates .into_iter() .map(|(name, templates)| { let templates = templates @@ -460,14 +512,7 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( .collect::>(); (name, templates) }) - .collect(); - - McpListToolsResponseEvent { - tools, - resources, - resource_templates, - auth_statuses, - } + .collect() } #[cfg(test)] diff --git a/codex-rs/core/src/mcp/mod_tests.rs b/codex-rs/core/src/mcp/mod_tests.rs index 6821fbce3223..667205b599ce 100644 --- a/codex-rs/core/src/mcp/mod_tests.rs +++ b/codex-rs/core/src/mcp/mod_tests.rs @@ -5,8 +5,10 @@ use crate::plugins::AppConnectorId; use crate::plugins::PluginCapabilitySummary; use codex_features::Feature; use pretty_assertions::assert_eq; +use rmcp::model::JsonObject; use std::fs; use std::path::Path; +use std::sync::Arc; use toml::Value; fn write_file(path: &Path, contents: &str) { @@ -31,27 +33,29 @@ fn plugin_config_toml() -> String { toml::to_string(&Value::Table(root)).expect("plugin test config should serialize") } -fn make_tool(name: &str) -> Tool { - Tool { - name: name.to_string(), - title: None, - description: None, - input_schema: serde_json::json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - icons: None, - meta: None, +fn make_tool_info(server_name: &str, tool_name: &str, tool_namespace: &str) -> ToolInfo { + ToolInfo { + server_name: server_name.to_string(), + tool_name: tool_name.to_string(), + tool_namespace: tool_namespace.to_string(), + tool: rmcp::model::Tool { + name: tool_name.to_string().into(), + title: None, + description: None, + input_schema: Arc::new(JsonObject::default()), + output_schema: None, + annotations: None, + execution: None, + icons: None, + meta: None, + }, + connector_id: None, + connector_name: None, + plugin_display_names: Vec::new(), + connector_description: None, } } -#[test] -fn split_qualified_tool_name_returns_server_and_tool() { - assert_eq!( - split_qualified_tool_name("mcp__alpha__do_thing"), - Some(("alpha".to_string(), "do_thing".to_string())) - ); -} - #[test] fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() { assert_eq!( @@ -61,33 +65,31 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() { } #[test] -fn split_qualified_tool_name_rejects_invalid_names() { - assert_eq!(split_qualified_tool_name("other__alpha__do_thing"), None); - assert_eq!(split_qualified_tool_name("mcp__alpha__"), None); +fn mcp_server_status_tool_name_preserves_hyphenated_mcp_tool_names() { + let tool_info = make_tool_info( + "music-studio", + "play-live-pattern", + "music-studio", + ); + + assert_eq!( + mcp_server_status_tool_name(&tool_info), + "play-live-pattern".to_string() + ); } #[test] -fn group_tools_by_server_strips_prefix_and_groups() { - let mut tools = HashMap::new(); - tools.insert("mcp__alpha__do_thing".to_string(), make_tool("do_thing")); - tools.insert( - "mcp__alpha__nested__op".to_string(), - make_tool("nested__op"), +fn mcp_server_status_tool_name_includes_codex_apps_connector_namespace() { + let tool_info = make_tool_info( + CODEX_APPS_MCP_SERVER_NAME, + "_property_search", + "mcp__codex_apps__zillow", ); - tools.insert("mcp__beta__do_other".to_string(), make_tool("do_other")); - - let mut expected_alpha = HashMap::new(); - expected_alpha.insert("do_thing".to_string(), make_tool("do_thing")); - expected_alpha.insert("nested__op".to_string(), make_tool("nested__op")); - - let mut expected_beta = HashMap::new(); - expected_beta.insert("do_other".to_string(), make_tool("do_other")); - let mut expected = HashMap::new(); - expected.insert("alpha".to_string(), expected_alpha); - expected.insert("beta".to_string(), expected_beta); - - assert_eq!(group_tools_by_server(&tools), expected); + assert_eq!( + mcp_server_status_tool_name(&tool_info), + "zillow_property_search".to_string() + ); } #[test] From 76d3fb4329f37cc5b34c6d9542123dfa8d85d383 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Mon, 6 Apr 2026 22:34:07 -0700 Subject: [PATCH 6/6] update --- .../app-server/src/codex_message_processor.rs | 52 ++++- codex-rs/codex-mcp/src/mcp/mod.rs | 209 ++++-------------- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 78 +++---- 3 files changed, 128 insertions(+), 211 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index cf3d5290b661..91641898d135 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -251,8 +251,9 @@ use codex_login::run_login_server; use codex_mcp::mcp::McpSnapshotDetail; use codex_mcp::mcp::auth::discover_supported_scopes; use codex_mcp::mcp::auth::resolve_oauth_scopes; -use codex_mcp::mcp::collect_mcp_server_status_snapshot_with_detail; +use codex_mcp::mcp::collect_mcp_snapshot_with_detail; use codex_mcp::mcp::effective_mcp_servers; +use codex_mcp::mcp::qualified_mcp_tool_name_prefix; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; @@ -5182,14 +5183,55 @@ impl CodexMessageProcessor { McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly, }; - let snapshot = collect_mcp_server_status_snapshot_with_detail( + let snapshot = collect_mcp_snapshot_with_detail( &mcp_config, auth.as_ref(), request_id.request_id.to_string(), detail, ) .await; + + // Rebuild the tool list per original server name instead of using + // `group_tools_by_server()`: qualified tool names are sanitized for the + // Responses API, so a config key like `some-server` is encoded as the + // `mcp__some_server__` prefix. Matching with the original server name's + // sanitized prefix preserves `/mcp` output for hyphenated names. let effective_servers = effective_mcp_servers(&mcp_config, auth.as_ref()); + let mut sanitized_prefix_counts = HashMap::::new(); + for name in effective_servers.keys() { + let prefix = qualified_mcp_tool_name_prefix(name); + *sanitized_prefix_counts.entry(prefix).or_default() += 1; + } + let tools_by_server = effective_servers + .keys() + .map(|name| { + let prefix = qualified_mcp_tool_name_prefix(name); + // If multiple server names normalize to the same prefix, the + // qualified tool namespace is ambiguous (for example + // `some-server` and `some_server` both become + // `mcp__some_server__`). In that case, avoid attributing the + // same tools to multiple servers. + let tools = if sanitized_prefix_counts + .get(&prefix) + .copied() + .unwrap_or_default() + == 1 + { + snapshot + .tools + .iter() + .filter_map(|(qualified_name, tool)| { + qualified_name + .strip_prefix(&prefix) + .map(|tool_name| (tool_name.to_string(), tool.clone())) + }) + .collect::>() + } else { + HashMap::new() + }; + (name.clone(), tools) + }) + .collect::>(); let mut server_names: Vec = config .mcp_servers @@ -5241,11 +5283,7 @@ impl CodexMessageProcessor { .iter() .map(|name| McpServerStatus { name: name.clone(), - tools: snapshot - .tools_by_server - .get(name) - .cloned() - .unwrap_or_default(), + tools: tools_by_server.get(name).cloned().unwrap_or_default(), resources: snapshot.resources.get(name).cloned().unwrap_or_default(), resource_templates: snapshot .resource_templates diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index f4bc3af0987f..faa8ecc2fc23 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -19,7 +19,6 @@ use codex_protocol::mcp::Resource; use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; use serde_json::Value; @@ -27,7 +26,6 @@ use serde_json::Value; use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::SandboxState; -use crate::mcp_connection_manager::ToolInfo; use crate::mcp_connection_manager::codex_apps_tools_cache_key; pub type McpManager = McpConnectionManager; @@ -294,14 +292,6 @@ pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance { ToolPluginProvenance::from_capability_summaries(&config.plugin_capability_summaries) } -#[derive(Debug, Clone, Default)] -pub struct McpServerStatusSnapshot { - pub tools_by_server: HashMap>, - pub resources: HashMap>, - pub resource_templates: HashMap>, - pub auth_statuses: HashMap, -} - pub async fn collect_mcp_snapshot( config: &McpConfig, auth: Option<&CodexAuth>, @@ -367,119 +357,33 @@ pub async fn collect_mcp_snapshot_with_detail( snapshot } -pub async fn collect_mcp_server_status_snapshot_with_detail( - config: &McpConfig, - auth: Option<&CodexAuth>, - submit_id: String, - detail: McpSnapshotDetail, -) -> McpServerStatusSnapshot { - let mcp_servers = effective_mcp_servers(config, auth); - let tool_plugin_provenance = tool_plugin_provenance(config); - if mcp_servers.is_empty() { - return McpServerStatusSnapshot::default(); +pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String)> { + let mut parts = qualified_name.split(MCP_TOOL_NAME_DELIMITER); + let prefix = parts.next()?; + if prefix != MCP_TOOL_NAME_PREFIX { + return None; } - - let mut sanitized_prefix_counts = HashMap::::new(); - for name in mcp_servers.keys() { - let prefix = qualified_mcp_tool_name_prefix(name); - *sanitized_prefix_counts.entry(prefix).or_default() += 1; + let server_name = parts.next()?; + let tool_name: String = parts.collect::>().join(MCP_TOOL_NAME_DELIMITER); + if tool_name.is_empty() { + return None; } - - let auth_status_entries = - compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await; - - let (tx_event, rx_event) = unbounded(); - drop(rx_event); - - let sandbox_state = SandboxState { - sandbox_policy: SandboxPolicy::new_read_only_policy(), - codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), - sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), - use_legacy_landlock: config.use_legacy_landlock, - }; - - let (mcp_connection_manager, cancel_token) = McpConnectionManager::new( - &mcp_servers, - config.mcp_oauth_credentials_store_mode, - auth_status_entries.clone(), - &config.approval_policy, - submit_id, - tx_event, - sandbox_state, - config.codex_home.clone(), - codex_apps_tools_cache_key(auth), - tool_plugin_provenance, - ) - .await; - - let snapshot = collect_mcp_server_status_snapshot_from_manager_with_detail( - &mcp_connection_manager, - auth_status_entries, - detail, - &sanitized_prefix_counts, - ) - .await; - - cancel_token.cancel(); - - snapshot + Some((server_name.to_string(), tool_name)) } -async fn collect_mcp_server_status_snapshot_from_manager_with_detail( - mcp_connection_manager: &McpConnectionManager, - auth_status_entries: HashMap, - detail: McpSnapshotDetail, - sanitized_prefix_counts: &HashMap, -) -> McpServerStatusSnapshot { - let (tools, resources, resource_templates) = tokio::join!( - mcp_connection_manager.list_all_tools(), - async { - if detail.include_resources() { - mcp_connection_manager.list_all_resources().await - } else { - HashMap::new() - } - }, - async { - if detail.include_resources() { - mcp_connection_manager.list_all_resource_templates().await - } else { - HashMap::new() - } - }, - ); - - let mut tools_by_server = HashMap::new(); - for tool_info in tools.into_values() { - let prefix = qualified_mcp_tool_name_prefix(&tool_info.server_name); - if sanitized_prefix_counts - .get(&prefix) - .copied() - .unwrap_or_default() - != 1 - { - continue; - } - - let tool_name = mcp_server_status_tool_name(&tool_info); - let server_name = tool_info.server_name.clone(); - if let Some(tool) = convert_mcp_tool(tool_info.tool) { - tools_by_server +pub fn group_tools_by_server( + tools: &HashMap, +) -> HashMap> { + let mut grouped = HashMap::new(); + for (qualified_name, tool) in tools { + if let Some((server_name, tool_name)) = split_qualified_tool_name(qualified_name) { + grouped .entry(server_name) .or_insert_with(HashMap::new) - .insert(tool_name, tool); + .insert(tool_name, tool.clone()); } } - - McpServerStatusSnapshot { - tools_by_server, - resources: convert_mcp_resources(resources), - resource_templates: convert_mcp_resource_templates(resource_templates), - auth_statuses: auth_status_entries - .iter() - .map(|(name, entry)| (name.clone(), entry.auth_status)) - .collect(), - } + grouped } pub async fn collect_mcp_snapshot_from_manager( @@ -524,52 +428,22 @@ pub async fn collect_mcp_snapshot_from_manager_with_detail( let tools = tools .into_iter() - .filter_map(|(name, tool_info)| convert_mcp_tool(tool_info.tool).map(|tool| (name, tool))) + .filter_map(|(name, tool)| match serde_json::to_value(tool.tool) { + Ok(value) => match Tool::from_mcp_value(value) { + Ok(tool) => Some((name, tool)), + Err(err) => { + tracing::warn!("Failed to convert MCP tool '{name}': {err}"); + None + } + }, + Err(err) => { + tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); + None + } + }) .collect::>(); - McpListToolsResponseEvent { - tools, - resources: convert_mcp_resources(resources), - resource_templates: convert_mcp_resource_templates(resource_templates), - auth_statuses, - } -} - -fn mcp_server_status_tool_name(tool: &ToolInfo) -> String { - if tool.server_name != CODEX_APPS_MCP_SERVER_NAME { - return tool.tool_name.clone(); - } - - let codex_apps_prefix = qualified_mcp_tool_name_prefix(CODEX_APPS_MCP_SERVER_NAME); - if let Some(connector_namespace) = tool.tool_namespace.strip_prefix(&codex_apps_prefix) { - return format!("{}{}", connector_namespace, tool.tool_name); - } - - tool.tool_name.clone() -} - -fn convert_mcp_tool(tool: rmcp::model::Tool) -> Option { - let value = match serde_json::to_value(tool) { - Ok(value) => value, - Err(err) => { - tracing::warn!("Failed to serialize MCP tool: {err}"); - return None; - } - }; - - match Tool::from_mcp_value(value) { - Ok(tool) => Some(tool), - Err(err) => { - tracing::warn!("Failed to convert MCP tool: {err}"); - None - } - } -} - -fn convert_mcp_resources( - resources: HashMap>, -) -> HashMap> { - resources + let resources = resources .into_iter() .map(|(name, resources)| { let resources = resources @@ -602,13 +476,9 @@ fn convert_mcp_resources( .collect::>(); (name, resources) }) - .collect() -} + .collect::>(); -fn convert_mcp_resource_templates( - resource_templates: HashMap>, -) -> HashMap> { - resource_templates + let resource_templates = resource_templates .into_iter() .map(|(name, templates)| { let templates = templates @@ -642,7 +512,14 @@ fn convert_mcp_resource_templates( .collect::>(); (name, templates) }) - .collect() + .collect::>(); + + McpListToolsResponseEvent { + tools, + resources, + resource_templates, + auth_statuses, + } } #[cfg(test)] diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index ca1791c14ab5..8dc29bbf98f1 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -5,10 +5,8 @@ use codex_plugin::AppConnectorId; use codex_plugin::PluginCapabilitySummary; use codex_protocol::protocol::AskForApproval; use pretty_assertions::assert_eq; -use rmcp::model::JsonObject; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::Arc; fn test_mcp_config(codex_home: PathBuf) -> McpConfig { McpConfig { @@ -27,29 +25,27 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig { } } -fn make_tool_info(server_name: &str, tool_name: &str, tool_namespace: &str) -> ToolInfo { - ToolInfo { - server_name: server_name.to_string(), - tool_name: tool_name.to_string(), - tool_namespace: tool_namespace.to_string(), - tool: rmcp::model::Tool { - name: tool_name.to_string().into(), - title: None, - description: None, - input_schema: Arc::new(JsonObject::default()), - output_schema: None, - annotations: None, - execution: None, - icons: None, - meta: None, - }, - connector_id: None, - connector_name: None, - plugin_display_names: Vec::new(), - connector_description: None, +fn make_tool(name: &str) -> Tool { + Tool { + name: name.to_string(), + title: None, + description: None, + input_schema: serde_json::json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + icons: None, + meta: None, } } +#[test] +fn split_qualified_tool_name_returns_server_and_tool() { + assert_eq!( + split_qualified_tool_name("mcp__alpha__do_thing"), + Some(("alpha".to_string(), "do_thing".to_string())) + ); +} + #[test] fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() { assert_eq!( @@ -59,27 +55,33 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() { } #[test] -fn mcp_server_status_tool_name_preserves_hyphenated_mcp_tool_names() { - let tool_info = make_tool_info("music-studio", "play-live-pattern", "music-studio"); - - assert_eq!( - mcp_server_status_tool_name(&tool_info), - "play-live-pattern".to_string() - ); +fn split_qualified_tool_name_rejects_invalid_names() { + assert_eq!(split_qualified_tool_name("other__alpha__do_thing"), None); + assert_eq!(split_qualified_tool_name("mcp__alpha__"), None); } #[test] -fn mcp_server_status_tool_name_includes_codex_apps_connector_namespace() { - let tool_info = make_tool_info( - CODEX_APPS_MCP_SERVER_NAME, - "_property_search", - "mcp__codex_apps__zillow", +fn group_tools_by_server_strips_prefix_and_groups() { + let mut tools = HashMap::new(); + tools.insert("mcp__alpha__do_thing".to_string(), make_tool("do_thing")); + tools.insert( + "mcp__alpha__nested__op".to_string(), + make_tool("nested__op"), ); + tools.insert("mcp__beta__do_other".to_string(), make_tool("do_other")); - assert_eq!( - mcp_server_status_tool_name(&tool_info), - "zillow_property_search".to_string() - ); + let mut expected_alpha = HashMap::new(); + expected_alpha.insert("do_thing".to_string(), make_tool("do_thing")); + expected_alpha.insert("nested__op".to_string(), make_tool("nested__op")); + + let mut expected_beta = HashMap::new(); + expected_beta.insert("do_other".to_string(), make_tool("do_other")); + + let mut expected = HashMap::new(); + expected.insert("alpha".to_string(), expected_alpha); + expected.insert("beta".to_string(), expected_beta); + + assert_eq!(group_tools_by_server(&tools), expected); } #[test]