Files
codex/codex-rs/core/src/session/mcp_runtime.rs
T
jif ee9e0f6387 Pin MCP runtimes to model steps (#30101)
## Why

An MCP refresh can replace the session's current manager while a model
step is still running. The step must execute calls through the same
manager whose tools it advertised.

## Boundary

```text
current session MCP runtime
          |
          | capture once for this model step
          v
StepContext.mcp
  - exact MCP config
  - exact connection manager
  - exact runtime environment context
```

```rust
pub struct McpRuntimeSnapshot {
    config: Arc<McpConfig>,
    manager: Arc<McpConnectionManager>,
    runtime_context: McpRuntimeContext,
}
```

## Example

```text
step A captures runtime A and advertises A's tools
refresh publishes runtime B
step A tool call -> runtime A
next step        -> runtime B
```

Capturing the snapshot is only an `Arc` clone. It does not restart MCPs
or make an RPC.

## What changes

- Captures one MCP runtime in `StepContext`.
- Uses it for tool planning, tool calls, resources, approvals, connector
attribution, and elicitation.
- Publishes replacement runtimes atomically.
- Lets an old runtime live only while an in-flight step or request still
holds its `Arc`.

Most of this diff is mechanical routing from the session-global manager
to `step_context.mcp`; it does not introduce selected-plugin discovery
yet.

## What does not change

- No plugin or extension migration.
- No new MCP cache policy.
- No environment file watching.
- No client sharing between separate managers.

## Stack

1. Extension-owned World State sections.
2. Project executor skills through World State.
3. **This PR:** pin one MCP runtime to each model step.
4. Project selected MCP/app/connector metadata by environment
availability.
5. One end-to-end integration scenario.
2026-06-26 00:53:07 +01:00

95 lines
3.3 KiB
Rust

use std::fmt;
use std::sync::Arc;
use codex_mcp::McpConfig;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpRuntimeContext;
/// MCP config, exact environment bindings, and manager used by one model request.
pub struct McpRuntimeSnapshot {
config: Arc<McpConfig>,
manager: Arc<McpConnectionManager>,
runtime_context: McpRuntimeContext,
}
impl McpRuntimeSnapshot {
pub(crate) fn new(
config: Arc<McpConfig>,
manager: Arc<McpConnectionManager>,
runtime_context: McpRuntimeContext,
) -> Self {
Self {
config,
manager,
runtime_context,
}
}
pub fn config(&self) -> &McpConfig {
self.config.as_ref()
}
pub fn manager(&self) -> &McpConnectionManager {
self.manager.as_ref()
}
pub(crate) fn manager_arc(&self) -> Arc<McpConnectionManager> {
Arc::clone(&self.manager)
}
pub fn runtime_context(&self) -> &McpRuntimeContext {
&self.runtime_context
}
#[cfg(test)]
pub(crate) fn new_uninitialized_for_test(config: &crate::config::Config) -> Arc<Self> {
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_mcp::ResolvedMcpCatalog;
use rmcp::model::ElicitationCapability;
let mcp_config = McpConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
apps_mcp_product_sku: config.apps_mcp_product_sku.clone(),
codex_home: config.codex_home.to_path_buf(),
mcp_oauth_credentials_store_mode: config.mcp_oauth_credentials_store_mode,
auth_keyring_backend_kind: config.auth_keyring_backend_kind(),
mcp_oauth_callback_port: config.mcp_oauth_callback_port,
mcp_oauth_callback_url: config.mcp_oauth_callback_url.clone(),
skill_mcp_dependency_install_enabled: config
.features
.enabled(Feature::SkillMcpDependencyInstall),
approval_policy: config.permissions.approval_policy.clone(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
use_legacy_landlock: config.features.use_legacy_landlock(),
apps_enabled: config.features.enabled(Feature::Apps),
prefix_mcp_tool_names: config.prefix_mcp_tool_names(),
client_elicitation_capability: ElicitationCapability::default(),
mcp_server_catalog: ResolvedMcpCatalog::default(),
connector_snapshot: codex_connectors::ConnectorSnapshot::default(),
};
let manager = McpConnectionManager::new_uninitialized_with_permission_profile(
&config.permissions.approval_policy,
config.permissions.permission_profile(),
config.prefix_mcp_tool_names(),
);
let runtime_context = McpRuntimeContext::new(
Arc::new(EnvironmentManager::default_for_tests()),
config.cwd.to_path_buf(),
);
Arc::new(Self::new(
Arc::new(mcp_config),
Arc::new(manager),
runtime_context,
))
}
}
impl fmt::Debug for McpRuntimeSnapshot {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("McpRuntimeSnapshot")
.finish_non_exhaustive()
}
}