Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
67d07f2
fix(core): stop MCP server rules from authorizing a colliding server
yiliang114 Sep 23, 2026
0342785
fix(core): keep a bare `*` from matching every MCP tool; correct matc…
yiliang114 Sep 23, 2026
bfc1c4d
Merge branch 'main' into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Sep 23, 2026
b6ae20e
fix(core): thread MCP permission aliases through every deny-side matcher
yiliang114 Sep 23, 2026
59ba562
fix(core): publish exact MCP raw identity for permission matching
yiliang114 Sep 23, 2026
84f4a8f
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Sep 24, 2026
3f82993
fix(core): judge the MCP server-level structure guard by every spelling
yiliang114 Sep 24, 2026
d941cc6
test(core): cover the alias resolver's positive branch in agent narro…
yiliang114 Sep 24, 2026
884a20d
style(core): apply prettier to the new collision rows
yiliang114 Sep 24, 2026
fcdc343
test(core): make the collision witnesses falsifiable, scope the doc c…
yiliang114 Sep 24, 2026
952e3ef
docs+test(core): name the real permission gates, restore two witnesse…
yiliang114 Sep 25, 2026
3c8afa3
chore(core): merge main (c3a4058a0c) into fix/issue-10199-mcp-permiss…
yiliang114 Sep 25, 2026
a900926
fix(core): keep legacy-spelled MCP wildcard prefixes covering their o…
yiliang114 Sep 25, 2026
1db81a2
docs(design): correct two false alias-threading claims in the MCP nam…
yiliang114 Sep 26, 2026
5ed8796
refactor(core): cut review-driven scope out of the MCP rule fix
yiliang114 Sep 26, 2026
c5a3b27
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Sep 26, 2026
a1a665d
test(core): pin legacy-spelled deny/ask coverage on a rewritten serve…
yiliang114 Sep 26, 2026
53f5735
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Sep 26, 2026
c94675a
Merge branch 'main' into fix/issue-10199-mcp-permission-alias-collision
qwen-code-dev-bot Sep 27, 2026
d1dcf87
fix(core): cover legacy-spelled MCP rules in every matcher arm (#10199)
qwen-code-dev-bot Sep 27, 2026
e0e8751
Merge branch 'main' into fix/issue-10199-mcp-permission-alias-collision
qwen-code-dev-bot Sep 27, 2026
e4099fc
fix(core): compare exact MCP entries against the gated legacy spellin…
qwen-code-dev-bot Sep 27, 2026
a05ab8b
fix(core): publish the MCP legacy alias only when its reduction vouch…
qwen-code-dev-bot Sep 28, 2026
8fa82bf
Merge remote-tracking branch 'origin/main' into fix/issue-10199-mcp-p…
yiliang114 Sep 28, 2026
53797dd
fix(core): gate truncated legacy MCP aliases on key-boundary provenance
yiliang114 Sep 28, 2026
1844413
test(core): pin the truncated-alias refusal on a producer-real fixture
yiliang114 Sep 29, 2026
c4a3d2e
fix(core): close R4-2 entrances - server-segment equality + ungated d…
yiliang114 Sep 29, 2026
991a66c
fix(core): restore tool-side operand in server arm + keep length-pres…
yiliang114 Sep 29, 2026
1fe811c
fix(core): consult disabledToolAliases in MCP app-tool and copy-disab…
yiliang114 Sep 29, 2026
6dadce0
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Oct 1, 2026
2da7c3f
test(core): build the alias-channel PMs with the compressed test helpers
yiliang114 Oct 1, 2026
1e29efd
fix(core): read the MCP server boundary from the producer, not a flat…
yiliang114 Oct 1, 2026
83c5961
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Oct 1, 2026
985683e
Merge concurrent branch head 1e29efde68 and origin/main into fix/issu…
yiliang114 Oct 1, 2026
5fa015c
fix(core): let MCP identity augment rule spelling matching, not repla…
yiliang114 Oct 2, 2026
f034073
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Oct 2, 2026
82359fd
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Oct 2, 2026
bbf9a82
fix(core): keep the wildcard identity arms augmenting, not replacing
yiliang114 Oct 2, 2026
882890c
Merge branch 'main' into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Oct 2, 2026
dbab8f4
fix(core): preserve exact legacy MCP restrictions
yiliang114 Oct 2, 2026
5bfea95
fix(core): keep restrictive rules matching cut MCP registrations
yiliang114 Oct 3, 2026
d62f5b7
fix(core): keep restrictive wildcards covering their own MCP key
yiliang114 Oct 3, 2026
1ace635
fix(core): preserve MCP wildcard restrictions and simplify matching
yiliang114 Oct 3, 2026
b5f5dcc
fix(core): reject ambiguous registered MCP alias grants
yiliang114 Oct 3, 2026
d5e9fc7
fix(core): guard MCP wildcard grants and reuse permission checks
yiliang114 Oct 4, 2026
272d3ef
refactor(core): simplify MCP permission checks and tests
yiliang114 Oct 4, 2026
7d5192b
fix(core): preserve reverse MCP wildcard ownership
yiliang114 Oct 4, 2026
6abd1eb
fix(permissions): resolve MCP wildcard grants by live server boundaries
yiliang114 Oct 5, 2026
7e489a5
fix(core): preserve producer-owned MCP rule coverage
yiliang114 Oct 5, 2026
cdb3cb1
fix(core): close partial-separator fail-open in restrictive MCP matching
yiliang114 Oct 6, 2026
0a5e943
Merge origin/main into fix/issue-10199-mcp-permission-alias-collision
yiliang114 Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/design/mcp-tool-name-provider-compatibility.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MCP Tool Name Provider Compatibility

[English](mcp-tool-name-provider-compatibility.md) | [简体中文](mcp-tool-name-provider-compatibility.zh-CN.md)

## Problem

Qwen Code currently accepts MCP tool names using Gemini's character set. Names such as `literature.search_pubmed` become `mcp__server__literature.search_pubmed`, which Gemini accepts but stricter OpenAI-compatible and Anthropic-compatible endpoints may reject before the tool can run.
Expand All @@ -15,15 +17,32 @@ Use one deterministic provider-safe normalization rule for MCP tool names:
- Keep the final name at 63 characters or fewer, which is accepted by Gemini and stricter OpenAI-compatible and Anthropic-compatible providers.
- Use the registered name throughout an MCP invocation instead of rebuilding it from raw server and tool names.
- Normalize MCP names in restored OpenAI and Anthropic request history so sessions created before the change remain sendable.
- Continue matching legacy MCP permission and disabled-tool entries by carrying the exact pre-normalization alias derived from the raw server and tool names. This also preserves names truncated by the previous middle-truncation algorithm without broadening wildcard matches.
- Continue matching legacy MCP permission and disabled-tool entries by carrying the exact pre-normalization identity derived from the raw server and tool names. This also preserves names truncated by the previous middle-truncation algorithm without broadening wildcard matches.

No provider-specific alias table is introduced. Legal existing names remain byte-for-byte unchanged, so Gemini behavior and normal built-in tools are unaffected.

Restored names produced by the previous middle-truncation algorithm are already provider-safe and remain unchanged in historical messages. Their removed middle cannot be reconstructed reliably, so converters do not guess a new hash-based name; exact permission and disabled-tool compatibility instead uses the raw-name alias available during MCP registration.

## Rule matching

Permission rules and `disallowedTools` blocklists may be written in a legacy spelling (`mcp__foo.bar__tool`) that no longer equals the registered provider-safe name. Matching works as follows (`packages/core/src/permissions/rule-parser.ts`):

- Each `DiscoveredMCPTool` advertises `permissionAliases`: the **exact raw identity** `mcp__<server>__<tool>` first, then the legacy `generateLegacyMcpToolName` reduction when it differs. A verbatim provider-safe registration lost nothing and advertises no alias. The registry (`ToolRegistry.getPermissionAliases`) and the invocation (`permissionFlow.ts`) read this same array.
- An alias is accepted as the tool's raw identity only when its own normalization **is** the registered name, so a different server's tool can never supply the raw identity a rule is matched against. The raw prefix comes from the user's configured server key, not from the server. Exact three-part entries are the one exception: they additionally match the advertised legacy reduction as a whole string, without this normalization check; grants additionally pass the registry ambiguity check below.
- Exact, server-level, and wildcard patterns are then compared **literally** against the registered name and the raw identity, plus the gated legacy reduction the next bullet describes. Nothing is reconstructed and nothing is hashed in this matcher: a registered name whose tail merely imitates a normalization hash proves nothing. An earlier design reconstructed candidate raw names and verified them against the unkeyed FNV-1a name hash; that proved only an existential (some raw name under the rule's prefix normalizes to this registered name) and was forgeable, so it was deleted rather than gated (#10199).
- Grant matching reads the legacy reduction only when it still vouches for its server. Restrictive matching additionally uses producer-derived spellings as described below. Character substitution leaves the server segment recognizable; past 63 characters `generateLegacyMcpToolName` truncates to `slice(0, 28) + '___' + slice(-32)`. Surviving the 28-character head window is necessary but not sufficient: `_` is legal in both segments, so two short keys that differ only around the separator (`acme-weather-forecast` vs `acme-weather-forecast_`, `github` vs `github__create_reposito`) can flatten to one byte-identical reduction that a flattened comparison cannot attribute. The vouching decision is made once, at the single publication point: `permissionAliases` advertises a truncated reduction only when the window also pins down where the key ends — the whole `__` separator fits inside it, or the key ends exactly at its edge, and the key's legacy image contains no `__` and does not end with `_` (`legacyReductionVouchesForServer`). A cut that reached the server segment keeps only its first 23 characters, so two different long keys can land in one byte-identical window; that reduction cannot authorize either server. The spelling-based matcher consumes the publication by alias membership — `__` is reserved in neither segment of `mcp__<server>__<tool>` and the reduction rewrites characters, so the server boundary cannot be re-derived from the flattened spelling. Prefix arms use the raw identity's length-preserving legacy substitution when the legacy alias is published. It contains the reduction's faithful head without its injected `___`, so no separate truncated window is needed. Exact entries still compare the whole published spelling.
- Identity-backed wildcard matching extracts the registered tool segment from the producer's provider-safe server boundary, independently of the rule's server spelling. Thus `mcp__foo.bar__get_data_*` on `foo.bar/get+data`, and prefixes reaching a registration hash or truncation cut, keep covering their own tool rather than losing a restriction.
- `disabledTools` never reaches `rule-parser.ts`. `ToolRegistry.isToolDisabled` matches it separately: it reads the ungated `disabledToolAliases` array by exact set membership, and additionally still compares `normalizeMcpToolName(entry)` against the registered name, so a legacy-spelled entry can also disable a colliding server's tool. That normalization arm predates #10199 and fails closed — do not delete it on this document's authority without a behaviour decision.
- The legacy `sanitizeToolNameForProvider` reduction was deliberately removed from matching: it made `mcp__foo.bar` rules reach the differently-registered server `foo_bar`. Do not reintroduce it. Pure per-tool matching still accepts provider and legacy spellings for compatibility and restrictive coverage. An allow match that depends on a lossy spelling is additionally rejected when the current session registry contains another raw identity claiming that server spelling or full tool rendering. Thus `mcp__foo_bar`, `mcp__foo_bar__*`, and a legacy exact entry cannot grant to `foo:bar` while `foo_bar` is also registered. A shared published middle-truncated legacy alias cannot grant either distinct tool. Wildcard grants resolve their server boundaries against the live producer identities. A complete raw whole-server boundary selects that server. Boundaries in the same contiguous underscore separator use the rule's explicit raw server segment; different separator positions make a tool prefix ambiguous and require confirmation for both claimants. Thus `mcp__foo____i*` selects `foo/__internal_debug` over `foo_/_internal_secret`, and `mcp__foo__*` selects `foo` over `foo__bar`; `mcp__foo__bar__deploy*` requires confirmation when both `foo/bar__deploy_x` and `foo__bar/deploy_y` are registered. A lossy or cut head without a complete boundary cannot grant while another registered rendering claims it. Raw exact rules and unique registered exact names retain priority. Bare whole-server rules support keys containing `__`, but cannot grant that server when another identity claims the string as a full tool name. A single producer's underscore tool prefix and deliberate coarse raw prefixes such as `mcp__foo*` remain valid; single-claimant aliases retain compatibility. Both permission evaluation and relevant-rule detection apply this guard. The registry is read on every decision across both model-visible and App-only tool pools, deduplicated by raw identity, so registration and removal take effect immediately without a cached alias table. Deny, ask, and `disallowedTools` preserve restrictive coverage under the boundary conditions below; `disabledTools` matching is unchanged. Direct matcher callers without a registry remain compatibility predicates, not an ambiguity-safe standalone grant authority.
- A bare `*` is not an MCP pattern and matches no MCP tool; `mcp__*` and `mcp__server__*` keep their documented meanings.
- Restrictive rules (`deny`, `ask`, and `disallowedTools`) also match the raw and historical spellings generated from the producer-carried `mcpIdentity`, even when alias publication withholds the legacy reduction. This preserves exact restrictions and faithful historical prefixes on long server keys. The existing identity-aware matcher retains the producer's server/tool boundary, and a registered-name cut may additionally restrict every matching claimant. A prefix that stops at or inside the key's own server separator, in its raw, provider-safe, or legacy spelling, restricts every tool of that key, including tools whose names start with `_`; this matches `main` and is restrictive-only. If several tools share a lossy restrictive spelling, all remain restricted; use the registered exact name to target just one. `allow` never uses this fallback, and no additional alias channel is published.
- Asymmetry: a lost match can remove an explicit restriction even when a trusted server would otherwise run without a prompt. Enforcement paths therefore carry both advertised aliases and producer identity. Callers without identity retain the gated spelling behavior; callers without a live registry cannot certify wildcard grant ambiguity.

## Verification

- Unit tests for valid, invalid, colliding, long, stable, and idempotent names.
- MCP tool tests for registration, permission rules, reconnect lookup, and disabled tools.
- Collision tests (`mcp-server-rule-collision.test.ts`): cross-server forgery witnesses (exact, server-level, and wildcard shapes), middle-truncation over-match, legacy-spelled deny coverage, and the no-alias posture.
- OpenAI and Anthropic converter tests for restored history containing dotted MCP names.
- Registry-backed allow tests with real producer identities: safe/unsafe server pairs, two unsafe claimants, exact same-server tool aliases, shared middle truncation, live registration/removal, single-claimant compatibility, raw/coarse rules, and unchanged deny/ask coverage.
- Core package build and typecheck.
Loading
Loading