This commit is contained in:
Matthew Zeng
2026-04-01 15:17:10 -04:00
parent 41052984fd
commit 86f6eb8e45
3 changed files with 140 additions and 96 deletions

View File

@@ -222,8 +222,7 @@ use codex_core::find_thread_names_by_ids;
use codex_core::find_thread_path_by_id_str;
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::collect_mcp_server_status_snapshot;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_core::parse_cursor;
use codex_core::plugins::MarketplaceError;
@@ -5057,9 +5056,7 @@ impl CodexMessageProcessor {
params: ListMcpServerStatusParams,
config: Config,
) {
let snapshot = collect_mcp_snapshot(&config).await;
let tools_by_server = group_tools_by_server(&snapshot.tools);
let snapshot = collect_mcp_server_status_snapshot(&config).await;
let mut server_names: Vec<String> = config
.mcp_servers
@@ -5107,7 +5104,7 @@ impl CodexMessageProcessor {
.iter()
.map(|name| McpServerStatus {
name: name.clone(),
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
tools: snapshot.tools_by_server.get(name).cloned().unwrap_or_default(),
resources: snapshot.resources.get(name).cloned().unwrap_or_default(),
resource_templates: snapshot
.resource_templates

View File

@@ -12,6 +12,7 @@ use async_channel::unbounded;
use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceTemplate;
use codex_protocol::mcp::Tool;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::McpListToolsResponseEvent;
use codex_protocol::protocol::SandboxPolicy;
use serde_json::Value;
@@ -24,6 +25,7 @@ use crate::config::types::McpServerTransportConfig;
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;
@@ -250,6 +252,13 @@ impl McpManager {
}
}
pub struct McpServerStatusSnapshot {
pub tools_by_server: HashMap<String, HashMap<String, Tool>>,
pub resources: HashMap<String, Vec<Resource>>,
pub resource_templates: HashMap<String, Vec<ResourceTemplate>>,
pub auth_statuses: HashMap<String, McpAuthStatus>,
}
fn configured_mcp_servers(
config: &Config,
plugins_manager: &PluginsManager,
@@ -276,7 +285,7 @@ fn effective_mcp_servers(
)
}
pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent {
pub async fn collect_mcp_server_status_snapshot(config: &Config) -> McpServerStatusSnapshot {
let auth_manager = AuthManager::shared(
config.codex_home.clone(),
/*enable_codex_api_key_env*/ false,
@@ -287,8 +296,8 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
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() {
return McpListToolsResponseEvent {
tools: HashMap::new(),
return McpServerStatusSnapshot {
tools_by_server: HashMap::new(),
resources: HashMap::new(),
resource_templates: HashMap::new(),
auth_statuses: HashMap::new(),
@@ -322,41 +331,48 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
)
.await;
let snapshot =
collect_mcp_snapshot_from_manager(&mcp_connection_manager, auth_status_entries).await;
let snapshot = collect_mcp_server_status_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)> {
let mut parts = qualified_name.split(MCP_TOOL_NAME_DELIMITER);
let prefix = parts.next()?;
if prefix != MCP_TOOL_NAME_PREFIX {
return None;
}
let server_name = parts.next()?;
let tool_name: String = parts.collect::<Vec<_>>().join(MCP_TOOL_NAME_DELIMITER);
if tool_name.is_empty() {
return None;
}
Some((server_name.to_string(), tool_name))
}
async fn collect_mcp_server_status_snapshot_from_manager(
mcp_connection_manager: &McpConnectionManager,
auth_status_entries: HashMap<String, crate::mcp::auth::McpAuthStatusEntry>,
) -> McpServerStatusSnapshot {
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(),
);
pub fn group_tools_by_server(
tools: &HashMap<String, Tool>,
) -> HashMap<String, HashMap<String, Tool>> {
let mut grouped = HashMap::new();
for (qualified_name, tool) in tools {
if let Some((server_name, tool_name)) = split_qualified_tool_name(qualified_name) {
grouped
let mut tools_by_server = HashMap::new();
for tool_info in tools.into_values() {
let tool_name = mcp_server_status_tool_name(&tool_info);
let server_name = tool_info.server_name.clone();
if let Some(tool) = convert_mcp_tool(tool_info.tool) {
tools_by_server
.entry(server_name)
.or_insert_with(HashMap::new)
.insert(tool_name, tool.clone());
.insert(tool_name, tool);
}
}
grouped
McpServerStatusSnapshot {
tools_by_server,
resources: convert_mcp_resources(resources),
resource_templates: convert_mcp_resource_templates(resource_templates),
auth_statuses: auth_status_entries
.iter()
.map(|(name, entry)| (name.clone(), entry.auth_status))
.collect(),
}
}
pub(crate) async fn collect_mcp_snapshot_from_manager(
@@ -376,22 +392,54 @@ 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) {
Ok(value) => match Tool::from_mcp_value(value) {
Ok(tool) => Some((name, tool)),
Err(err) => {
tracing::warn!("Failed to convert MCP tool '{name}': {err}");
None
}
},
Err(err) => {
tracing::warn!("Failed to serialize MCP tool '{name}': {err}");
None
}
.filter_map(|(name, tool_info)| {
convert_mcp_tool(tool_info.tool).map(|tool| (name, tool))
})
.collect();
let resources = resources
McpListToolsResponseEvent {
tools,
resources: convert_mcp_resources(resources),
resource_templates: convert_mcp_resource_templates(resource_templates),
auth_statuses,
}
}
fn mcp_server_status_tool_name(tool: &ToolInfo) -> String {
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
return tool.tool_name.clone();
}
let codex_apps_prefix = qualified_mcp_tool_name_prefix(CODEX_APPS_MCP_SERVER_NAME);
if let Some(connector_namespace) = tool.tool_namespace.strip_prefix(&codex_apps_prefix) {
return format!("{}{}", connector_namespace, tool.tool_name);
}
tool.tool_name.clone()
}
fn convert_mcp_tool(tool: rmcp::model::Tool) -> Option<Tool> {
let value = match serde_json::to_value(tool) {
Ok(value) => value,
Err(err) => {
tracing::warn!("Failed to serialize MCP tool: {err}");
return None;
}
};
match Tool::from_mcp_value(value) {
Ok(tool) => Some(tool),
Err(err) => {
tracing::warn!("Failed to convert MCP tool: {err}");
None
}
}
}
fn convert_mcp_resources(
resources: HashMap<String, Vec<rmcp::model::Resource>>,
) -> HashMap<String, Vec<Resource>> {
resources
.into_iter()
.map(|(name, resources)| {
let resources = resources
@@ -424,9 +472,13 @@ pub(crate) async fn collect_mcp_snapshot_from_manager(
.collect::<Vec<_>>();
(name, resources)
})
.collect();
.collect()
}
let resource_templates = resource_templates
fn convert_mcp_resource_templates(
resource_templates: HashMap<String, Vec<rmcp::model::ResourceTemplate>>,
) -> HashMap<String, Vec<ResourceTemplate>> {
resource_templates
.into_iter()
.map(|(name, templates)| {
let templates = templates
@@ -460,14 +512,7 @@ pub(crate) async fn collect_mcp_snapshot_from_manager(
.collect::<Vec<_>>();
(name, templates)
})
.collect();
McpListToolsResponseEvent {
tools,
resources,
resource_templates,
auth_statuses,
}
.collect()
}
#[cfg(test)]

View File

@@ -5,8 +5,10 @@ use crate::plugins::AppConnectorId;
use crate::plugins::PluginCapabilitySummary;
use codex_features::Feature;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use toml::Value;
fn write_file(path: &Path, contents: &str) {
@@ -31,27 +33,29 @@ fn plugin_config_toml() -> String {
toml::to_string(&Value::Table(root)).expect("plugin test config should serialize")
}
fn make_tool(name: &str) -> Tool {
Tool {
name: name.to_string(),
title: None,
description: None,
input_schema: serde_json::json!({"type": "object", "properties": {}}),
output_schema: None,
annotations: None,
icons: None,
meta: None,
fn make_tool_info(server_name: &str, tool_name: &str, tool_namespace: &str) -> ToolInfo {
ToolInfo {
server_name: server_name.to_string(),
tool_name: tool_name.to_string(),
tool_namespace: tool_namespace.to_string(),
tool: rmcp::model::Tool {
name: tool_name.to_string().into(),
title: None,
description: None,
input_schema: Arc::new(JsonObject::default()),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
connector_description: None,
}
}
#[test]
fn split_qualified_tool_name_returns_server_and_tool() {
assert_eq!(
split_qualified_tool_name("mcp__alpha__do_thing"),
Some(("alpha".to_string(), "do_thing".to_string()))
);
}
#[test]
fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
assert_eq!(
@@ -61,33 +65,31 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
}
#[test]
fn split_qualified_tool_name_rejects_invalid_names() {
assert_eq!(split_qualified_tool_name("other__alpha__do_thing"), None);
assert_eq!(split_qualified_tool_name("mcp__alpha__"), None);
fn mcp_server_status_tool_name_preserves_hyphenated_mcp_tool_names() {
let tool_info = make_tool_info(
"music-studio",
"play-live-pattern",
"music-studio",
);
assert_eq!(
mcp_server_status_tool_name(&tool_info),
"play-live-pattern".to_string()
);
}
#[test]
fn group_tools_by_server_strips_prefix_and_groups() {
let mut tools = HashMap::new();
tools.insert("mcp__alpha__do_thing".to_string(), make_tool("do_thing"));
tools.insert(
"mcp__alpha__nested__op".to_string(),
make_tool("nested__op"),
fn mcp_server_status_tool_name_includes_codex_apps_connector_namespace() {
let tool_info = make_tool_info(
CODEX_APPS_MCP_SERVER_NAME,
"_property_search",
"mcp__codex_apps__zillow",
);
tools.insert("mcp__beta__do_other".to_string(), make_tool("do_other"));
let mut expected_alpha = HashMap::new();
expected_alpha.insert("do_thing".to_string(), make_tool("do_thing"));
expected_alpha.insert("nested__op".to_string(), make_tool("nested__op"));
let mut expected_beta = HashMap::new();
expected_beta.insert("do_other".to_string(), make_tool("do_other"));
let mut expected = HashMap::new();
expected.insert("alpha".to_string(), expected_alpha);
expected.insert("beta".to_string(), expected_beta);
assert_eq!(group_tools_by_server(&tools), expected);
assert_eq!(
mcp_server_status_tool_name(&tool_info),
"zillow_property_search".to_string()
);
}
#[test]