Skip to content

Resolve Copilot CLI & VS Code built-in/hosted MCP identity (sanction/policy coverage)#181

Open
anonpran wants to merge 1 commit into
mainfrom
nanda/copilot-builtin-mcp-resolution
Open

Resolve Copilot CLI & VS Code built-in/hosted MCP identity (sanction/policy coverage)#181
anonpran wants to merge 1 commit into
mainfrom
nanda/copilot-builtin-mcp-resolution

Conversation

@anonpran

@anonpran anonpran commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the same MCP-identity gap PR #180 fixed for Claude Code — now for GitHub Copilot's built-in / hosted MCP servers (CLI + VS Code, shared hook). The runtime-provisioned github-mcp-server reaches the gateway with no or garbled identity → null/wrong fingerprint → sanction evasion or over-block, and canonical-group BLOCK/WARN/AUDIT policies + MCP analytics are wrong. This is an original identity bug exposed (not caused) by the sanction allow-list.

All changes are in one production file (copilot/hooks/unbound.py) + its tests. No gateway / gateway-data / sibling-hook changes.

Root cause (verified by live E2E)

  • CLI: built-in github tools emit as bare github-mcp-server-<tool>; github-mcp-server is not in ~/.copilot/mcp-config.json. detect_mcp_call mis-attributed it to a coincidental shorter github config entry → garbled tool (mcp-server-list_issues) + wrong URL (api.githubcopilot.com/mcp); with no coincidental entry → return {} (fail-open allow-list bypass).
  • VS Code: a hosted/registry server not in mcp.json_resolve_vscode_mcp returns (None,None,None) → the raw single-underscore name reaches the gateway unrecognized → fail-open allow.

