mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Resolve MCP server registrations through a catalog (#27634)
## Why MCP servers currently come from user config, local plugins, compatibility Apps synthesis, and host extensions. Those sources were composed by mutating a shared map, leaving registration identity, precedence, removal, and provenance implicit in assembly order. Before adding executor-owned MCPs, Codex needs one durable resolution boundary above `McpConnectionManager`. This PR introduces that boundary while preserving current server configuration, policy, and runtime behavior. Executor-scoped registrations and explicit policy layers remain follow-ups. ## What changed - Add typed `McpServerRegistration` inputs and an immutable `ResolvedMcpCatalog` in `codex-mcp`. - Retain each registration's complete `McpServerConfig`, including its environment binding, while recording its source and provenance. - Preserve the existing structural precedence between plugin, config, compatibility, and ordered extension sources. - Resolve equal-precedence actions by contribution order; provenance IDs are used only for diagnostics and cannot affect the winner. - Preserve extension removals and the existing name-scoped `enabled = false` veto. - Report same-tier conflicts with every contender and the final catalog outcome, including whether the winning action registers or removes the server. - Require MCP contributors to provide a stable diagnostic identity. - Derive materialized server maps and plugin ownership from the resolved catalog. `McpConnectionManager`, transport startup, tool calls, and resource routing continue to consume the same effective `McpServerConfig` values. ## Scope This PR does not add new MCP capabilities or change user-visible behavior. It does not add executor plugin discovery, thread-scoped registrations, dynamic refresh generations, or new user/managed policy semantics. ## Verification - Added focused catalog coverage for source precedence, complete configuration preservation, disabled vetoes, plugin ownership, contribution-order tie breaking, removal outcomes, and conflict diagnostics. - Extended hosted Apps coverage for ordered extension removal and Apps-disabled hosts with and without the hosted extension installed. - `cargo check -p codex-mcp --tests -p codex-extension-api -p codex-core`
This commit is contained in:
@@ -48,6 +48,9 @@ pub type ExtensionFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
/// Plugin-owned servers and their provenance continue to be resolved by the
|
||||
/// plugin manager until that ownership moves into an extension explicitly.
|
||||
pub trait McpServerContributor<C: Sync>: Send + Sync {
|
||||
/// Stable identity used for registration provenance and conflict diagnostics.
|
||||
fn id(&self) -> &'static str;
|
||||
|
||||
fn contribute<'a>(&'a self, config: &'a C) -> ExtensionFuture<'a, Vec<McpServerContribution>>;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ use codex_mcp::hosted_plugin_runtime_mcp_server_config;
|
||||
struct HostedPluginRuntimeExtension;
|
||||
|
||||
impl McpServerContributor<Config> for HostedPluginRuntimeExtension {
|
||||
fn id(&self) -> &'static str {
|
||||
"hosted_plugin_runtime"
|
||||
}
|
||||
|
||||
fn contribute<'a>(
|
||||
&'a self,
|
||||
config: &'a Config,
|
||||
|
||||
@@ -104,7 +104,7 @@ async fn legacy_fallback_overwrites_reserved_config_without_an_extension() -> Te
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extension_can_remove_legacy_fallback_while_apps_are_enabled() -> TestResult {
|
||||
async fn later_extension_can_remove_same_name_registration() -> TestResult {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
@@ -114,6 +114,7 @@ async fn extension_can_remove_legacy_fallback_while_apps_are_enabled() -> TestRe
|
||||
.await?;
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
codex_mcp_extension::install(&mut builder);
|
||||
builder.mcp_server_contributor(Arc::new(RemoveCodexApps));
|
||||
let manager = McpManager::new_with_extensions(
|
||||
Arc::new(PluginsManager::new(config.codex_home.to_path_buf())),
|
||||
@@ -145,7 +146,7 @@ async fn hosted_apps_mcp_requires_chatgpt_auth() -> TestResult {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_apps_remove_reserved_server_config() -> TestResult {
|
||||
async fn disabled_apps_remove_reserved_server_config_for_all_hosts() -> TestResult {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
@@ -159,11 +160,16 @@ async fn disabled_apps_remove_reserved_server_config() -> TestResult {
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
let manager = installed_manager(&config);
|
||||
|
||||
let servers = manager.runtime_servers(&config).await;
|
||||
|
||||
assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
||||
let managers = [
|
||||
installed_manager(&config),
|
||||
McpManager::new(Arc::new(PluginsManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
))),
|
||||
];
|
||||
for manager in managers {
|
||||
let servers = manager.runtime_servers(&config).await;
|
||||
assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -179,6 +185,10 @@ fn installed_manager(config: &Config) -> McpManager {
|
||||
struct RemoveCodexApps;
|
||||
|
||||
impl McpServerContributor<Config> for RemoveCodexApps {
|
||||
fn id(&self) -> &'static str {
|
||||
"remove_codex_apps"
|
||||
}
|
||||
|
||||
fn contribute<'a>(
|
||||
&'a self,
|
||||
_config: &'a Config,
|
||||
|
||||
Reference in New Issue
Block a user