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:
@@ -371,6 +371,30 @@ impl ResolvedMcpCatalog {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Replaces the resolved server set while preserving known server sources.
|
||||
///
|
||||
/// Names not present in the existing catalog are treated as config-owned.
|
||||
pub fn with_materialized_servers(&self, servers: HashMap<String, McpServerConfig>) -> Self {
|
||||
let mut builder = Self::builder();
|
||||
for (name, config) in servers {
|
||||
let source = self
|
||||
.server(&name)
|
||||
.map(|server| server.source.clone())
|
||||
.unwrap_or(McpServerSource::Config);
|
||||
let precedence = match &source {
|
||||
McpServerSource::Plugin(_) => RegistrationPrecedence::Plugin(Reverse(0)),
|
||||
McpServerSource::SelectedPlugin(_) => {
|
||||
RegistrationPrecedence::SelectedPlugin(Reverse(0))
|
||||
}
|
||||
McpServerSource::Config => RegistrationPrecedence::Config,
|
||||
McpServerSource::Compatibility { .. } => RegistrationPrecedence::Compatibility,
|
||||
McpServerSource::Extension { .. } => RegistrationPrecedence::Extension(0),
|
||||
};
|
||||
builder.register(McpServerRegistration::new(name, source, config, precedence));
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
/// Returns package attribution for each winning plugin-owned server.
|
||||
pub fn plugin_attributions_by_server_name(&self) -> HashMap<String, McpPluginAttribution> {
|
||||
self.servers
|
||||
|
||||
@@ -304,6 +304,17 @@ fn selected_plugins_override_discovered_plugins_but_not_config() {
|
||||
}]
|
||||
);
|
||||
|
||||
let refreshed = server("https://refreshed.example/mcp");
|
||||
let catalog =
|
||||
catalog.with_materialized_servers(HashMap::from([("docs".to_string(), refreshed.clone())]));
|
||||
assert_eq!(
|
||||
catalog.server("docs"),
|
||||
Some(&super::ResolvedMcpServer {
|
||||
source: selected_plugin_source("selected-alpha"),
|
||||
config: refreshed,
|
||||
})
|
||||
);
|
||||
|
||||
let mut builder = catalog.to_builder();
|
||||
let configured = server("https://config.example/mcp");
|
||||
builder.register(McpServerRegistration::from_config(
|
||||
|
||||
Reference in New Issue
Block a user