diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f5b929696b..571498de0b 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1213,12 +1213,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "type": "object" 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 73969a0cec..7db434e5b6 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 @@ -8785,12 +8785,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "title": "McpResourceReadParams", 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 e6b4583732..7f453e3b43 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 @@ -5599,12 +5599,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "title": "McpResourceReadParams", 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 7ea633b586..0242d4148b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/McpResourceReadParams.json @@ -4,12 +4,16 @@ "server": { "type": "string" }, + "threadId": { + "type": "string" + }, "uri": { "type": "string" } }, "required": [ "server", + "threadId", "uri" ], "title": "McpResourceReadParams", 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 c58bf8499a..51d650d9bb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpResourceReadParams.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type McpResourceReadParams = { server: string, uri: string, }; +export type McpResourceReadParams = { threadId: string, server: string, uri: string, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 0f7c826cc4..d3cc159104 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1972,6 +1972,7 @@ pub struct ListMcpServerStatusResponse { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct McpResourceReadParams { + pub thread_id: String, pub server: String, pub uri: String, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index f8121553c0..ce766705ad 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -187,7 +187,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, resources, resource templates, and auth status; supports cursor+limit pagination. -- `mcpResource/read` — read a resource from a configured MCP server by `server` and `uri`, returning the resource `contents`. +- `mcpResource/read` — read a resource from a thread's configured MCP server by `threadId`, `server`, and `uri`, returning the resource `contents`. - `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. - `config/read` — fetch the effective config on disk after resolving config layering. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d74ad32ca1..4bebe8a1be 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -224,7 +224,6 @@ use codex_core::mcp::auth::discover_supported_scopes; use codex_core::mcp::auth::resolve_oauth_scopes; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; -use codex_core::mcp::read_mcp_resource; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::plugins::MarketplaceError; @@ -5141,8 +5140,8 @@ impl CodexMessageProcessor { params: McpResourceReadParams, ) { let outgoing = Arc::clone(&self.outgoing); - let config = match self.load_latest_config(/*fallback_cwd*/ None).await { - Ok(config) => config, + let (_, thread) = match self.load_thread(¶ms.thread_id).await { + Ok(thread) => thread, Err(error) => { self.outgoing.send_error(request_id, error).await; return; @@ -5150,7 +5149,7 @@ impl CodexMessageProcessor { }; tokio::spawn(async move { - let result = read_mcp_resource(&config, ¶ms.server, ¶ms.uri).await; + let result = thread.read_mcp_resource(¶ms.server, ¶ms.uri).await; match result { Ok(result) => match serde_json::from_value::(result) { Ok(response) => { diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 8b467fc10b..5ead090f2c 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -22,6 +22,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::UserInput; +use rmcp::model::ReadResourceRequestParams; use std::path::PathBuf; use tokio::sync::Mutex; use tokio::sync::watch; @@ -198,6 +199,26 @@ impl CodexThread { self.codex.thread_config_snapshot().await } + pub async fn read_mcp_resource( + &self, + server: &str, + uri: &str, + ) -> anyhow::Result { + let result = self + .codex + .session + .read_resource( + server, + ReadResourceRequestParams { + meta: None, + uri: uri.to_string(), + }, + ) + .await?; + + Ok(serde_json::to_value(result)?) + } + pub fn enabled(&self, feature: Feature) -> bool { self.codex.enabled(feature) } diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index c05c2fd853..afeb5647ea 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -14,20 +14,16 @@ use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; -use rmcp::model::ReadResourceRequestParams; use serde_json::Value; -use tokio_util::sync::CancellationToken; use crate::AuthManager; use crate::CodexAuth; use crate::config::Config; use crate::config::types::McpServerConfig; use crate::config::types::McpServerTransportConfig; -use crate::mcp::auth::McpAuthStatusEntry; use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::SandboxState; -use crate::mcp_connection_manager::ToolInfo; use crate::mcp_connection_manager::codex_apps_tools_cache_key; use crate::plugins::PluginCapabilitySummary; use crate::plugins::PluginsManager; @@ -281,52 +277,6 @@ fn effective_mcp_servers( } pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent { - let (mcp_connection_manager, cancel_token, auth_status_entries) = - new_mcp_connection_manager(config).await; - if !mcp_connection_manager.has_servers() { - cancel_token.cancel(); - return McpListToolsResponseEvent { - tools: HashMap::new(), - resources: HashMap::new(), - resource_templates: HashMap::new(), - auth_statuses: HashMap::new(), - }; - } - - let snapshot = - collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; - - cancel_token.cancel(); - - snapshot -} - -pub async fn read_mcp_resource(config: &Config, server: &str, uri: &str) -> anyhow::Result { - let (mcp_connection_manager, cancel_token, _auth_status_entries) = - new_mcp_connection_manager(config).await; - - let result = mcp_connection_manager - .read_resource( - server, - ReadResourceRequestParams { - meta: None, - uri: uri.to_string(), - }, - ) - .await; - - cancel_token.cancel(); - - Ok(serde_json::to_value(result?)?) -} - -async fn new_mcp_connection_manager( - config: &Config, -) -> ( - McpConnectionManager, - CancellationToken, - HashMap, -) { let auth_manager = AuthManager::shared( config.codex_home.clone(), /*enable_codex_api_key_env*/ false, @@ -337,13 +287,12 @@ async fn new_mcp_connection_manager( let mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()); let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config); if mcp_servers.is_empty() { - let cancel_token = CancellationToken::new(); - cancel_token.cancel(); - return ( - McpConnectionManager::new_uninitialized(&config.permissions.approval_policy), - cancel_token, - HashMap::new(), - ); + return McpListToolsResponseEvent { + tools: HashMap::new(), + resources: HashMap::new(), + resource_templates: HashMap::new(), + auth_statuses: HashMap::new(), + }; } let auth_status_entries = @@ -352,6 +301,7 @@ async fn new_mcp_connection_manager( let (tx_event, rx_event) = unbounded(); drop(rx_event); + // Use ReadOnly sandbox policy for MCP snapshot collection (safest default) let sandbox_state = SandboxState { sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), @@ -372,7 +322,12 @@ async fn new_mcp_connection_manager( ) .await; - (mcp_connection_manager, cancel_token, auth_status_entries) + let snapshot = + collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; + + cancel_token.cancel(); + + snapshot } pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String)> { @@ -404,21 +359,6 @@ pub fn group_tools_by_server( grouped } -fn get_mcp_snapshot_tool_name(tool: &ToolInfo) -> String { - if tool.server_name != CODEX_APPS_MCP_SERVER_NAME { - format!( - "{}{}{}{}{}", - MCP_TOOL_NAME_PREFIX, - MCP_TOOL_NAME_DELIMITER, - tool.server_name, - MCP_TOOL_NAME_DELIMITER, - tool.tool_name - ) - } else { - format!("{}{}", tool.tool_namespace, tool.tool_name) - } -} - pub(crate) async fn collect_mcp_snapshot_from_manager( mcp_connection_manager: &McpConnectionManager, auth_status_entries: HashMap, @@ -436,22 +376,16 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( let tools = tools .into_iter() - .filter_map(|(_name, tool)| match serde_json::to_value(&tool.tool) { + .filter_map(|(name, tool)| match serde_json::to_value(tool.tool) { Ok(value) => match Tool::from_mcp_value(value) { - Ok(tool_value) => Some((get_mcp_snapshot_tool_name(&tool), tool_value)), + Ok(tool) => Some((name, tool)), Err(err) => { - tracing::warn!( - "Failed to convert MCP tool '{}': {err}", - get_mcp_snapshot_tool_name(&tool) - ); + tracing::warn!("Failed to convert MCP tool '{name}': {err}"); None } }, Err(err) => { - tracing::warn!( - "Failed to serialize MCP tool '{}': {err}", - get_mcp_snapshot_tool_name(&tool) - ); + tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); None } })