Speed up /mcp inventory listing (#16831)

Addresses #16244

This was a performance regression introduced when we moved the TUI on
top of the app server API.

Problem: `/mcp` rebuilt a full MCP inventory through
`mcpServerStatus/list`, including resources and resource templates that
made the TUI wait on slow inventory probes.

Solution: add a lightweight `detail` mode to `mcpServerStatus/list`,
have `/mcp` request tools-and-auth only, and cover the fast path with
app-server and TUI tests.

Testing: Confirmed slow (multi-second) response prior to change and
immediate response after change.

I considered two options:
1. Change the existing `mcpServerStatus/list` API to accept an optional
"details" parameter so callers can request only a subset of the
information.
2. Add a separate `mcpServer/list` API that returns only the servers,
tools, and auth but omits the resources.

I chose option 1, but option 2 is also a reasonable approach.
This commit is contained in:
Eric Traut
2026-04-06 16:27:02 -07:00
committed by GitHub
parent 756c45ec61
commit 9f737c28dd
16 changed files with 384 additions and 61 deletions

View File

@@ -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<String, crate::mcp::auth::McpAuthStatusEntry>,
) -> 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<String, crate::mcp::auth::McpAuthStatusEntry>,
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