Skip to content

fix: handle MCP v0.1 runtimeArguments.variables in copilot and codex adapters#1461

Merged
danielmeppiel merged 3 commits into
mainfrom
fix/1452-v01-variables-all-adapters
May 23, 2026
Merged

fix: handle MCP v0.1 runtimeArguments.variables in copilot and codex adapters#1461
danielmeppiel merged 3 commits into
mainfrom
fix/1452-v01-variables-all-adapters

Conversation

@sergio-sisternes-epam
Copy link
Copy Markdown
Collaborator

Problem

The _process_arguments method in copilot.py and codex.py only handled arg dicts with type=positional, type=named, or plain strings. Args using the v0.1 registry format (value_hint + variables dict, no type key) were silently dropped.

This meant Docker mount arguments with {workspaceFolder} placeholders produced empty arg lists in Copilot, Codex, Gemini, Cursor, and Claude adapters -- while VS Code handled them correctly.

Fix

Add an elif branch in both copilot.py and codex.py _process_arguments for v0.1 value_hint args (with optional variables dict) that performs {var_name} placeholder substitution, matching the existing logic in VSCodeClientAdapter._extract_package_args.

Gemini, Cursor, and Claude inherit from CopilotClientAdapter so they get the fix automatically.

Tests

  • 8 new regression tests covering both adapters (plain value_hint, variables substitution, unknown var placeholder, full 8-arg Docker fixture)
  • Mutation-break gate verified: removing the new branch causes all tests to fail
  • All 204 existing adapter tests pass

Fixes #1452

…adapters

The _process_arguments method in copilot.py and codex.py only handled
args with type=positional, type=named, or plain strings. Args using
the v0.1 registry format (value_hint + variables dict, no type key)
were silently dropped. This meant Docker mount arguments with
{workspaceFolder} placeholders produced empty arg lists in Copilot,
Codex, Gemini, Cursor, and Claude adapters.

Add an elif branch for v0.1 value_hint args (with optional variables
dict) that performs {var_name} placeholder substitution, matching the
existing logic in VSCodeClientAdapter._extract_package_args.

Gemini, Cursor, and Claude inherit from CopilotClientAdapter so they
get the fix automatically.

Fixes #1452

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 23, 2026 12:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 fixes MCP registry v0.1 runtimeArguments parsing in the non-VS Code MCP client adapters by teaching the Copilot and Codex adapters to accept the {value_hint, variables} argument shape and substitute {var} placeholders from runtime_vars, aligning behavior with the existing VS Code adapter so Docker-based servers generate correct argument lists.

Changes:

  • Add a v0.1 value_hint/variables parsing branch to _process_arguments in the Copilot adapter.
  • Add the same v0.1 parsing/substitution branch to _process_arguments in the Codex adapter (benefiting inheritors of the Copilot adapter as noted in the PR description).
  • Add unit regressions tests covering plain value_hint, variable substitution, unknown variables, and a full Docker arg fixture.

Reviewed changes

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

File Description
tests/unit/adapters/test_v01_variables_process_arguments.py Adds regression coverage for v0.1 value_hint + variables runtime arguments in Copilot and Codex.
src/apm_cli/adapters/client/copilot.py Extends _process_arguments to handle v0.1 value_hint args and substitute {var} placeholders.
src/apm_cli/adapters/client/codex.py Mirrors the Copilot _process_arguments v0.1 handling in the Codex adapter.

Comment thread src/apm_cli/adapters/client/copilot.py
Comment thread src/apm_cli/adapters/client/codex.py Outdated
Comment thread src/apm_cli/adapters/client/copilot.py
Comment thread src/apm_cli/adapters/client/codex.py
Comment thread tests/unit/adapters/test_v01_variables_process_arguments.py
@danielmeppiel
Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_with_followups

Restores MCP v0.1 variable resolution across all non-VS-Code adapters, unlocking Playwright MCP and other v0.1-shaped servers in 7 harnesses via a 30-line community fix with 8 regression tests.

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

All seven panelists converge cleanly: zero blocking findings, zero dissent on severity classification. The test-coverage-expert confirms 8 unit tests pass (4.37s) directly exercising the new elif path in both copilot and codex adapters, and verifies the inheritance chain (Gemini, Cursor, Claude, Windsurf, OpenCode all extend CopilotClientAdapter without overriding _process_arguments). This is load-bearing evidence that the user promise -- v0.1 runtimeArguments resolve correctly in every supported harness -- holds on this commit. Supply-chain-security-expert found no new trust boundary; the value_hint template is registry-controlled and already within the existing trust model.