Fix

  • CLI built-in registry (COPILOT_CLI_BUILTIN_MCP) merged into detect_mcp_call's candidate map ({**builtins, **mcp_servers} — configured servers still win). The built-in github-mcp-server (len 17) out-ranks the coincidental github (len 6) via the existing longest-prefix logic → correct identity (url:api.individual.githubcopilot.com/mcp/readonly) + un-garbled tool.
  • VS Code built-in fallback (COPILOT_VSCODE_BUILTIN_MCP) in _resolve_vscode_mcp, firing only when no configured server matches (setup#168 + the ambiguity guard fully preserved) → hosted github (url:api.githubcopilot.com/mcp, already in the GitHub canonical group → no backend change).
  • Fail-secure (VS Code): an unresolved mcp_<server>_<tool> now forwards a best-effort parsed identity so the gateway applies deny-by-default — only when a non-empty tool segment is present; degenerate mcp_-tokens are left as-is (not a regression). Safe because mcp_ is a reliable MCP signal (native tools never start with it). This intentionally flips one prior fail-open test to fail-secure.
  • Built-in registry configs are returned as copies (no in-process mutation of the shared literals).
  • Per-surface registries: CLI url (api.individual.githubcopilot.com/mcp/readonly) ≠ VS Code url (api.githubcopilot.com/mcp) — same logical server, different endpoints, intentional.

Deliberately deferred (noted, not a bug)

  • CLI fail-secure on BARE unresolved tokens still return {}. A delimiter-less <server>-<tool> name has no reliable split; blind-forwarding would over-block benign native tools fleet-wide the moment any group is sanctioned. Documented as a follow-up.

⚠️ Backend dependency (separate, out of scope)

For the CLI built-in to be allowed when GitHub is sanctioned, gateway-data must classify url:api.individual.githubcopilot.com/mcp and .../mcp/readonly into the GitHub canonical group. It is absent today (the classifier rule is api.githubcopilot.com/*, which doesn't cover the individual subdomain), so until that lands the CLI built-in resolves to a correct-but-uncataloged identity → fail-secure deny under an active allow-list. The VS Code hosted url is already cataloged → no backend dep.

Scope

copilot/hooks/unbound.py only. Cursor (project-scoped .cursor/mcp.json) is a separate follow-up.

Test plan

  • 27 hermetic tests (test_pretool_mcp.py): CLI built-in out-ranks coincidental github + un-garbled tool; built-in resolves with no config; configured server wins (precedence); unrelated bare token still unresolved; VS Code hosted built-in fallback resolves when unconfigured; configured VS Code server still wins (setup#168); fallback doesn't rescue a real ambiguity; sanctioned hosted built-in allowed e2e; unresolved mcp_ now fail-secure blocked; degenerate empty-tool token not forwarded / not falsely blocked.
  • py_compile + pyflakes clean.
  • Manual (needs the backend GitHub-group mapping): CLI github-mcp-server-* allowed when GitHub sanctioned.

🤖 Generated with Claude Code


Note

Medium Risk
Changes pre-tool MCP identity and sanction behavior (fail-open to fail-secure for some VS Code mcp_ calls), which can block tools under active allow-lists; CLI built-in allow still depends on gateway-data classifying the individual subdomain URL.

Overview
Fixes Copilot CLI and VS Code pre-tool hooks so runtime built-in github-mcp-server calls get correct MCP identity and sanction/policy coverage, instead of wrong attribution, missing config, or slipping the allow-list.

CLI: Adds COPILOT_CLI_BUILTIN_MCP and merges it into detect_mcp_call (user config still wins on name collision). Bare github-mcp-server-<tool> now resolves to the built-in readonly URL and beats a shorter coincidental github entry via longest-prefix matching.

VS Code: Adds COPILOT_VSCODE_BUILTIN_MCP and a built-in-only fallback in _resolve_vscode_mcp when no configured server matches (configured matches and ambiguity rules unchanged). Shared matching moves to _vscode_match_servers; built-in configs are returned as copies via _copy_if_builtin.

Fail-secure: Unresolved mcp_<server>_<tool> tokens with a non-empty tool now forward best-effort mcp_server / mcp_tool to the gateway so deny-by-default can block; degenerate tokens like mcp_x still forward nothing. Bare unresolved CLI MCP names remain fail-open (documented deferral).

Tests in test_pretool_mcp.py cover built-in precedence, fallback, sanction e2e, and the flipped unresolved behavior.

Reviewed by Cursor Bugbot for commit 18a004e. Bugbot is set up for automated code reviews on this repo. Configure here.

Greptile Summary

This PR fixes MCP identity mis-attribution for GitHub Copilot's built-in github-mcp-server on both the CLI and VS Code surfaces, closing a gap where absent or garbled identity could cause sanction bypass or over-block. All changes are confined to copilot/hooks/unbound.py and its test file.

  • CLI built-in: COPILOT_CLI_BUILTIN_MCP is merged into detect_mcp_call's candidate map so that the bare github-mcp-server-<tool> form resolves to the correct readonly URL (api.individual.githubcopilot.com/mcp/readonly) instead of being mis-attributed to a shorter configured github entry via longest-prefix.
  • VS Code built-in fallback: COPILOT_VSCODE_BUILTIN_MCP is tried in _resolve_vscode_mcp only when no configured server produces candidates (ambiguity guard fully preserved from setup#168); unresolved mcp_<server>_<tool> tokens now forward a best-effort server/tool identity for deny-by-default instead of silently failing open, while degenerate tokens with no tool segment remain unchanged.
  • 27 hermetic tests cover precedence, fallback, fail-secure blocking, and edge cases for both surfaces.

Confidence Score: 4/5

Safe to merge from a correctness standpoint; the identity resolution logic is sound, precedence rules are properly ordered, and the fail-secure flip is well-tested — but the new resolution flows ship without instrumentation, leaving the three new paths (CLI builtin match, VS Code builtin fallback, fail-secure forwarding) invisible in production metrics.

The core logic change is correct and thoroughly covered by 27 hermetic tests. The built-in registry merge preserves configured-server precedence, the VS Code ambiguity guard from setup#168 is intact, and the fail-secure behavior for degenerate tokens is explicitly guarded. The gap is that none of the three new observable flows emit counters or histograms, making it impossible to track resolution rates or detect regressions in production without adding instrumentation.

copilot/hooks/unbound.py — the three new resolution flows (CLI builtin merge, VS Code builtin fallback, fail-secure unresolved forwarding) each need success/failure counters before going live.

Important Files Changed

Filename Overview
copilot/hooks/unbound.py Core identity resolution logic updated: adds CLI/VS Code builtin registries, merges builtins into detect_mcp_call, adds VS Code builtin fallback in resolve_vscode_mcp, and flips unresolved mcp tokens from fail-open to fail-secure. Logic is correct and well-guarded; no new Prometheus metrics emitted for the three new observable flows.
copilot/hooks/test_pretool_mcp.py 27 tests added covering CLI builtin precedence, VS Code builtin fallback, ambiguity preservation, fail-secure flip, and degenerate token handling; the mock gateway accurately mirrors real gateway fingerprinting logic.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[raw_tool arrives] --> B{starts with mcp_ not mcp__?}
    B -- Yes VS Code --> C[_resolve_vscode_mcp]
    B -- No CLI bare --> D{is_mcp already?}
    D -- No --> E[detect_mcp_call merged builtins plus config]
    E --> F{match found?}
    F -- Yes --> G[forward with config to gateway]
    F -- No --> H[return empty - fail-open for bare CLI]
    C --> I[match against configured servers]
    I --> J{candidates found?}
    J -- Yes --> K{ambiguity guard passes?}
    K -- Yes --> G
    K -- No --> L[unresolved path]
    J -- No --> M[match against COPILOT_VSCODE_BUILTIN_MCP]
    M --> N{exactly 1 builtin match?}
    N -- Yes --> G
    N -- No --> L
    L --> O{first underscore split yields both server and tool?}
    O -- Yes --> P[best-effort identity forwarded - fail-secure deny]
    O -- No --> Q[no identity forwarded - degenerate token allow]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[raw_tool arrives] --> B{starts with mcp_ not mcp__?}
    B -- Yes VS Code --> C[_resolve_vscode_mcp]
    B -- No CLI bare --> D{is_mcp already?}
    D -- No --> E[detect_mcp_call merged builtins plus config]
    E --> F{match found?}
    F -- Yes --> G[forward with config to gateway]
    F -- No --> H[return empty - fail-open for bare CLI]
    C --> I[match against configured servers]
    I --> J{candidates found?}
    J -- Yes --> K{ambiguity guard passes?}
    K -- Yes --> G
    K -- No --> L[unresolved path]
    J -- No --> M[match against COPILOT_VSCODE_BUILTIN_MCP]
    M --> N{exactly 1 builtin match?}
    N -- Yes --> G
    N -- No --> L
    L --> O{first underscore split yields both server and tool?}
    O -- Yes --> P[best-effort identity forwarded - fail-secure deny]
    O -- No --> Q[no identity forwarded - degenerate token allow]
Loading

Reviews (2): Last reviewed commit: "Resolve Copilot CLI & VS Code built-in/h..." | Re-trigger Greptile

@anonpran anonpran requested a review from a team June 24, 2026 05:30

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 86c3704. Configure here.

Comment thread copilot/hooks/unbound.py
parsed_server, _, parsed_tool = body.partition('_')
if parsed_server and parsed_tool:
mcp_server, mcp_tool = parsed_server, parsed_tool
canonical = f"mcp__{mcp_server}__{mcp_tool}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ambiguous MCP names misparsed

Medium Severity

The unbound_hook function attempts to parse a server and tool from raw VS Code MCP input even when _resolve_vscode_mcp explicitly returns unresolved due to ambiguity. This bypasses the resolver's careful checks, potentially mis-identifying the server for sanctioning.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 86c3704. Configure here.

Comment thread copilot/hooks/unbound.py
if len(builtin_candidates) != 1:
return (None, None, None)
bc = builtin_candidates[0]
return (bc[2], bc[3], _copy_if_builtin(COPILOT_VSCODE_BUILTIN_MCP[bc[2]]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builtin fallback matches bare github

Medium Severity

The VS Code built-in fallback reuses truncated alias matching, so tool bodies like github_get_me can match the github-mcp-server alias github_mcp_server via alias.startswith('github') when no configured server matched. That attaches the hosted built-in URL to tokens meant for a separate github server entry.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 86c3704. Configure here.

Comment thread copilot/hooks/unbound.py
Comment on lines +1032 to 1037
body = raw_tool[len('mcp_'):]
parsed_server, _, parsed_tool = body.partition('_')
if parsed_server and parsed_tool:
mcp_server, mcp_tool = parsed_server, parsed_tool
canonical = f"mcp__{mcp_server}__{mcp_tool}"
log_error(f"copilot vscode mcp UNRESOLVED session={session_id} tool={raw_tool}", 'mcp_match')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "UNRESOLVED" logged even when best-effort parse succeeds

The log_error call on line 1037 always fires with the message "copilot vscode mcp UNRESOLVED", but when parsed_server and parsed_tool is truthy (lines 1034-1036), mcp_server and mcp_tool are populated and a full mcp__server__tool canonical is built and forwarded to the gateway. From logs alone it's impossible to tell whether the gateway received an identity (fail-secure block possible) or not (degenerate token, no identity forwarded, allow). Two distinct log messages — one for each branch — would let on-call engineers reconstruct exactly what happened from a failing session.

Comment thread copilot/hooks/unbound.py
Comment thread copilot/hooks/unbound.py
Comment on lines 670 to +709
@@ -652,7 +706,7 @@ def detect_mcp_call(raw_tool, mcp_servers):

if best is None:
return (None, None, None)
return (best[1], best[2], mcp_servers.get(best[1]))
return (best[1], best[2], _copy_if_builtin(merged.get(best[1])))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No Prometheus metrics for the new built-in resolution flows

Three new observable flows land in this PR but none emit metrics: (1) CLI built-in candidate merged and matched (detect_mcp_call now also resolves from COPILOT_CLI_BUILTIN_MCP), (2) VS Code built-in fallback resolved/unresolved (_resolve_vscode_mcp builtin path), and (3) the new fail-secure unresolved forwarding path in process_pre_tool_use. Per the observability rules, every new flow must ship at minimum a success/failure counter and a workflow-specific metric. Suggested metrics: mcp_builtin_resolution_total{surface="cli|vscode", outcome="resolved|unresolved"} and mcp_failsecure_forwarded_total{surface="vscode"}.

Context Used: P0 — Critical (must block merge)
Django / Backend ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…y fingerprinting

Copilot's runtime-provisioned built-in github-mcp-server (CLI: bare
github-mcp-server-<tool> not in mcp-config.json; VS Code: hosted server absent
from mcp.json) reached the gateway with no/garbled identity → null/wrong
fingerprint → sanction evasion or over-block + canonical-group policy/analytics
gaps. Same class as #180 (Claude Code), now for Copilot (shared hook).

- CLI (detect_mcp_call): COPILOT_CLI_BUILTIN_MCP merged into the candidate map
  ({**builtins, **mcp_servers}, configured wins) so github-mcp-server out-ranks a
  coincidental shorter `github` entry → correct url + un-garbled tool.
- VS Code (_resolve_vscode_mcp): COPILOT_VSCODE_BUILTIN_MCP fallback (fires only
  when no configured server matches; setup#168 + ambiguity guard preserved).
- VS Code unresolved mcp_<server>_<tool> now forwards a best-effort identity so
  deny-by-default applies (fail-open → fail-secure), only with a non-empty tool
  segment; degenerate mcp_ tokens left as-is.
- Built-in registry configs returned as copies (no in-process mutation).
- 27 hermetic tests.

Deferred (noted): CLI fail-secure on BARE unresolved tokens stays return {} — a
delimiter-less name can't be split safely; blind-forwarding would over-block
native tools. Backend dependency (gateway-data, out of scope): classify
api.individual.githubcopilot.com/mcp(+/readonly) into the GitHub canonical group
so the CLI built-in is allowed when GitHub is sanctioned (VS Code url already cataloged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@anonpran anonpran force-pushed the nanda/copilot-builtin-mcp-resolution branch from 86c3704 to 18a004e Compare June 24, 2026 05:38
@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

2 findings — 2 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Ambiguous VS Code MCP tokens re-parsed after resolver declined

  • Confidence: 🔴 HIGH
  • Location: copilot/hooks/unbound.py:988-998
  • Impact: When _resolve_vscode_mcp returns (None, None, None) for genuine ambiguity (e.g. mcp_linear_create_issue with overlapping linear / linear_create_safe configs), the else branch unconditionally partition('_') and forwards mcp_server/mcp_tool, bypassing the ambiguity guard and attributing an unprovable identity—risking mis-sanction (over-block or fail-open if the guessed name maps to an allowed group).
  • Fix: Distinguish “no candidates” from “ambiguous” in _resolve_vscode_mcp (sentinel/flag); only best-effort forward on no-match; for ambiguous tokens forward a neutral deny signal without attributing a specific server.
  • Reviewers: Cursor, Claude

VS Code built-in fallback prefix-matches unrelated github* tools

  • Confidence: 🔴 HIGH
  • Location: copilot/hooks/unbound.py:733-746
  • Impact: When no configured server matches, the built-in fallback reuses truncation-tolerant alias matching, so bodies like github_get_me can attach the hosted github-mcp-server URL via startswith('github')—confidently wrong identity and canonical-group attribution for an unconfigured server (sanction/analytics skew; impact limited because URL still lands in GitHub group).
  • Fix: Require stricter matching in the built-in fallback (exact sanitized-alias equality or single unambiguous full-prefix candidate) instead of reusing the truncation-tolerant matcher.
  • Reviewers: Cursor, Claude

Previously acknowledged (not re-flagged)

  • CLI bare unresolved <server>-<tool> tokens remain fail-open — deliberate deferral documented in PR description; accepted risk pending a safe delimiter/split follow-up (no reliable split without fleet-wide over-block risk).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 18a004ec · 2026-06-24T05:45Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants