Skip to content

Add extra passthrough for harness-specific MCP keys (#1670)#1765

Open
sergio-sisternes-epam wants to merge 2 commits into
mainfrom
sergio-sisternes-epam/fix-mcp-extra-passthrough-1670
Open

Add extra passthrough for harness-specific MCP keys (#1670)#1765
sergio-sisternes-epam wants to merge 2 commits into
mainfrom
sergio-sisternes-epam/fix-mcp-extra-passthrough-1670

Conversation

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

TL;DR

MCPDependency now captures unknown keys from apm.yml into a generic extra dict and round-trips them into generated target manifests, enabling harness-specific configuration (e.g. Claude Code's oauth block) to survive the install pipeline.

Problem

When a user declares harness-specific keys (like oauth for Claude Code remote-MCP OAuth) in an apm.yml MCP dependency entry, those keys are silently dropped during parsing because MCPDependency.from_dict() only recognises a fixed set of known fields. The warning was already landed, but the keys were still discarded -- they never appeared in the generated .mcp.json.

Approach

A generic extra: dict[str, Any] | None field on MCPDependency captures all unknown keys and passes them through the full data flow:

apm.yml  ->  from_dict() [captures into extra]
         ->  to_dict()   [merges extra at top level]
         ->  _build_self_defined_info() [passes as _extra]
         ->  adapter._format_server_config() [merges via _merge_extra()]
         ->  target manifest (.mcp.json)

Shadow prevention: extra keys never override known fields or adapter-set keys. In both to_dict() and _merge_extra(), known keys take precedence.

Changes

File Change
src/apm_cli/models/dependency/mcp.py Added extra field, updated from_dict(), to_dict(), __repr__
src/apm_cli/integration/mcp_integrator.py Added _extra passthrough in _build_self_defined_info()
src/apm_cli/adapters/client/base.py Added _merge_extra() static method
src/apm_cli/adapters/client/{copilot,codex,cursor,gemini,kiro,vscode}.py Added _merge_extra() calls at all return points
tests/unit/test_mcp_from_dict_unknown_keys.py Updated existing tests, added 4 new test classes (25 tests total)
docs/src/content/docs/reference/manifest-schema.md Documented extra passthrough behaviour and example
docs/src/content/docs/consumer/install-mcp-servers.md Added example with oauth extra key
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Added example with oauth extra key

Validation

  • All 17150 unit tests pass
  • Full lint chain passes (ruff check, ruff format, pylint R0801, auth-signals)
  • Warning message updated from "dropped" to "preserved in extra"

Out of scope

  • apm pack credential stripping for extra keys (follow-up)
  • Per-harness override blocks (claude:, vscode:) -- generic extra is simpler and sufficient

Closes #1670

MCPDependency now captures unknown keys from apm.yml into an 'extra'
dict instead of silently dropping them. The extra fields round-trip
through to_dict() and flow through _build_self_defined_info() into
every client adapter's _format_server_config(), where _merge_extra()
writes them into the generated target manifest without shadowing
known keys.

This enables harness-specific configuration such as Claude Code's
oauth block for remote-MCP OAuth client config to be declared once
in apm.yml and appear in the generated .mcp.json.

Closes #1670

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 13, 2026 13:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a forward-compatible “extra passthrough” mechanism for MCP dependencies: unknown keys found in apm.yml MCP dependency objects are preserved on the model and then merged into the generated target manifests so harness-specific config (e.g., an oauth block) is not lost.

Changes:

  • Extend MCPDependency to capture unknown keys into extra and round-trip them via to_dict().
  • Plumb _extra through MCP integration and merge it into per-harness adapter configs.
  • Add/adjust unit tests and update docs to describe the passthrough behavior with examples.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/apm_cli/models/dependency/mcp.py Adds extra field and merges unknown keys into extra, then into to_dict() output.
src/apm_cli/integration/mcp_integrator.py Adds _extra passthrough for adapter consumption (currently only in self-defined path; see comments).
src/apm_cli/adapters/client/base.py Introduces _merge_extra() helper used by adapters to merge passthrough keys.
src/apm_cli/adapters/client/copilot.py Calls _merge_extra() before returning formatted config.
src/apm_cli/adapters/client/codex.py Calls _merge_extra() on some return paths (remote-only path still misses it; see comments).
src/apm_cli/adapters/client/cursor.py Calls _merge_extra() before returning formatted config.
src/apm_cli/adapters/client/gemini.py Calls _merge_extra() before returning formatted config.
src/apm_cli/adapters/client/kiro.py Calls _merge_extra() before returning formatted config.
src/apm_cli/adapters/client/vscode.py Calls _merge_extra() before returning formatted config.
tests/unit/test_mcp_from_dict_unknown_keys.py Updates/adds tests for unknown-key preservation, round-tripping, and merge behavior.
docs/src/content/docs/reference/manifest-schema.md Documents extra passthrough semantics and adds an example.
docs/src/content/docs/consumer/install-mcp-servers.md Adds an example using a harness-specific extra key (oauth).
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Updates dependency docs with an extra-key example for MCP.

Comment thread src/apm_cli/models/dependency/mcp.py
Comment thread src/apm_cli/integration/mcp_integrator.py
Comment thread src/apm_cli/adapters/client/codex.py
…remote path

- Add 'extra' to _KNOWN_DICT_KEYS so an explicit 'extra:' YAML block
  merges into dep.extra without nesting
- Propagate dep.extra via _apply_overlay for registry-resolved deps
- Add _merge_extra call in codex remote-only return path
- Add 7 new tests covering all three fixes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 15, 2026
@github-actions

Copy link
Copy Markdown

APM Review Panel: ship_with_followups

Extra passthrough fixes Claude Code oauth silent-drop regression: apm.yml unknown keys now round-trip to target manifests, restoring the single-source-of-truth promise for remote-MCP users.

cc @sergio-sisternes-epam @danielmeppiel -- a fresh advisory pass is ready for your review.

This PR closes issue #1670 where Claude Code oauth blocks were silently dropped during apm install, breaking APM's define-once, generate-everywhere promise. The fix is architecturally clean: unknown keys are captured in a typed extra dict at parse time, known fields always shadow them (supply-chain-expert confirmed shadow prevention holds in both to_dict and _merge_extra), and keys are flattened back to top-level in each adapter's generated manifest. All eight active panelists converge on ship-with-followups; zero panelists identified a blocking defect. The 25 unit tests cover each pipeline link in isolation, lint passes, and the python-architect's two quality findings are nits that can travel in a follow-up.

The panel's two highest-signal pre-merge items are both single-line text changes, not logic changes. First, the warning symbol and body are both wrong: [!] (yellow alarm) signals data loss, but keys are now intentionally preserved; and the body "preserved in extra" misleads users into looking for an extra sub-key in .mcp.json when oauth in fact lands at top-level verbatim -- the internal dict name is an implementation detail, not the output shape (cli-logging-expert and devx-ux-expert, convergent). Second, CHANGELOG has no entry for the behavioral change from warn-and-drop to warn-and-preserve (oss-growth-hacker); upgrading users who grep CI logs for the old warning text receive no migration signal. A third pre-merge touch -- one sentence in manifest-schema.md -- is flagged independently by supply-chain, auth, devx-ux, and oss-growth-hacker: the apm pack sanitizer does not recognize 'oauth' or 'client_id' as sensitive names, so extra blocks survive into packed bundles unredacted; users need the advisory before the feature is publicly promoted.

Two structural concerns survive as post-merge follow-ups. Auth-expert identifies that the oauth extra block silently broadcasts to all seven adapter configs (copilot, claude, cursor, kiro, codex, gemini, vscode) while the feature is documented as Claude Code-specific; a per-adapter passthrough_keys allowlist or explicit broadcast documentation is the remediation -- this finding is also reinforced by supply-chain's command-injection note (an extra: {command: evil_bin} for an HTTP/SSE server would inject command into the generated config since those adapters do not set command). Test-coverage-expert surfaces, with evidence outcome=missing, that no test exercises the full apm.yml -> MCPIntegrator -> adapter -> manifest file pipeline with real objects, and that 17 _merge_extra call sites across six adapters have zero per-adapter wiring tests. These are regression-trap gaps on a new behavioral surface and should be tracked as issues before the next minor release.

Dissent. The panel is broadly convergent; no two panelists disagreed on any finding's severity classification. The one weighting note: test-coverage-expert's finding on the missing Scenario Evidence table in the PR body is procedural rather than behavioral and is ranked below the two evidence-backed outcome=missing findings in the follow-up list.

Aligned with: Portable by Manifest (extra passthrough directly serves this principle: harness-specific keys defined once in apm.yml now reach all generated target manifests). Secure by Default (partial regression: the apm pack sanitizer does not match 'oauth' or 'client_id' as sensitive key names, so extra blocks survive into packed bundles; docs advisory is pre-merge). Multi-Harness / Multi-Host (_merge_extra broadcasts to all seven adapter configs; per-adapter passthrough_keys allowlisting is the architectural path forward). Pragmatic as npm (extra passthrough mirrors cargo [metadata] and npm engines escape-hatch, signaling config-model parity with mature package ecosystems).

Growth signal. The release note frame for this PR is: "APM restores the single-source-of-truth promise for Claude Code remote-MCP OAuth -- define once in apm.yml, stop manually patching .mcp.json after every install." This directly targets Claude Code's fast-growing remote-MCP user base and reinforces APM's AI-native package manager positioning. The extra passthrough pattern also signals that APM's config model has reached parity with cargo and npm escape valves, which is a trust-building signal for adopters evaluating maturity. Caution: amplifying this feature publicly before the pack credential gap has both a follow-up issue and an in-docs advisory risks a community complaint within one release cycle.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 2 Solid passthrough chain; two quality findings: hasattr guard on always-MCPDependency param, _merge_extra mutates in-place and returns (all callers ignore return).
CLI Logging Expert 0 2 2 Stale 'dropped' docstring contradicts new behavior; [!] symbol misfires for working-as-intended passthrough; message body leaves outcome unstated.
DevX UX Expert 0 3 2 Feature works; 3 UX gaps worth fixing: warning fires on documented pattern, message misleads about output shape, pack credential gap unacknowledged in docs.
Supply Chain Security Expert 0 2 1 Shadow prevention holds in both to_dict and _merge_extra; apm pack sanitizer leaves non-credential-named oauth sub-keys intact; HTTP server command injection via extra is possible (consumer-controlled only).
OSS Growth Hacker 0 2 2 Extra passthrough fixes a real retention-killer; two gaps undercut the trust story: missing CHANGELOG entry and no in-docs pack-safety advisory for extra keys.
Auth Expert 0 2 2 No auth bypass or AuthResolver interaction; two recommended findings on secret persistence and cross-adapter oauth leakage; one nit on env-via-extra.
Doc Writer 0 4 1 5 gaps: stale form-count, no cross-link, undocumented explicit extra: key, warning asymmetry, duplicate example -- no blockers.
Test Coverage Expert 0 3 0 25 unit tests cover each isolated segment well; missing integration-with-fixtures for full apm.yml->manifest pipeline; 6 adapters have 17 uncovered _merge_extra call sites.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [CLI Logging Expert] Replace [!] with _rich_info and rewrite warning body to name the target manifest output, not the internal dict name -- Warning fires on the documented top-level oauth: example; [!] implies data loss where none exists; "preserved in extra" misleads about output shape (oauth appears at top-level in .mcp.json, not nested under an extra key). Convergent with devx-ux-expert. Pre-merge, one-line fix: suggested body "MCP dependency '{name}': unknown key(s) will appear verbatim in target manifests: {safe_keys}".
  2. [OSS Growth Hacker] Add CHANGELOG [Unreleased] entry for the warn-and-drop to warn-and-preserve behavior change -- Behavior change is undocumented in the durable release record; upgrading users who grep CI logs for the old dropped-key warning text get no migration signal. Pre-merge, one line.
  3. [Supply Chain Security Expert] Add one-sentence pack safety advisory to manifest-schema.md: extra keys are written verbatim to packed bundles; do not place secrets in extra fields -- Four panelists flag this independently: the pack sanitizer does not strip 'oauth' or 'client_id', so a clientSecret in the extra block survives apm pack unredacted. Pre-merge, one sentence.
  4. [Auth Expert] Add per-adapter passthrough_keys allowlist or document explicitly that extra broadcasts to all adapter configs -- oauth is Claude Code-specific per docs but _merge_extra fires in all seven adapters; a future harness that interprets oauth differently receives the Claude Code client config. Post-merge issue.
  5. [Test Coverage Expert] Add integration-with-fixtures test for full apm.yml -> MCPIntegrator -> adapter -> manifest pipeline, and per-adapter _merge_extra wiring tests -- Evidence outcome=missing: no test exercises the complete chain with real objects; 17 call sites across six adapters have no per-adapter wiring test. Post-merge issue.

Architecture

classDiagram
    direction LR
    class MCPDependency {
        <<DataClass>>
        +name str
        +transport str|None
        +extra dict|None
        +from_dict(d) MCPDependency
        +to_dict() dict
        +validate(strict) None
    }
    note for MCPDependency "extra captures unknown apm.yml keys; explicit extra block wins over same-named auto-captured keys; known fields win in to_dict"
    class MCPIntegrator {
        <<Service>>
        -_build_self_defined_info(dep) dict
        -_apply_overlay(cache, dep) None
        +get_server_configs(deps) dict
    }
    note for MCPIntegrator "dep untyped in both statics; hasattr guard always True for MCPDependency -- add dep: MCPDependency annotation"
    class MCPClientAdapter {
        <<AbstractBase>>
        +target_name str
        +mcp_servers_key str
        +_merge_extra(config,server_info) dict
        +configure_mcp_server()
    }
    note for MCPClientAdapter "_merge_extra mutates config in-place AND returns it; all callers ignore return -- remove return, annotate void"
    class CopilotClientAdapter {
        <<Template>>
        #_format_server_config(server_info) dict
    }
    class CursorClientAdapter {
        <<ConcreteAdapter>>
        #_format_server_config(server_info) dict
    }
    class CodexClientAdapter {
        <<ConcreteAdapter>>
        #_format_server_config(server_info) dict
    }
    class GeminiClientAdapter {
        <<ConcreteAdapter>>
        #_format_server_config(server_info) dict
    }
    class KiroClientAdapter {
        <<ConcreteAdapter>>
        #_format_server_config(server_info) dict
    }
    class VSCodeClientAdapter {
        <<ConcreteAdapter>>
        #_format_server_config(server_info) tuple
    }
    class ClaudeClientAdapter {
        <<ConcreteAdapter>>
        #_format_server_config(server_info) dict
    }
    class WindsurfClientAdapter {
        <<ConcreteAdapter>>
    }
    MCPIntegrator ..> MCPDependency : reads
    MCPIntegrator ..> MCPClientAdapter : dispatches to
    MCPClientAdapter <|-- CopilotClientAdapter
    MCPClientAdapter <|-- CursorClientAdapter
    MCPClientAdapter <|-- CodexClientAdapter
    MCPClientAdapter <|-- GeminiClientAdapter
    MCPClientAdapter <|-- KiroClientAdapter
    MCPClientAdapter <|-- VSCodeClientAdapter
    CopilotClientAdapter <|-- ClaudeClientAdapter
    CopilotClientAdapter <|-- WindsurfClientAdapter
    class MCPDependency:::touched
    class MCPIntegrator:::touched
    class MCPClientAdapter:::touched
    class CopilotClientAdapter:::touched
    class CursorClientAdapter:::touched
    class CodexClientAdapter:::touched
    class GeminiClientAdapter:::touched
    class KiroClientAdapter:::touched
    class VSCodeClientAdapter:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A["apm.yml: MCP dep with unknown keys"] --> B
    B["mcp.py: from_dict()\n_KNOWN_DICT_KEYS check\nunknown keys detected"] --> C
    C{"explicit extra: block in input?"}
    C -- Yes --> D["extra = unknown_keys merged with explicit_extra\nexplicit_extra wins on collision"]
    C -- No --> E["extra = unknown keys only"]
    D --> F
    E --> F
    F["MCPDependency.extra assigned\n_rich_warning emitted for unknown keys"] --> G
    G{"dep.is_self_defined?"}
    G -- "yes: registry=False" --> H["MCPIntegrator._build_self_defined_info(dep)\nbuild synthetic server_info dict"]
    G -- "no: overlay dep" --> I["MCPIntegrator._apply_overlay(cache, dep)\nmutate cached server_info in-place"]
    H --> J["if dep.extra:\n  server_info['_extra'] = dict(dep.extra)\nnote: hasattr guard always True for MCPDependency"]
    I --> J
    J --> K["adapter._format_server_config(server_info, ...)"]
    K --> L{"server_info branch"}
    L -- "_raw_stdio" --> M["build command+args\n_merge_extra(config, server_info)\nreturn config"]
    L -- "remotes" --> N["build url+headers\n_merge_extra(config, server_info)\nreturn config"]
    L -- "packages" --> O["build command+args from package\n_merge_extra(config, server_info)\nreturn config"]
    M --> P
    N --> P
    O --> P
    P["base.py: _merge_extra()\nfor k,v in _extra.items():\n  if k not in config: config[k] = v\nall callers ignore return value"] --> Q
    Q["write target manifest\n.mcp.json: oauth block appears at top level"]
Loading

Recommendation

Ship after three pre-merge text changes land in this PR: (1) replace [!] with _rich_info and rewrite the warning body to state that keys will appear verbatim in target manifests; (2) add CHANGELOG [Unreleased] entry for the behavioral change; (3) add one-sentence pack safety advisory to manifest-schema.md. The two structural post-merge follow-ups -- per-adapter extra key allowlist for oauth broadcast and integration-with-fixtures pipeline test -- should be filed as issues before the next minor release. The feature is architecturally sound, closes a real user-facing regression, and is unit-tested at every pipeline link.


Full per-persona findings

Python Architect

  • [recommended] hasattr(dep, 'extra') guard is always True -- dep is always MCPDependency; replace with type annotation at src/apm_cli/integration/mcp_integrator.py:350
    _build_self_defined_info(dep) and _apply_overlay(cache, dep) both accept an untyped dep param. Every call site passes a concrete MCPDependency which always declares extra as a dataclass field. The hasattr guard is dead code and misleads future readers.
    Suggested: def _build_self_defined_info(dep: MCPDependency) -> dict: ... if dep.extra: info['_extra'] = dict(dep.extra). Same for _apply_overlay.

  • [nit] _merge_extra mutates config in-place AND returns the same dict; all call sites discard the return at src/apm_cli/adapters/client/base.py:132
    Returning a mutated argument signals fluent/functional style. Every adapter calls it as a void statement. Remove return config; change return annotation to -> None.

  • [nit] explicit extra: block silently wins over same-named auto-captured unknown keys in from_dict() at src/apm_cli/models/dependency/mcp.py:83
    from_dict() merges as extra = {**unknown_keys, **explicit_extra}. Undocumented. Add comment at merge site.

CLI Logging Expert

  • [recommended] from_dict docstring still says keys are 'dropped' -- directly contradicts the new preserved-in-extra behavior at src/apm_cli/models/dependency/mcp.py
    Line 71 reads "Unknown keys are dropped with a warning naming each discarded key." Developers reading this docstring get false information about the method contract.
    Suggested: Update to: "Unknown keys are collected into dep.extra with a warning naming each preserved key; explicit extra: blocks are merged in without a warning."

  • [recommended] [!] warning symbol is now wrong -- passthrough is working-as-intended, not a data-loss alert at src/apm_cli/models/dependency/mcp.py
    The old 'dropped' behavior justified [!]. Now keys are intentionally preserved. Keeping [!] means users copying the documented example see a yellow alarm on every command invocation.
    Suggested: Replace _rich_warning(..., symbol='warning') with _rich_info(..., symbol='info') at line 87-90. Or gate behind --verbose.

  • [nit] "preserved in extra" names the mechanism but not the user-visible outcome at src/apm_cli/models/dependency/mcp.py
    User cannot confirm from this message that their oauth block will appear in the generated .mcp.json.
    Suggested: "MCP dependency '{safe_name}': unknown key(s) will pass through verbatim to the generated target manifest: {safe_keys}"

  • [nit] Warning fires on every apm.yml parse, not only apm install -- repeated noise across all commands at src/apm_cli/models/dependency/mcp.py
    MCPDependency.from_dict is invoked whenever any command loads apm.yml (list, run, plan, etc.). A project with oauth keys sees the message on every APM invocation.
    Suggested: Emit only during apm install via CommandLogger, or gate on --verbose.

DevX UX Expert

  • [recommended] Warning fires on the DOCUMENTED top-level oauth: pattern, implying user error where none exists at docs/src/content/docs/consumer/install-mcp-servers.md
    The install guide shows oauth: at top-level as supported config. Yet the code warns "unknown key(s) preserved in extra: oauth". A user copying the documented example gets a yellow alarm on first run.
    Suggested: (a) Downgrade warning to debug/verbose-only now that passthrough is intentional, or (b) add a callout in docs noting the expected warning and confirming the key will appear in the output.

  • [recommended] "preserved in extra" warning misleads user about the output manifest shape at src/apm_cli/models/dependency/mcp.py
    dep.extra is stored internally, then _merge_extra() flattens it back to top-level in .mcp.json -- oauth appears alongside name/transport/url, NOT nested under an 'extra' key. Users who see "preserved in extra" will search for extra.oauth in their output file.
    Suggested: "MCP dependency '{safe_name}': unknown key(s) will appear verbatim in target manifests: {safe_keys}"

  • [recommended] apm pack credential stripping for extra keys is noted as out-of-scope but undisclosed to users in docs at docs/src/content/docs/reference/manifest-schema.md
    A user who puts clientSecret in the oauth block will have that secret packed unredacted. The PR makes this more likely by documenting the feature.
    Suggested: Add: "Note: extra keys are included verbatim in packed bundles. Do not place secrets or tokens in extra fields -- use env: with ${VAR} references instead."

  • [nit] from_dict docstring still says "Unknown keys are dropped" -- now factually wrong at src/apm_cli/models/dependency/mcp.py

  • [nit] Explicit extra: block is a silent alternative path not documented or discoverable at docs/src/content/docs/reference/manifest-schema.md
    Users can write oauth: at top-level (warning fires) or extra: { oauth: ... } (no warning, keys preserved). The no-warning path and its different merge semantics are undocumented.

Supply Chain Security Expert

  • [recommended] apm pack credential sanitizer does not strip opaque extra blocks like oauth; non-credential-named sub-keys survive into plugin.json verbatim at src/apm_cli/core/plugin_manifest.py
    _sanitize_value() strips keys whose normalized names contain token/secret/password/etc. But 'oauth' normalizes to 'oauth' (no match) and 'client_id' normalizes to 'clientid' (no match). A short or non-standard secret value would survive pack.
    Suggested: Add 'oauth' to _SENSITIVE_MCP_KEY_NAMES so the entire block is stripped from pack output. Or document that extra blocks are intentionally retained and add a pack-time warning. At minimum, add a regression test.

  • [recommended] HTTP/SSE server configs do not set 'command'; extra: {command: evil_bin} would be injected into those configs at src/apm_cli/adapters/client/base.py
    _merge_extra adds keys only when absent from config. HTTP/SSE adapters set url/headers but not command. Consumer-controlled only, but the injection is silent and undocumented.
    Suggested: Warn in _merge_extra when extra contains transport-semantic fields (command, args, url) inconsistent with the adapter config's transport type.

  • [nit] extra: dict[str, Any] has no JSON-serializability check; YAML timestamps produce opaque TypeError at JSON write time at src/apm_cli/models/dependency/mcp.py
    Suggested: Add try: json.dumps(extra) except (TypeError, ValueError) as e: raise ValueError(...) after building extra in from_dict.

OSS Growth Hacker

  • [recommended] CHANGELOG.md has no entry for the warn-and-drop to warn-and-preserve behavior change at CHANGELOG.md
    PR feat: warn on unknown keys dropped in MCPDependency.from_dict() #1674 added the warning and is documented; PR Add extra passthrough for harness-specific MCP keys (#1670) #1765 changes the behavior but adds nothing to [Unreleased]. Upgrading users who rely on the old warning text in CI log greps will be silently broken with no changelog signal.
    Suggested: Add to [Unreleased] Added: "Unknown MCP dependency keys in apm.yml are now preserved in an extra passthrough dict and round-tripped verbatim into generated target manifests (e.g. Claude Code oauth block for remote-MCP OAuth). Previously they were silently dropped. A parse-time warning names each preserved key. (Add extra passthrough for harness-specific MCP keys (#1670) #1765)"

  • [recommended] No in-docs advisory that apm pack does not strip extra keys; users can silently bundle sensitive values at docs/src/content/docs/reference/manifest-schema.md
    The PR body correctly flags pack credential stripping as out of scope, but that note lives only in the PR body. A user who puts a bearer token in extra and runs apm pack will bundle it with zero warning.
    Suggested: Append to the passthrough paragraph: "Note: apm pack does not strip extra keys from published artifacts -- do not place secrets, tokens, or client credentials in extra fields if you intend to publish the package."

  • [nit] install-mcp-servers.md comment gives no Claude Code scent trail; users searching for Claude Code OAuth setup will not land here at docs/src/content/docs/consumer/install-mcp-servers.md
    Suggested: Change comment to "# Claude Code remote-MCP OAuth (harness-specific extra keys -- see manifest schema Section 4.2.2)"

  • [nit] Placeholder <pre-registered-client-id> has no companion link; users cannot act on this example without external research at docs/src/content/docs/consumer/install-mcp-servers.md

Auth Expert

  • [recommended] oauth extra block written to .mcp.json in plaintext; client_secret would persist unredacted at src/apm_cli/adapters/client/base.py
    repr correctly redacts extra (shows count only). But _merge_extra writes the raw value dict straight into adapter config, which is JSON-serialised to .mcp.json. APM has no scan gate on the extra dict comparable to _ENV_ASSIGN_SECRET_RE in plugin_manifest.py.
    Suggested: Document clearly in the warning emitted for unknown keys that extra values are written verbatim to generated manifests and are not treated as secrets. Optionally apply _ENV_ASSIGN_SECRET_RE / _AUTH_SCHEME_RE heuristics against serialized extra values and emit a warning when a credential-shaped value is detected.

  • [recommended] oauth block (Claude Code-specific) propagates to every adapter via _merge_extra; other harnesses receive unknown config at src/apm_cli/adapters/client/base.py
    _merge_extra is called in copilot, claude, cursor, kiro, codex, gemini, and vscode. The oauth block is Claude Code-specific but all adapters write it. A future harness that interprets oauth differently would receive the Claude Code client config with no gate.
    Suggested: Add per-adapter extra key allowlist or a passthrough_keys field on MCPClientAdapter (empty by default, overridden by ClaudeClientAdapter to include 'oauth'). At minimum, document the broadcast behaviour so users know oauth will appear in every generated manifest.

  • [nit] Explicit extra: block allows injecting known auth keys (env, headers) bypassing CRLF/type validation at src/apm_cli/models/dependency/mcp.py
    env and headers cannot reach dep.extra from top-level apm.yml keys. But they CAN be nested inside an explicit extra: dict. _merge_extra then injects them if the key is absent from config. For a registry remote server, env is not set in config, so extra: {env: {FOO: value}} would succeed bypassing the dep.env CRLF-check path.
    Suggested: Strip known auth/config keys (env, headers) from the explicit extra: block during from_dict parsing, or apply the same CRLF + type validation.

  • [nit] AuthResolver and GitHubTokenManager have zero interaction with the extra dict -- no auth bypass at src/apm_cli/core/auth.py
    Confirming the auth surface is clean for reviewers. No action needed.

Doc Writer

  • [recommended] install-mcp-servers.md intro says "Three forms are valid" but now shows four numbered examples at docs/src/content/docs/consumer/install-mcp-servers.md
    Line 23 reads "Three forms are valid -- pick the one that matches the source you have:" but the code block has a 'Add ARM64 Linux support to CI/CD pipeline #4' numbered entry. Factual inconsistency.
    Suggested: Change to "Three forms are valid; any self-defined server also accepts harness-specific extra keys (see manifest schema):" and fold example 4 into a note after example 3.

  • [recommended] install-mcp-servers.md example 4 has no prose and no cross-link to the passthrough spec at docs/src/content/docs/consumer/install-mcp-servers.md
    The oauth example appears with only a YAML comment. No sentence explains that this works for any unknown key, that a warning fires, or where to find the full spec.
    Suggested: Add 1-2 sentences after the code block: "Any key not in the field table above is preserved and written verbatim into generated target manifests. See the manifest schema reference for collision rules and the explicit extra: block alternative."

  • [recommended] The explicit 'extra:' named YAML key is undocumented -- it is a separate, warning-free API at docs/src/content/docs/reference/manifest-schema.md
    'extra' is in _KNOWN_DICT_KEYS; extra: {oauth: {...}} is a supported first-class input that does NOT trigger a warning. Current docs show only top-level unknown keys (which DO trigger the warning). Users copying the example will see a warning on every install; users who know the explicit extra: form avoid it.
    Suggested: Add 'extra' as a row in the MCP dependency field table with description: "Explicit passthrough block; contents merged with any unknown top-level keys and round-tripped verbatim. Does not emit a warning. Known fields always win on collision."

  • [recommended] Warning behavior documented but no-warning path via explicit extra: is missing, creating asymmetric expectations at docs/src/content/docs/reference/manifest-schema.md
    manifest-schema.md states "A warning is emitted at parse time naming each non-standard key." This is only true for top-level unknown keys -- the explicit extra: block flows with zero warnings. Users cannot know (a) whether the warning is expected, (b) that it can be suppressed.
    Suggested: Amend to: "When keys appear directly at the top level (not under an explicit extra: block) a warning is emitted at parse time. To suppress the warning, nest the keys under an explicit extra: block instead."

  • [nit] Duplicate oauth example in packages/apm-guide dependencies.md violates state-once principle at packages/apm-guide/.apm/skills/apm-usage/dependencies.md
    Same slack/oauth block appears verbatim in both install-mcp-servers.md and dependencies.md.
    Suggested: Replace the full slack/oauth block in dependencies.md with a one-line comment pointing to install-mcp-servers.md or manifest-schema.md.

Test Coverage Expert

  • [recommended] No integration-with-fixtures test for the full extra passthrough pipeline: apm.yml -> MCPIntegrator -> adapter -> manifest file at tests/integration/test_mcp_integrator_install_flow.py
    Unit tests exist for each link in isolation, but no test exercises the complete chain with real objects (no mocks). Floor tier for cross-module integration is integration-with-fixtures. Probed: grep -rn '_merge_extra|_extra|oauth' tests/integration/ -- zero matches related to this feature.
    Suggested: Add a hermetic fixture test: construct MCPDependency(name='slack', transport='http', url='...', extra={'oauth': {'clientId': 'abc', 'callbackPort': 3118}}), call _build_self_defined_info(dep), call adapter._format_server_config(info), assert config['oauth'] == {'clientId': 'abc', 'callbackPort': 3118}.
    Proof (missing at integration-with-fixtures): tests/integration/test_mcp_integrator_install_flow.py::test_self_defined_http_oauth_extra_survives_to_adapter_config -- proves: An oauth block declared in apm.yml survives the full MCPIntegrator->adapter pipeline [portability-by-manifest, devx]
    assert config['oauth'] == {'clientId': 'abc', 'callbackPort': 3118}

  • [recommended] Six adapter _format_server_config methods have 17 _merge_extra call sites with no per-adapter test passing _extra in server_info at tests/unit/test_copilot_adapter.py
    Probed: grep -rn '_extra' tests/unit/test_copilot_adapter.py tests/unit/test_cursor_mcp.py -- zero matches. TestAdapterMergeExtra proves the base class method works in isolation but does not prove each adapter calls it. A PR removing one call site would cause silent regression.
    Suggested: In each adapter's test file, add one scenario that passes _extra in server_info and asserts the key appears in the returned config.
    Proof (missing at unit): tests/unit/test_copilot_adapter.py::test_format_server_config_raw_stdio_includes_extra_keys -- proves: CopilotClientAdapter._format_server_config merges _extra into config on the raw_stdio path [devx, multi-harness-support]
    assert config['oauth'] == {'clientId': 'abc'}

  • [recommended] PR body lacks a Scenario Evidence table; no user-words scenario mapped to APM principle with verified test path
    The PR has a well-written Validation section listing test counts and lint results but no scenario-to-principle mapping. Without this table the panel cannot audit whether all changed behavioral surfaces are covered.
    Suggested: Add a Scenario Evidence table with rows: (1) User declares oauth: in apm.yml; manifest contains oauth -> DevX; (2) Extra key cannot override type or url -> Secure by default; (3) Registry-resolved dep with extra overlay: _apply_overlay sets _extra -> Portability.

Performance Expert -- inactive

All 13 changed files are in src/apm_cli/models/, src/apm_cli/integration/, src/apm_cli/adapters/client/, docs/, and tests/ -- none touch cache layout, dependency resolution, install phases, git transport, or any materialization hot path.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

Generated by PR Review Panel for issue #1765 · sonnet46 20.9M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 15, 2026
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.

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

3 participants