Skip to content

fix: resolve env-var references in MCP server environment config (closes #656)#666

Open
VJ-yadav wants to merge 3 commits intoAltimateAI:mainfrom
VJ-yadav:fix/mcp-env-var-interpolation
Open

fix: resolve env-var references in MCP server environment config (closes #656)#666
VJ-yadav wants to merge 3 commits intoAltimateAI:mainfrom
VJ-yadav:fix/mcp-env-var-interpolation

Conversation

@VJ-yadav
Copy link
Copy Markdown
Contributor

@VJ-yadav VJ-yadav commented Apr 9, 2026

What does this PR do?

Fixes #656${VAR} and {env:VAR} patterns in MCP server environment config blocks were passed as literal strings to child processes instead of being resolved to actual environment variable values. This caused auth failures for MCP servers expecting tokens (e.g. gitlab-mcp-server receiving literal "${GITLAB_PERSONAL_ACCESS_TOKEN}" instead of the token).

Root Cause

PR #655 added env-var interpolation to the config parsing pipeline (ConfigPaths.parseText → substitute()), but two code paths bypassed it:

  1. MCP launch site (mcp/index.ts:512) — mcp.environment values were spread directly into the child process env without resolving any remaining ${VAR} patterns. If interpolation failed upstream (config updates via updateGlobal(), timing issues), the literal string overwrote the correct value already present in process.env.

  2. External MCP discovery (discover.ts:readJsonSafe()) — configs from Claude Code (.claude.json), Cursor (.cursor/mcp.json), VS Code (.vscode/mcp.json), Copilot, and Gemini were parsed via parseJsonc() directly, completely skipping the substitute() interpolation pipeline.

Changes

File Change
packages/opencode/src/mcp/index.ts Added resolveEnvVars() safety net that resolves ${VAR}, ${VAR:-default}, {env:VAR}, and $${VAR} (escape) patterns in mcp.environment values before spawning the child process
packages/opencode/src/mcp/discover.ts Changed readJsonSafe() to use ConfigPaths.parseText() which runs substitute() on raw text before JSONC parsing, with graceful fallback to direct parse on failure
packages/opencode/test/mcp/env-var-interpolation.test.ts 14 new tests

Supported syntaxes (all three work in MCP environment blocks)

Syntax Behavior Example
${VAR} Resolves to env value, empty string if unset "TOKEN": "${GITLAB_TOKEN}"
${VAR:-default} Resolves to env value, fallback if unset "MODE": "${APP_MODE:-production}"
{env:VAR} Raw text injection (backward compat) "KEY": "{env:API_KEY}"
$${VAR} Escape — preserves literal ${VAR} "TPL": "$${VAR}""${VAR}"

Test plan

  • 11 unit tests for resolveEnvVars: ${VAR}, {env:VAR}, defaults, escapes, unset vars, plain passthrough, multiple vars in one value, mixed entries, bare $VAR not matched, empty object
  • 3 integration tests for discovery: .vscode/mcp.json with ${VAR}, .cursor/mcp.json with {env:VAR}, fallback defaults
  • 18 existing discover.test.ts tests still pass
  • 34 existing paths-parsetext.test.ts tests still pass
  • TypeScript typecheck clean (only pre-existing ClickHouse driver error)

Checklist

  • No new dependencies
  • No any types in new code
  • Follows altimate_change marker convention
  • Regex pattern matches the one in paths.ts:substitute() for consistency
  • Graceful fallback: if ConfigPaths.parseText() fails in discovery, falls back to direct parse (no regression for malformed external configs)

Fixes #656


Summary by cubic

Fixes env-var interpolation in MCP server environments so ${VAR} placeholders resolve before spawn, preventing literals from reaching child processes. Also applies interpolation to external MCP configs (Cursor, VS Code, Claude Code, Copilot, Gemini). Fixes #656.

  • Bug Fixes

    • Resolve ${VAR}, ${VAR:-default}, and {env:VAR}; support $${VAR} to escape.
    • Run resolver at launch so mcp.environment is resolved before child process spawn.
    • Use ConfigPaths.parseText() to interpolate external configs, with fallback to direct parse.
    • Export resolveEnvVars; 14 tests cover syntax, defaults, escapes, and external discovery.
  • Refactors

    • Tests use shared tmpdir fixture for automatic cleanup.

Written for commit b8ab0d9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • MCP config files and local MCP environment values now support environment-variable interpolation using ${VAR}, ${VAR:-default}, and {env:VAR}; escaped $$ sequences are preserved. External MCP JSON configs are attempted to be interpolated first, with a fallback to the previous JSONC parsing behavior if interpolation fails.
  • Tests

    • Added unit and integration tests validating interpolation behavior and discovery of external MCP configs.

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ece22ef6-28cb-4bd4-b36f-e99409c63599

📥 Commits

Reviewing files that changed from the base of the PR and between 1070d8e and b8ab0d9.

📒 Files selected for processing (3)
  • packages/opencode/src/mcp/discover.ts
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/test/mcp/env-var-interpolation.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/test/mcp/env-var-interpolation.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/src/mcp/discover.ts

📝 Walkthrough

Walkthrough

Attempts env-var interpolation when reading external MCP JSON via ConfigPaths.parseText(...); on parse failure it falls back to JSONC parsing. Adds an exported resolveEnvVars() and applies it to MCP environment. Includes unit and integration tests for interpolation patterns and fallbacks.

Changes

Cohort / File(s) Summary
MCP discovery parsing
packages/opencode/src/mcp/discover.ts
readJsonSafe() now dynamically imports ConfigPaths and calls ConfigPaths.parseText(text, filePath, "empty") to interpolate env placeholders; logs debug and falls back to parseJsonc(...) on error.
Runtime env resolution
packages/opencode/src/mcp/index.ts
Added exported resolveEnvVars(environment: Record<string,string>). When constructing the local MCP StdioClientTransport, mcp.environment is passed through resolveEnvVars (or omitted if falsy) and merged with process.env and a BUN_BE_BUN override.
Tests
packages/opencode/test/mcp/env-var-interpolation.test.ts
New Bun test file covering resolveEnvVars unit cases and integration tests for discoverExternalMcp(...) with temp .vscode/mcp.json / .cursor/mcp.json, asserting ${VAR}, ${VAR:-default}, {env:VAR}, escaped $${VAR}, and cleanup of process.env.

Sequence Diagram(s)

sequenceDiagram
    participant File as External MCP File
    participant Discover as discover.ts (readJsonSafe)
    participant ConfigPaths as ConfigPaths.parseText
    participant Index as mcp/index.ts (resolveEnvVars)
    participant Transport as StdioClientTransport

    File->>Discover: provide raw config text
    Discover->>ConfigPaths: parseText(text, filePath, "empty")
    alt parseText succeeds
        ConfigPaths-->>Discover: parsed + interpolated MCP definition
        Discover-->>Index: return MCP definition
        Index->>Index: resolveEnvVars(environment)
        Index-->>Transport: pass resolved environment
    else parseText fails
        ConfigPaths-->>Discover: throw/error
        Discover->>Discover: fallback to parseJsonc(text, errors, {allowTrailingComma:true})
        Discover-->>Index: return parsed MCP definition (uninterpolated if errors)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I nibble at config strings by night,

${VAR} glows and hops into sight,
Defaults leap in when none are found,
Escaped dollars stay safe and sound,
Hooray — the MCPs all run bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary fix: resolving env-var references in MCP server environment configuration, and explicitly references the issue being closed.
Description check ✅ Passed The description is comprehensive and well-structured. It includes a detailed summary of what changed and why, a thorough test plan with specific test counts and verification, and a complete checklist covering tests, documentation, and dependencies.
Linked Issues check ✅ Passed The PR fully addresses issue #656 by implementing env-var interpolation in MCP server environment configs, supporting ${VAR}, ${VAR:-default}, and {env:VAR} syntaxes, applying fixes to both launch sites and external discovery paths, and providing comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing env-var interpolation in MCP environments: resolveEnvVars() helper, discover.ts parsing updates, and comprehensive test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@VJ-yadav VJ-yadav force-pushed the fix/mcp-env-var-interpolation branch from f77c8bd to 8e1671f Compare April 9, 2026 03:19
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/test/mcp/env-var-interpolation.test.ts">

<violation number="1" location="packages/opencode/test/mcp/env-var-interpolation.test.ts:19">
P1: These unit tests exercise a **copy** of `resolveEnvVars` pasted into the test file, not the actual production function from `src/mcp/index.ts` (which is not exported). If the production regex or logic changes, these 11 tests will still pass against the stale local copy, giving false confidence.

Export `resolveEnvVars` (and optionally `ENV_VAR_PATTERN`) from the source module and import it here instead of duplicating the implementation.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

@VJ-yadav
Copy link
Copy Markdown
Contributor Author

VJ-yadav commented Apr 9, 2026

Testing: MCP env-var interpolation fix

The problem: When users configure MCP servers with environment variable references like "GITLAB_TOKEN": "${GITLAB_TOKEN}", the MCP server receives the literal string "${GITLAB_TOKEN}" instead of the actual token value, causing auth failures (HTTP 401).

Why it happens: PR #655 (v0.5.19) added ${VAR} interpolation to the config parser, but two code paths bypass it:

  • The MCP launch site spreads mcp.environment into the child process without resolving remaining ${VAR} patterns
  • External MCP discovery (Claude Code, Cursor, Copilot, Gemini configs) parses JSON directly via parseJsonc(), skipping the substitute() pipeline entirely

The fix:

  • resolveEnvVars() safety net at the MCP launch site — catches all code paths regardless of how the config was loaded
  • readJsonSafe() in discovery now uses ConfigPaths.parseText() for interpolation before parsing, with graceful fallback

14 new tests, all passing (importing from production source, not duplicated):

Screenshot 2026-04-08 at 11 27 40 PM

Covers all syntaxes: ${VAR}, ${VAR:-default}, {env:VAR}, $${VAR} escape, unset vars, plain passthrough, multiple vars in one value, bare $VAR not matched, and 3 discovery integration tests with .vscode/mcp.json and .cursor/mcp.json.

Also addressed cubic-dev-ai review: resolveEnvVars is now exported from src/mcp/index.ts and imported in the test file instead of duplicating the implementation.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/opencode/test/mcp/env-var-interpolation.test.ts (2)

16-33: Consider testing the actual implementation rather than duplicating it.

The test file duplicates ENV_VAR_PATTERN and resolveEnvVars from index.ts. If the source implementation changes (e.g., bug fix or new pattern), these tests will still pass against the stale duplicated code, creating a false sense of coverage.

Consider either:

  1. Exporting resolveEnvVars from index.ts (or a shared utility) and importing it here
  2. Testing indirectly through the MCP launch flow (integration-style)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/mcp/env-var-interpolation.test.ts` around lines 16 -
33, The test duplicates ENV_VAR_PATTERN and resolveEnvVars; instead export the
real resolveEnvVars (and/or ENV_VAR_PATTERN) from index.ts or a shared util and
import it into packages/opencode/test/mcp/env-var-interpolation.test.ts so the
test exercises the actual implementation; if resolveEnvVars is not exported, add
an export (named) in the source module (index.ts or the appropriate utility
module) and update the test to import and use that exported resolveEnvVars
rather than the duplicated copy, or alternatively replace this unit test with an
integration-style test that exercises the MCP launch flow which uses
resolveEnvVars.

139-148: Use tmpdir() from fixture/fixture.ts per coding guidelines.

The test uses manual mkdtemp/rm for temporary directories. As per coding guidelines, tests should use the tmpdir function with await using syntax for automatic cleanup.

♻️ Suggested refactor using tmpdir()
+import { tmpdir as createTmpDir } from "../fixture/fixture"
+
 describe("discoverExternalMcp with env-var interpolation", () => {
-  let tempDir: string
   const ORIGINAL_ENV = { ...process.env }
 
-  beforeEach(async () => {
-    tempDir = await mkdtemp(path.join(tmpdir(), "mcp-envvar-"))
+  test("resolves ${VAR} in discovered .vscode/mcp.json environment", async () => {
+    await using tmp = await createTmpDir()
     process.env["TEST_MCP_TOKEN"] = "glpat-secret-token"
     process.env["TEST_MCP_HOST"] = "https://gitlab.internal.com"
-  })
-
-  afterEach(async () => {
-    process.env = { ...ORIGINAL_ENV }
-    await rm(tempDir, { recursive: true, force: true })
-  })
-
-  test("resolves ${VAR} in discovered .vscode/mcp.json environment", async () => {
-    await mkdir(path.join(tempDir, ".vscode"), { recursive: true })
+    
+    await mkdir(path.join(tmp.path, ".vscode"), { recursive: true })
     // ... rest of test using tmp.path instead of tempDir

As per coding guidelines: "Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup" and "Always use await using syntax with tmpdir() for automatic cleanup when the variable goes out of scope".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/mcp/env-var-interpolation.test.ts` around lines 139 -
148, Replace the manual mkdtemp/rm setup in the test (the beforeEach/afterEach
that sets tempDir and calls mkdtemp and rm) with the tmpdir helper from
fixture/fixture.ts and use it via "await using" so the temporary directory is
created and automatically cleaned up; update the test to remove manual
process.env restore in afterEach if handled elsewhere, and reference the same
tempDir variable name in the test body but obtain it from await using tmpdir()
instead of mkdtemp, removing the explicit rm call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/opencode/test/mcp/env-var-interpolation.test.ts`:
- Around line 16-33: The test duplicates ENV_VAR_PATTERN and resolveEnvVars;
instead export the real resolveEnvVars (and/or ENV_VAR_PATTERN) from index.ts or
a shared util and import it into
packages/opencode/test/mcp/env-var-interpolation.test.ts so the test exercises
the actual implementation; if resolveEnvVars is not exported, add an export
(named) in the source module (index.ts or the appropriate utility module) and
update the test to import and use that exported resolveEnvVars rather than the
duplicated copy, or alternatively replace this unit test with an
integration-style test that exercises the MCP launch flow which uses
resolveEnvVars.
- Around line 139-148: Replace the manual mkdtemp/rm setup in the test (the
beforeEach/afterEach that sets tempDir and calls mkdtemp and rm) with the tmpdir
helper from fixture/fixture.ts and use it via "await using" so the temporary
directory is created and automatically cleaned up; update the test to remove
manual process.env restore in afterEach if handled elsewhere, and reference the
same tempDir variable name in the test body but obtain it from await using
tmpdir() instead of mkdtemp, removing the explicit rm call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c57bcd95-245a-4e8f-a47f-0a3996dada1a

📥 Commits

Reviewing files that changed from the base of the PR and between dafd16a and 8e1671f.

📒 Files selected for processing (3)
  • packages/opencode/src/mcp/discover.ts
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/test/mcp/env-var-interpolation.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/opencode/test/mcp/env-var-interpolation.test.ts (1)

151-152: Avoid any casts when asserting discovered server environments.

These casts can be replaced with a small typed helper/assertion to keep test type-safety intact.

Typed alternative (example)
-    const env = (servers["gitlab"] as any).environment
+    const env = (servers["gitlab"] as { environment: Record<string, string> }).environment

Also applies to: 178-179, 202-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/mcp/env-var-interpolation.test.ts` around lines 151 -
152, Replace the unsafe `(servers["gitlab"] as any).environment` casts with a
small typed helper to preserve type-safety: add a helper like
`getServerEnv(servers, key)` (or `assertServerHasEnvironment`) that accepts the
`servers` map and a server key, narrows/validates the server type, and returns
the typed `environment` object; then use that helper in the three places where
`as any` is used (the assertions currently using `(servers["gitlab"] as
any).environment` and the similar occurrences around lines 178-179 and 202-203)
so tests assert `env.GITLAB_TOKEN` (and other env keys) against a correctly
typed environment rather than using `any`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/test/mcp/env-var-interpolation.test.ts`:
- Around line 3-5: Replace the manual temp-dir lifecycle (calls to mkdtemp, rm,
mkdir, writeFile) in env-var-interpolation.test.ts with the shared tmpdir
fixture: import tmpdir from "fixture/fixture.ts" and in each test use "await
using const dir = tmpdir()" to obtain a temporary directory (then create files
inside dir.path). Remove explicit mkdtemp/rm cleanup and any manual tmpdir
string handling; update tests that currently perform temp setup (the block
around the existing mkdtemp/mkdir/writeFile usage and the similar logic
referenced later) to write files into dir.path and rely on automatic cleanup
when the await-using scoped variable is released.

---

Nitpick comments:
In `@packages/opencode/test/mcp/env-var-interpolation.test.ts`:
- Around line 151-152: Replace the unsafe `(servers["gitlab"] as
any).environment` casts with a small typed helper to preserve type-safety: add a
helper like `getServerEnv(servers, key)` (or `assertServerHasEnvironment`) that
accepts the `servers` map and a server key, narrows/validates the server type,
and returns the typed `environment` object; then use that helper in the three
places where `as any` is used (the assertions currently using
`(servers["gitlab"] as any).environment` and the similar occurrences around
lines 178-179 and 202-203) so tests assert `env.GITLAB_TOKEN` (and other env
keys) against a correctly typed environment rather than using `any`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 469399ce-ff20-4305-9eac-8752b3d75c9f

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1671f and a396557.

📒 Files selected for processing (2)
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/test/mcp/env-var-interpolation.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/src/mcp/index.ts

@anandgupta42
Copy link
Copy Markdown
Contributor

Thanks @VJ-yadav . Can you please fix the CI-issue?

VJ-yadav added 3 commits April 9, 2026 11:18
 AltimateAI#656)

${VAR}, ${VAR:-default}, and {env:VAR} patterns in MCP server environment
blocks were passed as literal strings to child processes, causing auth
failures for tools like gitlab-mcp-server.

Two gaps fixed:
- mcp/index.ts: add resolveEnvVars() safety net at launch site that
  resolves env-var patterns in mcp.environment before spawning
- discover.ts: use ConfigPaths.parseText() in readJsonSafe() so
  external MCP configs (Claude Code, Cursor, Copilot, Gemini) get
  interpolation before JSON parsing

14 new tests covering both ${VAR} and {env:VAR} syntax, defaults,
escapes, and discovery integration.
Addresses cubic-dev-ai review: tests were exercising a copy of the
function, not the production code. Now imports from src/mcp directly.
Addresses coderabbitai review: switched discovery integration tests to
use await using tmpdir() from fixture/fixture.ts for automatic cleanup,
matching repository test standards.
@VJ-yadav VJ-yadav force-pushed the fix/mcp-env-var-interpolation branch from 58b27a9 to b8ab0d9 Compare April 9, 2026 15:18
@VJ-yadav
Copy link
Copy Markdown
Contributor Author

VJ-yadav commented Apr 9, 2026

@anandgupta42 Rebased onto latest main (v0.5.20) and CI re-ran — same 7 failures, all pre-existing infra flakes unrelated to this PR:

  • worker.test.ts:67 — TUI worker type assertion
  • dbt-first-execution.test.ts — dispatcher call failures (4 tests)
  • tracing-adversarial-2.test.ts:588 — fetch error
  • oauth-callback.ts:188 — OAuth browser test
  • ripgrep.ts:238 — file listing

None of our changed files (mcp/index.ts, mcp/discover.ts, test/mcp/env-var-interpolation.test.ts) are involved. Same failures showing on PR #649 and other open PRs in the batch. 7020 tests pass, only these 7 flaky ones fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

${VAR} env-var interpolation in MCP server config still not working on v0.5.19

2 participants