Scope MCP resource reads by connector (#39244)

## What changed

- Add an optional `connectorId` to `mcpServer/resource/read`.
- Forward the selected connector through `x-codex-turn-metadata` for threaded and threadless resource reads, restricting hosted app resources to that connector.
- Update the generated protocol schemas and app-server documentation.

GitOrigin-RevId: 5cc94d703abbbb6086665910c1c9ed5dc7075210
This commit is contained in:
victor-openai
2026-08-18 18:01:39 +00:00
committed by copyberry
parent 19d185fec8
commit a1dc95d5af
17 changed files with 69 additions and 12 deletions
@@ -1912,6 +1912,12 @@
},
"McpResourceReadParams": {
"properties": {
"connectorId": {
"type": [
"string",
"null"
]
},
"originCallId": {
"description": "Originating MCP tool call used to select the resource's app.",
"type": [
@@ -13295,6 +13295,12 @@
"McpResourceReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"connectorId": {
"type": [
"string",
"null"
]
},
"originCallId": {
"description": "Originating MCP tool call used to select the resource's app.",
"type": [
@@ -9525,6 +9525,12 @@
"McpResourceReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"connectorId": {
"type": [
"string",
"null"
]
},
"originCallId": {
"description": "Originating MCP tool call used to select the resource's app.",
"type": [
@@ -1,6 +1,12 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"connectorId": {
"type": [
"string",
"null"
]
},
"originCallId": {
"description": "Originating MCP tool call used to select the resource's app.",
"type": [
@@ -6,4 +6,4 @@ export type McpResourceReadParams = { threadId?: string | null,
/**
* Originating MCP tool call used to select the resource's app.
*/
originCallId?: string | null, server: string, uri: string, };
originCallId?: string | null, server: string, uri: string, connectorId?: string | null, };
@@ -2331,6 +2331,7 @@ mod tests {
origin_call_id: None,
server: "server-a".to_string(),
uri: "file:///tmp/resource".to_string(),
connector_id: None,
},
};
assert_eq!(
@@ -2519,6 +2520,7 @@ mod tests {
origin_call_id: None,
server: "server-a".to_string(),
uri: "file:///tmp/resource".to_string(),
connector_id: None,
},
};
assert_eq!(mcp_resource_read.serialization_scope(), None);
@@ -90,6 +90,8 @@ pub struct McpResourceReadParams {
pub origin_call_id: Option<String>,
pub server: String,
pub uri: String,
#[ts(optional = nullable)]
pub connector_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+1 -1
View File
@@ -278,7 +278,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, owning `pluginId` (`null` for servers not contributed by a plugin), 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`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. Pass `originCallId` with `threadId` to scope a Codex app widget to the app and account of the completed tool call that produced it; successful scoped reads return the same `originCallId`. 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`, and `uri`, returning text/blob resource `contents`. Pass `originCallId` with `threadId` to scope a Codex app widget to the app and account of the completed tool call that produced it; successful scoped reads return the same `originCallId`. Optional `connectorId` restricts other hosted app resources to their originating connector. 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.
@@ -1,6 +1,7 @@
use super::*;
use codex_core::McpManager;
use codex_mcp::McpServerSource;
use codex_mcp::ReadResourceRequestParams;
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
@@ -425,7 +426,22 @@ impl McpRequestProcessor {
origin_call_id,
server,
uri,
connector_id,
} = params;
let mut resource_params = ReadResourceRequestParams::new(uri);
if let Some(connector_id) = connector_id {
resource_params.meta = Some(
serde_json::Map::from_iter([(
"x-codex-turn-metadata".to_string(),
serde_json::json!({
"mcp_request_meta": {
"selected_connector_ids": [connector_id],
},
}),
)])
.into(),
);
}
if let Some(thread_id) = thread_id {
let (_, thread) = self.load_thread(&thread_id).await?;
@@ -435,8 +451,12 @@ impl McpRequestProcessor {
let origin_call_id =
origin_call_id.filter(|_| server == codex_mcp::CODEX_APPS_MCP_SERVER_NAME);
let result = match origin_call_id.as_deref() {
Some(call_id) => thread.read_mcp_resource_for_call(call_id, &uri).await,
None => thread.read_mcp_resource(&server, &uri).await,
Some(call_id) => {
thread
.read_mcp_resource_for_call(call_id, &resource_params.uri)
.await
}
None => thread.read_mcp_resource(&server, resource_params).await,
};
Self::send_mcp_resource_read_response(outgoing, request_id, result, origin_call_id)
.await;
@@ -470,7 +490,7 @@ impl McpRequestProcessor {
codex_apps_tools_cache,
tool_catalog_cache,
&server,
&uri,
resource_params,
)
.await
.and_then(|result| serde_json::to_value(result).map_err(anyhow::Error::from));
@@ -124,6 +124,7 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
origin_call_id: None,
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
connector_id: None,
},
})
.await?;
@@ -597,6 +598,7 @@ apps = true
origin_call_id: None,
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
connector_id: None,
},
})
.await?;
@@ -609,6 +611,7 @@ apps = true
origin_call_id: None,
server: "codex_apps".to_string(),
uri: TEST_ELICITATION_RESOURCE_URI.to_string(),
connector_id: None,
},
})
.await?;
@@ -677,6 +680,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
origin_call_id: None,
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
connector_id: None,
},
})
.await;
@@ -167,6 +167,7 @@ async fn widget_reads_survive_history_modes_compaction_restarts_and_app_only_vis
origin_call_id: Some(call_id.to_string()),
server: "codex_apps".to_string(),
uri: uri.to_string(),
connector_id: None,
})
.await?;
let error = timeout(
@@ -289,6 +290,7 @@ async fn read_widget(
origin_call_id: Some(call_id.to_string()),
server: "codex_apps".to_string(),
uri: TEST_WIDGET_RESOURCE_URI.to_string(),
connector_id: None,
},
})
.await
+1
View File
@@ -15,6 +15,7 @@ pub use resource_client::McpResourceClient;
pub use resource_client::McpResourceClientCacheKey;
pub use resource_client::McpResourcePage;
pub use resource_client::McpResourceReadResult;
pub use rmcp::model::ReadResourceRequestParams;
pub use rmcp_client::MCP_SANDBOX_STATE_META_CAPABILITY;
pub use runtime::McpRuntime;
pub use runtime::McpRuntimeContext;
+2 -4
View File
@@ -349,7 +349,7 @@ pub async fn read_mcp_resource(
codex_apps_tools_cache: ConnectorRuntimeManager<ToolInfo>,
tool_catalog_cache: crate::McpToolCatalogCache,
server: &str,
uri: &str,
params: ReadResourceRequestParams,
) -> anyhow::Result<ReadResourceResult> {
let mut mcp_servers = effective_mcp_servers(config, auth);
mcp_servers.retain(|name, _| name == server);
@@ -382,9 +382,7 @@ pub async fn read_mcp_resource(
)
.await;
let result = manager
.read_resource(server, ReadResourceRequestParams::new(uri))
.await;
let result = manager.read_resource(server, params).await;
cancel_token.cancel();
result
}
+2 -2
View File
@@ -734,14 +734,14 @@ impl CodexThread {
pub async fn read_mcp_resource(
&self,
server: &str,
uri: &str,
params: ReadResourceRequestParams,
) -> anyhow::Result<serde_json::Value> {
self.session.refresh_mcp_if_dirty().await;
let result = self
.session
.services
.mcp_runtime
.latest_read_resource(server, ReadResourceRequestParams::new(uri))
.latest_read_resource(server, params)
.await?;
Ok(serde_json::to_value(result)?)
@@ -49,6 +49,7 @@ use core_test_support::skip_if_no_network;
use core_test_support::wait_for_event;
use core_test_support::wait_for_mcp_server;
use pretty_assertions::assert_eq;
use rmcp::model::ReadResourceRequestParams;
use serde::Deserialize;
use serde_json::Value;
use serde_json::json;
@@ -597,7 +598,10 @@ startup_timeout_sec = 0.1
let _ = test
.codex
.read_mcp_resource("refreshed", "test://resource")
.read_mcp_resource(
"refreshed",
ReadResourceRequestParams::new("test://resource"),
)
.await;
assert!(resource_client.has_server("refreshed").await);
Ok(())