MCP collision check incorrectly suppresses claude.ai connectors that point to a different workspace than the same-named plugin — and there is no scriptable workaround
MCP collision check incorrectly suppresses claude.ai connectors that point to a different workspace than the same-named plugin — and there is no scriptable workaround
Summary
When a user has both a built-in plugin MCP (e.g. plugin:slack:slack, plugin:atlassian:atlassian) and the corresponding claude.ai-side connector (e.g. claude.ai Slack, claude.ai Atlassian) intentionally pointed at different upstream workspaces / instances, the /mcp UI suppresses the claude.ai connector with:
claude.ai Slack · ◌ hidden — same URL as your server 'plugin:slack:slack'To use this connector instead, disable the plugin server in /plugins
The two connectors are not duplicates: they authenticate to different workspaces, hold different OAuth tokens, and expose different data. The collision detector matches on a normalized URL signature that strips the very identity that distinguishes them.
The only known workaround is a manual /mcp UI dance (disable both plugins → quit and restart Claude Code → re-enable both plugins) that must be repeated on every cold restart. A scripted version — even one that edits the exact file the UI Disable button writes — does not work, because the UI does two things (file write + in-memory plugin disconnect/load) and the file write alone is insufficient. Detail in "Why no scripted workaround exists" below.
This is closely related to (but materially different from) #39511, which was closed as "not planned." That issue described colliding with unconnected claude.ai connectors. This issue is about valid, intentional, simultaneously-active dual-connector configurations that the current logic makes unusable.
Reproduction
Configuration:
- Slack:
plugin:slack:slackauthenticated to Workspace A;claude.ai Slackauthenticated to Workspace B (different workspace, different OAuth identity). - Atlassian:
plugin:atlassian:atlassianauthenticated to Jira Instance A;claude.ai Atlassianauthenticated to Jira Instance B (different cloud ID, different account ID).
After a fresh claude start, /mcp shows both claude.ai Slack and claude.ai Atlassian as hidden — same URL as your server 'plugin:...'. The plugin connectors function; the claude.ai connectors are unreachable until manually re-enabled.
Manual workaround that currently fixes it (per restart):
/mcp→ highlightplugin:slack:slack→ Disable./mcp→ highlightplugin:atlassian:atlassian→ Disable.- Quit and restart Claude Code completely.
/mcp→ re-enableplugin:slack:slack./mcp→ re-enableplugin:atlassian:atlassian.
After step 5 all four connectors (both plugin-side and both claude.ai-side) appear and function simultaneously. The bug recurs on the next cold restart with both plugins enabled — at which point the collision suppression runs again at startup against the freshly-fetched claude.ai catalog.
Root cause
Verified against Claude Code 2.1.139 (Bun-bundled SEA, extracted from the macOS arm64 binary). Minified identifiers rotate every release; the stable anchors are the log strings, which a maintainer can grep on for the corresponding source lines. Names below are from 2.1.139.
The URL-normalization function strips everything except ?mcp_url=... for URLs routing through the Anthropic MCP gateway:
VG7 = [
"/v2/session_ingress/shttp/mcp/",
"/v2/session_ingress/mcp/ws/",
"/v2/ccr-sessions/",
];
function vG7(H) {
if (!VG7.some((_) => H.includes(_))) return H;
try { return new URL(H).searchParams.get("mcp_url") || H; }
catch { return H; }
}
That normalizer is called by the signature builder, which produces the collision key:
function $wH(H) {
let _ = E16(H); if (_) return `stdio:${hH(_)}`;
let q = S16(H); if (q) return `url:${vG7(q)}`;
return null;
}
And the suppression itself happens in (search for the log string Suppressing claude.ai connector to locate this function in source):
function QJ_(H, _) { // H = claude.ai connectors, _ = manually configured
let q = L16() ?? {}, K = new Map();
for (let [A, z] of Object.entries(_)) {
// ...; build map from signature → {name, scope}
let $ = $wH(z);
if ($ && !K.has($)) K.set($, { name: A, scope: z.scope });
}
let O = {}, T = [];
for (let [A, z] of Object.entries(H)) {
let $ = $wH(z), Y = $ !== null ? K.get($) : void 0;
if (Y !== void 0) {
v(`Suppressing claude.ai connector "${A}": duplicates manually-configured "${Y.name}"`);
T.push({ name: A, duplicateOf: Y.name, duplicateOfScope: Y.scope });
continue;
}
O[A] = z;
}
return { servers: O, suppressed: T };
}
Both the plugin-side (type === "claudeai-proxy") and the claude.ai-side connector for the same product (Slack, Atlassian, etc.) carry the same mcp_url value when their URLs route through the gateway, because both ultimately target the same upstream MCP product endpoint. The piece that distinguishes them — the user's chosen workspace / OAuth identity — is not part of the URL and therefore not part of the collision key.
A symmetric path also exists for the inverse case (search for Suppressing plugin MCP server), but only the claude.ai-side suppression carries duplicateOfScope.
Why no scripted workaround exists (and the file-edit dead-end)
This matters for anyone hoping to automate around the bug.
Reasonable hypothesis: since the /mcp UI's Disable button writes to ~/.claude.json → projects[<cwd>].disabledMcpServers[], a script that edits the same array should be equivalent to clicking the button. It is not.
The UI Disable button does two things in one action:
- Persists the plugin name to
disabledMcpServersin~/.claude.json(visible to the next startup's collision check). - Disconnects the plugin in-memory in the running Claude Code session, immediately.
The UI Enable button is the symmetric pair:
- Removes the plugin from
disabledMcpServers. - Loads the plugin's tool schemas into the running session, making it callable without a restart.
A script can do (1) trivially — jq can rewrite the array. A script cannot do (2): there is no publicly documented IPC mechanism we could locate that lets an external process disconnect or hot-load an MCP plugin inside a running Claude Code process.
The practical consequence, tested directly on 2026-05-11:
- Script-driven "Phase 2 re-enable" after restart: the file is in the desired "both enabled" state, claude.ai-side registered cleanly at startup (no collision), but plugin tool schemas are not surfaced in the session — they were absent at startup and no hot-load happened on the file edit. The user then has to restart again to pick up the plugins. On that restart,
disabledMcpServersis empty → collision detector runs against both plugins → claude.ai-side suppressed again → square one.
- Script-driven "Phase 1 disable" before restart: harmless, because the user is about to restart anyway. But it provides no benefit over the UI Disable click either, since the script can't disconnect the plugin in-memory — which doesn't matter pre-restart.
In short: the UI dance works because each click does file + in-memory state. Any approach that only writes the file produces a split-brain session that requires another restart to recover, which puts the file back into the state that triggers suppression. The two operations have to happen together, and the in-memory half is not exposed.
For completeness, these alternatives also don't work:
- Editing
~/.claude/settings.json→enabledPluginsdoes nothing for MCP collision. That field belongs to the broader plugin/extension system and is not consulted by the collision logic. - Editing
disabledMcpServersdirectly while Claude Code is running has no visible effect in the current session.
Suggested fix
The signature used for collision purposes should incorporate workspace / authentication identity, not just the upstream product URL. Concretely, one of:
- Include the connector's stable
id(thez.idfield that's already stored onclaudeai-proxyconfigs alongsideurl,scope, andtoolPermissions) in the signature built by$wH. That field uniquely identifies the upstream and is already present. - Skip URL-based collision detection entirely for
type === "claudeai-proxy"entries on the claude.ai-side, since they're managed server-side and the user has explicitly opted into them via claude.ai connector settings. - Match only when the OAuth account / workspace identifiers are also equal.
Secondary ask, if (1)–(3) are out of scope: expose an IPC or CLI command — e.g. claude mcp reload <plugin> — that performs the in-memory disconnect/load the UI buttons do. That would at least make the workaround scriptable instead of requiring a five-step manual dance on every cold restart.
How #39511 differs
#39511 reported a benign cosmetic warning when a disconnected claude.ai connector triggered the same collision logic. It was reasonable to close that as not planned — the plugins worked.
This issue is different on two axes:
- With both endpoints intentionally connected to different workspaces, the suppression is not cosmetic — the claude.ai connector is unreachable until the user performs the disable/restart/re-enable workaround, every time.
- There is no scriptable workaround, because the file-edit equivalent of the UI dance does not produce the same in-memory state. This makes the dance not just annoying but unautomatable for users who hit it on every cold start.
Environment
- Claude Code 2.1.139 (root-cause analysis verified against this version)
- macOS 15.7.3 (Darwin 24.6.0, arm64)
- Two plugin MCPs and two claude.ai connectors, intentionally pointed at distinct workspaces:
plugin:slack:slack→ Workspace A;claude.ai Slack→ Workspace Bplugin:atlassian:atlassian→ Jira Instance A;claude.ai Atlassian→ Jira Instance B
References
- Closely related: #39511 (closed not-planned; cosmetic warning on disconnected claude.ai connectors)
- Suppression log line:
Suppressing claude.ai connector "X": duplicates manually-configured "Y" - Symmetric path log line:
Suppressing plugin MCP server "X": duplicates manually-configured "Y" - Suppression UI string:
hidden — same URL as your server '<name>'inManage MCP servers
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