Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions codex-rs/codex-mcp/src/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ use tracing::warn;
pub struct McpConnectionManager {
clients: HashMap<String, AsyncManagedClient>,
server_metadata: HashMap<String, McpServerMetadata>,
tool_plugin_provenance: Arc<ToolPluginProvenance>,
host_owned_codex_apps_enabled: bool,
elicitation_requests: ElicitationRequestManager,
startup_cancellation_token: CancellationToken,
Expand All @@ -91,6 +92,7 @@ impl McpConnectionManager {
Self {
clients: HashMap::new(),
server_metadata: HashMap::new(),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
host_owned_codex_apps_enabled: false,
elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(),
Expand Down Expand Up @@ -136,6 +138,11 @@ impl McpConnectionManager {
.is_none_or(|metadata| metadata.pollutes_memory)
}

pub fn plugin_id_for_mcp_server_name(&self, server_name: &str) -> Option<&str> {
self.tool_plugin_provenance
.plugin_id_for_mcp_server_name(server_name)
}

pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
}
Expand Down Expand Up @@ -283,6 +290,7 @@ impl McpConnectionManager {
let manager = Self {
clients,
server_metadata,
tool_plugin_provenance,
host_owned_codex_apps_enabled,
elicitation_requests: elicitation_requests.clone(),
startup_cancellation_token: cancel_token.clone(),
Expand Down
17 changes: 14 additions & 3 deletions codex-rs/codex-mcp/src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ pub struct McpConfig {
///
/// Runtime-only additions are merged later by [`effective_mcp_servers`].
pub configured_mcp_servers: HashMap<String, McpServerConfig>,
/// Winning plugin owner for plugin-provided MCP servers, keyed by server name.
pub plugin_ids_by_mcp_server_name: HashMap<String, String>,
/// Plugin metadata used to attribute MCP tools/connectors to plugin display names.
pub plugin_capability_summaries: Vec<PluginCapabilitySummary>,
}
Expand All @@ -146,6 +148,7 @@ pub struct McpConfig {
pub struct ToolPluginProvenance {
plugin_display_names_by_connector_id: HashMap<String, Vec<String>>,
plugin_display_names_by_mcp_server_name: HashMap<String, Vec<String>>,
plugin_ids_by_mcp_server_name: HashMap<String, String>,
}

impl ToolPluginProvenance {
Expand All @@ -163,9 +166,15 @@ impl ToolPluginProvenance {
.unwrap_or(&[])
}

fn from_capability_summaries(capability_summaries: &[PluginCapabilitySummary]) -> Self {
pub fn plugin_id_for_mcp_server_name(&self, server_name: &str) -> Option<&str> {
self.plugin_ids_by_mcp_server_name
.get(server_name)
.map(String::as_str)
}

fn from_config(config: &McpConfig) -> Self {
let mut tool_plugin_provenance = Self::default();
for plugin in capability_summaries {
for plugin in &config.plugin_capability_summaries {
for connector_id in &plugin.app_connector_ids {
tool_plugin_provenance
.plugin_display_names_by_connector_id
Expand Down Expand Up @@ -195,6 +204,8 @@ impl ToolPluginProvenance {
plugin_names.sort_unstable();
plugin_names.dedup();
}
tool_plugin_provenance.plugin_ids_by_mcp_server_name =
config.plugin_ids_by_mcp_server_name.clone();

tool_plugin_provenance
}
Expand Down Expand Up @@ -244,7 +255,7 @@ pub fn effective_mcp_servers_from_configured(
}

pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance {
ToolPluginProvenance::from_capability_summaries(&config.plugin_capability_summaries)
ToolPluginProvenance::from_config(config)
}

pub async fn read_mcp_resource(
Expand Down
18 changes: 16 additions & 2 deletions codex-rs/codex-mcp/src/mcp/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
apps_enabled: false,
client_elicitation_capability: ElicitationCapability::default(),
configured_mcp_servers: HashMap::new(),
plugin_ids_by_mcp_server_name: HashMap::new(),
plugin_capability_summaries: Vec::new(),
}
}
Expand Down Expand Up @@ -124,7 +125,10 @@ fn mcp_prompt_auto_approval_rejects_auto_mode_in_default_permission_mode() {

#[test]
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
let provenance = ToolPluginProvenance::from_capability_summaries(&[
let mut config = test_mcp_config(PathBuf::new());
config.plugin_ids_by_mcp_server_name =
HashMap::from([("alpha".to_string(), "alpha@test".to_string())]);
config.plugin_capability_summaries = vec![
PluginCapabilitySummary {
display_name: "alpha-plugin".to_string(),
app_connector_ids: vec![AppConnectorId("connector_example".to_string())],
Expand All @@ -140,7 +144,8 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
mcp_server_names: vec!["beta".to_string()],
..PluginCapabilitySummary::default()
},
]);
];
let provenance = tool_plugin_provenance(&config);

assert_eq!(
provenance,
Expand All @@ -159,8 +164,17 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
("alpha".to_string(), vec!["alpha-plugin".to_string()]),
("beta".to_string(), vec!["beta-plugin".to_string()]),
]),
plugin_ids_by_mcp_server_name: HashMap::from([(
"alpha".to_string(),
"alpha@test".to_string(),
)]),
}
);
assert_eq!(
provenance.plugin_id_for_mcp_server_name("alpha"),
Some("alpha@test")
);
assert_eq!(provenance.plugin_id_for_mcp_server_name("beta"), None);
}

#[test]
Expand Down
57 changes: 57 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3849,6 +3849,63 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config()
mcp_config.configured_mcp_servers.get("sample"),
Some(&http_mcp("https://sample.example/mcp"))
);
assert_eq!(
mcp_config.plugin_ids_by_mcp_server_name,
HashMap::from([("sample".to_string(), "sample@test".to_string())])
);

Ok(())
}

#[tokio::test]
async fn to_mcp_config_omits_plugin_id_when_user_server_shadows_plugin_mcp() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let plugin_root = codex_home
.path()
.join("plugins/cache")
.join("test/sample/local");
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{"name":"sample"}"#,
)?;
std::fs::write(
plugin_root.join(".mcp.json"),
r#"{
"mcpServers": {
"sample": {
"type": "http",
"url": "https://plugin.example/mcp"
}
}
}"#,
)?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
[features]
plugins = true

[mcp_servers.sample]
url = "https://user.example/mcp"

[plugins."sample@test"]
enabled = true
"#,
)?;

let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let mcp_config = config.to_mcp_config(&plugins_manager).await;

assert_eq!(
mcp_config.configured_mcp_servers.get("sample"),
Some(&http_mcp("https://user.example/mcp"))
);
assert!(mcp_config.plugin_ids_by_mcp_server_name.is_empty());

Ok(())
}
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::hash_map::Entry;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -1243,6 +1244,7 @@ impl Config {
let plugins_input = self.plugins_config_input();
let loaded_plugins = plugins_manager.plugins_for_config(&plugins_input).await;
let mut configured_mcp_servers = self.mcp_servers.get().clone();
let mut plugin_ids_by_mcp_server_name = HashMap::new();
for plugin in loaded_plugins
.plugins()
.iter()
Expand All @@ -1255,7 +1257,10 @@ impl Config {
self.config_layer_stack.requirements().plugins.as_ref(),
);
for (name, plugin_server) in plugin_mcp_servers {
configured_mcp_servers.entry(name).or_insert(plugin_server);
if let Entry::Vacant(entry) = configured_mcp_servers.entry(name.clone()) {
entry.insert(plugin_server);
plugin_ids_by_mcp_server_name.insert(name, plugin.config_name.clone());
}
}
}
if let Some(mcp_requirements) = self.config_layer_stack.requirements().mcp_servers.as_ref()
Expand All @@ -1265,6 +1270,8 @@ impl Config {
// above.
filter_mcp_servers_by_requirements(&mut configured_mcp_servers, Some(mcp_requirements));
}
plugin_ids_by_mcp_server_name
.retain(|server_name, _| configured_mcp_servers.contains_key(server_name));

