Skip to content

MCP: smart per-runtime sourcing for dual-mode registry entries + GitHub auth host validation #816

@danielmeppiel

Description

@danielmeppiel

Summary

Surfaced during PR #810 panel review (rubber-duck pass on a proposed README simplification). Two related items, scoped together because the security fix is a precondition for safely changing default selection.

Item 1: Smart per-runtime sourcing for dual-mode entries

When a registry entry advertises both packages (e.g. docker stdio) and remotes (HTTPS), adapter selection is currently non-deterministic across clients:

  • Copilot (src/apm_cli/adapters/client/copilot.py:186-236): prefers remotes first.
  • VS Code (src/apm_cli/adapters/client/vscode.py:227-345): prefers packages first.
  • Codex (src/apm_cli/adapters/client/codex.py:122-131): falls through to packages if both exist; only skips when remotes and not packages.

This forces the README to use transport: http as a deterministic-selection workaround, which in turn introduced the UX confusion that PR #810 patched with inline disambiguators (# MCP transport name, not URL scheme -- connects over HTTPS).

Naive fix ("strip packages when both exist + no overlay"): blocked because (a) Codex would skip with 'remote not supported' on previously-working installs, (b) VS Code would silently flip from stdio to remote.

Proper fix needs per-runtime sourcing rules -- a small policy table per adapter declaring its preference order and whether it supports remote/stdio at all. The smart-default then becomes 'pick the first variant the runtime supports' rather than a global mutation.

Acceptance criteria

  • Each adapter exposes a declared transport-preference order and capability set (e.g. Codex: stdio-only; Copilot: remote-first then stdio; VS Code: stdio-first then remote).
  • Pre-fetch normalization step uses each adapter's declared preference to pick a single variant per runtime.
  • README example (and docs) can drop transport: http overlay -- bare io.github.github/github-mcp-server works deterministically across all runtimes (Copilot uses remote, VS Code uses docker, Codex uses docker, etc).
  • Behavior change CHANGELOG entry under ### Changed.
  • No interactive token prompt on first-run with the GitHub MCP server when only Copilot is installed (bare shorthand should pick remote on Copilot without prompting).

Item 2: _is_github_server host validation

src/apm_cli/adapters/client/copilot.py:675-717 returns True (and triggers GitHub token injection at copilot.py:201-212) on either name match OR hostname match. A poisoned/custom registry entry named github-mcp-server pointing at https://evil.example.com/mcp/ would receive a GitHub token at the non-GitHub host.

This is latent today (only known registry entries with that name are GitHub's own) but tightly coupled to Item 1: if the smart-default expands the remote-path footprint, this risk grows.

Acceptance criteria

  • _is_github_server requires BOTH name match AND is_github_hostname(parsed_url.hostname) returning True before injecting a GitHub token. Name match alone is no longer sufficient.
  • Unit test: poisoned-name + non-GitHub URL does not get a token header.
  • Unit test: legitimate GitHub server (github-mcp-server at api.githubcopilot.com) still gets the token.
  • Threat-model sentence in docs/.../mcp-servers.md Security section.

Why these are bundled

  • (1) without (2) is a security regression amplification.
  • (2) without (1) is fine to ship standalone if (1) takes longer.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/mcp-configMCP server configuration depth, transports, variable resolution.area/mcp-trustTransitive MCP trust prompts, consent contract, transport security.priority/highShips in current or next milestonesecurityDeprecated: use theme/security. Kept for issue history; will be removed in milestone 0.10.0.status/needs-designDirection approved, design discussion required before code.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/securitySecure by default. Content scanning, lockfile integrity, MCP trust boundaries.type/bugSomething does not work as documented.

    Type

    No type

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions