From 765309d5a611ea02be842ead0ab1a2828196fae9 Mon Sep 17 00:00:00 2001 From: martinauyeung-oai Date: Thu, 18 Jun 2026 14:02:54 -0700 Subject: [PATCH] Emit Trusted MCP App Identity on Tool-Call Items (#27132) ## Summary - Add optional `appContext` to app-server MCP tool-call items with trusted `connectorId`, `linkId`, and `mcpAppResourceUri` metadata. - Preserve that context across tool-call events, persisted history, reconnects, and thread resume. - Keep the deprecated top-level `mcpAppResourceUri` temporarily for client migration. The consumer contract is `{ appContext: { connectorId, linkId, mcpAppResourceUri }, tool }`. ## Validation - Full GitHub Actions suite passes, including CLA, Bazel tests, clippy, release builds, and argument-comment lint. --------- Co-authored-by: martinauyeung-oai <280153141+martinauyeung-oai@users.noreply.github.com> --- .../analytics/src/analytics_client_tests.rs | 1 + .../schema/json/ServerNotification.json | 34 +++++++++++ .../codex_app_server_protocol.schemas.json | 34 +++++++++++ .../codex_app_server_protocol.v2.schemas.json | 34 +++++++++++ .../json/v2/ItemCompletedNotification.json | 34 +++++++++++ .../json/v2/ItemStartedNotification.json | 34 +++++++++++ .../schema/json/v2/ReviewStartResponse.json | 34 +++++++++++ .../schema/json/v2/ThreadForkResponse.json | 34 +++++++++++ .../schema/json/v2/ThreadListResponse.json | 34 +++++++++++ .../json/v2/ThreadMetadataUpdateResponse.json | 34 +++++++++++ .../schema/json/v2/ThreadReadResponse.json | 34 +++++++++++ .../schema/json/v2/ThreadResumeResponse.json | 34 +++++++++++ .../json/v2/ThreadRollbackResponse.json | 34 +++++++++++ .../schema/json/v2/ThreadStartResponse.json | 34 +++++++++++ .../json/v2/ThreadStartedNotification.json | 34 +++++++++++ .../json/v2/ThreadUnarchiveResponse.json | 34 +++++++++++ .../json/v2/TurnCompletedNotification.json | 34 +++++++++++ .../schema/json/v2/TurnStartResponse.json | 34 +++++++++++ .../json/v2/TurnStartedNotification.json | 34 +++++++++++ .../typescript/v2/McpToolCallAppContext.ts | 5 ++ .../schema/typescript/v2/ThreadItem.ts | 7 ++- .../schema/typescript/v2/index.ts | 1 + .../src/protocol/thread_history.rs | 28 +++++++++- .../src/protocol/v2/item.rs | 16 ++++++ .../src/protocol/v2/tests.rs | 53 ++++++++++++++++++ codex-rs/app-server/README.md | 2 +- .../thread_resume_redaction.rs | 13 +++++ .../app-server/tests/suite/v2/mcp_tool.rs | 1 + .../tests/suite/v2/thread_resume.rs | 25 ++++++++- codex-rs/core/src/mcp_tool_call.rs | 44 ++++++++++++--- codex-rs/core/src/mcp_tool_call_tests.rs | 56 ++++++++++++++++++- .../core/src/tools/handlers/mcp_resource.rs | 4 ++ .../core/tests/common/apps_test_server.rs | 4 ++ codex-rs/core/tests/suite/rmcp_client.rs | 2 + codex-rs/core/tests/suite/search_tool.rs | 3 + ...event_processor_with_jsonl_output_tests.rs | 1 + .../tests/event_processor_with_json_output.rs | 5 ++ codex-rs/protocol/src/items.rs | 10 ++++ codex-rs/protocol/src/protocol.rs | 20 +++++++ .../src/chatwidget/tests/history_replay.rs | 1 + 40 files changed, 899 insertions(+), 15 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index fcdd97af8..dac4829ab 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -4121,6 +4121,7 @@ async fn turn_event_counts_completed_tool_items() { tool: "search".to_string(), status, arguments: json!({}), + app_context: None, mcp_app_resource_uri: None, plugin_id: Some("sample@test".to_string()), result: None, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index bfafd5483..8509a1c69 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2396,6 +2396,29 @@ ], "type": "object" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -4066,6 +4089,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -4089,6 +4122,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 8ef80b7b5..e9611c67e 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 @@ -12119,6 +12119,29 @@ "title": "McpServerToolCallResponse", "type": "object" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -17511,6 +17534,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/v2/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -17534,6 +17567,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 00d7a4bbd..bac9a48e7 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 @@ -8543,6 +8543,29 @@ "title": "McpServerToolCallResponse", "type": "object" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -15290,6 +15313,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -15313,6 +15346,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 027edd036..d7bfb3827 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -297,6 +297,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -784,6 +807,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -807,6 +840,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 64152e9cf..770fc8bd4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -297,6 +297,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -784,6 +807,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -807,6 +840,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 61f61e74d..545ae64d4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -434,6 +434,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -928,6 +951,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -951,6 +984,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 43a76f145..9ced571cf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -539,6 +539,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1420,6 +1443,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1443,6 +1476,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 1e1504b73..9925c5411 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -460,6 +460,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1235,6 +1258,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1258,6 +1291,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 6e62eae14..1e142947e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -460,6 +460,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1235,6 +1258,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1258,6 +1291,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 ff70a017b..532a71280 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -460,6 +460,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1235,6 +1258,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1258,6 +1291,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 f9a65cdd8..28a5f5b24 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -539,6 +539,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1420,6 +1443,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1443,6 +1476,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 f455f6dff..9f394b31c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -460,6 +460,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1235,6 +1258,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1258,6 +1291,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 0c3eac4de..1baf6e4bd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -539,6 +539,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1420,6 +1443,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1443,6 +1476,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 0e44cfb25..22801809c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -460,6 +460,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1235,6 +1258,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1258,6 +1291,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 f88a1fae5..3009e86e7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -460,6 +460,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1235,6 +1258,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -1258,6 +1291,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 7e6cef2c3..b9a940157 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -434,6 +434,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -928,6 +951,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -951,6 +984,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 083767a6c..da10713dd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -434,6 +434,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -928,6 +951,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -951,6 +984,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" 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 3a43d7d3f..6b9349822 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -434,6 +434,29 @@ "LegacyAppPathString": { "type": "string" }, + "McpToolCallAppContext": { + "properties": { + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -928,6 +951,16 @@ }, { "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", @@ -951,6 +984,7 @@ "type": "string" }, "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", "type": [ "string", "null" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.ts new file mode 100644 index 000000000..0a8343428 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallAppContext.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 McpToolCallAppContext = { connectorId: string, linkId: string | null, resourceUri: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 1cdac99c6..eb8c6486b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -16,6 +16,7 @@ import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputCo import type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; import type { FileUpdateChange } from "./FileUpdateChange"; import type { HookPromptFragment } from "./HookPromptFragment"; +import type { McpToolCallAppContext } from "./McpToolCallAppContext"; import type { McpToolCallError } from "./McpToolCallError"; import type { McpToolCallResult } from "./McpToolCallResult"; import type { McpToolCallStatus } from "./McpToolCallStatus"; @@ -55,7 +56,11 @@ exitCode: number | null, /** * The duration of the command execution in milliseconds. */ -durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, mcpAppResourceUri?: string, pluginId: string | null, result: McpToolCallResult | null, error: McpToolCallError | null, +durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, appContext: McpToolCallAppContext | null, +/** + * Deprecated: use `appContext.resourceUri` instead. + */ +mcpAppResourceUri?: string, pluginId: string | null, result: McpToolCallResult | null, error: McpToolCallError | null, /** * The duration of the MCP tool call in milliseconds. */ 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 2e59b0fe1..9b1bd3b6e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -243,6 +243,7 @@ export type { McpServerStatusDetail } from "./McpServerStatusDetail"; export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification"; export type { McpServerToolCallParams } from "./McpServerToolCallParams"; export type { McpServerToolCallResponse } from "./McpServerToolCallResponse"; +export type { McpToolCallAppContext } from "./McpToolCallAppContext"; 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/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 001fa7cf9..6b27e72d4 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -10,6 +10,7 @@ use crate::protocol::v2::CollabAgentToolCallStatus; use crate::protocol::v2::CommandExecutionStatus; use crate::protocol::v2::DynamicToolCallOutputContentItem; use crate::protocol::v2::DynamicToolCallStatus; +use crate::protocol::v2::McpToolCallAppContext; use crate::protocol::v2::McpToolCallError; use crate::protocol::v2::McpToolCallResult; use crate::protocol::v2::McpToolCallStatus; @@ -765,6 +766,14 @@ impl ThreadHistoryBuilder { .arguments .clone() .unwrap_or(serde_json::Value::Null), + app_context: payload + .connector_id + .clone() + .map(|connector_id| McpToolCallAppContext { + connector_id, + link_id: payload.link_id.clone(), + resource_uri: payload.mcp_app_resource_uri.clone(), + }), mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(), plugin_id: payload.plugin_id.clone(), result: None, @@ -807,6 +816,14 @@ impl ThreadHistoryBuilder { .arguments .clone() .unwrap_or(serde_json::Value::Null), + app_context: payload + .connector_id + .clone() + .map(|connector_id| McpToolCallAppContext { + connector_id, + link_id: payload.link_id.clone(), + resource_uri: payload.mcp_app_resource_uri.clone(), + }), mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(), plugin_id: payload.plugin_id.clone(), result, @@ -1567,7 +1584,6 @@ mod tests { use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandSource; - use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::McpInvocation; use codex_protocol::protocol::McpToolCallEndEvent; @@ -2397,7 +2413,9 @@ mod tests { tool: "lookup".into(), arguments: Some(serde_json::json!({"id":"123"})), }, + connector_id: None, mcp_app_resource_uri: None, + link_id: None, plugin_id: None, duration: Duration::from_millis(8), result: Err("boom".into()), @@ -2447,6 +2465,7 @@ mod tests { tool: "lookup".into(), status: McpToolCallStatus::Failed, arguments: serde_json::json!({"id":"123"}), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None, @@ -2475,7 +2494,9 @@ mod tests { tool: "lookup".into(), arguments: Some(serde_json::json!({"id":"123"})), }, + connector_id: Some("calendar".into()), mcp_app_resource_uri: Some("ui://widget/lookup.html".into()), + link_id: Some("link_calendar".into()), plugin_id: Some("sample@test".into()), duration: Duration::from_millis(8), result: Ok(CallToolResult { @@ -2506,6 +2527,11 @@ mod tests { tool: "lookup".into(), status: McpToolCallStatus::Completed, arguments: serde_json::json!({"id":"123"}), + app_context: Some(McpToolCallAppContext { + connector_id: "calendar".into(), + link_id: Some("link_calendar".into()), + resource_uri: Some("ui://widget/lookup.html".into()), + }), mcp_app_resource_uri: Some("ui://widget/lookup.html".into()), plugin_id: Some("sample@test".into()), result: Some(Box::new(McpToolCallResult { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 34cfe8e13..e630de634 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -290,8 +290,10 @@ pub enum ThreadItem { tool: String, status: McpToolCallStatus, arguments: JsonValue, + app_context: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + /// Deprecated: use `appContext.resourceUri` instead. mcp_app_resource_uri: Option, plugin_id: Option, result: Option>, @@ -384,6 +386,15 @@ pub enum ThreadItem { ContextCompaction { id: String }, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub struct McpToolCallAppContext { + pub connector_id: String, + pub link_id: Option, + pub resource_uri: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase", export_to = "v2/")] @@ -877,6 +888,11 @@ impl From for ThreadItem { tool: mcp.tool, status: McpToolCallStatus::from(mcp.status), arguments: mcp.arguments, + app_context: mcp.connector_id.map(|connector_id| McpToolCallAppContext { + connector_id, + link_id: mcp.link_id, + resource_uri: mcp.mcp_app_resource_uri.clone(), + }), mcp_app_resource_uri: mcp.mcp_app_resource_uri, plugin_id: mcp.plugin_id, result: mcp.result.map(McpToolCallResult::from).map(Box::new), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index bb66aa632..0368bf45f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2615,7 +2615,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { server: "server".to_string(), tool: "tool".to_string(), arguments: json!({"arg": "value"}), + connector_id: Some("calendar".to_string()), mcp_app_resource_uri: Some("app://connector".to_string()), + link_id: Some("link_calendar".to_string()), plugin_id: Some("sample@test".to_string()), status: CoreMcpToolCallStatus::InProgress, result: None, @@ -2631,6 +2633,11 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { tool: "tool".to_string(), status: McpToolCallStatus::InProgress, arguments: json!({"arg": "value"}), + app_context: Some(McpToolCallAppContext { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + resource_uri: Some("app://connector".to_string()), + }), mcp_app_resource_uri: Some("app://connector".to_string()), plugin_id: Some("sample@test".to_string()), result: None, @@ -2644,7 +2651,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { server: "server".to_string(), tool: "tool".to_string(), arguments: JsonValue::Null, + connector_id: None, mcp_app_resource_uri: None, + link_id: None, plugin_id: None, status: CoreMcpToolCallStatus::Completed, result: Some(CallToolResult { @@ -2665,6 +2674,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { tool: "tool".to_string(), status: McpToolCallStatus::Completed, arguments: JsonValue::Null, + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: Some(Box::new(McpToolCallResult { @@ -2678,6 +2688,49 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { ); } +#[test] +fn mcp_tool_call_app_context_serializes_connector_id() { + let item = ThreadItem::McpToolCall { + id: "mcp-1".to_string(), + server: "codex_apps".to_string(), + tool: "calendar.create_event".to_string(), + status: McpToolCallStatus::InProgress, + arguments: json!({}), + app_context: Some(McpToolCallAppContext { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + resource_uri: Some("app://connector".to_string()), + }), + mcp_app_resource_uri: Some("app://connector".to_string()), + plugin_id: None, + result: None, + error: None, + duration_ms: None, + }; + + assert_eq!( + serde_json::to_value(item).expect("MCP tool call should serialize"), + json!({ + "type": "mcpToolCall", + "id": "mcp-1", + "server": "codex_apps", + "tool": "calendar.create_event", + "status": "inProgress", + "arguments": {}, + "appContext": { + "connectorId": "calendar", + "linkId": "link_calendar", + "resourceUri": "app://connector", + }, + "mcpAppResourceUri": "app://connector", + "pluginId": null, + "result": null, + "error": null, + "durationMs": null, + }) + ); +} + #[test] fn user_input_into_core_preserves_image_detail() { assert_eq!( diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index b9c7899fc..deda1c517 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1365,7 +1365,7 @@ Today both notifications carry an empty `items` array even when item events were - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). - `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. - `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`. -- `mcpToolCall` — `{id, server, tool, status, arguments, mcpAppResourceUri?, pluginId, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`. +- `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, and `resourceUri` points to the widget template. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the invoked action. `status` is `inProgress`, `completed`, or `failed`. - `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. - `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion. - `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool. diff --git a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs index 110d6d5cb..b7d864069 100644 --- a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs +++ b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs @@ -52,6 +52,7 @@ fn redacted_mcp_tool_call_result() -> McpToolCallResult { #[cfg(test)] mod tests { use super::*; + use codex_app_server_protocol::McpToolCallAppContext; use codex_app_server_protocol::McpToolCallError; use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::SessionSource; @@ -78,6 +79,11 @@ mod tests { tool: "lookup".to_string(), status: McpToolCallStatus::Completed, arguments: serde_json::json!({"secret":"argument"}), + app_context: Some(McpToolCallAppContext { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + resource_uri: Some("ui://widget/lookup.html".to_string()), + }), mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()), plugin_id: Some("sample@test".to_string()), result: Some(Box::new(McpToolCallResult { @@ -120,6 +126,11 @@ mod tests { tool: "lookup".to_string(), status: McpToolCallStatus::Completed, arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()), + app_context: Some(McpToolCallAppContext { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + resource_uri: Some("ui://widget/lookup.html".to_string()), + }), mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()), plugin_id: Some("sample@test".to_string()), result: Some(Box::new(redacted_mcp_tool_call_result())), @@ -137,6 +148,7 @@ mod tests { tool: "lookup".to_string(), status: McpToolCallStatus::Failed, arguments: serde_json::json!({"secret":"argument"}), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None, @@ -156,6 +168,7 @@ mod tests { tool: "lookup".to_string(), status: McpToolCallStatus::Failed, arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None, diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs index 10c8a6ef0..07c2e9fed 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -513,6 +513,7 @@ url = "{mcp_server_url}/mcp" tool, status, arguments: json!({ "message": LARGE_RESPONSE_MESSAGE }), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: Some(result), diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index c191263d1..cd508fc68 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -23,6 +23,7 @@ use codex_app_server_protocol::FileChangeRequestApprovalResponse; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::McpToolCallAppContext; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind; use codex_app_server_protocol::RequestId; @@ -729,6 +730,7 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( .expect("remote resume should include redacted MCP item"); let ThreadItem::McpToolCall { arguments, + app_context, result, error, .. @@ -737,6 +739,14 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( unreachable!("matched MCP item"); }; assert_eq!(arguments, &json!("[redacted]")); + assert_eq!( + app_context, + &Some(McpToolCallAppContext { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + resource_uri: Some("ui://widget/lookup.html".to_string()), + }) + ); let result = result.as_ref().expect("redacted MCP result"); assert_eq!( result.content, @@ -770,12 +780,23 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( .find(|item| matches!(item, ThreadItem::McpToolCall { .. })) .expect("normal resume should include MCP item"); let ThreadItem::McpToolCall { - arguments, result, .. + arguments, + app_context, + result, + .. } = normal_mcp_item else { unreachable!("matched MCP item"); }; assert_eq!(arguments, &json!({"secret":"argument"})); + assert_eq!( + app_context, + &Some(McpToolCallAppContext { + connector_id: "calendar".to_string(), + link_id: Some("link_calendar".to_string()), + resource_uri: Some("ui://widget/lookup.html".to_string()), + }) + ); let result = result.as_ref().expect("normal MCP result"); assert_eq!( result.content, @@ -877,7 +898,9 @@ fn append_resume_redaction_history( tool: "lookup".to_string(), arguments: Some(json!({"secret":"argument"})), }, + connector_id: Some("calendar".to_string()), mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()), + link_id: Some("link_calendar".to_string()), plugin_id: None, duration: Duration::from_millis(8), result: Ok(CallToolResult { diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index a89951542..56d6e678a 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -143,14 +143,7 @@ pub(crate) async fn handle_mcp_tool_call( let metadata = lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await; - let item_metadata = McpToolCallItemMetadata { - mcp_app_resource_uri: metadata - .as_ref() - .and_then(|metadata| metadata.mcp_app_resource_uri.clone()), - plugin_id: metadata - .as_ref() - .and_then(|metadata| metadata.plugin_id.clone()), - }; + let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, metadata.as_ref()); let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME { let annotations = metadata .as_ref() @@ -312,12 +305,32 @@ pub(crate) struct HandledMcpToolCall { pub(crate) tool_input: JsonValue, } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] struct McpToolCallItemMetadata { + connector_id: Option, + link_id: Option, mcp_app_resource_uri: Option, plugin_id: Option, } +impl McpToolCallItemMetadata { + fn from_tool_metadata(server: &str, metadata: Option<&McpToolApprovalMetadata>) -> Self { + let trusted_mcp_app_metadata = if server == CODEX_APPS_MCP_SERVER_NAME { + metadata + } else { + None + }; + Self { + connector_id: trusted_mcp_app_metadata + .and_then(|metadata| metadata.connector_id.clone()), + link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()), + mcp_app_resource_uri: metadata + .and_then(|metadata| metadata.mcp_app_resource_uri.clone()), + plugin_id: metadata.and_then(|metadata| metadata.plugin_id.clone()), + } + } +} + async fn handle_approved_mcp_tool_call( sess: &Session, turn_context: &TurnContext, @@ -892,7 +905,9 @@ async fn notify_mcp_tool_call_started( server, tool, arguments: arguments.unwrap_or(JsonValue::Null), + connector_id: item_metadata.connector_id, mcp_app_resource_uri: item_metadata.mcp_app_resource_uri, + link_id: item_metadata.link_id, plugin_id: item_metadata.plugin_id, status: McpToolCallStatus::InProgress, result: None, @@ -932,7 +947,9 @@ async fn notify_mcp_tool_call_completed( server, tool, arguments: arguments.unwrap_or(JsonValue::Null), + connector_id: item_metadata.connector_id, mcp_app_resource_uri: item_metadata.mcp_app_resource_uri, + link_id: item_metadata.link_id, plugin_id: item_metadata.plugin_id, status, result, @@ -998,6 +1015,7 @@ enum McpToolApprovalDecision { pub(crate) struct McpToolApprovalMetadata { annotations: Option, connector_id: Option, + link_id: Option, connector_name: Option, connector_description: Option, plugin_id: Option, @@ -1010,6 +1028,7 @@ pub(crate) struct McpToolApprovalMetadata { const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate"; const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri"; +const MCP_TOOL_LINK_ID_META_KEY: &str = "link_id"; const MCP_TOOL_PLUGIN_ID_META_KEY: &str = "plugin_id"; const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId"; @@ -1493,6 +1512,13 @@ pub(crate) async fn lookup_mcp_tool_metadata( Some(McpToolApprovalMetadata { annotations: tool_info.tool.annotations, connector_id: tool_info.connector_id, + link_id: tool_info + .tool + .meta + .as_ref() + .and_then(|meta| meta.get(MCP_TOOL_LINK_ID_META_KEY)) + .and_then(serde_json::Value::as_str) + .map(str::to_string), connector_name: tool_info.connector_name, connector_description, plugin_id, diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 24abfd9e0..eb500b0f6 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -78,6 +78,7 @@ fn approval_metadata( McpToolApprovalMetadata { annotations: None, connector_id: connector_id.map(str::to_string), + link_id: None, connector_name: connector_name.map(str::to_string), connector_description: connector_description.map(str::to_string), plugin_id: None, @@ -1149,8 +1150,39 @@ async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() { ); } +#[test] +fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() { + let mut metadata = approval_metadata( + Some("asdk_app_0123456789abcdef0123456789abcdef"), + /*connector_name*/ None, + /*connector_description*/ None, + /*tool_title*/ None, + /*tool_description*/ None, + ); + metadata.link_id = Some("link_fedcba9876543210fedcba9876543210".to_string()); + + assert_eq!( + McpToolCallItemMetadata::from_tool_metadata(CODEX_APPS_MCP_SERVER_NAME, Some(&metadata),), + McpToolCallItemMetadata { + connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()), + link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()), + mcp_app_resource_uri: None, + plugin_id: None, + } + ); + assert_eq!( + McpToolCallItemMetadata::from_tool_metadata("custom_server", Some(&metadata)), + McpToolCallItemMetadata { + connector_id: None, + link_id: None, + mcp_app_resource_uri: None, + plugin_id: None, + } + ); +} + #[tokio::test] -async fn mcp_tool_call_item_includes_plugin_id() { +async fn mcp_tool_call_item_includes_app_identity() { let (session, turn_context, rx_event) = make_session_and_context_with_rx().await; notify_mcp_tool_call_started( @@ -1158,11 +1190,13 @@ async fn mcp_tool_call_item_includes_plugin_id() { &turn_context, "call-plugin", McpInvocation { - server: "sample".to_string(), + server: CODEX_APPS_MCP_SERVER_NAME.to_string(), tool: "echo".to_string(), arguments: None, }, McpToolCallItemMetadata { + connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()), + link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()), mcp_app_resource_uri: None, plugin_id: Some("sample@test".to_string()), }, @@ -1180,6 +1214,14 @@ async fn mcp_tool_call_item_includes_plugin_id() { panic!("expected MCP tool call item"); }; + assert_eq!( + item.connector_id.as_deref(), + Some("asdk_app_0123456789abcdef0123456789abcdef") + ); + assert_eq!( + item.link_id.as_deref(), + Some("link_fedcba9876543210fedcba9876543210") + ); assert_eq!(item.plugin_id.as_deref(), Some("sample@test")); } @@ -1193,6 +1235,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps let metadata = McpToolApprovalMetadata { annotations: None, connector_id: Some("calendar".to_string()), + link_id: None, connector_name: Some("Calendar".to_string()), connector_description: Some("Manage events".to_string()), plugin_id: None, @@ -1661,6 +1704,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() { let metadata = McpToolApprovalMetadata { annotations: Some(annotations(Some(false), Some(true), Some(true))), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2326,6 +2370,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() { /*open_world*/ None, )), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2400,6 +2445,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() { /*open_world*/ None, )), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2457,6 +2503,7 @@ async fn permission_request_hook_allows_mcp_tool_call() { /*open_world*/ None, )), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2593,6 +2640,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() { /*open_world*/ None, )), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2680,6 +2728,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() { let metadata = McpToolApprovalMetadata { annotations: Some(annotations(Some(false), Some(true), Some(true))), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2734,6 +2783,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval /*open_world*/ None, )), connector_id: None, + link_id: None, connector_name: None, connector_description: None, plugin_id: None, @@ -2789,6 +2839,7 @@ async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() { let metadata = McpToolApprovalMetadata { annotations: Some(annotations(Some(false), Some(true), Some(true))), connector_id: Some("calendar".to_string()), + link_id: None, connector_name: Some("Calendar".to_string()), connector_description: Some("Manage events".to_string()), plugin_id: None, @@ -2842,6 +2893,7 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() { let metadata = McpToolApprovalMetadata { annotations: Some(annotations(Some(false), Some(true), Some(true))), connector_id: Some("calendar".to_string()), + link_id: None, connector_name: Some("Calendar".to_string()), connector_description: Some("Manage events".to_string()), plugin_id: None, diff --git a/codex-rs/core/src/tools/handlers/mcp_resource.rs b/codex-rs/core/src/tools/handlers/mcp_resource.rs index e2f83859b..2f97c02a4 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource.rs @@ -203,7 +203,9 @@ async fn emit_tool_call_begin( server, tool, arguments: arguments.unwrap_or(Value::Null), + connector_id: None, mcp_app_resource_uri: None, + link_id: None, plugin_id: None, status: McpToolCallStatus::InProgress, result: None, @@ -242,7 +244,9 @@ async fn emit_tool_call_end( server, tool, arguments: arguments.unwrap_or(Value::Null), + connector_id: None, mcp_app_resource_uri: None, + link_id: None, plugin_id: None, status, result, diff --git a/codex-rs/core/tests/common/apps_test_server.rs b/codex-rs/core/tests/common/apps_test_server.rs index 0e7141281..8a671ea5b 100644 --- a/codex-rs/core/tests/common/apps_test_server.rs +++ b/codex-rs/core/tests/common/apps_test_server.rs @@ -17,6 +17,7 @@ use wiremock::matchers::path; use wiremock::matchers::path_regex; const CONNECTOR_ID: &str = "calendar"; +pub const LINK_ID: &str = "link_calendar"; const CONNECTOR_NAME: &str = "Calendar"; const DISCOVERABLE_CALENDAR_ID: &str = "connector_2128aebfecb84f64a069897515042a44"; const DISCOVERABLE_GMAIL_ID: &str = "connector_68df038e0ba48191908c8434991bbac2"; @@ -352,6 +353,7 @@ impl Respond for CodexAppsJsonRpcResponder { }, "_meta": { "connector_id": CONNECTOR_ID, + "link_id": LINK_ID, "connector_name": self.connector_name.clone(), "connector_description": self.connector_description.clone(), "openai/outputTemplate": CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI, @@ -378,6 +380,7 @@ impl Respond for CodexAppsJsonRpcResponder { }, "_meta": { "connector_id": CONNECTOR_ID, + "link_id": LINK_ID, "connector_name": self.connector_name.clone(), "connector_description": self.connector_description.clone(), "_codex_apps": { @@ -410,6 +413,7 @@ impl Respond for CodexAppsJsonRpcResponder { }, "_meta": { "connector_id": CONNECTOR_ID, + "link_id": LINK_ID, "connector_name": self.connector_name.clone(), "connector_description": self.connector_description.clone(), "openai/fileParams": ["file"], diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 2c64b5fba..dde226a19 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1337,7 +1337,9 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { tool: "image".to_string(), arguments: Some(json!({})), }, + connector_id: None, mcp_app_resource_uri: None, + link_id: None, plugin_id: None, }, ); diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 284ee49b8..722854772 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -25,6 +25,7 @@ use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_ use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_RESOURCE_URI; use core_test_support::apps_test_server::DIRECT_CALENDAR_CREATE_EVENT_TOOL as CALENDAR_CREATE_TOOL; use core_test_support::apps_test_server::DIRECT_CALENDAR_LIST_EVENTS_TOOL as CALENDAR_LIST_TOOL; +use core_test_support::apps_test_server::LINK_ID; use core_test_support::apps_test_server::SEARCH_CALENDAR_APP_ONLY_TOOL; use core_test_support::apps_test_server::SEARCH_CALENDAR_CREATE_TOOL; use core_test_support::apps_test_server::SEARCH_CALENDAR_LIST_TOOL; @@ -559,10 +560,12 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() - unreachable!("event guard guarantees McpToolCallEnd"); }; assert_eq!(end.call_id, "calendar-call-1"); + assert_eq!(end.connector_id.as_deref(), Some("calendar")); assert_eq!( end.mcp_app_resource_uri.as_deref(), Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI) ); + assert_eq!(end.link_id.as_deref(), Some(LINK_ID)); assert_eq!( end.invocation, McpInvocation { diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs index f8521071e..83cd6d3c6 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs @@ -98,6 +98,7 @@ fn mcp_tool_call_result_preserves_meta_in_jsonl_event() { tool: "web_run".to_string(), status: McpToolCallStatus::Completed, arguments: json!({"search_query": [{"q": "OpenAI Codex CLI documentation"}]}), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: Some(Box::new(codex_app_server_protocol::McpToolCallResult { 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 0f8867094..1f44ca31a 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -478,6 +478,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { tool: "tool_x".to_string(), status: ApiMcpToolCallStatus::InProgress, arguments: json!({ "key": "value" }), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None, @@ -496,6 +497,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { tool: "tool_x".to_string(), status: ApiMcpToolCallStatus::Completed, arguments: json!({ "key": "value" }), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: Some(Box::new(McpToolCallResult { @@ -568,6 +570,7 @@ fn mcp_tool_call_failure_sets_failed_status() { tool: "tool_y".to_string(), status: ApiMcpToolCallStatus::Failed, arguments: json!({ "param": 42 }), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None, @@ -617,6 +620,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { tool: "tool_z".to_string(), status: ApiMcpToolCallStatus::InProgress, arguments: serde_json::Value::Null, + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None, @@ -635,6 +639,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { tool: "tool_z".to_string(), status: ApiMcpToolCallStatus::Completed, arguments: serde_json::Value::Null, + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: Some(Box::new(McpToolCallResult { diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 01f44576a..08d257dc3 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -188,9 +188,15 @@ pub struct McpToolCallItem { pub arguments: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub connector_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mcp_app_resource_uri: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub link_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub plugin_id: Option, pub status: McpToolCallStatus, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -546,7 +552,9 @@ impl McpToolCallItem { tool: self.tool.clone(), arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), }, + connector_id: self.connector_id.clone(), mcp_app_resource_uri: self.mcp_app_resource_uri.clone(), + link_id: self.link_id.clone(), plugin_id: self.plugin_id.clone(), }) } @@ -566,6 +574,8 @@ impl McpToolCallItem { arguments: (!self.arguments.is_null()).then(|| self.arguments.clone()), }, mcp_app_resource_uri: self.mcp_app_resource_uri.clone(), + connector_id: self.connector_id.clone(), + link_id: self.link_id.clone(), plugin_id: self.plugin_id.clone(), duration: self.duration?, result, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a71eb64fe..258150aa8 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2324,9 +2324,15 @@ pub struct McpToolCallBeginEvent { pub invocation: McpInvocation, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub connector_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mcp_app_resource_uri: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub link_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub plugin_id: Option, } @@ -2337,9 +2343,15 @@ pub struct McpToolCallEndEvent { pub invocation: McpInvocation, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub connector_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub mcp_app_resource_uri: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub link_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub plugin_id: Option, #[ts(type = "string")] pub duration: Duration, @@ -4940,7 +4952,9 @@ mod tests { server: "server".into(), tool: "tool".into(), arguments: json!({"arg": "value"}), + connector_id: Some("connector".into()), mcp_app_resource_uri: Some("app://connector".into()), + link_id: Some("link_123".into()), plugin_id: Some("sample@test".into()), status: McpToolCallStatus::InProgress, result: None, @@ -4956,10 +4970,12 @@ mod tests { assert_eq!(event.call_id, "mcp-1"); assert_eq!(event.invocation.server, "server"); assert_eq!(event.invocation.tool, "tool"); + assert_eq!(event.connector_id.as_deref(), Some("connector")); assert_eq!( event.mcp_app_resource_uri.as_deref(), Some("app://connector") ); + assert_eq!(event.link_id.as_deref(), Some("link_123")); assert_eq!(event.plugin_id.as_deref(), Some("sample@test")); } _ => panic!("expected McpToolCallBegin event"), @@ -5047,7 +5063,9 @@ mod tests { server: "server".into(), tool: "tool".into(), arguments: json!({"arg": "value"}), + connector_id: Some("connector".into()), mcp_app_resource_uri: Some("app://connector".into()), + link_id: Some("link_123".into()), plugin_id: Some("sample@test".into()), status: McpToolCallStatus::Completed, result: Some(CallToolResult { @@ -5068,10 +5086,12 @@ mod tests { assert_eq!(event.call_id, "mcp-1"); assert_eq!(event.invocation.server, "server"); assert_eq!(event.invocation.tool, "tool"); + assert_eq!(event.connector_id.as_deref(), Some("connector")); assert_eq!( event.mcp_app_resource_uri.as_deref(), Some("app://connector") ); + assert_eq!(event.link_id.as_deref(), Some("link_123")); assert_eq!(event.plugin_id.as_deref(), Some("sample@test")); assert_eq!(event.duration, Duration::from_millis(42)); assert!(event.is_success()); diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index e6b9d4ede..f8626dfcc 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -973,6 +973,7 @@ async fn replayed_in_progress_mcp_tool_call_stays_active() { tool: "copilot".to_string(), status: codex_app_server_protocol::McpToolCallStatus::InProgress, arguments: json!({"action": "wait"}), + app_context: None, mcp_app_resource_uri: None, plugin_id: None, result: None,