diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 7c9441984425..a436323daa4a 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1042,6 +1042,17 @@ "null" ] }, + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerStatusDetail" + }, + { + "type": "null" + } + ], + "description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted." + }, "limit": { "description": "Optional page size; defaults to a server-defined value.", "format": "uint32", @@ -1235,6 +1246,13 @@ ], "type": "object" }, + "McpServerStatusDetail": { + "enum": [ + "full", + "toolsAndAuthOnly" + ], + "type": "string" + }, "MergeStrategy": { "enum": [ "replace", 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 a58990303236..313414e1e431 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 @@ -8604,6 +8604,17 @@ "null" ] }, + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/v2/McpServerStatusDetail" + }, + { + "type": "null" + } + ], + "description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted." + }, "limit": { "description": "Optional page size; defaults to a server-defined value.", "format": "uint32", @@ -9044,6 +9055,13 @@ ], "type": "object" }, + "McpServerStatusDetail": { + "enum": [ + "full", + "toolsAndAuthOnly" + ], + "type": "string" + }, "McpServerStatusUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { 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 f041f8aae839..8e6eb42aa317 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 @@ -5427,6 +5427,17 @@ "null" ] }, + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerStatusDetail" + }, + { + "type": "null" + } + ], + "description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted." + }, "limit": { "description": "Optional page size; defaults to a server-defined value.", "format": "uint32", @@ -5867,6 +5878,13 @@ ], "type": "object" }, + "McpServerStatusDetail": { + "enum": [ + "full", + "toolsAndAuthOnly" + ], + "type": "string" + }, "McpServerStatusUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusParams.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusParams.json index e78dbeac1690..52149d9fb89e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusParams.json @@ -1,5 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpServerStatusDetail": { + "enum": [ + "full", + "toolsAndAuthOnly" + ], + "type": "string" + } + }, "properties": { "cursor": { "description": "Opaque pagination cursor returned by a previous call.", @@ -8,6 +17,17 @@ "null" ] }, + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerStatusDetail" + }, + { + "type": "null" + } + ], + "description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted." + }, "limit": { "description": "Optional page size; defaults to a server-defined value.", "format": "uint32", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ListMcpServerStatusParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ListMcpServerStatusParams.ts index 05c02c19f818..8225c462b1c3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ListMcpServerStatusParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ListMcpServerStatusParams.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerStatusDetail } from "./McpServerStatusDetail"; export type ListMcpServerStatusParams = { /** @@ -10,4 +11,9 @@ cursor?: string | null, /** * Optional page size; defaults to a server-defined value. */ -limit?: number | null, }; +limit?: number | null, +/** + * Controls how much MCP inventory data to fetch for each server. + * Defaults to `Full` when omitted. + */ +detail?: McpServerStatusDetail | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusDetail.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusDetail.ts new file mode 100644 index 000000000000..ab97cc2f31d2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatusDetail.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerStatusDetail = "full" | "toolsAndAuthOnly"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 495572fd7831..8a16a3a4fd3a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -181,6 +181,7 @@ export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse" export type { McpServerRefreshResponse } from "./McpServerRefreshResponse"; export type { McpServerStartupState } from "./McpServerStartupState"; export type { McpServerStatus } from "./McpServerStatus"; +export type { McpServerStatusDetail } from "./McpServerStatusDetail"; export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification"; export type { McpToolCallError } from "./McpToolCallError"; export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index cdc78647a1b1..5e875190273d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1949,6 +1949,18 @@ pub struct ListMcpServerStatusParams { /// Optional page size; defaults to a server-defined value. #[ts(optional = nullable)] pub limit: Option, + /// Controls how much MCP inventory data to fetch for each server. + /// Defaults to `Full` when omitted. + #[ts(optional = nullable)] + pub detail: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum McpServerStatusDetail { + Full, + ToolsAndAuthOnly, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 3660ea02011b..e6de801bbb88 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -187,7 +187,7 @@ Example with notification opt-out: - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `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, resources, resource templates, and auth status; supports cursor+limit pagination. +- `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus optional resources/resource templates when requested; supports cursor+limit pagination. If `detail` is omitted, the server defaults to `full`. - `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 8d9dbe05edf5..cf9b0261d606 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -81,6 +81,7 @@ use codex_app_server_protocol::McpServerOauthLoginParams; use codex_app_server_protocol::McpServerOauthLoginResponse; use codex_app_server_protocol::McpServerRefreshResponse; use codex_app_server_protocol::McpServerStatus; +use codex_app_server_protocol::McpServerStatusDetail; use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; @@ -245,9 +246,10 @@ use codex_login::default_client::set_default_client_residency_requirement; use codex_login::login_with_api_key; use codex_login::request_device_code; use codex_login::run_login_server; +use codex_mcp::mcp::McpSnapshotDetail; use codex_mcp::mcp::auth::discover_supported_scopes; use codex_mcp::mcp::auth::resolve_oauth_scopes; -use codex_mcp::mcp::collect_mcp_snapshot; +use codex_mcp::mcp::collect_mcp_snapshot_with_detail; use codex_mcp::mcp::effective_mcp_servers; use codex_mcp::mcp::qualified_mcp_tool_name_prefix; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; @@ -5159,10 +5161,16 @@ impl CodexMessageProcessor { mcp_config: codex_mcp::mcp::McpConfig, auth: Option, ) { - let snapshot = collect_mcp_snapshot( + let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) { + McpServerStatusDetail::Full => McpSnapshotDetail::Full, + McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly, + }; + + let snapshot = collect_mcp_snapshot_with_detail( &mcp_config, auth.as_ref(), request_id.request_id.to_string(), + detail, ) .await; 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 8b3844b521c4..e637be237d5c 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 @@ -12,15 +12,20 @@ use app_test_support::write_mock_responses_config_toml; use axum::Router; use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::ListMcpServerStatusResponse; +use codex_app_server_protocol::McpServerStatusDetail; use codex_app_server_protocol::RequestId; use pretty_assertions::assert_eq; use rmcp::handler::server::ServerHandler; use rmcp::model::JsonObject; +use rmcp::model::ListResourceTemplatesResult; +use rmcp::model::ListResourcesResult; use rmcp::model::ListToolsResult; +use rmcp::model::PaginatedRequestParams; use rmcp::model::ServerCapabilities; use rmcp::model::ServerInfo; use rmcp::model::Tool; use rmcp::model::ToolAnnotations; +use rmcp::service::RequestContext; use rmcp::transport::StreamableHttpServerConfig; use rmcp::transport::StreamableHttpService; use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; @@ -64,6 +69,7 @@ url = "{mcp_server_url}/mcp" .send_list_mcp_server_status_request(ListMcpServerStatusParams { cursor: None, limit: None, + detail: None, }) .await?; let response = timeout( @@ -127,6 +133,133 @@ impl ServerHandler for McpStatusServer { } } +#[derive(Clone)] +struct SlowInventoryServer { + tool_name: Arc, +} + +impl ServerHandler for SlowInventoryServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + capabilities: ServerCapabilities::builder() + .enable_tools() + .enable_resources() + .build(), + ..ServerInfo::default() + } + } + + async fn list_tools( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + let input_schema: JsonObject = serde_json::from_value(json!({ + "type": "object", + "additionalProperties": false + })) + .map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?; + + let mut tool = Tool::new( + Cow::Owned(self.tool_name.as_ref().clone()), + Cow::Borrowed("Look up test data."), + Arc::new(input_schema), + ); + tool.annotations = Some(ToolAnnotations::new().read_only(true)); + + Ok(ListToolsResult { + tools: vec![tool], + next_cursor: None, + meta: None, + }) + } + + async fn list_resources( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + tokio::time::sleep(Duration::from_secs(2)).await; + Ok(ListResourcesResult { + resources: Vec::new(), + next_cursor: None, + meta: None, + }) + } + + async fn list_resource_templates( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + tokio::time::sleep(Duration::from_secs(2)).await; + Ok(ListResourceTemplatesResult { + resource_templates: Vec::new(), + next_cursor: None, + meta: None, + }) + } +} + +#[tokio::test] +async fn mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls() -> Result<()> { + let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; + let (mcp_server_url, mcp_server_handle) = start_slow_inventory_mcp_server("lookup").await?; + let codex_home = TempDir::new()?; + write_mock_responses_config_toml( + codex_home.path(), + &server.uri(), + &BTreeMap::new(), + /*auto_compact_limit*/ 1024, + /*requires_openai_auth*/ None, + "mock_provider", + "compact", + )?; + + let config_path = codex_home.path().join("config.toml"); + let mut config_toml = std::fs::read_to_string(&config_path)?; + config_toml.push_str(&format!( + r#" +[mcp_servers.some-server] +url = "{mcp_server_url}/mcp" +"# + )); + std::fs::write(config_path, config_toml)?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_list_mcp_server_status_request(ListMcpServerStatusParams { + cursor: None, + limit: None, + detail: Some(McpServerStatusDetail::ToolsAndAuthOnly), + }) + .await?; + let response = timeout( + Duration::from_millis(500), + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: ListMcpServerStatusResponse = to_response(response)?; + + assert_eq!(response.next_cursor, None); + assert_eq!(response.data.len(), 1); + let status = &response.data[0]; + assert_eq!(status.name, "some-server"); + assert_eq!( + status.tools.keys().cloned().collect::>(), + BTreeSet::from(["lookup".to_string()]) + ); + assert_eq!(status.resources, Vec::new()); + assert_eq!(status.resource_templates, Vec::new()); + + mcp_server_handle.abort(); + let _ = mcp_server_handle.await; + + Ok(()) +} + #[tokio::test] async fn mcp_server_status_list_does_not_duplicate_tools_for_sanitized_name_collisions() -> Result<()> { @@ -165,6 +298,7 @@ url = "{underscore_server_url}/mcp" .send_list_mcp_server_status_request(ListMcpServerStatusParams { cursor: None, limit: None, + detail: None, }) .await?; let response = timeout( @@ -215,3 +349,25 @@ async fn start_mcp_server(tool_name: &str) -> Result<(String, JoinHandle<()>)> { Ok((format!("http://{addr}"), handle)) } + +async fn start_slow_inventory_mcp_server(tool_name: &str) -> Result<(String, JoinHandle<()>)> { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?; + let tool_name = Arc::new(tool_name.to_string()); + let mcp_service = StreamableHttpService::new( + move || { + Ok(SlowInventoryServer { + tool_name: Arc::clone(&tool_name), + }) + }, + Arc::new(LocalSessionManager::default()), + StreamableHttpServerConfig::default(), + ); + let router = Router::new().nest_service("/mcp", mcp_service); + + let handle = tokio::spawn(async move { + let _ = axum::serve(listener, router).await; + }); + + Ok((format!("http://{addr}"), handle)) +} diff --git a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs index 8f944dca0788..2850c7b74f28 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs @@ -50,7 +50,7 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> { "call_sleep", )?]) .await; - create_config_toml(&codex_home, &server.uri(), "never")?; + create_config_toml(&codex_home, &server.uri(), "never", "danger-full-access")?; let mut mcp = McpProcess::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -124,14 +124,11 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> { #[tokio::test] async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()> { - #[cfg(target_os = "windows")] let shell_command = vec![ - "powershell".to_string(), - "-Command".to_string(), - "Start-Sleep -Seconds 10".to_string(), + "python3".to_string(), + "-c".to_string(), + "print(42)".to_string(), ]; - #[cfg(not(target_os = "windows"))] - let shell_command = vec!["sleep".to_string(), "10".to_string()]; let tmp = TempDir::new()?; let codex_home = tmp.path().join("codex_home"); @@ -143,10 +140,10 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<() shell_command.clone(), Some(&working_directory), Some(10_000), - "call_sleep_approval", + "call_python_approval", )?]) .await; - create_config_toml(&codex_home, &server.uri(), "untrusted")?; + create_config_toml(&codex_home, &server.uri(), "untrusted", "read-only")?; let mut mcp = McpProcess::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -168,7 +165,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<() .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { - text: "run sleep".to_string(), + text: "run python".to_string(), text_elements: Vec::new(), }], cwd: Some(working_directory), @@ -190,7 +187,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<() let ServerRequest::CommandExecutionRequestApproval { request_id, params } = request else { panic!("expected CommandExecutionRequestApproval request"); }; - assert_eq!(params.item_id, "call_sleep_approval"); + assert_eq!(params.item_id, "call_python_approval"); assert_eq!(params.thread_id, thread.id); assert_eq!(params.turn_id, turn.id); @@ -242,6 +239,7 @@ fn create_config_toml( codex_home: &std::path::Path, server_uri: &str, approval_policy: &str, + sandbox_mode: &str, ) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write( @@ -250,7 +248,7 @@ fn create_config_toml( r#" model = "mock-model" approval_policy = "{approval_policy}" -sandbox_mode = "danger-full-access" +sandbox_mode = "{sandbox_mode}" model_provider = "mock_provider" diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index b843bbadc02c..1e197372a8ff 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -34,6 +34,19 @@ const MCP_TOOL_NAME_DELIMITER: &str = "__"; pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps"; const CODEX_CONNECTORS_TOKEN_ENV_VAR: &str = "CODEX_CONNECTORS_TOKEN"; +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum McpSnapshotDetail { + #[default] + Full, + ToolsAndAuthOnly, +} + +impl McpSnapshotDetail { + fn include_resources(self) -> bool { + matches!(self, Self::Full) + } +} + /// The Responses API requires tool names to match `^[a-zA-Z0-9_-]+$`. /// MCP server/tool names are user-controlled, so sanitize the fully-qualified /// name we expose to the model by replacing any disallowed character with `_`. @@ -283,6 +296,15 @@ pub async fn collect_mcp_snapshot( config: &McpConfig, auth: Option<&CodexAuth>, submit_id: String, +) -> McpListToolsResponseEvent { + collect_mcp_snapshot_with_detail(config, auth, submit_id, McpSnapshotDetail::Full).await +} + +pub async fn collect_mcp_snapshot_with_detail( + config: &McpConfig, + auth: Option<&CodexAuth>, + submit_id: String, + detail: McpSnapshotDetail, ) -> McpListToolsResponseEvent { let mcp_servers = effective_mcp_servers(config, auth); let tool_plugin_provenance = tool_plugin_provenance(config); @@ -323,8 +345,12 @@ pub async fn collect_mcp_snapshot( ) .await; - let snapshot = - collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await; + let snapshot = collect_mcp_snapshot_from_manager_with_detail( + &mcp_connection_manager, + auth_status_entries, + detail, + ) + .await; cancel_token.cancel(); @@ -363,11 +389,36 @@ pub fn group_tools_by_server( pub async fn collect_mcp_snapshot_from_manager( mcp_connection_manager: &McpConnectionManager, auth_status_entries: HashMap, +) -> McpListToolsResponseEvent { + collect_mcp_snapshot_from_manager_with_detail( + mcp_connection_manager, + auth_status_entries, + McpSnapshotDetail::Full, + ) + .await +} + +pub async fn collect_mcp_snapshot_from_manager_with_detail( + mcp_connection_manager: &McpConnectionManager, + auth_status_entries: HashMap, + detail: McpSnapshotDetail, ) -> McpListToolsResponseEvent { let (tools, resources, resource_templates) = tokio::join!( mcp_connection_manager.list_all_tools(), - mcp_connection_manager.list_all_resources(), - mcp_connection_manager.list_all_resource_templates(), + async { + if detail.include_resources() { + mcp_connection_manager.list_all_resources().await + } else { + HashMap::new() + } + }, + async { + if detail.include_resources() { + mcp_connection_manager.list_all_resource_templates().await + } else { + HashMap::new() + } + }, ); let auth_statuses = auth_status_entries diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4feefcac9309..38e331d44f94 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -64,6 +64,7 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse; use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::ListMcpServerStatusResponse; use codex_app_server_protocol::McpServerStatus; +use codex_app_server_protocol::McpServerStatusDetail; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; use codex_app_server_protocol::PluginListParams; @@ -1871,8 +1872,8 @@ impl App { Ok(()) } - /// Spawn a background task that fetches the full MCP server inventory from the - /// app-server via paginated RPCs, then delivers the result back through + /// Spawn a background task that fetches MCP server status from the app-server + /// via paginated RPCs, then delivers the result back through /// `AppEvent::McpInventoryLoaded`. /// /// The spawned task is fire-and-forget: no `JoinHandle` is stored, so a stale @@ -2125,7 +2126,9 @@ impl App { self.chat_widget .add_to_history(history_cell::new_mcp_tools_output_from_statuses( - &config, &statuses, + &config, + &statuses, + McpServerStatusDetail::ToolsAndAuthOnly, )); } @@ -6002,8 +6005,9 @@ impl App { } } -/// Collect every MCP server status from the app-server by walking the paginated -/// `mcpServerStatus/list` RPC until no `next_cursor` is returned. +/// Collect every MCP server status needed for `/mcp` from the app-server by +/// walking the paginated `mcpServerStatus/list` RPC until no `next_cursor` is +/// returned. /// /// All pages are eagerly gathered into a single `Vec` so the caller can render /// the inventory atomically. Each page requests up to 100 entries. @@ -6021,6 +6025,7 @@ async fn fetch_all_mcp_server_statuses( params: ListMcpServerStatusParams { cursor: cursor.clone(), limit: Some(100), + detail: Some(McpServerStatusDetail::ToolsAndAuthOnly), }, }) .await diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index cf65f918e52b..6821adb5a869 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -40,6 +40,7 @@ use crate::wrapping::adaptive_wrap_line; use crate::wrapping::adaptive_wrap_lines; use base64::Engine; use codex_app_server_protocol::McpServerStatus; +use codex_app_server_protocol::McpServerStatusDetail; use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; #[cfg(test)] @@ -1979,10 +1980,12 @@ pub(crate) fn new_mcp_tools_output( /// transport details such as command, URL, cwd, and environment display. /// /// This mirrors the layout of [`new_mcp_tools_output`] but sources data from -/// the paginated RPC response rather than the in-process `McpManager`. +/// the paginated RPC response rather than the in-process `McpManager`. The +/// `detail` flag controls whether resources and resource templates are rendered. pub(crate) fn new_mcp_tools_output_from_statuses( config: &Config, statuses: &[McpServerStatus], + detail: McpServerStatusDetail, ) -> PlainHistoryCell { let mut lines: Vec> = vec![ "/mcp".magenta().into(), @@ -2094,48 +2097,50 @@ pub(crate) fn new_mcp_tools_output_from_statuses( lines.push(vec![" • Tools: ".into(), names.join(", ").into()].into()); } - let server_resources = status - .map(|status| status.resources.clone()) - .unwrap_or_default(); - if server_resources.is_empty() { - lines.push(" • Resources: (none)".into()); - } else { - let mut spans: Vec> = vec![" • Resources: ".into()]; + if matches!(detail, McpServerStatusDetail::Full) { + let server_resources = status + .map(|status| status.resources.clone()) + .unwrap_or_default(); + if server_resources.is_empty() { + lines.push(" • Resources: (none)".into()); + } else { + let mut spans: Vec> = vec![" • Resources: ".into()]; - for (idx, resource) in server_resources.iter().enumerate() { - if idx > 0 { - spans.push(", ".into()); + for (idx, resource) in server_resources.iter().enumerate() { + if idx > 0 { + spans.push(", ".into()); + } + + let label = resource.title.as_ref().unwrap_or(&resource.name); + spans.push(label.clone().into()); + spans.push(" ".into()); + spans.push(format!("({})", resource.uri).dim()); } - let label = resource.title.as_ref().unwrap_or(&resource.name); - spans.push(label.clone().into()); - spans.push(" ".into()); - spans.push(format!("({})", resource.uri).dim()); + lines.push(spans.into()); } - lines.push(spans.into()); - } + let server_templates = status + .map(|status| status.resource_templates.clone()) + .unwrap_or_default(); + if server_templates.is_empty() { + lines.push(" • Resource templates: (none)".into()); + } else { + let mut spans: Vec> = vec![" • Resource templates: ".into()]; - let server_templates = status - .map(|status| status.resource_templates.clone()) - .unwrap_or_default(); - if server_templates.is_empty() { - lines.push(" • Resource templates: (none)".into()); - } else { - let mut spans: Vec> = vec![" • Resource templates: ".into()]; + for (idx, template) in server_templates.iter().enumerate() { + if idx > 0 { + spans.push(", ".into()); + } - for (idx, template) in server_templates.iter().enumerate() { - if idx > 0 { - spans.push(", ".into()); + let label = template.title.as_ref().unwrap_or(&template.name); + spans.push(label.clone().into()); + spans.push(" ".into()); + spans.push(format!("({})", template.uri_template).dim()); } - let label = template.title.as_ref().unwrap_or(&template.name); - spans.push(label.clone().into()); - spans.push(" ".into()); - spans.push(format!("({})", template.uri_template).dim()); + lines.push(spans.into()); } - - lines.push(spans.into()); } lines.push(Line::from("")); @@ -3340,7 +3345,11 @@ mod tests { auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported, }]; - let cell = new_mcp_tools_output_from_statuses(&config, &statuses); + let cell = new_mcp_tools_output_from_statuses( + &config, + &statuses, + McpServerStatusDetail::ToolsAndAuthOnly, + ); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered); diff --git a/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap b/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap index ee8477102783..709ce6d69133 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap @@ -10,5 +10,3 @@ expression: rendered • Auth: Unsupported • Command: docs-server --stdio • Tools: lookup - • Resources: (none) - • Resource templates: (none)