The strongest signal from the panel is the doc-writer + oss-growth-hacker convergence on a missing CHANGELOG entry. Per .github/instructions/changelog.instructions.md, every merged code-change PR must carry a changelog line. This is not a nit -- it is a process requirement that should be satisfied before or at merge. The entry should name all seven adapters to prevent users from assuming only copilot+codex were fixed, and should credit @sergio-sisternes-epam to close the contributor-retention loop.

Strategically this PR is a low-risk, high-signal proof point for multi-harness reliability and community approachability. A community contributor diagnosed a cross-adapter regression, wrote a symmetric fix mirroring #1444, and delivered mutation-break-gated tests -- exactly the contributor archetype APM wants to attract. Shipping quickly with public credit maximizes the retention signal.

Aligned with: Multi-harness multi-host (core unlock: v0.1 variable substitution now works identically across Copilot, Codex, Gemini, Cursor, Claude, Windsurf, and OpenCode -- parity with the VS Code fix shipped in 0.14.2); OSS community-driven (external contributor @sergio-sisternes-epam shipped a surgical cross-adapter fix with full test coverage; crediting in CHANGELOG and release notes closes the retention loop); Portable by manifest (MCP v0.1 registry packages declaring runtimeArguments.variables now resolve portably across all adapters without author-side workarounds).

Growth signal. Named-tool unlock (Microsoft Playwright MCP across 7 non-VS-Code adapters) is a concrete, repostable claim for release comms. Frame as "community-powered multi-harness reliability" -- a single external contributor delivering cross-adapter parity in <30 LOC demonstrates the repo is approachable for surgical fixes. Use in Q4 release notes and contributor spotlight.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 1 Correct fix with acceptable duplication; shared helper on MCPClientAdapter is the natural follow-up.
CLI Logging Expert 0 0 1 No CLI output surface touched; silent placeholder fallback matches VS Code parity.
DevX UX Expert 0 0 1 Cross-harness parity restored; inheritance claim verified across all 5 dependent adapters.
Supply Chain Security Expert 0 0 0 No new trust boundary; runtime_vars are user-supplied via interactive prompt.
OSS Growth Hacker 0 1 0 Named third-party tool unlock deserves a CHANGELOG line and release-note beat; contributor credit is a retention lever.
Doc Writer 0 1 1 CHANGELOG drift: [Unreleased] is empty; needs a Fixed entry symmetric to the 0.14.2 entry for #1444.
Test Coverage Expert 0 0 0 8 new unit tests pass (4.37s); mutation-break holds; inheritance verified; no coverage gaps.

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

