[codex] Dedupe plugin MCPs by app declaration name (#27607)

## Context

This is the next step in the plugin auth-routing stack. The earlier PRs
make `PluginsManager` auth-aware and move the broad App/MCP surface
decision into that layer. This PR narrows the ChatGPT/SIWC behavior so
we only hide a plugin MCP server when it conflicts with an App
declaration of the same name.

In product terms: if a plugin exposes both an App route and MCP route
for `foo`, ChatGPT/SIWC sessions should use the App route for `foo`. If
the same plugin also exposes a separate MCP server like `foo2`, that MCP
server should remain available.

```json
// .app.json
{
  "apps": {
    "foo": {
      "id": "connector_abc"
    }
  }
}
```

```json
// .mcp.json
{
  "mcpServers": {
    "foo": {
      "url": "https://mcp.foo.com/mcp"
    },
    "foo2": {
      "url": "https://mcp.foo2.com/mcp"
    }
  }
}
```

## Stack

- PR1: #27652 seed plugin manager auth at construction.
- PR2: #27459 route plugin surfaces by auth mode.
- PR3: #27607 dedupe plugin MCP servers by App declaration name.
- PR4: #27602 preserve plugin Apps in connector listings.
- PR5: #27461 skip install-time plugin MCP OAuth for matching App
routes.

## Summary

- Preserve App declaration names in loaded plugin metadata.
- Keep public effective App outputs as deduped connector IDs for
existing callers.
- For ChatGPT/SIWC, suppress only plugin MCP servers whose names match
declared App names.

## Validation

```bash
cargo fmt --all
cargo test -p codex-core-plugins plugin_auth_projection
cargo test -p codex-core-plugins effective_apps
cargo test -p codex-core-plugins read_plugin_for_config_installed_git_source_reads_from_cache_without_cloning
cargo test -p codex-core explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins
cargo test -p codex-core explicit_plugin_mentions_keep_non_conflicting_mcp_for_chatgpt_auth
cargo test -p codex-app-server --test all plugin_install_filters_disallowed_apps_needing_auth
git diff --check
```

---------

Co-authored-by: Xin Lin <xl@openai.com>
This commit is contained in:
felixxia-oai
2026-06-14 01:53:09 +01:00
committed by GitHub
Unverified
parent 0fed4497f5
commit 51316ead4a
7 changed files with 419 additions and 140 deletions
+22
View File
@@ -1,5 +1,7 @@
//! Shared plugin package models, source providers, identifiers, and telemetry summaries.
use std::collections::HashSet;
pub use codex_utils_plugins::mention_syntax;
pub use codex_utils_plugins::plugin_namespace_for_skill_path;
@@ -26,6 +28,26 @@ pub use provider::ResolvedPluginLocation;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AppConnectorId(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppDeclaration {
pub name: String,
pub connector_id: AppConnectorId,
pub category: Option<String>,
}
pub fn app_connector_ids_from_declarations<'a>(
app_declarations: impl IntoIterator<Item = &'a AppDeclaration>,
) -> Vec<AppConnectorId> {
let mut connector_ids = Vec::new();
let mut seen_connector_ids = HashSet::new();
for app in app_declarations {
if seen_connector_ids.insert(&app.connector_id) {
connector_ids.push(app.connector_id.clone());
}
}
connector_ids
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginCapabilitySummary {
pub config_name: String,
+10 -14
View File
@@ -5,8 +5,10 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_plugins::PluginSkillRoot;
use crate::AppConnectorId;
use crate::AppDeclaration;
use crate::PluginCapabilitySummary;
use crate::PluginHookSource;
use crate::app_connector_ids_from_declarations;
const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024;
@@ -22,7 +24,7 @@ pub struct LoadedPlugin<M> {
pub disabled_skill_paths: HashSet<AbsolutePathBuf>,
pub has_enabled_skills: bool,
pub mcp_servers: HashMap<String, M>,
pub apps: Vec<AppConnectorId>,
pub apps: Vec<AppDeclaration>,
pub hook_sources: Vec<PluginHookSource>,
pub hook_load_warnings: Vec<String>,
pub error: Option<String>,
@@ -53,7 +55,7 @@ fn plugin_capability_summary_from_loaded<M>(
description: prompt_safe_plugin_description(plugin.manifest_description.as_deref()),
has_skills: plugin.has_enabled_skills,
mcp_server_names,
app_connector_ids: plugin.apps.clone(),
app_connector_ids: app_connector_ids_from_declarations(&plugin.apps),
};
(summary.has_skills
@@ -149,18 +151,12 @@ impl<M: Clone> PluginLoadOutcome<M> {
}
pub fn effective_apps(&self) -> Vec<AppConnectorId> {
let mut apps = Vec::new();
let mut seen_connector_ids = HashSet::new();
for plugin in self.plugins.iter().filter(|plugin| plugin.is_active()) {
for connector_id in &plugin.apps {
if seen_connector_ids.insert(connector_id.clone()) {
apps.push(connector_id.clone());
}
}
}
apps
app_connector_ids_from_declarations(
self.plugins
.iter()
.filter(|plugin| plugin.is_active())
.flat_map(|plugin| plugin.apps.iter()),
)
}
pub fn effective_plugin_hook_sources(&self) -> Vec<PluginHookSource> {