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 c2a157f9dd..16d5193ba8 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 @@ -13227,6 +13227,12 @@ "name": { "type": "string" }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, "resourceTemplates": { "items": { "$ref": "#/definitions/v2/ResourceTemplate" 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 adb7afe132..9bfeaae430 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 @@ -9564,6 +9564,12 @@ "name": { "type": "string" }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, "resourceTemplates": { "items": { "$ref": "#/definitions/ResourceTemplate" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json index f30c1cc214..d0345a8643 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json @@ -60,6 +60,12 @@ "name": { "type": "string" }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, "resourceTemplates": { "items": { "$ref": "#/definitions/ResourceTemplate" diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 79ff2a4489..ba19ae47ce 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 0407afe6b5..e664e871fd 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts index d2e99ce96f..ca84669094 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts @@ -7,4 +7,4 @@ import type { ResourceTemplate } from "../ResourceTemplate"; import type { Tool } from "../Tool"; import type { McpAuthStatus } from "./McpAuthStatus"; -export type McpServerStatus = { name: string, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array, resourceTemplates: Array, authStatus: McpAuthStatus, }; +export type McpServerStatus = { name: string, pluginId: string | null, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array, resourceTemplates: Array, authStatus: McpAuthStatus, }; 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 979d92ecb2..8ebbe21418 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -61,6 +61,7 @@ pub enum McpServerStatusDetail { #[ts(export_to = "v2/")] pub struct McpServerStatus { pub name: String, + pub plugin_id: Option, pub server_info: Option, pub tools: std::collections::HashMap, pub resources: Vec, 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 3476094e46..dbbe186f68 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2422,6 +2422,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() { let response = ListMcpServerStatusResponse { data: vec![McpServerStatus { name: "not-ready".to_string(), + plugin_id: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), @@ -2436,6 +2437,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() { json!({ "data": [{ "name": "not-ready", + "pluginId": null, "serverInfo": null, "tools": {}, "resources": [], @@ -2507,6 +2509,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() { let response = ListMcpServerStatusResponse { data: vec![McpServerStatus { name: "initialized".to_string(), + plugin_id: Some("lookup@test".to_string()), server_info: Some(McpServerInfo { name: "lookup-server".to_string(), title: None, @@ -2528,6 +2531,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() { json!({ "data": [{ "name": "initialized", + "pluginId": "lookup@test", "serverInfo": { "name": "lookup-server", "title": null, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 3616f7e55b..1e06484675 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -267,7 +267,7 @@ Example with notification opt-out: - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes. - `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`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported. +- `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`. 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`. 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 a6281e2227..fbdc8e8dde 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -1,5 +1,6 @@ use super::*; use codex_core::McpManager; +use codex_mcp::McpServerSource; const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId"; @@ -374,6 +375,17 @@ impl McpRequestProcessor { .iter() .map(|name| McpServerStatus { name: name.clone(), + plugin_id: mcp_config.mcp_server_catalog.server(name).and_then( + |server| match server.source() { + McpServerSource::Plugin(plugin) + | McpServerSource::SelectedPlugin(plugin) => { + Some(plugin.plugin_id().to_owned()) + } + McpServerSource::Config + | McpServerSource::Compatibility { .. } + | McpServerSource::Extension { .. } => None, + }, + ), server_info: server_infos.get(name).cloned(), tools: tools_by_server.get(name).cloned().unwrap_or_default(), resources: resources.get(name).cloned().unwrap_or_default(), diff --git a/codex-rs/app-server/tests/suite/v2/executor_mcp.rs b/codex-rs/app-server/tests/suite/v2/executor_mcp.rs index b02a6d2ccb..b452849b97 100644 --- a/codex-rs/app-server/tests/suite/v2/executor_mcp.rs +++ b/codex-rs/app-server/tests/suite/v2/executor_mcp.rs @@ -11,6 +11,7 @@ use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::ListMcpServerStatusResponse; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::McpServerOauthLoginResponse; +use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::McpServerToolCallResponse; use codex_app_server_protocol::RequestId; @@ -40,6 +41,7 @@ use rmcp::transport::StreamableHttpService; use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; use serde_json::json; use std::borrow::Cow; +use std::collections::BTreeMap; use std::sync::Arc; use std::time::Duration; use tempfile::TempDir; @@ -447,26 +449,37 @@ startup_timeout_sec = 10 Some(json!("ECHOING: refresh applied")) ); - let selected_server_names = mcp_server_names(&mut app_server, selected_thread).await?; - assert!( - selected_server_names - .iter() - .any(|name| name == MCP_SERVER_NAME) - ); - assert!( - selected_server_names - .iter() - .any(|name| name == HTTP_MCP_SERVER_NAME) - ); - assert!( - selected_server_names - .iter() - .any(|name| name == OAUTH_MCP_SERVER_NAME) + let selected_server_owners = mcp_server_statuses(&mut app_server, selected_thread) + .await? + .into_iter() + .map(|server| (server.name, server.plugin_id)) + .collect::>(); + assert_eq!( + selected_server_owners, + BTreeMap::from([ + ( + HTTP_MCP_SERVER_NAME.to_string(), + Some("executor-demo@1".to_string()), + ), + ( + MCP_SERVER_NAME.to_string(), + Some("executor-demo@1".to_string()), + ), + ( + OAUTH_MCP_SERVER_NAME.to_string(), + Some("executor-demo@1".to_string()), + ), + (REFRESH_PROBE_SERVER_NAME.to_string(), None), + ]) ); let unselected_thread = start_thread(&mut app_server, /*selected_capability_roots*/ None).await?; - let unselected_server_names = mcp_server_names(&mut app_server, unselected_thread).await?; + let unselected_server_names = mcp_server_statuses(&mut app_server, unselected_thread) + .await? + .into_iter() + .map(|server| server.name) + .collect::>(); assert!(unselected_server_names.iter().all(|name| { name != MCP_SERVER_NAME && name != HTTP_MCP_SERVER_NAME && name != OAUTH_MCP_SERVER_NAME })); @@ -525,10 +538,10 @@ impl ServerHandler for ExecutorHttpMcpServer { } } -async fn mcp_server_names( +async fn mcp_server_statuses( app_server: &mut TestAppServer, thread_id: String, -) -> Result> { +) -> Result> { let request_id = app_server .send_list_mcp_server_status_request(ListMcpServerStatusParams { cursor: None, @@ -539,11 +552,7 @@ async fn mcp_server_names( .await?; let response: ListMcpServerStatusResponse = timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??; - Ok(response - .data - .into_iter() - .map(|server| server.name) - .collect()) + Ok(response.data) } async fn start_thread( diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs index 47504f6d6f..86d39f1cdb 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs @@ -112,6 +112,7 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<() assert_eq!(response.data.len(), 1); let status = &response.data[0]; assert_eq!(status.name, "some-server"); + assert_eq!(status.plugin_id, None); assert_eq!( status.tools.keys().cloned().collect::>(), BTreeSet::from(["look-up.raw".to_string()]) diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 1eaea4ff51..b7db1407cb 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -25,6 +25,8 @@ use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppSummary; use codex_app_server_protocol::AppsListParams; use codex_app_server_protocol::AppsListResponse; +use codex_app_server_protocol::ListMcpServerStatusParams; +use codex_app_server_protocol::ListMcpServerStatusResponse; use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginAvailability; use codex_app_server_protocol::PluginInstallParams; @@ -32,6 +34,7 @@ use codex_app_server_protocol::PluginInstallResponse; use codex_app_server_protocol::RequestId; use codex_config::types::AuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; +use core_test_support::stdio_server_bin; use flate2::Compression; use flate2::write::GzEncoder; use pretty_assertions::assert_eq; @@ -2264,13 +2267,13 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests write_plugin_source(repo_root.path(), "sample-plugin", &[])?; std::fs::write( repo_root.path().join("sample-plugin/.mcp.json"), - r#"{ - "mcpServers": { - "sample-mcp": { - "command": "echo" - } - } -}"#, + serde_json::to_vec(&json!({ + "mcpServers": { + "sample-mcp": { + "command": stdio_server_bin()?, + } + } + }))?, )?; let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; @@ -2294,7 +2297,28 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests assert_eq!(response.apps_needing_auth, Vec::::new()); let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; assert!(!config.contains("[mcp_servers.sample-mcp]")); - assert!(!config.contains("command = \"echo\"")); + + let request_id = mcp + .send_list_mcp_server_status_request(ListMcpServerStatusParams { + cursor: None, + limit: None, + detail: None, + thread_id: None, + }) + .await?; + let response: ListMcpServerStatusResponse = + timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; + assert_eq!( + response + .data + .into_iter() + .map(|server| (server.name, server.plugin_id)) + .collect::>(), + vec![( + "sample-mcp".to_string(), + Some("sample-plugin@debug".to_string()), + )] + ); let request_id = mcp .send_raw_request( diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index c590acfd5f..72a7df5ccf 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -1443,6 +1443,7 @@ mod tests { let statuses = vec![ McpServerStatus { name: "docs".to_string(), + plugin_id: None, server_info: None, tools: HashMap::from([( "list".to_string(), @@ -1463,6 +1464,7 @@ mod tests { }, McpServerStatus { name: "disabled".to_string(), + plugin_id: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 1057fb8267..8507e97cc7 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -225,6 +225,7 @@ async fn handle_mcp_inventory_result_respects_origin_thread() { app.handle_mcp_inventory_result( Ok(vec![McpServerStatus { name: "docs".to_string(), + plugin_id: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 1a94689e13..e81efd2024 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -964,6 +964,7 @@ async fn mcp_tools_output_lists_tools_for_hyphenated_server_names() { fn mcp_tools_output_from_statuses_renders_status_only_servers() { let statuses = vec![McpServerStatus { name: "plugin_docs".to_string(), + plugin_id: None, server_info: None, tools: HashMap::from([( "lookup".to_string(), @@ -994,6 +995,7 @@ fn mcp_tools_output_from_statuses_renders_status_only_servers() { fn mcp_tools_output_from_statuses_renders_verbose_inventory() { let statuses = vec![McpServerStatus { name: "plugin_docs".to_string(), + plugin_id: None, server_info: None, tools: HashMap::from([( "lookup".to_string(),