mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
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:
@@ -1912,6 +1912,12 @@
|
||||
},
|
||||
"McpResourceReadParams": {
|
||||
"properties": {
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"originCallId": {
|
||||
"description": "Originating MCP tool call used to select the resource's app.",
|
||||
"type": [
|
||||
|
||||
+6
@@ -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": [
|
||||
|
||||
+6
@@ -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": [
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user