Add selected-plugin precedence and attribution to the MCP catalog (#27884)

## Why

**In short:** this PR resolves already-discovered MCP registrations. It
does not read selected plugins or discover their MCP servers.

The resolved MCP catalog currently builds config and auto-discovered
plugin registrations before runtime contributors are applied. A
thread-selected plugin needs a distinct precedence tier in that same
initial resolution pass: otherwise a disabled lower-precedence winner
can leave stale name-level state behind, and the winning MCP tools
cannot be attributed to the selected package reliably.

This PR adds that catalog boundary before executor discovery is
connected.

## What changed

- Added an explicit selected-plugin registration tier between
auto-discovered plugins and explicit config.
- Collected selected-plugin contributions before the initial catalog
build, while leaving compatibility and generic extension overlays in
their existing runtime phase.
- Retained the winning plugin ID and display name directly on
plugin-owned catalog registrations.
- Derived MCP tool provenance from the winning catalog entry instead of
joining against local-only plugin summaries.
- Retained the winning selected server's tool approval policy in the
running connection manager, so a selected registration cannot inherit
approval behavior from a losing local plugin.
- Kept remembered approval session-scoped for selected plugins until
there is an authority-aware persistence contract; Codex will not write
approval back to an unrelated local plugin.
- Preserved existing name-level disabled vetoes for discovered plugins
and config, while keeping a selected package's own disabled registration
scoped to that registration.
- Preserved deterministic selection order and existing config,
compatibility, and extension precedence.

The resulting order is:

```text
auto-discovered plugin
  < selected plugin
  < explicit config
  < compatibility registration
  < extension overlay
```

## Behavior and scope

This is a catalog and provenance change only. No production host
contributes selected-plugin MCP registrations yet, so existing local MCP
behavior remains unchanged.

The stacked follow-up, #27870, installs the executor plugin provider
that produces these registrations. App-server activation remains a
separate final step.

## Verification

Focused tests cover precedence, deterministic selected-plugin conflicts,
disabled-veto behavior across catalog phases, managed requirements
before selected-plugin resolution, winning-server approval policy, and
attribution when local and selected packages share an ID or server name.
CI owns execution of the test suite.
This commit is contained in:
jif authored and GitHub committed 2026-06-15 11:10:51 +02:00
1 parent dfd03ea01b
commit c3a479620f
16 files changed
+585 -89

No files matched your search

@@ -12,14 +12,18 @@ use crate::elicitation::elicitation_is_rejected_by_policy;
use crate::rmcp_client::AsyncManagedClient;
use crate::rmcp_client::ManagedClient;
use crate::rmcp_client::StartupOutcomeError;
use crate::server::EffectiveMcpServer;
use crate::server::McpServerMetadata;
use crate::server::McpServerOrigin;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
use crate::tools::normalize_tools_for_model_with_prefix;
use crate::tools::tool_with_model_visible_input_schema;
use codex_config::AppToolApproval;
use codex_config::Constrained;
use codex_config::McpServerConfig;
use codex_config::McpServerToolConfig;
use codex_config::types::AuthKeyringBackendKind;
use codex_exec_server::EnvironmentManager;
use codex_protocol::ToolName;
@@ -1128,6 +1132,8 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
"https://docs.example".to_string(),
)),
supports_parallel_tool_calls: true,
default_tools_approval_mode: None,
tool_approval_modes: HashMap::new(),
},
);
manager.clients.insert(
@@ -1150,6 +1156,28 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
assert_eq!(tool.server_origin.as_deref(), Some("https://docs.example"));
}
#[test]
fn server_metadata_preserves_tool_approval_policy() {
let mut config = crate::codex_apps_mcp_server_config(
"https://docs.example",
/*apps_mcp_product_sku*/ None,
);
config.default_tools_approval_mode = Some(AppToolApproval::Prompt);
config.tools.insert(
"search".to_string(),
McpServerToolConfig {
approval_mode: Some(AppToolApproval::Approve),
},
);
let metadata = McpServerMetadata::from(&EffectiveMcpServer::configured(config));
assert_eq!(metadata.tool_approval_mode("read"), AppToolApproval::Prompt);
assert_eq!(
metadata.tool_approval_mode("search"),
AppToolApproval::Approve
);
}
#[tokio::test]
async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);