mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Route hosted Apps MCP through extensions (#27191)
## Stack - Base: #27184 - This PR is the second vertical and should be reviewed against `jif/external-plugins-1`, not `main`. ## Why CCA is moving toward a split runtime where the orchestrator may have no filesystem or executor, but it still needs to activate remotely hosted plugin components. HTTP MCP servers are the simplest complete example: they need configuration and host authentication, but they do not need an executor process. The Apps MCP endpoint is currently synthesized by a special-purpose loader inside the MCP runtime. That works locally, but it leaves hosted MCP activation outside the extension model being established in #27184. It also makes the Apps path a poor foundation for plugins whose skills, MCP servers, connectors, and hooks may come from different sources or execute in different places. This PR moves that one behavior behind an extension-owned contribution while preserving the existing local fallback. It deliberately does not introduce a generic plugin activation framework. ## What changed ### MCP extension contribution `codex-extension-api` gains an ordered `McpServerContributor` contract. A contributor returns typed `Set` or `Remove` overlays for MCP server configuration; later contributors win for the names they own. The contract stays at the existing MCP configuration boundary. Extensions do not create a second connection manager or transport abstraction. ### Hosted Apps MCP extension A new `codex-mcp-extension` contributes the reserved `codex_apps` server from the existing Apps feature, ChatGPT base URL, path override, and product SKU configuration. When `apps_mcp_path_override` is enabled for `https://chatgpt.com`, the resulting streamable HTTP endpoint is `https://chatgpt.com/backend-api/ps/mcp`. The existing ChatGPT-auth gate remains authoritative, so this server can run in an orchestrator-only process without being exposed for API-key sessions. ### One resolved runtime view `McpManager` now distinguishes three views: - **configured:** config- and plugin-backed servers before extension overlays; - **runtime:** configured servers plus host-installed extension contributions; - **effective:** runtime servers after auth gating and compatibility built-ins. App-server installs the hosted MCP extension and uses the runtime view for thread startup, refresh, status, threadless resource reads, connector discovery, and MCP OAuth lookup. This keeps `mcpServer/oauth/login` consistent with the servers exposed by the other MCP APIs. The hosted Apps server itself continues to use existing ChatGPT host authentication rather than MCP OAuth. ## Compatibility Hosts that do not install the MCP extension retain the existing Apps MCP synthesis path. This preserves current local-only, CLI, and standalone-host behavior while app-server exercises the extension path. Disabling Apps removes the reserved `codex_apps` entry, and losing ChatGPT auth removes it from the effective runtime view. Executor availability is not consulted for this HTTP transport. ## Follow-ups The next vertical will resolve a manifest-declared stdio MCP server from an executor-selected plugin root and execute it in the environment that owns that root. Later verticals can add backend-owned skills, connector metadata, hooks, durable selection semantics, and incremental local convergence without changing the component-specific runtime boundaries introduced here. ## Verification Focused coverage was added for: - contributing the hosted Apps MCP at `/backend-api/ps/mcp` without an executor; - requiring ChatGPT auth in the effective runtime view; - removing a reserved configured Apps server when the Apps feature is disabled. `cargo check -p codex-app-server -p codex-mcp-extension -p codex-extension-api -p codex-mcp` passed. Tests and Clippy were not run locally under the current development instruction; CI provides the full validation pass.
This commit is contained in:
Generated
+19
@@ -1982,6 +1982,7 @@ dependencies = [
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-mcp-extension",
|
||||
"codex-memories-extension",
|
||||
"codex-memories-write",
|
||||
"codex-model-provider",
|
||||
@@ -2920,6 +2921,7 @@ name = "codex-extension-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-config",
|
||||
"codex-context-fragments",
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
@@ -3247,6 +3249,23 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp-extension"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-core-plugins",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp-server"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -49,6 +49,7 @@ members = [
|
||||
"ext/guardian",
|
||||
"ext/image-generation",
|
||||
"ext/memories",
|
||||
"ext/mcp",
|
||||
"ext/skills",
|
||||
"ext/web-search",
|
||||
"external-agent-migration",
|
||||
@@ -191,6 +192,7 @@ codex-web-search-extension = { path = "ext/web-search" }
|
||||
codex-memories-read = { path = "memories/read" }
|
||||
codex-memories-write = { path = "memories/write" }
|
||||
codex-mcp = { path = "codex-mcp" }
|
||||
codex-mcp-extension = { path = "ext/mcp" }
|
||||
codex-mcp-server = { path = "mcp-server" }
|
||||
codex-model-provider-info = { path = "model-provider-info" }
|
||||
codex-models-manager = { path = "models-manager" }
|
||||
|
||||
@@ -61,6 +61,7 @@ codex-memories-extension = { workspace = true }
|
||||
codex-web-search-extension = { workspace = true }
|
||||
codex-memories-write = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-mcp-extension = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -50,6 +50,7 @@ where
|
||||
}
|
||||
codex_guardian::install(&mut builder, guardian_agent_spawner);
|
||||
codex_memories_extension::install(&mut builder, codex_otel::global());
|
||||
codex_mcp_extension::install(&mut builder);
|
||||
codex_web_search_extension::install(&mut builder, auth_manager.clone());
|
||||
codex_image_generation_extension::install(&mut builder, auth_manager);
|
||||
codex_skills_extension::install_with_providers(
|
||||
|
||||
@@ -67,10 +67,7 @@ async fn build_refresh_config(
|
||||
let config = config_manager
|
||||
.load_latest_config_for_thread(thread_config.as_ref())
|
||||
.await?;
|
||||
let mcp_servers = thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(&config)
|
||||
.await;
|
||||
let mcp_servers = thread_manager.mcp_manager().runtime_servers(&config).await;
|
||||
Ok(McpServerRefreshConfig {
|
||||
mcp_servers: serde_json::to_value(mcp_servers).map_err(io::Error::other)?,
|
||||
mcp_oauth_credentials_store_mode: serde_json::to_value(
|
||||
|
||||
@@ -280,6 +280,7 @@ use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::CodexThreadSettingsOverrides;
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::McpManager;
|
||||
use codex_core::NewThread;
|
||||
#[cfg(test)]
|
||||
use codex_core::SessionMeta;
|
||||
|
||||
@@ -88,11 +88,19 @@ impl AppsRequestProcessor {
|
||||
let request = request_id.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let mcp_manager = self.thread_manager.mcp_manager();
|
||||
let shutdown_token = self.shutdown_token.child_token();
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = shutdown_token.cancelled() => {}
|
||||
_ = Self::apps_list_task(outgoing, request, params, config, environment_manager) => {}
|
||||
_ = Self::apps_list_task(
|
||||
outgoing,
|
||||
request,
|
||||
params,
|
||||
config,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
) => {}
|
||||
}
|
||||
});
|
||||
Ok(None)
|
||||
@@ -108,11 +116,15 @@ impl AppsRequestProcessor {
|
||||
params: AppsListParams,
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) {
|
||||
let retry_params = params.clone();
|
||||
let retry_config = config.clone();
|
||||
let retry_environment_manager = Arc::clone(&environment_manager);
|
||||
let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await;
|
||||
let retry_mcp_manager = Arc::clone(&mcp_manager);
|
||||
let result =
|
||||
Self::apps_list_response(&outgoing, params, config, environment_manager, mcp_manager)
|
||||
.await;
|
||||
let should_retry = result
|
||||
.as_ref()
|
||||
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
|
||||
@@ -128,6 +140,7 @@ impl AppsRequestProcessor {
|
||||
retry_params,
|
||||
retry_config,
|
||||
retry_environment_manager,
|
||||
retry_mcp_manager,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -141,6 +154,7 @@ impl AppsRequestProcessor {
|
||||
params: AppsListParams,
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
|
||||
let AppsListParams {
|
||||
cursor,
|
||||
@@ -167,14 +181,14 @@ impl AppsRequestProcessor {
|
||||
let accessible_config = config.clone();
|
||||
let accessible_tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result =
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
&accessible_config,
|
||||
force_refetch,
|
||||
Arc::clone(&environment_manager),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to load accessible apps: {err}"));
|
||||
let result = connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
&accessible_config,
|
||||
force_refetch,
|
||||
Arc::clone(&environment_manager),
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to load accessible apps: {err}"));
|
||||
let _ = accessible_tx.send(AppListLoadResult::Accessible(result));
|
||||
});
|
||||
|
||||
|
||||
@@ -120,12 +120,16 @@ impl McpRequestProcessor {
|
||||
timeout_secs,
|
||||
} = params;
|
||||
|
||||
let configured_servers = self
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let effective_servers = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(&config)
|
||||
.effective_servers(&config, auth.as_ref())
|
||||
.await;
|
||||
let Some(server) = configured_servers.get(&name) else {
|
||||
let Some(server) = effective_servers
|
||||
.get(&name)
|
||||
.and_then(codex_mcp::EffectiveMcpServer::configured_config)
|
||||
else {
|
||||
return Err(invalid_request(format!(
|
||||
"No MCP server named '{name}' found."
|
||||
)));
|
||||
@@ -210,8 +214,10 @@ impl McpRequestProcessor {
|
||||
}
|
||||
None => self.load_latest_config(/*fallback_cwd*/ None).await?,
|
||||
};
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
|
||||
let mcp_config = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.runtime_config(&config)
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
@@ -361,8 +367,10 @@ impl McpRequestProcessor {
|
||||
}
|
||||
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
|
||||
let mcp_config = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
.runtime_config(&config)
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
|
||||
@@ -1036,6 +1036,7 @@ impl PluginRequestProcessor {
|
||||
&config,
|
||||
&outcome.plugin.apps,
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
)
|
||||
.await;
|
||||
let visible_skills = outcome
|
||||
@@ -1118,6 +1119,7 @@ impl PluginRequestProcessor {
|
||||
&config,
|
||||
&plugin_apps,
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
)
|
||||
.await;
|
||||
remote_plugin_detail_to_info(remote_detail, app_summaries)
|
||||
@@ -1611,10 +1613,11 @@ impl PluginRequestProcessor {
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
||||
connectors::list_all_connectors_with_options(config, /*force_refetch*/ false),
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
/*force_refetch*/ true,
|
||||
Arc::clone(&environment_manager)
|
||||
Arc::clone(&environment_manager),
|
||||
self.thread_manager.mcp_manager(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1881,6 +1884,7 @@ async fn load_plugin_app_summaries(
|
||||
config: &Config,
|
||||
plugin_apps: &[codex_plugin::AppConnectorId],
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> Vec<AppSummary> {
|
||||
if plugin_apps.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -1900,10 +1904,11 @@ async fn load_plugin_app_summaries(
|
||||
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
|
||||
|
||||
let accessible_connectors =
|
||||
match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
match connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
/*force_refetch*/ false,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_connectors::merge::merge_plugin_connectors;
|
||||
use codex_core::config::Config;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
|
||||
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use auth_elicitation::build_auth_elicitation_plan;
|
||||
pub use auth_elicitation::connector_auth_failure_from_tool_result;
|
||||
pub use codex_apps::CodexAppsToolsCacheKey;
|
||||
pub use codex_apps::codex_apps_tools_cache_key;
|
||||
pub use mcp::codex_apps_mcp_server_config;
|
||||
pub use mcp::configured_mcp_servers;
|
||||
pub use mcp::effective_mcp_servers;
|
||||
pub use mcp::effective_mcp_servers_from_configured;
|
||||
|
||||
@@ -131,6 +131,11 @@ pub struct McpConfig {
|
||||
/// ChatGPT auth is checked separately at runtime before the host-owned apps
|
||||
/// MCP server is added.
|
||||
pub apps_enabled: bool,
|
||||
/// Whether to synthesize the legacy host-owned Apps MCP server.
|
||||
///
|
||||
/// Hosts that install an MCP extension for this server disable the legacy
|
||||
/// loader and contribute the server through the normal runtime overlay.
|
||||
pub legacy_apps_mcp_loader_enabled: bool,
|
||||
/// Whether model-visible MCP tool namespaces should keep the legacy
|
||||
/// `mcp__` prefix.
|
||||
pub prefix_mcp_tool_names: bool,
|
||||
@@ -218,10 +223,20 @@ pub fn with_codex_apps_mcp(
|
||||
auth: Option<&CodexAuth>,
|
||||
config: &McpConfig,
|
||||
) -> HashMap<String, EffectiveMcpServer> {
|
||||
if !config.legacy_apps_mcp_loader_enabled {
|
||||
if !host_owned_codex_apps_enabled(config, auth) {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
}
|
||||
return servers;
|
||||
}
|
||||
if host_owned_codex_apps_enabled(config, auth) {
|
||||
servers.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
EffectiveMcpServer::configured(codex_apps_mcp_server_config(config)),
|
||||
EffectiveMcpServer::configured(codex_apps_mcp_server_config(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
config.apps_mcp_product_sku.as_deref(),
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
@@ -381,13 +396,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub(crate) fn codex_apps_mcp_url(config: &McpConfig) -> String {
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 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 `_`.
|
||||
@@ -443,10 +451,14 @@ fn codex_apps_mcp_url_for_base_url(base_url: &str, apps_mcp_path_override: Optio
|
||||
format!("{base_url}/{path}")
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
let url = codex_apps_mcp_url(config);
|
||||
let http_headers = config.apps_mcp_product_sku.as_ref().map(|product_sku| {
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.clone())])
|
||||
pub fn codex_apps_mcp_server_config(
|
||||
chatgpt_base_url: &str,
|
||||
apps_mcp_path_override: Option<&str>,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
) -> McpServerConfig {
|
||||
let url = codex_apps_mcp_url_for_base_url(chatgpt_base_url, apps_mcp_path_override);
|
||||
let http_headers = apps_mcp_product_sku.map(|product_sku| {
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())])
|
||||
});
|
||||
|
||||
McpServerConfig {
|
||||
|
||||
@@ -27,6 +27,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
apps_enabled: false,
|
||||
legacy_apps_mcp_loader_enabled: true,
|
||||
prefix_mcp_tool_names: true,
|
||||
client_elicitation_capability: ElicitationCapability::default(),
|
||||
configured_mcp_servers: HashMap::new(),
|
||||
@@ -206,16 +207,6 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
|
||||
let config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url(&config),
|
||||
"https://chatgpt.com/backend-api/wham/apps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
|
||||
let mut config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
|
||||
@@ -1428,6 +1428,7 @@ impl Config {
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: self.features.use_legacy_landlock(),
|
||||
apps_enabled: self.features.enabled(Feature::Apps),
|
||||
legacy_apps_mcp_loader_enabled: true,
|
||||
prefix_mcp_tool_names: self.prefix_mcp_tool_names(),
|
||||
client_elicitation_capability: if self.features.enabled(Feature::AuthElicitation) {
|
||||
ElicitationCapability {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
@@ -41,8 +40,8 @@ use codex_mcp::ToolInfo;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::host_owned_codex_apps_enabled;
|
||||
use codex_mcp::with_codex_apps_mcp;
|
||||
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
|
||||
@@ -220,6 +219,23 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
|
||||
let mcp_manager = Arc::new(McpManager::new(plugins_manager));
|
||||
list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config,
|
||||
force_refetch,
|
||||
environment_manager,
|
||||
mcp_manager,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
config: &Config,
|
||||
force_refetch: bool,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
) -> anyhow::Result<AccessibleConnectorsStatus> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
@@ -234,8 +250,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
});
|
||||
}
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
|
||||
let mcp_manager = McpManager::new(Arc::clone(&plugins_manager));
|
||||
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await;
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
||||
{
|
||||
@@ -250,8 +264,9 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
});
|
||||
}
|
||||
|
||||
let mcp_config = config.to_mcp_config(plugins_manager.as_ref()).await;
|
||||
let mcp_servers = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config);
|
||||
let mcp_config = mcp_manager.runtime_config(config).await;
|
||||
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME);
|
||||
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
|
||||
if mcp_servers.is_empty() {
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
|
||||
@@ -4,8 +4,12 @@ use std::sync::Arc;
|
||||
use crate::config::Config;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::McpServerContribution;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::EffectiveMcpServer;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::configured_mcp_servers;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
@@ -14,29 +18,91 @@ use codex_mcp::tool_plugin_provenance as collect_tool_plugin_provenance;
|
||||
#[derive(Clone)]
|
||||
pub struct McpManager {
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
}
|
||||
|
||||
impl McpManager {
|
||||
pub fn new(plugins_manager: Arc<PluginsManager>) -> Self {
|
||||
Self { plugins_manager }
|
||||
Self {
|
||||
plugins_manager,
|
||||
extensions: codex_extension_api::empty_extension_registry(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a manager that resolves host-installed MCP contributions.
|
||||
pub fn new_with_extensions(
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
plugins_manager,
|
||||
extensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the MCP config after applying runtime-only extension overlays.
|
||||
pub async fn runtime_config(&self, config: &Config) -> McpConfig {
|
||||
let mut mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
let contributions = self.contributions(config).await;
|
||||
if contributions
|
||||
.iter()
|
||||
.any(|contribution| contribution.name() == CODEX_APPS_MCP_SERVER_NAME)
|
||||
{
|
||||
mcp_config.legacy_apps_mcp_loader_enabled = false;
|
||||
}
|
||||
Self::apply_to_configured_servers(&contributions, &mut mcp_config.configured_mcp_servers);
|
||||
mcp_config
|
||||
}
|
||||
|
||||
/// Returns config- and plugin-backed servers without runtime contributions.
|
||||
pub async fn configured_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
configured_mcp_servers(&mcp_config)
|
||||
}
|
||||
|
||||
/// Returns configured and host-contributed servers before auth gating.
|
||||
pub async fn runtime_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
||||
let mcp_config = self.runtime_config(config).await;
|
||||
configured_mcp_servers(&mcp_config)
|
||||
}
|
||||
|
||||
/// Returns runtime servers after auth gating and compatibility built-ins.
|
||||
pub async fn effective_servers(
|
||||
&self,
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, EffectiveMcpServer> {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
let mcp_config = self.runtime_config(config).await;
|
||||
effective_mcp_servers(&mcp_config, auth)
|
||||
}
|
||||
|
||||
/// Returns provenance for plugin-owned servers in the configured view.
|
||||
pub async fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
collect_tool_plugin_provenance(&mcp_config)
|
||||
}
|
||||
|
||||
async fn contributions(&self, config: &Config) -> Vec<McpServerContribution> {
|
||||
let mut contributions = Vec::new();
|
||||
for contributor in self.extensions.mcp_server_contributors() {
|
||||
contributions.extend(contributor.contribute(config).await);
|
||||
}
|
||||
contributions
|
||||
}
|
||||
|
||||
fn apply_to_configured_servers(
|
||||
contributions: &[McpServerContribution],
|
||||
servers: &mut HashMap<String, McpServerConfig>,
|
||||
) {
|
||||
for contribution in contributions {
|
||||
match contribution {
|
||||
McpServerContribution::Set { name, config } => {
|
||||
servers.insert(name.clone(), config.as_ref().clone());
|
||||
}
|
||||
McpServerContribution::Remove { name } => {
|
||||
servers.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies(
|
||||
let installed = sess
|
||||
.services
|
||||
.mcp_manager
|
||||
.configured_servers(config.as_ref())
|
||||
.runtime_servers(config.as_ref())
|
||||
.await;
|
||||
let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed);
|
||||
if missing.is_empty() {
|
||||
@@ -98,7 +98,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
}
|
||||
|
||||
let codex_home = config.codex_home.clone();
|
||||
let installed = sess.services.mcp_manager.configured_servers(config).await;
|
||||
let installed = sess.services.mcp_manager.runtime_servers(config).await;
|
||||
let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed);
|
||||
if missing.is_empty() {
|
||||
return;
|
||||
@@ -190,16 +190,22 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh from the config-backed merged MCP map (global + repo + managed)
|
||||
// and overlay the updated global servers so we don't drop repo-scoped
|
||||
// servers. Runtime additions such as built-ins are rebuilt by the refresh
|
||||
// path from the current config.
|
||||
let mut refresh_servers = sess.services.mcp_manager.configured_servers(config).await;
|
||||
let mut refresh_config = config.clone();
|
||||
let mut configured_servers = config.mcp_servers.get().clone();
|
||||
for (name, server_config) in &servers {
|
||||
refresh_servers
|
||||
configured_servers
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| server_config.clone());
|
||||
}
|
||||
if let Err(err) = refresh_config.mcp_servers.set(configured_servers) {
|
||||
warn!("failed to refresh MCP dependencies for mentioned skills: {err}");
|
||||
return;
|
||||
}
|
||||
let refresh_servers = sess
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_servers(&refresh_config)
|
||||
.await;
|
||||
sess.refresh_mcp_servers_now(
|
||||
turn_context,
|
||||
refresh_servers,
|
||||
|
||||
@@ -311,8 +311,10 @@ impl Session {
|
||||
) {
|
||||
let auth = self.services.auth_manager.auth().await;
|
||||
let config = self.get_config().await;
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.services.plugins_manager.as_ref())
|
||||
let mcp_config = self
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_config(config.as_ref())
|
||||
.await;
|
||||
let tool_plugin_provenance = self
|
||||
.services
|
||||
|
||||
@@ -272,7 +272,10 @@ impl ThreadManager {
|
||||
codex_home.to_path_buf(),
|
||||
restriction_product,
|
||||
));
|
||||
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
||||
let mcp_manager = Arc::new(McpManager::new_with_extensions(
|
||||
Arc::clone(&plugins_manager),
|
||||
Arc::clone(&extensions),
|
||||
));
|
||||
let skills_manager = Arc::new(SkillsManager::new_with_restriction_product(
|
||||
codex_home,
|
||||
config.bundled_skills_enabled(),
|
||||
|
||||
@@ -15,6 +15,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-context-fragments = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-tools = { workspace = true }
|
||||
|
||||
@@ -10,12 +10,14 @@ use codex_tools::ToolExecutor;
|
||||
|
||||
use crate::ExtensionData;
|
||||
|
||||
mod mcp;
|
||||
mod prompt;
|
||||
mod thread_lifecycle;
|
||||
mod tool_lifecycle;
|
||||
mod turn_input;
|
||||
mod turn_lifecycle;
|
||||
|
||||
pub use mcp::McpServerContribution;
|
||||
pub use prompt::PromptFragment;
|
||||
pub use prompt::PromptSlot;
|
||||
pub use thread_lifecycle::ThreadIdleInput;
|
||||
@@ -34,6 +36,18 @@ pub use turn_lifecycle::TurnErrorInput;
|
||||
pub use turn_lifecycle::TurnStartInput;
|
||||
pub use turn_lifecycle::TurnStopInput;
|
||||
|
||||
/// Extension contribution that resolves runtime MCP servers from host config.
|
||||
///
|
||||
/// Contributors run in registration order. Later contributions for the same
|
||||
/// name replace earlier ones. Implementations must contribute only names they
|
||||
/// own and must apply any source-specific policy before returning a server.
|
||||
/// Plugin-owned servers and their provenance continue to be resolved by the
|
||||
/// plugin manager until that ownership moves into an extension explicitly.
|
||||
#[async_trait::async_trait]
|
||||
pub trait McpServerContributor<C: Sync>: Send + Sync {
|
||||
async fn contribute(&self, config: &C) -> Vec<McpServerContribution>;
|
||||
}
|
||||
|
||||
/// Extension contribution that adds prompt fragments during prompt assembly.
|
||||
pub trait ContextContributor: Send + Sync {
|
||||
fn contribute<'a>(
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use codex_config::McpServerConfig;
|
||||
|
||||
/// One extension-owned overlay for the runtime MCP server configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum McpServerContribution {
|
||||
/// Adds or replaces a named MCP server.
|
||||
Set {
|
||||
name: String,
|
||||
config: Box<McpServerConfig>,
|
||||
},
|
||||
/// Removes a named MCP server.
|
||||
Remove { name: String },
|
||||
}
|
||||
|
||||
impl McpServerContribution {
|
||||
/// Returns the stable server name owned by this contribution.
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Set { name, .. } | Self::Remove { name } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ pub use codex_tools::parse_tool_input_schema_without_compaction;
|
||||
pub use contributors::ApprovalReviewContributor;
|
||||
pub use contributors::ConfigContributor;
|
||||
pub use contributors::ContextContributor;
|
||||
pub use contributors::McpServerContribution;
|
||||
pub use contributors::McpServerContributor;
|
||||
pub use contributors::PromptFragment;
|
||||
pub use contributors::PromptSlot;
|
||||
pub use contributors::ThreadIdleInput;
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::ConfigContributor;
|
||||
use crate::ContextContributor;
|
||||
use crate::ExtensionData;
|
||||
use crate::ExtensionEventSink;
|
||||
use crate::McpServerContributor;
|
||||
use crate::NoopExtensionEventSink;
|
||||
use crate::ThreadLifecycleContributor;
|
||||
use crate::TokenUsageContributor;
|
||||
@@ -24,6 +25,7 @@ pub struct ExtensionRegistryBuilder<C: Sync> {
|
||||
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
|
||||
token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
|
||||
context_contributors: Vec<Arc<dyn ContextContributor>>,
|
||||
mcp_server_contributors: Vec<Arc<dyn McpServerContributor<C>>>,
|
||||
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
|
||||
tool_contributors: Vec<Arc<dyn ToolContributor>>,
|
||||
tool_lifecycle_contributors: Vec<Arc<dyn ToolLifecycleContributor>>,
|
||||
@@ -41,6 +43,7 @@ impl<C: Sync> Default for ExtensionRegistryBuilder<C> {
|
||||
token_usage_contributors: Vec::new(),
|
||||
approval_review_contributors: Vec::new(),
|
||||
context_contributors: Vec::new(),
|
||||
mcp_server_contributors: Vec::new(),
|
||||
turn_input_contributors: Vec::new(),
|
||||
tool_contributors: Vec::new(),
|
||||
tool_lifecycle_contributors: Vec::new(),
|
||||
@@ -101,6 +104,11 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
|
||||
self.context_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Registers one runtime MCP server contributor.
|
||||
pub fn mcp_server_contributor(&mut self, contributor: Arc<dyn McpServerContributor<C>>) {
|
||||
self.mcp_server_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Registers one turn-input contributor.
|
||||
pub fn turn_input_contributor(&mut self, contributor: Arc<dyn TurnInputContributor>) {
|
||||
self.turn_input_contributors.push(contributor);
|
||||
@@ -131,6 +139,7 @@ impl<C: Sync> ExtensionRegistryBuilder<C> {
|
||||
token_usage_contributors: self.token_usage_contributors,
|
||||
approval_review_contributors: self.approval_review_contributors,
|
||||
context_contributors: self.context_contributors,
|
||||
mcp_server_contributors: self.mcp_server_contributors,
|
||||
turn_input_contributors: self.turn_input_contributors,
|
||||
tool_contributors: self.tool_contributors,
|
||||
tool_lifecycle_contributors: self.tool_lifecycle_contributors,
|
||||
@@ -147,6 +156,7 @@ pub struct ExtensionRegistry<C: Sync> {
|
||||
config_contributors: Vec<Arc<dyn ConfigContributor<C>>>,
|
||||
token_usage_contributors: Vec<Arc<dyn TokenUsageContributor>>,
|
||||
context_contributors: Vec<Arc<dyn ContextContributor>>,
|
||||
mcp_server_contributors: Vec<Arc<dyn McpServerContributor<C>>>,
|
||||
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
|
||||
tool_contributors: Vec<Arc<dyn ToolContributor>>,
|
||||
tool_lifecycle_contributors: Vec<Arc<dyn ToolLifecycleContributor>>,
|
||||
@@ -205,6 +215,11 @@ impl<C: Sync> ExtensionRegistry<C> {
|
||||
&self.context_contributors
|
||||
}
|
||||
|
||||
/// Returns the registered runtime MCP server contributors.
|
||||
pub fn mcp_server_contributors(&self) -> &[Arc<dyn McpServerContributor<C>>] {
|
||||
&self.mcp_server_contributors
|
||||
}
|
||||
|
||||
/// Returns the registered turn-input contributors.
|
||||
pub fn turn_input_contributors(&self) -> &[Arc<dyn TurnInputContributor>] {
|
||||
&self.turn_input_contributors
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "mcp",
|
||||
crate_name = "codex_mcp_extension",
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-mcp-extension"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_mcp_extension"
|
||||
path = "src/lib.rs"
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-config = { workspace = true }
|
||||
codex-core-plugins = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
@@ -0,0 +1,31 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::McpServerContribution;
|
||||
use codex_extension_api::McpServerContributor;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::codex_apps_mcp_server_config;
|
||||
|
||||
struct HostedAppsMcpExtension;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl McpServerContributor<Config> for HostedAppsMcpExtension {
|
||||
async fn contribute(&self, config: &Config) -> Vec<McpServerContribution> {
|
||||
let name = CODEX_APPS_MCP_SERVER_NAME.to_string();
|
||||
if !config.features.enabled(codex_features::Feature::Apps) {
|
||||
return vec![McpServerContribution::Remove { name }];
|
||||
}
|
||||
|
||||
vec![McpServerContribution::Set {
|
||||
name,
|
||||
config: Box::new(codex_apps_mcp_server_config(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
config.apps_mcp_product_sku.as_deref(),
|
||||
)),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install(builder: &mut ExtensionRegistryBuilder<Config>) {
|
||||
builder.mcp_server_contributor(std::sync::Arc::new(HostedAppsMcpExtension));
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_core::McpManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn contributes_hosted_apps_mcp_without_an_executor() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cli_overrides(vec![
|
||||
("features.apps".to_string(), true.into()),
|
||||
("features.apps_mcp_path_override".to_string(), true.into()),
|
||||
("chatgpt_base_url".to_string(), "https://chatgpt.com".into()),
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let manager = installed_manager(&config);
|
||||
|
||||
let runtime_config = manager.runtime_config(&config).await;
|
||||
assert!(!runtime_config.legacy_apps_mcp_loader_enabled);
|
||||
let servers = manager.effective_servers(&config, Some(&auth)).await;
|
||||
let server = servers
|
||||
.get(CODEX_APPS_MCP_SERVER_NAME)
|
||||
.and_then(|server| server.configured_config())
|
||||
.ok_or("Apps MCP should be contributed as a configured server")?;
|
||||
let McpServerTransportConfig::StreamableHttp { url, .. } = &server.transport else {
|
||||
panic!("Apps MCP should use streamable HTTP");
|
||||
};
|
||||
assert_eq!(url, "https://chatgpt.com/backend-api/ps/mcp");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hosted_apps_mcp_requires_chatgpt_auth() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cli_overrides(vec![("features.apps".to_string(), true.into())])
|
||||
.build()
|
||||
.await?;
|
||||
let auth = CodexAuth::from_api_key("test");
|
||||
let manager = installed_manager(&config);
|
||||
|
||||
let servers = manager.effective_servers(&config, Some(&auth)).await;
|
||||
assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_apps_remove_reserved_server_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cli_overrides(vec![
|
||||
("features.apps".to_string(), false.into()),
|
||||
(
|
||||
"mcp_servers.codex_apps.url".to_string(),
|
||||
"https://example.com/mcp".into(),
|
||||
),
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
let manager = installed_manager(&config);
|
||||
|
||||
let servers = manager.runtime_servers(&config).await;
|
||||
|
||||
assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn installed_manager(config: &Config) -> McpManager {
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
codex_mcp_extension::install(&mut builder);
|
||||
McpManager::new_with_extensions(
|
||||
Arc::new(PluginsManager::new(config.codex_home.to_path_buf())),
|
||||
Arc::new(builder.build()),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user