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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions