mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Add opt-in protocol discovery for local stdio MCP servers (#57210)
## Why Stdio servers without `CODEX_MCP_PROTOCOL_VERSION` use the legacy handshake, even when they support MCP `2026-07-28`. ## What changed - Add the disabled-by-default `mcp_connection_sharing` feature to enable discovery for eligible local stdio servers. - Recover from failed or late discovery with a fresh handshake when needed, waiting for the previous helper to release resources before restarting. - Retain a working legacy connection when an upgrade attempt fails for the same account, except on authentication errors. - Keep tool catalogs separate by protocol and invalidate legacy catalogs when discovery negotiates the modern protocol. ## Testing Add regression coverage for failed and late discovery, legacy fallback, protocol-specific catalog reuse, message limits, and Windows descendant cleanup. GitOrigin-RevId: 36c675ef3b81d32fccf06ccbddda2312cfe602c9
This commit is contained in:
@@ -184,6 +184,7 @@ impl McpToolCatalogCacheContext {
|
||||
cached_deadline.deadline
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn current_tools(&self) -> Option<Vec<ToolInfo>> {
|
||||
self.current_tools_or(/*fallback*/ None)
|
||||
}
|
||||
@@ -235,25 +236,28 @@ impl McpToolCatalogCacheContext {
|
||||
state.snapshot = None;
|
||||
}
|
||||
|
||||
/// An observed protocol change invalidates a legacy catalog, including any older fetch in flight.
|
||||
pub(crate) fn invalidate_if_newest(&self, ticket: &McpToolCatalogFetchTicket) {
|
||||
if let Some(mut state) = self.accept_generation(ticket.generation) {
|
||||
state.snapshot = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn publish_if_newest(
|
||||
&self,
|
||||
ticket: McpToolCatalogFetchTicket,
|
||||
tools: &[ToolInfo],
|
||||
meta: Option<Arc<serde_json::Value>>,
|
||||
) {
|
||||
let mut state = lock_unpoisoned(&self.entry.state);
|
||||
if state.disabled_by_server || ticket.generation <= state.last_accepted_generation {
|
||||
let Some(mut state) = self.accept_generation(ticket.generation) else {
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
let mut tools = tools.to_vec();
|
||||
for tool in &mut tools {
|
||||
// Tool annotations affect approval and parallelism decisions, so only the live
|
||||
// connection may supply them.
|
||||
tool.tool.annotations = None;
|
||||
}
|
||||
state.last_accepted_generation = ticket.generation;
|
||||
state.optional_startup_deadline = None;
|
||||
state.snapshot = Some(ToolCatalogSnapshot {
|
||||
tools,
|
||||
meta,
|
||||
@@ -261,6 +265,16 @@ impl McpToolCatalogCacheContext {
|
||||
tool_list_revision: ticket.tool_list_revision,
|
||||
});
|
||||
}
|
||||
|
||||
fn accept_generation(&self, generation: u64) -> Option<MutexGuard<'_, ToolCatalogCacheState>> {
|
||||
let mut state = lock_unpoisoned(&self.entry.state);
|
||||
if state.disabled_by_server || generation <= state.last_accepted_generation {
|
||||
return None;
|
||||
}
|
||||
state.last_accepted_generation = generation;
|
||||
state.optional_startup_deadline = None;
|
||||
Some(state)
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
|
||||
@@ -417,6 +431,13 @@ impl ToolCatalogTransportIdentity {
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
// An unmarked helper can expose different tools after a legacy fallback.
|
||||
if matches!(
|
||||
connection_identity,
|
||||
Some((_, McpProtocolMode::DiscoverLocalStdio, _))
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
if env_vars
|
||||
.iter()
|
||||
.any(codex_config::McpServerEnvVar::is_remote_source)
|
||||
@@ -438,6 +459,7 @@ impl ToolCatalogTransportIdentity {
|
||||
env_vars,
|
||||
cwd,
|
||||
&config.environment_id,
|
||||
connection_identity.map(|(_, mode, _)| mode.preferred_protocol_version()),
|
||||
client_elicitation_capability,
|
||||
client_mcp_extensions.iter().collect::<BTreeMap<_, _>>(),
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user