Forward caller metadata on MCP resource reads (#59509)

## What changed

Accept an optional `_meta` object on `mcpServer/resource/read` and forward it to MCP `resources/read`, including reads bound to an originating tool call. Update the protocol schemas and generated TypeScript and Python types.

Reject caller metadata containing host-owned `threadId`, `x-codex-turn-metadata`, or `codex/connectionLease` keys. For `codex_apps`, also reserve `connector_id`, `link_id`, and `_codex_apps.resource_uri` to preserve app and account routing.

## Testing

Add integration coverage for metadata forwarding through stdio, hosted app reads with and without a thread, and origin-bound widget reads. Cover rejection of reserved keys and non-object metadata.

GitOrigin-RevId: 227ee0fb5fe15fa2ff2ec66d4d5ae16de8935a8c
This commit is contained in:
Joe Savona
2026-11-04 22:55:22 +00:00
committed by copyberry
parent ad5e6e57a1
commit aa1399cbcd
22 changed files with 279 additions and 40 deletions
+2 -1
View File
@@ -39,7 +39,8 @@ use crate::tool_catalog_cache::McpToolCatalogCacheContext;
/// Opt-in resource lifecycle for negotiated stdio connections. Tool calls and resource reads
/// carry a host-owned lease ID. Catalog requests remain independent of a particular lease.
pub(crate) const LEASE_CAPABILITY: &str = "codex/connection-lease";
pub(crate) const LEASE_META_KEY: &str = "codex/connectionLease";
/// Request metadata set by the host when a connection lease is negotiated.
pub const LEASE_META_KEY: &str = "codex/connectionLease";
const LEASE_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone)]
+1
View File
@@ -10,6 +10,7 @@ pub use codex_rmcp_client::McpProtocolMode;
pub use connection_manager::NetworkPolicyMetadata;
pub use connection_manager::NetworkPolicyMetadataProvider;
pub use connection_manager::tool_is_model_visible;
pub use connection_pool::LEASE_META_KEY as MCP_CONNECTION_LEASE_META_KEY;
pub use connection_pool::McpConnectionPool;
pub use elicitation::ElicitationLifecycle;
pub use elicitation::ElicitationReviewRequest;
+10 -11
View File
@@ -249,9 +249,9 @@ impl ResourceOrigin {
&self,
binding: &McpBinding,
thread_id: ThreadId,
uri: &str,
mut params: ReadResourceRequestParams,
) -> anyhow::Result<ReadResourceResult> {
if self.uri != uri {
if self.uri != params.uri {
anyhow::bail!("originating MCP tool call does not match the requested resource");
}
if self.ambiguous_account {
@@ -297,20 +297,19 @@ impl ResourceOrigin {
anyhow::bail!("originating MCP tool is disabled by app configuration");
}
let meta = serde_json::from_value(serde_json::json!({
"threadId": thread_id,
"x-codex-turn-metadata": {
let meta = params.meta.get_or_insert_default();
meta.insert("threadId".into(), serde_json::json!(thread_id));
meta.insert(
"x-codex-turn-metadata".into(),
serde_json::json!({
"mcp_request_meta": {
"selected_connector_ids": [&self.connector_id],
"link_id": &self.link_id,
}
},
}))?;
}),
);
binding
.read_resource(
CODEX_APPS_MCP_SERVER_NAME,
ReadResourceRequestParams::new(uri).with_meta(meta),
)
.read_resource(CODEX_APPS_MCP_SERVER_NAME, params)
.await
}
}
+2 -2
View File
@@ -329,7 +329,7 @@ impl McpRuntime {
&self,
thread_id: ThreadId,
call_id: &str,
uri: &str,
params: ReadResourceRequestParams,
) -> anyhow::Result<ReadResourceResult> {
let origin = self
.resource_origins
@@ -341,7 +341,7 @@ impl McpRuntime {
.await
.ok_or_else(|| anyhow::anyhow!("codex_apps MCP server is unavailable"))?;
origin.read(&binding, thread_id, uri).await
origin.read(&binding, thread_id, params).await
}
pub async fn new(input: McpRuntimeInput) -> Self {