mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
4ec3b8eeea
## 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.
94 lines
3.3 KiB
Rust
94 lines
3.3 KiB
Rust
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()),
|
|
)
|
|
}
|