From dbea2f91adfb1af94d17a3b0e592210592256289 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Fri, 10 Apr 2026 12:10:17 -0700 Subject: [PATCH] update --- .../schema/json/ClientRequest.json | 45 ++++ .../codex_app_server_protocol.schemas.json | 69 +++++ .../codex_app_server_protocol.v2.schemas.json | 69 +++++ .../json/v2/McpServerToolCallParams.json | 23 ++ .../json/v2/McpServerToolCallResponse.json | 22 ++ .../schema/typescript/ClientRequest.ts | 3 +- .../typescript/v2/McpServerToolCallParams.ts | 6 + .../v2/McpServerToolCallResponse.ts | 6 + .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 5 + .../app-server-protocol/src/protocol/v2.rs | 43 ++++ codex-rs/app-server/README.md | 1 + .../app-server/src/codex_message_processor.rs | 46 ++++ .../app-server/tests/common/mcp_process.rs | 10 + .../app-server/tests/suite/v2/mcp_tool.rs | 243 ++++++++++++++++++ codex-rs/app-server/tests/suite/v2/mod.rs | 1 + codex-rs/core/src/codex_thread.rs | 14 + 17 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallResponse.ts create mode 100644 codex-rs/app-server/tests/suite/v2/mcp_tool.rs diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 3372758cd002..b62895d43c2f 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1277,6 +1277,27 @@ ], "type": "string" }, + "McpServerToolCallParams": { + "properties": { + "_meta": true, + "arguments": true, + "server": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + } + }, + "required": [ + "server", + "threadId", + "tool" + ], + "type": "object" + }, "MergeStrategy": { "enum": [ "replace", @@ -4587,6 +4608,30 @@ "title": "McpServer/resource/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServer/tool/call" + ], + "title": "McpServer/tool/callRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerToolCallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/tool/callRequest", + "type": "object" + }, { "properties": { "id": { 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 e4ced5070cc1..456abe7e9fdf 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 @@ -1249,6 +1249,30 @@ "title": "McpServer/resource/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "mcpServer/tool/call" + ], + "title": "McpServer/tool/callRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/McpServerToolCallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/tool/callRequest", + "type": "object" + }, { "properties": { "id": { @@ -9326,6 +9350,51 @@ "title": "McpServerStatusUpdatedNotification", "type": "object" }, + "McpServerToolCallParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "arguments": true, + "server": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + } + }, + "required": [ + "server", + "threadId", + "tool" + ], + "title": "McpServerToolCallParams", + "type": "object" + }, + "McpServerToolCallResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "isError": { + "type": [ + "boolean", + "null" + ] + }, + "structuredContent": true + }, + "required": [ + "content" + ], + "title": "McpServerToolCallResponse", + "type": "object" + }, "McpToolCallError": { "properties": { "message": { 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 d1f3cbda550c..cff2e3c47045 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 @@ -1911,6 +1911,30 @@ "title": "McpServer/resource/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServer/tool/call" + ], + "title": "McpServer/tool/callRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerToolCallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/tool/callRequest", + "type": "object" + }, { "properties": { "id": { @@ -6109,6 +6133,51 @@ "title": "McpServerStatusUpdatedNotification", "type": "object" }, + "McpServerToolCallParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "arguments": true, + "server": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + } + }, + "required": [ + "server", + "threadId", + "tool" + ], + "title": "McpServerToolCallParams", + "type": "object" + }, + "McpServerToolCallResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "isError": { + "type": [ + "boolean", + "null" + ] + }, + "structuredContent": true + }, + "required": [ + "content" + ], + "title": "McpServerToolCallResponse", + "type": "object" + }, "McpToolCallError": { "properties": { "message": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallParams.json new file mode 100644 index 000000000000..3465e60c83b0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallParams.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "arguments": true, + "server": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + } + }, + "required": [ + "server", + "threadId", + "tool" + ], + "title": "McpServerToolCallParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallResponse.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallResponse.json new file mode 100644 index 000000000000..0e5ecdf78de0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerToolCallResponse.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "isError": { + "type": [ + "boolean", + "null" + ] + }, + "structuredContent": true + }, + "required": [ + "content" + ], + "title": "McpServerToolCallResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 95432a90986c..30a53e648a05 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -35,6 +35,7 @@ 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 { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { PluginInstallParams } from "./v2/PluginInstallParams"; import type { PluginListParams } from "./v2/PluginListParams"; @@ -66,4 +67,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/addCreditsNudgeEmail", id: RequestId, params: ThreadAddCreditsNudgeEmailParams, } | { "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": "mcpServer/resource/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, }; +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/addCreditsNudgeEmail", id: RequestId, params: ThreadAddCreditsNudgeEmailParams, } | { "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": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "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/McpServerToolCallParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallParams.ts new file mode 100644 index 000000000000..046a3fdc28c0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallParams.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 McpServerToolCallParams = { threadId: string, server: string, tool: string, arguments?: JsonValue, _meta?: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallResponse.ts new file mode 100644 index 000000000000..fe14692ad263 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerToolCallResponse.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 McpServerToolCallResponse = { content: Array, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, }; 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 d8dd15c97ccd..64983abc1810 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -188,6 +188,8 @@ export type { McpServerStartupState } from "./McpServerStartupState"; export type { McpServerStatus } from "./McpServerStatus"; export type { McpServerStatusDetail } from "./McpServerStatusDetail"; export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification"; +export type { McpServerToolCallParams } from "./McpServerToolCallParams"; +export type { McpServerToolCallResponse } from "./McpServerToolCallResponse"; export type { McpToolCallError } from "./McpToolCallError"; export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification"; export type { McpToolCallResult } from "./McpToolCallResult"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 14eb1f5f3931..48695fee8a7c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -474,6 +474,11 @@ client_request_definitions! { response: v2::McpResourceReadResponse, }, + McpServerToolCall => "mcpServer/tool/call" { + params: v2::McpServerToolCallParams, + response: v2::McpServerToolCallResponse, + }, + WindowsSandboxSetupStart => "windowsSandbox/setupStart" { params: v2::WindowsSandboxSetupStartParams, response: v2::WindowsSandboxSetupStartResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 66ac1a602010..a7a3a1ebd6d6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -28,6 +28,7 @@ use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::TurnItem as CoreTurnItem; +use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult; use codex_protocol::mcp::Resource as McpResource; pub use codex_protocol::mcp::ResourceContent as McpResourceContent; use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; @@ -2024,6 +2025,48 @@ pub struct McpResourceReadResponse { pub contents: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerToolCallParams { + pub thread_id: String, + pub server: String, + pub tool: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub arguments: Option, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerToolCallResponse { + pub content: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub structured_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub is_error: Option, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +impl From for McpServerToolCallResponse { + fn from(result: CoreMcpCallToolResult) -> Self { + Self { + content: result.content, + structured_content: result.structured_content, + is_error: result.is_error, + meta: result.meta, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index eeaf9ef23e15..dbf53fa2c082 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -190,6 +190,7 @@ Example with notification opt-out: - `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 and auth status, plus resources/resource templates for `full` detail; supports cursor+limit pagination. If `detail` is omitted, the server defaults to `full`. - `mcpServer/resource/read` — read a resource from a thread's configured MCP server by `threadId`, `server`, and `uri`, returning text/blob resource `contents`. +- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. - `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 795723f9d3c3..b4ec4e4e6fe7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -85,6 +85,8 @@ use codex_app_server_protocol::McpServerOauthLoginResponse; use codex_app_server_protocol::McpServerRefreshResponse; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerStatusDetail; +use codex_app_server_protocol::McpServerToolCallParams; +use codex_app_server_protocol::McpServerToolCallResponse; use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; @@ -1007,6 +1009,10 @@ impl CodexMessageProcessor { self.read_mcp_resource(to_connection_request_id(request_id), params) .await; } + ClientRequest::McpServerToolCall { request_id, params } => { + self.call_mcp_server_tool(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; @@ -5517,6 +5523,46 @@ impl CodexMessageProcessor { }); } + async fn call_mcp_server_tool( + &self, + request_id: ConnectionRequestId, + params: McpServerToolCallParams, + ) { + let outgoing = Arc::clone(&self.outgoing); + let (_, thread) = match self.load_thread(¶ms.thread_id).await { + Ok(thread) => thread, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + tokio::spawn(async move { + let result = thread + .call_mcp_tool(¶ms.server, ¶ms.tool, params.arguments, params.meta) + .await; + match result { + Ok(result) => { + outgoing + .send_response(request_id, McpServerToolCallResponse::from(result)) + .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/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 01309662136e..c9d24572ff0e 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -48,6 +48,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::LoginAccountParams; use codex_app_server_protocol::McpResourceReadParams; +use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::PluginInstallParams; @@ -504,6 +505,15 @@ impl McpProcess { self.send_request("mcpServer/resource/read", params).await } + /// Send an `mcpServer/tool/call` JSON-RPC request. + pub async fn send_mcp_server_tool_call_request( + &mut self, + params: McpServerToolCallParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("mcpServer/tool/call", params).await + } + /// Send a `skills/list` JSON-RPC request. pub async fn send_skills_list_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs new file mode 100644 index 000000000000..87556c83646c --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -0,0 +1,243 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::to_response; +use app_test_support::write_mock_responses_config_toml; +use axum::Router; +use codex_app_server_protocol::JSONRPCError; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::McpServerToolCallParams; +use codex_app_server_protocol::McpServerToolCallResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use core_test_support::responses; +use pretty_assertions::assert_eq; +use rmcp::handler::server::ServerHandler; +use rmcp::model::CallToolRequestParams; +use rmcp::model::CallToolResult; +use rmcp::model::Content; +use rmcp::model::JsonObject; +use rmcp::model::ListToolsResult; +use rmcp::model::Meta; +use rmcp::model::ServerCapabilities; +use rmcp::model::ServerInfo; +use rmcp::model::Tool; +use rmcp::model::ToolAnnotations; +use rmcp::service::RequestContext; +use rmcp::service::RoleServer; +use rmcp::transport::StreamableHttpServerConfig; +use rmcp::transport::StreamableHttpService; +use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; +use serde_json::json; +use tempfile::TempDir; +use tokio::net::TcpListener; +use tokio::task::JoinHandle; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); +const TEST_SERVER_NAME: &str = "tool_server"; +const TEST_TOOL_NAME: &str = "echo_tool"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn mcp_server_tool_call_returns_tool_result() -> Result<()> { + let responses_server = responses::start_mock_server().await; + let (mcp_server_url, mcp_server_handle) = start_mcp_server().await?; + let codex_home = TempDir::new()?; + write_mock_responses_config_toml( + codex_home.path(), + &responses_server.uri(), + &BTreeMap::new(), + /*auto_compact_limit*/ 1024, + /*requires_openai_auth*/ None, + "mock_provider", + "compact", + )?; + + let config_path = codex_home.path().join("config.toml"); + let mut config_toml = std::fs::read_to_string(&config_path)?; + config_toml.push_str(&format!( + r#" +[mcp_servers.{TEST_SERVER_NAME}] +url = "{mcp_server_url}/mcp" +"# + )); + std::fs::write(config_path, config_toml)?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_start_id = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?; + + let tool_call_request_id = mcp + .send_mcp_server_tool_call_request(McpServerToolCallParams { + thread_id: thread.id, + server: TEST_SERVER_NAME.to_string(), + tool: TEST_TOOL_NAME.to_string(), + arguments: Some(json!({ + "message": "hello from app", + })), + meta: Some(json!({ + "source": "mcp-app", + })), + }) + .await?; + let tool_call_response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(tool_call_request_id)), + ) + .await??; + let response: McpServerToolCallResponse = to_response(tool_call_response)?; + + assert_eq!(response.content.len(), 1); + assert_eq!(response.content[0].get("type"), Some(&json!("text"))); + assert_eq!( + response.content[0].get("text"), + Some(&json!("echo: hello from app")) + ); + assert_eq!( + response.structured_content, + Some(json!({ + "echoed": "hello from app", + })) + ); + assert_eq!(response.is_error, Some(false)); + assert_eq!( + response.meta, + Some(json!({ + "calledBy": "mcp-app", + })) + ); + + mcp_server_handle.abort(); + let _ = mcp_server_handle.await; + + Ok(()) +} + +#[tokio::test] +async fn mcp_server_tool_call_returns_error_for_unknown_thread() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_mcp_server_tool_call_request(McpServerToolCallParams { + thread_id: "00000000-0000-4000-8000-000000000000".to_string(), + server: TEST_SERVER_NAME.to_string(), + tool: TEST_TOOL_NAME.to_string(), + arguments: Some(json!({})), + meta: None, + }) + .await?; + let error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert!( + error.error.message.contains("thread not found"), + "expected thread-not-found error, got: {error:?}" + ); + + Ok(()) +} + +#[derive(Clone, Default)] +struct ToolAppsMcpServer; + +impl ServerHandler for ToolAppsMcpServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + capabilities: ServerCapabilities::builder().enable_tools().build(), + ..ServerInfo::default() + } + } + + async fn list_tools( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + let input_schema: JsonObject = serde_json::from_value(json!({ + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "additionalProperties": false + })) + .map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?; + + let mut tool = Tool::new( + Cow::Borrowed(TEST_TOOL_NAME), + Cow::Borrowed("Echo a message."), + Arc::new(input_schema), + ); + tool.annotations = Some(ToolAnnotations::new().read_only(true)); + + Ok(ListToolsResult { + tools: vec![tool], + next_cursor: None, + meta: None, + }) + } + + async fn call_tool( + &self, + request: CallToolRequestParams, + _context: RequestContext, + ) -> Result { + assert_eq!(request.name.as_ref(), TEST_TOOL_NAME); + let message = request + .arguments + .as_ref() + .and_then(|arguments| arguments.get("message")) + .and_then(|value| value.as_str()) + .unwrap_or_default(); + + let mut meta = Meta::new(); + meta.0.insert("calledBy".to_string(), json!("mcp-app")); + + let mut result = CallToolResult::structured(json!({ + "echoed": message, + })); + result.content = vec![Content::text(format!("echo: {message}"))]; + result.meta = Some(meta); + Ok(result) + } +} + +async fn start_mcp_server() -> Result<(String, JoinHandle<()>)> { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?; + let mcp_service = StreamableHttpService::new( + || Ok(ToolAppsMcpServer), + Arc::new(LocalSessionManager::default()), + StreamableHttpServerConfig::default(), + ); + let router = Router::new().nest_service("/mcp", mcp_service); + + let handle = tokio::spawn(async move { + let _ = axum::serve(listener, router).await; + }); + + Ok((format!("http://{addr}"), handle)) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 4723c6708da5..1f4062c51834 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -18,6 +18,7 @@ mod initialize; mod mcp_resource; mod mcp_server_elicitation; mod mcp_server_status; +mod mcp_tool; mod model_list; mod output_schema; mod plan_item; diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index e90dd0f80a10..86decbe6b22b 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -9,6 +9,7 @@ use codex_protocol::config_types::Personality; use codex_protocol::config_types::ServiceTier; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; +use codex_protocol::mcp::CallToolResult; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; @@ -224,6 +225,19 @@ impl CodexThread { Ok(serde_json::to_value(result)?) } + pub async fn call_mcp_tool( + &self, + server: &str, + tool: &str, + arguments: Option, + meta: Option, + ) -> anyhow::Result { + self.codex + .session + .call_tool(server, tool, arguments, meta) + .await + } + pub fn enabled(&self, feature: Feature) -> bool { self.codex.enabled(feature) }