mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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.
This commit is contained in:
@@ -121,17 +121,11 @@ impl McpRequestProcessor {
|
||||
} = params;
|
||||
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let (config, mcp_config) = match thread_id.as_deref() {
|
||||
let (mcp_config, runtime_context) = match thread_id.as_deref() {
|
||||
Some(thread_id) => {
|
||||
let (_, thread) = self.load_thread(thread_id).await?;
|
||||
let thread_config = thread.config().await;
|
||||
let config = self
|
||||
.config_manager
|
||||
.load_latest_config_for_thread(thread_config.as_ref())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
|
||||
let mcp_config = thread.runtime_mcp_config(&config).await;
|
||||
(config, mcp_config)
|
||||
let runtime = thread.current_mcp_runtime();
|
||||
(runtime.config().clone(), runtime.runtime_context().clone())
|
||||
}
|
||||
None => {
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
@@ -140,7 +134,11 @@ impl McpRequestProcessor {
|
||||
.mcp_manager()
|
||||
.runtime_config(&config)
|
||||
.await;
|
||||
(config, mcp_config)
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
self.thread_manager.environment_manager(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
(mcp_config, runtime_context)
|
||||
}
|
||||
};
|
||||
let effective_servers = codex_mcp::effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
@@ -167,10 +165,6 @@ impl McpRequestProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
self.thread_manager.environment_manager(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
let http_client = runtime_context
|
||||
.resolve_http_client(&name, server)
|
||||
.map_err(|err| {
|
||||
@@ -189,16 +183,16 @@ impl McpRequestProcessor {
|
||||
let handle = perform_oauth_login_return_url_with_http_client(
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
mcp_config.mcp_oauth_credentials_store_mode,
|
||||
mcp_config.auth_keyring_backend_kind,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
&resolved_scopes.scopes,
|
||||
server.oauth_client_id(),
|
||||
server.oauth_resource.as_deref(),
|
||||
timeout_secs,
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
mcp_config.mcp_oauth_callback_port,
|
||||
mcp_config.mcp_oauth_callback_url.as_deref(),
|
||||
http_client,
|
||||
)
|
||||
.await
|
||||
@@ -250,18 +244,16 @@ impl McpRequestProcessor {
|
||||
None => (self.load_latest_config(/*fallback_cwd*/ None).await?, None),
|
||||
};
|
||||
let mcp_manager = self.thread_manager.mcp_manager();
|
||||
let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache();
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let mcp_config = match thread {
|
||||
Some(thread) => thread.runtime_mcp_config(&config).await,
|
||||
None => mcp_manager.runtime_config(&config).await,
|
||||
};
|
||||
let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache();
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
// This status path has no turn-selected environment. Use config cwd
|
||||
// as the local stdio fallback; named environment stdio MCPs must
|
||||
// declare their own absolute cwd.
|
||||
let runtime_context =
|
||||
McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf());
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
self.thread_manager.environment_manager(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::list_mcp_server_status_task(
|
||||
|
||||
Reference in New Issue
Block a user