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:
jif
2026-06-09 22:44:16 +02:00
committed by GitHub
Unverified
parent 5a0f913426
commit 4ec3b8eeea
28 changed files with 424 additions and 65 deletions
@@ -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,
}
}
}
+2
View File
@@ -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