Skip to content

MCP model silently drops harness-specific keys (e.g. Claude Code oauth) — no passthrough for remote-MCP OAuth client config #1670

@hinderberg

Description

@hinderberg

Summary

apm's MCP dependency model carries only a fixed field set and silently discards any other keys on render. There is no way to express harness-specific MCP config that apm doesn't model, notably Claude Code's remote-MCP oauth block (clientId / callbackPort). For a remote MCP server that requires a pre-registered OAuth client because it does not support Dynamic Client Registration (RFC 7591), this makes the server unusable when configured through apm.

Environment

  • Reproduced on apm 0.15.0; confirmed still present in source on v0.17.0 (latest).
  • Target: Claude Code (.mcp.json).

What happens

MCPDependency (src/apm_cli/models/dependency/mcp.py) models only:
name, transport, env, args, version, registry, package, headers, tools, url, command.

  • from_dict: "Unknown keys are silently ignored for forward compatibility."
  • to_dict: serializes only that whitelist.

So this apm.yml:

dependencies:
  mcp:
    - name: slack
      registry: false
      transport: http
      url: https://mcp.slack.com/mcp
      oauth:
        clientId: "<pre-registered-client-id>"
        callbackPort: 3118

produces a generated .mcp.json slack entry with no oauth key. No warning is emitted; apm install reports success.

Why it matters (concrete failure)

Claude Code's .mcp.json supports an oauth block to pin a pre-registered OAuth client. Slack's remote MCP (https://mcp.slack.com/mcp) does not support Dynamic Client Registration, so without a pre-registered clientId Claude Code's OAuth fails with:

SDK auth failed: Incompatible auth server: does not support dynamic client registration

Because apm cannot carry the oauth block, a Slack MCP configured via apm cannot authenticate. The same applies to any remote MCP needing harness-specific auth config apm doesn't model.

Two distinct problems

  1. No passthrough for harness-specific MCP fields. The universal model is a lowest-common-denominator with no escape hatch (e.g. a per-harness claude: / extra: map) to round-trip fields verbatim into the target manifest.
  2. Silent drop. Unknown keys are discarded with no warning, so a user who supplies the correct field name gets no signal it was ignored. It fails closed and silently.

Note: apm pack already strips credential-shaped keys from the Claude .mcp.json manifest, which suggests apm knows .mcp.json carries richer config than the model represents. It currently treats the surplus as something to drop, never to preserve.

Proposed fixes

  • Add a passthrough / per-harness override for MCP entries that round-trips into the generated manifest (an oauth field, or a generic extra / claude block).
  • At minimum, emit a warning when from_dict discards unknown keys instead of dropping them silently.

Workaround

Re-inject the field into the generated .mcp.json after apm install via a post-install hook, since apm regenerates the file from apm.yml on every run.

Metadata

Metadata

Assignees

Labels

area/docs-sitedocs/src/content (Starlight), README, doc generation.area/mcp-configMCP server configuration depth, transports, variable resolution.priority/highShips in current or next milestonestatus/acceptedDirection approved, safe to start work.status/shepherdingActively being driven by an APM shepherd runstatus/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
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