Files
codex/codex-rs/codex-mcp/src/server_key.rs
T
sayan-oai 561f0f5cf0 Keep same-named MCP servers separate across environments (#60905)
## Why

MCP servers discovered on different executors can share a name. Resolving them by name alone exposes only one server and prevents tools and resources from being addressed independently.

## What changed

- Add the disabled-by-default `multi_environment_mcp` feature to expose environment-scoped servers, including multiple installations of the same plugin.
- Carry exact `McpServerKey` identities through catalogs, connections, tool calls, resources, and approval tracking. Give environment-scoped tools stable namespaces derived from their environment.
- Include `environment_id` in scoped resource listings and accept it in resource requests. Require it for environment-owned servers when multiple environments are selected.
- Preserve server ownership through hooks and use the owning environment's permissions for scoped elicitation review.

## Testing

Add coverage for same-named servers across environments, independent calls and approvals, resource selection, plugin discovery, and rejection of hook changes to the tool environment.

GitOrigin-RevId: 5593372b06a15c16aa3220faa3b19f5d3b20e67e
2026-11-11 23:36:19 +00:00

56 lines
1.9 KiB
Rust

//! Exact MCP registration identity, separate from model-visible names and execution location.
//!
//! An unscoped key identifies a thread-wide declaration. A scoped key distinguishes
//! same-named declarations by environment; neither form is a model-facing tool name.
use serde::Deserialize;
use serde::Serialize;
/// The exact identity of one MCP server registration.
///
/// The optional environment is registration scope, not necessarily the server's
/// execution location. Unscoped registrations may execute on a remote environment.
/// Its display form is for diagnostics, not for serialization or lookup.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct McpServerKey {
#[serde(rename = "server_name")]
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
scoped_environment_id: Option<String>,
}
impl McpServerKey {
/// Identifies a thread-wide declaration by its configured name.
pub fn unscoped(name: impl Into<String>) -> Self {
Self {
name: name.into(),
scoped_environment_id: None,
}
}
/// Identifies a declaration contributed by a particular environment.
pub fn in_environment(environment: impl Into<String>, name: impl Into<String>) -> Self {
Self {
name: name.into(),
scoped_environment_id: Some(environment.into()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn scoped_environment_id(&self) -> Option<&str> {
self.scoped_environment_id.as_deref()
}
}
impl std::fmt::Display for McpServerKey {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.scoped_environment_id() {
Some(environment) => write!(formatter, "{} (environment {environment})", self.name()),
None => formatter.write_str(self.name()),
}
}
}