fix(mcp): resolve self-defined stdio env ${VAR} placeholders at install time (closes #1963)#1966
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes install-time resolution of ${VAR} placeholders for self-defined stdio MCP server env blocks, ensuring Claude Code and Codex receive literal env values rather than ${VAR} strings caused by env_overrides shadowing os.environ.
Changes:
- Adjusts self-defined MCP install flow so stdio
envis resolved via the adapter pipeline (not reused asenv_overrides). - Adds a regression test covering both Claude (
.mcp.json) and Codex (.codex/config.toml) output. - Updates documentation/guide text and adds a changelog entry describing the behavior.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/install/test_mcp_integrator_install_flow.py | Adds regression test asserting ${MY_TOKEN} resolves for self-defined stdio servers in Claude + Codex. |
| src/apm_cli/integration/mcp_integrator_install.py | Stops passing self-defined stdio dep.env as env_overrides to avoid shadowing os.environ. |
| packages/apm-guide/.apm/skills/apm-usage/dependencies.md | Updates placeholder behavior notes (currently inconsistent with manifest-schema docs; see comments). |
| docs/src/content/docs/producer/author-primitives/mcp-as-primitive.md | Updates narrative on placeholder handling; suggested wording tweak to reflect prompting behavior. |
| CHANGELOG.md | Adds Unreleased entry (needs PR number reference update; see comment). |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Low
| # Cursor/Windsurf/OpenCode/Claude/Gemini: resolved at install time. | ||
| # Codex: passed through unchanged. | ||
| # Codex remote headers: passed through unchanged. | ||
| # Codex self-defined stdio env: resolved at install time. |
| Headers and env values are never shell-expanded by APM. For harnesses | ||
| that support runtime env placeholders, APM preserves the placeholder so | ||
| the harness resolves it when the server starts or the request is made. | ||
| For harnesses that require literal values, APM resolves `${VAR}` from | ||
| the install process environment and leaves unresolved placeholders | ||
| unchanged. Keep the real secret in the consumer's environment (or their | ||
| secret manager). |
|
|
||
| ### Fixed | ||
|
|
||
| - Self-defined stdio MCP env placeholders now resolve from the install process environment for Claude Code and Codex, matching remote header handling. (#1963) |
Fold review follow-ups for PR #1966 by clarifying the stdio-env docs and preserving the non-stdio self-defined env-overrides branch with a regression-trap test. The test prevents future changes from collapsing the branch that remote transports still rely on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel: ship_now
cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review. This PR resolves a real-world bug where Panel signals converge cleanly: python-architect finds the change structurally sound, supply-chain-security and auth see no new surface, CLI logging is unaffected, and test coverage now validates both the stdio and non-stdio branches with mutation-break evidence. No specialist raised a remaining in-scope finding after the fold. Aligned with: portable_by_manifest: Ensures MCP server definitions in apm.yml resolve env placeholders consistently across adapters; multi_harness_multi_host: Fixes adapter-specific resolution for Claude Code and Codex; pragmatic_as_npm: Users expect env vars to just work in server definitions. Growth signal. This removes a first-run trust issue for Claude Code and Codex MCP authors. Panel summary
RecommendationAll in-scope findings have been folded, tests cover both branches, and mutation-break gates passed. No follow-ups remain. Merge at will after maintainer review. Folded in this run
Copilot signals reviewed
Regression-trap evidence (mutation-break gate)
Lint contract
CI
Mergeability statusCaptured from
Convergence1 outer iteration; 2 Copilot fetch rounds. Final panel stance: Ready for maintainer review. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fold review follow-ups for PR #1966 by clarifying the stdio-env docs and preserving the non-stdio self-defined env-overrides branch with a regression-trap test. The test prevents future changes from collapsing the branch that remote transports still rely on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses apm-review-panel follow-ups for PR #1966 by documenting interactive placeholder prompting and naming why stdio env must avoid env_overrides shadowing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
122d237 to
c576efa
Compare
Addresses final apm-review-panel nits by making the changelog symptom searchable and clarifying that placeholder prompts require an attached terminal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses Copilot's guide-doc signal by matching the manifest schema: Codex placeholders resolve at install time for both remote headers and self-defined stdio env. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 0 | Stdio env routing fix is minimal and correct; adapter pipeline separation preserved. |
| CLI Logging Expert | 0 | 0 | 0 | No CLI output or logging surface touched; no secret leak introduced. |
| DevX UX Expert | 0 | 0 | 0 | Placeholder resolution now matches user expectations and docs explain per-target behavior. |
| Supply Chain Security Expert | 0 | 0 | 0 | Existing adapter pipeline is reused; no new credential surface or secret logging. |
| OSS Growth Hacker | 0 | 0 | 0 | Fix removes a silent first-run failure for Claude Code and Codex users. |
| Auth Expert | 0 | 0 | 0 | No AuthResolver bypass or direct token env handling introduced. |
| Doc Writer | 0 | 0 | 0 | Docs are accurate, concise, and consistent after folds. |
| Test Coverage Expert | 0 | 0 | 0 | Regression trap covers Claude and Codex; non-stdio branch remains covered. |
Folded in this run
- (copilot) Align Codex placeholder guide with manifest schema -- resolved in c9bf77f.
- (copilot) Document optional interactive prompting before unresolved placeholders are preserved -- resolved in 498be31.
- (copilot) Use PR number in the changelog entry -- resolved in 019dc5b.
- (panel) Clarify the source comment so future readers see why
env_overrideswould shadowos.environ-- resolved in c576efa. - (panel) Neutralize the docs header comment so it does not claim every harness expands at runtime -- resolved in c576efa.
- (panel) Link the producer docs back to the canonical per-target manifest rules -- resolved in c576efa.
- (panel) Make the changelog symptom searchable and clarify TTY-only prompting wording -- resolved in 498be31.
Copilot signals reviewed
packages/apm-guide/.apm/skills/apm-usage/dependencies.md:373-- LEGIT: guide contradicted the manifest-schema Codex placeholder contract (resolved in c9bf77f).docs/src/content/docs/producer/author-primitives/mcp-as-primitive.md-- LEGIT: docs omitted the optional interactive prompt step (resolved in 498be31).CHANGELOG.md-- LEGIT: changelog needed the PR number rather than issue number (resolved in 019dc5b).
Regression-trap evidence (mutation-break gate)
tests/unit/install/test_mcp_integrator_install_flow.py::TestRunMcpInstallSelfDefined::test_self_defined_stdio_env_placeholders_resolve_from_process_env-- deletedself_defined_env = {} if transport_label == "stdio" else dep.env or {}; test FAILED as expected for Claude and Codex; guard restored.
Lint contract
uv run --extra dev ruff check src/ tests/ and
uv run --extra dev ruff format --check src/ tests/ both silent. Full local lint mirror also passed with YAML I/O guard, file-length guard, raw relative_to guard, pylint R0801, and scripts/lint-auth-signals.sh.
CI
All GitHub checks passed on c9bf77f: Lint, Build & Test Shard 1/2, PR Binary Smoke, APM Self-Check, Coverage Combine, CodeQL, NOTICE Drift Check, Spec conformance, Merge Gate, Deploy Docs build, and license/cla (deploy skipped as expected) after 0 CI fix iteration(s).
Mergeability status
Captured from gh pr view 1966 --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push of this run.
| PR | head SHA | CEO stance | iters | folds | defers | Copilot rounds | CI | mergeable | mergeStateStatus | notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #1966 | c9bf77f |
ship_now | 1 | 7 | 0 | 2 | green | MERGEABLE | BLOCKED | pending required review |
Convergence
1 outer iteration; 2 Copilot rounds. Final panel stance: ship_now.
Ready for maintainer review.
… process env (#1963) Adds a hermetic end-to-end integration test that installs a self-defined MCP stdio server whose env declares a ${VAR} placeholder, drives the real install path, and asserts the materialized config resolves the value from the process environment (not the literal placeholder) without leaking the secret into logs. Proves issue #1963 is fixed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ship_now
|
Why
Self-defined MCP transports were inconsistent: HTTP headers already resolved
${VAR}placeholders from the install process environment for literal-only harnesses, but self-defined stdioenvvalues were passed back asenv_overrides, causing the authored placeholder to shadow the real process environment. That made Claude Code and Codex write${VAR}verbatim and break authenticated servers, a P6 reliability issue.What changed
envblocks asenv_overrides; the adapter now resolves_raw_stdio.envthrough the same placeholder pipeline used by headers.${MY_TOKEN}resolves while literal env values remain unchanged.How to test
Exact repro:
Validation run:
Mutation-break gate: removed
self_defined_env = {} if transport_label == "stdio" else dep.env or {}and restoredself_defined_env = dep.env or {}; the new regression test failed for both Claude and Codex with${MY_TOKEN}written verbatim (failed_without_guard=true).Closes #1963