Skip to content

MCP extra passthrough: tighten to explicit extra:-only (fail-closed) with deprecation path #1806

Description

@danielmeppiel

Context

Follow-up to #1670 / PR #1765, which added a harness-specific extra passthrough to MCPDependency so keys like Claude Code's remote-MCP oauth block (clientId / callbackPort) survive apm.yml -> generated .mcp.json rendering instead of being silently dropped.

PR #1765 ships the bare-key ergonomics: any unknown top-level key on a self-defined MCP dependency is auto-captured into extra (matching the documented oauth: examples). It also adds a modeled-field-name denylist (a passthrough value can never shadow/redirect name/transport/command/url/headers/env/... or the Codex http_headers alias) so the concrete injection edge is closed.

The deferred tightening

The supply-chain-security review lens flagged that auto-capturing bare unknown keys is fail-open: a transitive dependency package's apm.yml is parsed straight through from_dict, and the prior contract dropped unknown keys (fail-closed) whereas the new contract preserves them. The denylist closes the high-severity shadow/redirect edge, but the residual posture question remains: should passthrough require an explicit extra: block and drop bare unknown keys with a warning (fail-closed), so only keys the author deliberately nests under extra: flow through?

This was deliberately sequenced out of #1765 by maintainer decision (Option B + denylist) because it is a user-facing authoring-contract change (the documented examples use bare top-level oauth:), i.e. a breaking change that needs a deprecation path rather than a silent flip.

Proposed scope (this issue)

  1. Introduce explicit extra: as the only sanctioned passthrough surface.
  2. Deprecation window: keep capturing bare unknown keys, but emit a deprecation warning instructing authors to nest them under extra:. After the window, drop bare unknown keys (restore fail-closed drop-and-warn).
  3. Rewrite the three doc examples (install-mcp-servers.md, reference/manifest-schema.md, apm-usage/dependencies.md) to nest oauth: under extra:.
  4. Update test_mcp_from_dict_unknown_keys.py expectations accordingly.

Also captured here (deferred from #1765, non-blocking)

  • Per-harness scoping: extra: { copilot: {...}, codex: {...} } so a Claude-specific block is not broadcast to every target.
  • Secret-shaped-value scan: warn when an extra value looks like a token/secret being written to a world-readable config.
  • to_dict collision debug-warn: emit a debug log when an extra key shadows a known field during re-serialization.

Why not now

Per the PR #1765 maintainer decision, #1765 keeps bare-key ergonomics + denylist (closes the concrete trust-boundary edge) and defers this fail-closed contract tightening here so the posture change is explicit, deprecation-gated, and reviewable on its own.

/cc the #1670 thread.

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.breaking-changestatus/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/featureNew capability, new flag, new primitive.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions