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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user