McpConfig {
chatgpt_base_url: self.chatgpt_base_url.clone(),
Expand Down Expand Up @@ -1292,6 +1299,7 @@ impl Config {
ElicitationCapability::default()
},
configured_mcp_servers,
plugin_ids_by_mcp_server_name,
plugin_capability_summaries: loaded_plugins.capability_summaries().to_vec(),
}
}
Expand Down
21 changes: 14 additions & 7 deletions codex-rs/core/src/mcp_tool_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,7 @@ pub(crate) struct McpToolApprovalMetadata {
connector_id: Option<String>,
connector_name: Option<String>,
connector_description: Option<String>,
plugin_id: Option<String>,
tool_title: Option<String>,
tool_description: Option<String>,
mcp_app_resource_uri: Option<String>,
Expand All @@ -983,6 +984,7 @@ pub(crate) struct McpToolApprovalMetadata {

const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";
const MCP_TOOL_PLUGIN_ID_META_KEY: &str = "plugin_id";
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";

async fn custom_mcp_tool_approval_mode(
Expand Down Expand Up @@ -1068,6 +1070,12 @@ fn build_mcp_tool_call_request_meta(
serde_json::Value::Object(codex_apps_meta),
);
}
if let Some(plugin_id) = metadata.and_then(|metadata| metadata.plugin_id.as_ref()) {
request_meta.insert(
MCP_TOOL_PLUGIN_ID_META_KEY.to_string(),
serde_json::Value::String(plugin_id.clone()),
);
}

(!request_meta.is_empty()).then_some(serde_json::Value::Object(request_meta))
}
Expand Down Expand Up @@ -1480,13 +1488,11 @@ pub(crate) async fn lookup_mcp_tool_metadata(
server: &str,
tool_name: &str,
) -> Option<McpToolApprovalMetadata> {
let tools = sess
.services
.mcp_connection_manager
.read()
.await
.list_all_tools()
.await;
let manager = sess.services.mcp_connection_manager.read().await;
let plugin_id = manager
.plugin_id_for_mcp_server_name(server)
.map(str::to_string);
let tools = manager.list_all_tools().await;
let tool_info = tools
.into_iter()
.find(|tool_info| tool_info.server_name == server && tool_info.tool.name == tool_name)?;
Expand Down Expand Up @@ -1519,6 +1525,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
connector_id: tool_info.connector_id,
connector_name: tool_info.connector_name,
connector_description,
plugin_id,
tool_title: tool_info.tool.title,
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
Expand Down
Loading
Loading