Top 4 follow-ups

  1. [Doc Writer] Add CHANGELOG Fixed entry under [Unreleased] naming all 7 adapters and crediting the contributor. -- Process requirement per changelog.instructions.md; symmetric to the 0.14.2 entry for fix(vscode): handle v0.1 runtimeArguments with variables for Docker MCP (#1391) #1444. Also the highest-signal growth lever (named-tool unlock visible to upgrading users).
  2. [Python Architect] Extract v0.1 variable substitution to a shared static helper on MCPClientAdapter. -- The 15-line elif block is now copy-pasted across copilot.py, codex.py, and vscode.py (with a minor divergence). A shared helper DRYs the logic and makes substitution semantics unit-testable in isolation. Follow-up issue, not blocking.
  3. [DevX UX Expert] Add inline comment explaining intentional omission of workspaceFolder special-case in copilot/codex path. -- Prevents a future contributor from "fixing" a divergence that is intentional (non-VS-Code harnesses have no native ${workspaceFolder} expansion).
  4. [CLI Logging Expert] Emit a verbose-only diagnostic when a variable falls back to a literal placeholder. -- Aids user debugging when runtime_vars lacks a declared variable; parity with VS Code adapter (both currently silent) means this is a future improvement, not a regression.

Architecture

classDiagram
    direction TB
    class MCPClientAdapter {
      <<Abstract>>
      +_resolve_variable_placeholders(value, resolved_env, runtime_vars) str
    }
    class CopilotClientAdapter {
      +_process_arguments(arguments, resolved_env, runtime_vars) list
    }
    class CodexClientAdapter {
      +_process_arguments(arguments, resolved_env, runtime_vars) list
    }
    class VSCodeClientAdapter {
      +_extract_package_args(package, runtime_vars) list
    }
    class ClaudeClientAdapter
    class CursorClientAdapter
    class GeminiClientAdapter
    class OpenCodeClientAdapter
    class WindsurfClientAdapter
    MCPClientAdapter <|-- CopilotClientAdapter
    MCPClientAdapter <|-- CodexClientAdapter
    MCPClientAdapter <|-- VSCodeClientAdapter
    CopilotClientAdapter <|-- ClaudeClientAdapter
    CopilotClientAdapter <|-- CursorClientAdapter
    CopilotClientAdapter <|-- GeminiClientAdapter
    CopilotClientAdapter <|-- OpenCodeClientAdapter
    CopilotClientAdapter <|-- WindsurfClientAdapter
    class CopilotClientAdapter:::touched
    class CodexClientAdapter:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A[apm install / add command] --> B[adapter._process_arguments runtime_arguments]
    B --> C{isinstance arg dict?}
    C -->|Yes| D{arg_type == positional?}
    C -->|No| K[isinstance arg str: resolve placeholders]
    D -->|Yes| E[_resolve_variable_placeholders value]
    D -->|No| F{arg_type == named?}
    F -->|Yes| G[append flag + optional value]
    F -->|No| H{not arg_type and value_hint in arg?}
    H -->|Yes| I[v0.1 path: substitute variables into value_hint]
    H -->|No| L[arg silently skipped]
    I --> J[_resolve_variable_placeholders substituted_value]
    J --> M[processed.append result]
    E --> M
    G --> M
    K --> M
    M --> N[return processed list]
    style I fill:#fff3b0,stroke:#d47600
    style H fill:#fff3b0,stroke:#d47600
Loading

Recommendation

Merge once a CHANGELOG entry is added under [Unreleased] -> ### Fixed (process requirement, not optional). The fix is correct, fully tested, inheritance-verified, and symmetric to the already-shipped #1444. The shared-helper extraction and inline-comment nits are post-merge follow-ups the team should track in a separate issue. Recommend the maintainer request the CHANGELOG addition from the contributor or add it at merge to keep the PR moving.


Full per-persona findings

Python Architect

  • [recommended] Extract v0.1 variable substitution to a shared helper on MCPClientAdapter. at src/apm_cli/adapters/client/codex.py:422
    The 15-line v0.1 elif block is now copy-pasted in copilot.py:L982 and codex.py:L422. VSCodeClientAdapter._extract_package_args has a third near-identical variant with an extra workspaceFolder special case. A static helper _substitute_v01_variables(value_hint, variables, runtime_vars) -> str on MCPClientAdapter would DRY all three sites and make substitution semantics unit-testable in isolation. Not blocking because pylint disable is already in place and the fix is correct; this is a follow-up the team should track.
  • [nit] Redundant str() cast on value already known to be a string. at src/apm_cli/adapters/client/copilot.py:994
    arg['value_hint'] is always a str from JSON deserialization; wrapping it in str() before passing to _resolve_variable_placeholders is a no-op cast that adds visual noise.

CLI Logging Expert

  • [nit] Consider a verbose-only diagnostic when an arg variable falls back to a literal placeholder. at src/apm_cli/adapters/client/copilot.py
    When runtime_vars lacks a declared variable, the code emits a ${varName} literal silently. A DiagnosticCollector info-tier note (verbose-only) would let users debug why an arg kept a raw placeholder. The vscode adapter is also silent so parity is correct for now; this is purely a future-improvement nit.

DevX UX Expert

  • [nit] Consider one-line comment explaining the intentional divergence from vscode's workspaceFolder special-case. at src/apm_cli/adapters/client/copilot.py:992
    The vscode adapter special-cases workspaceFolder to emit the VS Code-native ${workspaceFolder} token. The new copilot/codex branch emits a generic ${varName} for all unknowns (correct for non-vscode harnesses since they have no native ${workspaceFolder} expansion). A short inline comment would prevent a future contributor from "fixing" this divergence.

Supply Chain Security Expert

No findings.

OSS Growth Hacker

Auth Expert -- inactive

PR touches only MCP runtime-argument parsing in copilot.py and codex.py adapters with a matching test file; no auth, token, credential, or host-classification surface is affected.

Doc Writer

Test Coverage Expert

No findings.

Evidence: tests/unit/adapters/test_v01_variables_process_arguments.py -- 8 unit tests passed in 4.37s; tests directly exercise the new v0.1 elif path in both adapters; mutation-break claim holds (removing the new branch would fail all 8). Inheritance verified: gemini/cursor/claude/windsurf/opencode extend CopilotClientAdapter without override.

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

…for #1461

Per .github/instructions/changelog.instructions.md and the apm-review-panel
recommendation. Names all 7 adapters that inherit the fix so users grepping
for MCP v0.1 find both halves of the symmetric repair (vscode in 0.14.2,
copilot/codex + inherited adapters here).

Co-authored-by: sergio-sisternes-epam <sergio-sisternes-epam@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator

Follow-ups from the apm-review-panel pass have landed. Summary:

No regression-trap tests were added in this completion pass; the PR's existing 8 unit tests already cover the new elif branch in both copilot and codex adapters with mutation-break-verified coverage (test-coverage-expert confirmed in the panel pass).

Lint contract: uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ both silent.

CI: All 12 checks successful (0 failing, 0 pending) on head af4bee1.

Ready for maintainer review.

@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label May 23, 2026
- Extract v0.1 value_hint processing into shared helper
  src/apm_cli/adapters/client/_mcp_runtime_args.py to eliminate
  the R0801 duplicate-code violation between copilot.py and codex.py

- Add is_required guard in process_v01_value_hint_arg(): entries with
  is_required: False are legacy optional hints and must be skipped;
  absent is_required defaults to True (required)

- Add test cases for legacy {is_required: False, value_hint: ...}
  shape in both Copilot and Codex test classes to prevent regression

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label May 23, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel: ship_with_followups

Fix: MCP v0.1 runtimeArguments.variables now resolve correctly in Copilot, Codex, and 5 inherited adapters -- Docker volume mounts no longer silently dropped.

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

This PR patches a silent-failure bug where Docker MCP packages using the v0.1 runtimeArguments.variables protocol produced empty or placeholder arg lists with zero user-visible signal. The fix lands the correct elif branch in both CopilotClientAdapter._process_arguments and CodexClientAdapter._process_arguments, and since Claude, Cursor, Gemini, Windsurf, and OpenCode inherit from CopilotClientAdapter, all seven non-VS-Code adapters now match the VS Code behavior shipped in #1444. The doc-writer concern about 'packageArguments' in the CHANGELOG is resolved: _process_arguments is called for both runtimeArguments and packageArguments at lines 623 and 626 of copilot.py respectively, so the CHANGELOG scope is accurate and needs no edit.

Four panelists independently flag the same highest-signal gap from different angles: when a required variable is absent from runtime_vars, the adapter silently emits a raw '${varName}' token into the Docker args list. cli-logging-expert wants a _rich_warning; devx-ux-expert traces the downstream Docker bind-mount failure the user sees; supply-chain-security-expert notes value_hint is not type-checked before .replace() (a malformed registry entry causes an unguarded AttributeError on that codepath); and test-coverage-expert's missing integration-with-fixtures evidence row confirms there is no automated guardrail that would catch a regression here. These four signals converge on a single actionable: emit a warning for unresolved required variables and add one integration-with-fixtures test through the full adapter download pipeline. Neither is blocking for ship, but together they represent the most important follow-up surface.

Python-architect correctly identifies that this PR deepens pre-existing _process_arguments duplication between copilot.py and codex.py -- codex.py even carries a pylint disable=duplicate-code comment as a marker of acknowledged debt. The PR is not the right moment to refactor (it is a targeted bug fix), but the diverging named-arg field mapping (name/value swapped between the two files) flagged by python-architect is a latent correctness bug that predates this PR and deserves a follow-up regression test before the next protocol version branch lands. The oss-growth-hacker CHANGELOG rewrite suggestion is high-value: the 'parity with VS Code' frame is a real conversion hook and the current entry buries it.

Dissent. No material disagreement between panelists. Auth-expert correctly self-deactivated. The python-architect named-arg field-swap finding (name vs value inverted between copilot.py and codex.py) is pre-existing and not introduced by this PR; it is flagged as recommended, not blocking -- it belongs in a follow-up issue, not as a gate on this fix.

Aligned with: Portable by manifest (strengthened -- v0.1 variables now resolve across all 7 adapters). Multi-harness/multi-host (directly addressed -- behavioral parity across Copilot, Codex, Claude, Cursor, Gemini, Windsurf, OpenCode). Secure by default (partial gap -- unresolved-variable placeholders and lack of path-traversal validation noted; not introduced by this PR). Pragmatic as npm (supported -- minimal targeted fix, not a premature refactor).

Growth signal. Docker-based MCP packages with workspaceFolder mounts are a confirmed and growing user pattern. The 'all non-VS-Code adapters now have parity with VS Code' release narrative is a recurring conversion lever -- users choosing between editors care about this explicitly. The CHANGELOG entry should be rewritten to lead with the user-trust story and the VS Code parity frame before this PR ships.

Panel summary

Persona B R N Takeaway
Python Architect 0 2 1 Fix is correct and safe; the duplicate _process_arguments across Copilot+Codex is the pre-existing debt this PR compounds -- lift to MCPClientAdapter base.
CLI Logging Expert 0 2 1 No logging in _process_arguments; missing verbose-mode trace for unresolved vars leaves agent operators blind to placeholder substitution fallback.
DevX UX Expert 0 2 1 Fix is correct and silently safe for the happy path; missing-required-variable fallback is a silent time-bomb that will confuse users at runtime.
Supply Chain Security Expert 0 2 1 No shell-injection vector; args go to exec not shell. Two recommended hardening gaps: no type-check on value_hint, no sanitization of user-supplied runtime_vars before Docker arg substitution.
OSS Growth Hacker 0 2 1 High-value silent-failure fix for Docker/MCP v0.1 packages across 7 adapters; CHANGELOG undersells the 'parity with VS Code' story.
Doc Writer 0 1 2 CHANGELOG adapter list is correct (Windsurf/OpenCode confirmed CopilotClientAdapter subclasses); CEO confirms packageArguments scope is also covered.
Test Coverage Expert 0 1 2 8 unit regression tests cover the bug-fix path well; one tier-floor gap: the v0.1 branch has no integration-with-fixtures test through the full adapter download pipeline.

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

Top 5 follow-ups

  1. [Test Coverage Expert] Add integration-with-fixtures test: build a fake v0.1 registry response with variables args, feed through full adapter download pipeline, assert resolved Docker mount arg appears in generated config. -- Missing evidence outcome on an install-pipeline surface. Unit tests pass even if upstream arg shape changes before _process_arguments; only an integration test catches that regression. Highest-signal gap in the PR.

  2. [CLI Logging Expert] Emit _rich_warning when a required (is_required=True) variable is absent from runtime_vars and falls back to the ${varName} placeholder; emit logger.debug for optional variables and for silently dropped unknown arg formats. -- Three panelists (cli-logging, devx-ux, supply-chain) independently identified this silent-failure path. Docker bind-mount errors produced by unresolved placeholders are cryptic and hard to diagnose without an upstream warning. This is the highest-signal UX gap.

  3. [OSS Growth Hacker] Rewrite CHANGELOG entry to lead with the user-trust story and the VS Code parity frame, name all seven adapters, and cite the closing issue. -- The current entry reads as an internal diff note. 'parity with VS Code' is a conversion hook for users choosing between editors and belongs in the headline of the entry, not implied by the code.

  4. [Python Architect] Open a follow-up issue to audit the inverted name/value field mapping in codex.py _process_arguments named-arg branch vs copilot.py, add a regression test, and track the lift of _process_arguments to MCPClientAdapter base. -- The field swap (flag reads arg.get('value'), value reads arg.get('name') in codex.py vs the reverse in copilot.py) is a latent correctness bug. The duplication makes any future protocol version branch require two synchronized edits and risks further drift.

  5. [Supply Chain Security Expert] Add isinstance(value, str) guard after value = arg['value_hint'] in both copilot.py and codex.py; evaluate path-traversal validation for runtime_vars values that resolve to filesystem paths. -- A malformed registry entry supplying a list or dict as value_hint causes an unguarded AttributeError. The test-coverage-expert separately confirmed value_hint=None with a sibling variables dict is also untested and triggers the same crash.

Architecture

classDiagram
    direction LR
    class MCPClientAdapter {
        <<ABC>>
        +install(server_info, env_overrides, runtime_vars)
        +_resolve_variable_placeholders(value, env, vars) str
        +_warn_input_variables(env, name, label)
    }
    class CopilotClientAdapter {
        <<ConcreteAdapter>>
        +_process_arguments(arguments, resolved_env, runtime_vars) list
        +_format_server_config(server_info, env_overrides, runtime_vars)
        +_resolve_environment_variables(env_vars, env_overrides) dict
    }
    class CodexClientAdapter {
        <<ConcreteAdapter>>
        +_process_arguments(arguments, resolved_env, runtime_vars) list
        +_format_server_config(server_info, env_overrides, runtime_vars)
        +_process_environment_variables(env_vars, env_overrides) dict
    }
    class VSCodeClientAdapter {
        <<ConcreteAdapter>>
        +_extract_package_args(package, runtime_vars) list
    }
    class GeminiClientAdapter {
        <<ConcreteAdapter - inherits fix>>
    }
    class CursorClientAdapter {
        <<ConcreteAdapter - inherits fix>>
    }
    class ClaudeClientAdapter {
        <<ConcreteAdapter - inherits fix>>
    }
    class WindsurfClientAdapter {
        <<ConcreteAdapter - inherits fix>>
    }
    class OpenCodeClientAdapter {
        <<ConcreteAdapter - inherits fix>>
    }
    MCPClientAdapter <|-- CopilotClientAdapter
    MCPClientAdapter <|-- CodexClientAdapter
    MCPClientAdapter <|-- VSCodeClientAdapter
    CopilotClientAdapter <|-- GeminiClientAdapter
    CopilotClientAdapter <|-- CursorClientAdapter
    CopilotClientAdapter <|-- ClaudeClientAdapter
    CopilotClientAdapter <|-- WindsurfClientAdapter
    CopilotClientAdapter <|-- OpenCodeClientAdapter
    class CopilotClientAdapter:::touched
    class CodexClientAdapter:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A["apm install --client copilot or codex"] --> B["install() -- base.py MCPClientAdapter"]
    B --> C["_format_server_config() -- copilot.py / codex.py"]
    C --> D["[I/O] package registry fetch"]
    D --> E{"arg format?"}
    E -->|"type=positional"| F["arg.get value/default\n-> _resolve_variable_placeholders()"]
    E -->|"type=named"| G["append flag name + optional value"]
    E -->|"no type + value_hint\n[NEW BRANCH]"| H["value = arg[value_hint]\nfor var_name in variables:\n  resolve from runtime_vars\n  or fall back to placeholder\n-> _resolve_variable_placeholders()"]
    E -->|"str"| I["_resolve_variable_placeholders() direct"]
    F --> J["processed list append"]
    G --> J
    H --> J
    I --> J
    J --> K["[FS] write config JSON/TOML"]
Loading

Recommendation

Ship this PR. The bug fix is correct, the test coverage for the new branch is solid at the unit tier, and the VS Code parity story is genuinely valuable for the seven affected adapters. The CHANGELOG entry should be rewritten before merge (growth-hacker finding, five-minute change). The five follow-ups above should be filed as issues immediately after merge -- the integration-with-fixtures gap and the unresolved-required-variable warning are the two that will matter most to operators running Docker MCP packages in production.


Full per-persona findings

Python Architect

  • [recommended] _process_arguments is fully duplicated in copilot.py and codex.py; the pylint suppress comment admits it at src/apm_cli/adapters/client/base.py:95
    The PR correctly adds identical elif branches to both files, but the method is byte-for-byte duplicated (codex.py even carries a pylint disable=duplicate-code comment as a marker). Any future protocol version branch will require a third synchronized edit. Lifting _process_arguments to MCPClientAdapter eliminates the drift surface.
    Suggested: Add _process_arguments to MCPClientAdapter with the full copilot.py implementation as the canonical body. Remove it from CopilotClientAdapter and CodexClientAdapter (audit the named-arg field delta before merging).

  • [recommended] named-arg field mapping diverges between copilot.py and codex.py -- possible latent bug pre-dating this PR at src/apm_cli/adapters/client/codex.py:409
    In copilot.py the named branch reads flag from arg.get('name') and value from arg.get('value'); in codex.py it reads flag from arg.get('value') and value from arg.get('name'). The field swap is suspicious. One adapter is probably wrong.
    Suggested: Add a regression test for a named-arg round-trip through both adapters, then audit which field mapping matches the registry schema and align both adapters.

  • [nit] fallback placeholder uses shell syntax ${var} instead of registry syntax {var} -- needs a comment explaining intent at src/apm_cli/adapters/client/copilot.py:992
    Suggested: Add inline comment: # emit ${var} so unresolved placeholders are visible in the output config

CLI Logging Expert

  • [recommended] Unresolved v0.1 variables fall back to '${varName}' placeholder with no diagnostic output at src/apm_cli/adapters/client/copilot.py
    When a required variable (is_required: True) is missing from runtime_vars, the arg is passed downstream as a raw token. For Docker mount args this silently produces an invalid bind-mount path. Operators running apm with --verbose have no way to detect this at install/run time.
    Suggested: After the else branch, emit a _rich_warning for required variables, or log at debug/verbose for optional ones.

  • [recommended] Completely unrecognized dict args (no type, no value_hint) are silently dropped with no diagnostic at src/apm_cli/adapters/client/copilot.py
    An unknown arg format could indicate a registry schema version the adapter does not yet handle.
    Suggested: Add an else branch: logger.debug('Skipping unrecognized arg format (keys: %s)', list(arg.keys()))

  • [nit] Identical code block in codex.py and copilot.py -- no logging consistency risk now but will diverge at src/apm_cli/adapters/client/codex.py
    Suggested: Consider extracting the shared _process_arguments body into a mixin or base-class method so logging changes propagate automatically.

DevX UX Expert

  • [recommended] Required variables that are unresolved silently produce a ${varName} placeholder passed to Docker, causing a cryptic mount failure at runtime at src/apm_cli/adapters/client/copilot.py
    When is_required=true and the variable is absent from runtime_vars, the adapter emits '${customVar}:/data' as a literal Docker argument. Docker will fail with an obscure bind-mount error or mount a directory literally named '${customVar}'.
    Suggested: After the substitution loop, check if any is_required variable was not resolved and emit a _rich_warning naming the variable and a hint ('pass --var workspaceFolder=(path) or set it in apm.yml'). Do the same in codex.py.

  • [recommended] Silent drop of empty value_hint string is invisible to the user and can produce a wrong arg list without any signal at src/apm_cli/adapters/client/copilot.py
    The 'if value:' guard silently skips an arg whose value resolves to an empty string, potentially shifting positional Docker arguments with no warning.
    Suggested: Log at debug/verbose level when value is empty after substitution, including the original value_hint template.

  • [nit] Test name 'unknown var gets placeholder' frames a footgun as expected behavior at tests/unit/adapters/test_v01_variables_process_arguments.py
    Suggested: Rename to 'test_v01_required_var_missing_falls_back_to_literal_placeholder' and add a TODO comment noting that a warning should accompany this path.

Supply Chain Security Expert

  • [recommended] value_hint is not type-checked before string operations; a non-string registry value causes an unguarded AttributeError at src/apm_cli/adapters/client/copilot.py
    The code does value = arg['value_hint'] then value.replace(...) without asserting it is a str. A malformed or adversarial registry entry supplying a list or dict as value_hint would cause an AttributeError.
    Suggested: Add isinstance(value, str) guard immediately after value = arg['value_hint'], before the replace loop.

  • [recommended] User-supplied runtime_vars values flow into Docker volume mount args with no path-traversal sanitization at src/apm_cli/adapters/client/copilot.py
    runtime_vars is populated by _prompt_for_environment_variables, which reads raw user input. A user entering ../../etc is substituted verbatim and lands in config['args'] for Docker. The new branch widens an existing surface.
    Suggested: After variable substitution, if the resolved value looks like a path, run it through path_security.ensure_path_within or validate_path_segments.

  • [nit] ${varName} fallback string written to JSON config could mislead downstream tooling at src/apm_cli/adapters/client/copilot.py
    Shell-based launchers that later see this string in the JSON config file may attempt to expand it. Very low risk because shell=True is not used.
    Suggested: Log a warning via existing _rich_warning helper when a variable falls back to the literal placeholder.

OSS Growth Hacker

Doc Writer

  • [recommended] CHANGELOG adds 'packageArguments' alongside 'runtimeArguments' -- CEO has confirmed this is accurate (_process_arguments is called for both at lines 623 and 626 of copilot.py). PR body should be updated to mention packageArguments for completeness. at CHANGELOG.md
    Suggested: Update PR body to note that the fix covers both runtimeArguments and packageArguments.

  • [nit] Windsurf and OpenCode are correctly listed in CHANGELOG -- confirmed CopilotClientAdapter subclasses at CHANGELOG.md
    No action needed.

  • [nit] Adapter enumeration order is arbitrary in the CHANGELOG entry at CHANGELOG.md
    Suggested: 'Copilot and Codex (and Cursor, Claude, Gemini, OpenCode, Windsurf adapters that inherit from CopilotClientAdapter) now handle ...'

Test Coverage Expert

  • [recommended] v0.1 _process_arguments branch has no integration-with-fixtures coverage through the adapter download pipeline at tests/integration/test_download_copilot_phase3.py
    The new elif branch is only tested via direct unit calls to _process_arguments. If upstream code changes its arg shape before reaching _process_arguments, the unit tests would still pass but users would again get silently dropped Docker args.
    Suggested: Add a test class that builds a fake registry response with v0.1 value_hint+variables args, feeds it through the adapter full pipeline, and asserts the produced config contains the resolved Docker mount arg.
    Proof (missing at integration-with-fixtures): tests/integration/test_download_copilot_phase3.py::TestCopilotAdapterV01RuntimeArgs::test_v01_docker_args_flow_through_full_pipeline -- proves: A Docker MCP server registered with v0.1 runtimeArguments.variables produces a correctly resolved config after apm install [multi-harness-support]
    assert '/home/user/project:/workspace' in generated_config['servers']['srv']['args']

  • [nit] value_hint=None with a sibling variables dict causes unguarded AttributeError -- not tested at tests/unit/adapters/test_v01_variables_process_arguments.py
    value = arg['value_hint'] is assigned before the variables loop. If value_hint is None and variables is also present, value.replace(...) raises AttributeError before the if value: guard.
    Suggested: Add test_v01_value_hint_none_with_variables_skipped, then guard copilot.py:987 with 'if not isinstance(value, str): continue'.
    Proof (missing at unit): tests/unit/adapters/test_v01_variables_process_arguments.py::TestCopilotProcessArgumentsV01Variables::test_v01_value_hint_none_with_variables_skipped
    self.assertEqual(adapter._process_arguments([{'value_hint': None, 'variables': {'x': {}}}], {}, {}), [])

  • [nit] Inherited adapters (Claude, Cursor, Gemini) have no explicit smoke test -- acceptable given pure inheritance but worth noting at tests/unit/adapters/test_v01_variables_process_arguments.py
    grep confirms none of claude.py, cursor.py, gemini.py override _process_arguments. A parametrized test would make the inheritance assumption explicit.
    Suggested: @pytest.mark.parametrize('adapter_cls', [CursorClientAdapter, ClaudeClientAdapter, GeminiClientAdapter]) on test_v01_plain_value_hint_args_extracted.
    Proof (missing at unit): tests/unit/adapters/test_v01_variables_process_arguments.py::TestInheritedAdaptersV01Variables::test_cursor_v01_plain_value_hint
    self.assertEqual(cursor_adapter._process_arguments([{'value_hint': 'run'}], {}, {}), ['run'])

Auth Expert -- inactive

PR only touches adapter argument-parsing logic (codex.py, copilot.py) and tests -- no auth, token, credential, or host-classification code is modified.

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

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • pypi.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "pypi.org"

See Network Configuration for more information.

Generated by PR Review Panel for issue #1461 · ● 3M ·

@danielmeppiel danielmeppiel added this pull request to the merge queue May 23, 2026
Merged via the queue into main with commit 7f79a37 May 23, 2026
47 checks passed
@danielmeppiel danielmeppiel deleted the fix/1452-v01-variables-all-adapters branch May 23, 2026 14:30
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.

Extend MCP v0.1 runtimeArguments.variables handling to all client adapters (P3 follow-up to #1444)

3 participants