From b1eac050c291ed920267d7697ae0f6eb18d71dd4 Mon Sep 17 00:00:00 2001 From: Drew Schuster Date: Wed, 24 Jun 2026 20:52:12 -0700 Subject: [PATCH 1/2] feat(app-server): forward MCP resource read metadata --- .../src/protocol/common.rs | 2 + .../src/protocol/v2/mcp.rs | 3 + .../src/protocol/v2/tests.rs | 18 +++++ codex-rs/app-server/README.md | 2 +- .../src/request_processors/mcp_processor.rs | 4 +- .../tests/common/test_app_server.rs | 12 ++++ .../app-server/tests/suite/v2/mcp_resource.rs | 68 +++++++++++++++---- codex-rs/codex-mcp/src/mcp/mod.rs | 12 +++- codex-rs/core/src/codex_thread.rs | 13 ++-- 9 files changed, 109 insertions(+), 25 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 51a37fbaa94f..2776a69fcd9c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1930,6 +1930,7 @@ mod tests { thread_id: Some("thread-1".to_string()), server: "server-a".to_string(), uri: "file:///tmp/resource".to_string(), + meta: None, }, }; assert_eq!( @@ -2108,6 +2109,7 @@ mod tests { thread_id: None, server: "server-a".to_string(), uri: "file:///tmp/resource".to_string(), + meta: None, }, }; assert_eq!(mcp_resource_read.serialization_scope(), None); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index cdfeb524f569..b9d7dc1de2ec 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -79,6 +79,9 @@ pub struct McpResourceReadParams { pub thread_id: Option, pub server: String, pub uri: String, + #[serde(rename = "_meta")] + #[ts(optional = nullable, rename = "_meta")] + pub meta: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] 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 7c78fee4c83e..fe5aa79dffd1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -61,6 +61,24 @@ fn test_absolute_path() -> AbsolutePathBuf { absolute_path("readable") } +#[test] +fn mcp_resource_read_params_deserialize_meta() { + let params: McpResourceReadParams = serde_json::from_value(json!({ + "server": "codex_apps", + "uri": "test://codex/resource", + "_meta": { "traceId": "trace-123" }, + })) + .expect("deserialize resource read params"); + + assert_eq!( + params.meta, + Some(BTreeMap::from([( + "traceId".to_string(), + json!("trace-123") + )])) + ); +} + #[test] fn thread_sources_round_trip_as_scalar_labels() { for (source, label) in [ diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 246b2bb0c0bf..b8af5e7baf8a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -233,7 +233,7 @@ Example with notification opt-out: - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. -- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly. +- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, `uri`, and protocol `_meta`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly. - `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. diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 08d464598b9b..bd8e408f8c21 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -360,6 +360,7 @@ impl McpRequestProcessor { thread_id, server, uri, + meta, } = params; if let Some(thread_id) = thread_id { @@ -367,7 +368,7 @@ impl McpRequestProcessor { let request_id = request_id.clone(); tokio::spawn(async move { - let result = thread.read_mcp_resource(&server, &uri).await; + let result = thread.read_mcp_resource(&server, &uri, meta).await; Self::send_mcp_resource_read_response(outgoing, request_id, result).await; }); return Ok(()); @@ -395,6 +396,7 @@ impl McpRequestProcessor { runtime_context, &server, &uri, + meta, ) .await .and_then(|result| serde_json::to_value(result).map_err(anyhow::Error::from)); diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index fbe43e7689b3..c4a63684a5e8 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -827,6 +827,18 @@ impl TestAppServer { self.send_request("mcpServer/resource/read", params).await } + /// Send an `mcpServer/resource/read` JSON-RPC request with protocol metadata. + pub async fn send_mcp_resource_read_request_with_meta( + &mut self, + params: McpResourceReadParams, + meta: serde_json::Value, + ) -> anyhow::Result { + let mut params = serde_json::to_value(params)?; + params["_meta"] = meta; + self.send_request("mcpServer/resource/read", Some(params)) + .await + } + /// Send an `mcpServer/tool/call` JSON-RPC request. pub async fn send_mcp_server_tool_call_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs index 98b0fa351296..8c876d951006 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::sync::Mutex; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::time::Duration; @@ -84,9 +85,9 @@ const SKILLS_READ_CALL_ID: &str = "skills-read"; const SKILLS_READ_AGAIN_CALL_ID: &str = "skills-read-again"; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn mcp_resource_read_returns_resource_contents() -> Result<()> { +async fn mcp_resource_read_forwards_meta() -> Result<()> { let responses_server = responses::start_mock_server().await; - let (apps_server_url, _apps_server_calls, apps_server_handle) = + let (apps_server_url, apps_server_calls, apps_server_handle) = start_resource_apps_mcp_server().await?; let responses_server_uri = responses_server.uri(); let (_codex_home, mut mcp) = @@ -105,12 +106,17 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> { .await??; let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?; + let expected_meta = resource_read_request_meta(); let read_request_id = mcp - .send_mcp_resource_read_request(McpResourceReadParams { - thread_id: Some(thread.id), - server: "codex_apps".to_string(), - uri: TEST_RESOURCE_URI.to_string(), - }) + .send_mcp_resource_read_request_with_meta( + McpResourceReadParams { + thread_id: Some(thread.id), + server: "codex_apps".to_string(), + uri: TEST_RESOURCE_URI.to_string(), + meta: None, + }, + serde_json::to_value(&expected_meta)?, + ) .await?; let read_response: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -121,6 +127,7 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> { to_response::(read_response)?, expected_resource_read_response() ); + assert_eq!(apps_server_calls.resource_read_metas(), vec![expected_meta]); apps_server_handle.abort(); let _ = apps_server_handle.await; @@ -526,8 +533,8 @@ enabled = false } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn mcp_resource_read_returns_resource_contents_without_thread() -> Result<()> { - let (apps_server_url, _apps_server_calls, apps_server_handle) = +async fn mcp_resource_read_forwards_meta_without_thread() -> Result<()> { + let (apps_server_url, apps_server_calls, apps_server_handle) = start_resource_apps_mcp_server().await?; let codex_home = TempDir::new()?; @@ -555,12 +562,17 @@ apps = true let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let expected_meta = resource_read_request_meta(); let read_request_id = mcp - .send_mcp_resource_read_request(McpResourceReadParams { - thread_id: None, - server: "codex_apps".to_string(), - uri: TEST_RESOURCE_URI.to_string(), - }) + .send_mcp_resource_read_request_with_meta( + McpResourceReadParams { + thread_id: None, + server: "codex_apps".to_string(), + uri: TEST_RESOURCE_URI.to_string(), + meta: None, + }, + serde_json::to_value(&expected_meta)?, + ) .await?; let read_response: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -572,6 +584,7 @@ apps = true to_response::(read_response)?, expected_resource_read_response() ); + assert_eq!(apps_server_calls.resource_read_metas(), vec![expected_meta]); apps_server_handle.abort(); let _ = apps_server_handle.await; @@ -624,6 +637,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> { thread_id: Some("00000000-0000-4000-8000-000000000000".to_string()), server: "codex_apps".to_string(), uri: TEST_RESOURCE_URI.to_string(), + meta: None, }, }) .await; @@ -746,6 +760,7 @@ struct ResourceAppsMcpCalls { list_resources: AtomicUsize, main_prompt_reads: AtomicUsize, reference_reads: AtomicUsize, + resource_read_metas: Mutex>, } impl ResourceAppsMcpCalls { @@ -756,6 +771,13 @@ impl ResourceAppsMcpCalls { reference_reads: self.reference_reads.load(Ordering::Relaxed), } } + + fn resource_read_metas(&self) -> Vec { + self.resource_read_metas + .lock() + .expect("resource read metadata lock poisoned") + .clone() + } } #[derive(Debug, PartialEq, Eq)] @@ -827,8 +849,17 @@ impl ServerHandler for ResourceAppsMcpServer { async fn read_resource( &self, request: ReadResourceRequestParams, - _context: RequestContext, + context: RequestContext, ) -> Result { + let mut meta = context.meta; + // rmcp adds a transport progress token to outgoing requests. Capture + // only the client-supplied metadata that this test is exercising. + meta.0.remove("progressToken"); + self.calls + .resource_read_metas + .lock() + .expect("resource read metadata lock poisoned") + .push(meta); let uri = request.uri; if uri == SKILL_MAIN_PROMPT_URI { self.calls.main_prompt_reads.fetch_add(1, Ordering::Relaxed); @@ -899,3 +930,10 @@ fn skill_resource_meta(plugin_name: &str, skill_name: &str) -> Meta { ("skill_name".to_string(), json!(skill_name)), ])) } + +fn resource_read_request_meta() -> Meta { + Meta(serde_json::Map::from_iter([ + ("traceId".to_string(), json!("trace-123")), + ("context".to_string(), json!({ "source": "app-server" })), + ])) +} diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index f2c8f1d02009..e1d646f710cb 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -11,6 +11,7 @@ pub use auth::should_retry_without_scopes; pub(crate) mod auth; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::env; @@ -36,6 +37,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; use rmcp::model::ElicitationCapability; +use rmcp::model::Meta; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; use serde_json::Value; @@ -301,6 +303,7 @@ pub async fn read_mcp_resource( runtime_context: McpRuntimeContext, server: &str, uri: &str, + meta: Option>, ) -> anyhow::Result { let mut mcp_servers = effective_mcp_servers(config, auth); mcp_servers.retain(|name, _| name == server); @@ -336,9 +339,12 @@ pub async fn read_mcp_resource( ) .await; - let result = manager - .read_resource(server, ReadResourceRequestParams::new(uri)) - .await; + let params = if let Some(meta) = meta { + ReadResourceRequestParams::new(uri).with_meta(Meta(meta.into_iter().collect())) + } else { + ReadResourceRequestParams::new(uri) + }; + let result = manager.read_resource(server, params).await; cancel_token.cancel(); result } diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 0cd1ca25b0dd..4631e72ff92f 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -44,6 +44,7 @@ use codex_thread_store::ThreadStoreResult; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; +use rmcp::model::Meta; use rmcp::model::ReadResourceRequestParams; use std::collections::BTreeMap; use std::collections::HashMap; @@ -607,12 +608,14 @@ impl CodexThread { &self, server: &str, uri: &str, + meta: Option>, ) -> anyhow::Result { - let result = self - .codex - .session - .read_resource(server, ReadResourceRequestParams::new(uri)) - .await?; + let params = if let Some(meta) = meta { + ReadResourceRequestParams::new(uri).with_meta(Meta(meta.into_iter().collect())) + } else { + ReadResourceRequestParams::new(uri) + }; + let result = self.codex.session.read_resource(server, params).await?; Ok(serde_json::to_value(result)?) } From a0c925bf3a8398747a6d54d527b0db38100a4498 Mon Sep 17 00:00:00 2001 From: Drew Schuster Date: Wed, 24 Jun 2026 20:52:24 -0700 Subject: [PATCH 2/2] chore(app-server): regenerate protocol schemas --- .../app-server-protocol/schema/json/ClientRequest.json | 7 +++++++ .../schema/json/codex_app_server_protocol.schemas.json | 7 +++++++ .../schema/json/codex_app_server_protocol.v2.schemas.json | 7 +++++++ .../schema/json/v2/McpResourceReadParams.json | 7 +++++++ .../schema/typescript/v2/McpResourceReadParams.ts | 3 ++- 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 68c242504f7d..5cc783fd8661 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1575,6 +1575,13 @@ }, "McpResourceReadParams": { "properties": { + "_meta": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, "server": { "type": "string" }, 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 4d417ffb2f53..175fe896afe4 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 @@ -11924,6 +11924,13 @@ "McpResourceReadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "_meta": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, "server": { "type": "string" }, 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 0df923a0f4e6..e60e6807d4a8 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 @@ -8328,6 +8328,13 @@ "McpResourceReadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "_meta": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, "server": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json index 2fe58155d28e..04d4aee7d15c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json @@ -1,6 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "_meta": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, "server": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts index c48795f27e88..9c23c9b89ba0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts @@ -1,5 +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 McpResourceReadParams = { threadId?: string | null, server: string, uri: string, }; +export type McpResourceReadParams = { threadId?: string | null, server: string, uri: string, _meta?: { [key in string]?: JsonValue } | null, };