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 for MCP server environments so placeholders resolve before spawn, preventing tokens and other sensitive values from being passed as literals. Also interpolates 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; add 14 tests covering syntax, defaults, escapes, and discovery.
  • Refactors

    • Tests now use shared tmpdir fixture for automatic cleanup.

Written for commit 1070d8e. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • MCP configuration files and local MCP environment values now support environment-variable interpolation using ${VAR}, ${VAR:-default}, and {env:VAR} patterns; escaped $$ sequences are preserved and unset variables fall back to defaults or empty strings. External MCP JSON configs are also processed for interpolation before parsing.
  • 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: 665729cb-1b62-42b4-b585-104d49885ba1

📥 Commits

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

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

📝 Walkthrough

Walkthrough

Attempts env-var interpolation for external MCP JSON by calling ConfigPaths.parseText(...) during safe read; falls back to JSONC parsing on failure. Exposes and applies resolveEnvVars() to expand ${VAR}, ${VAR:-default}, and {env:VAR} in runtime mcp.environment. Adds tests for unit and integration flows.

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 before falling back to existing JSONC parsing on error.
Runtime env resolution
packages/opencode/src/mcp/index.ts
Added exported resolveEnvVars(environment: Record<string,string>) and apply it when constructing local MCP (StdioClientTransport) so mcp.environment entries are expanded (${VAR}, ${VAR:-default}, {env:VAR}), preserving escaped $$ sequences.
Tests
packages/opencode/test/mcp/env-var-interpolation.test.ts
New Bun tests covering resolveEnvVars unit cases and integration tests for discoverExternalMcp(...) using temp .vscode/mcp.json / .cursor/mcp.json, verifying resolved environment values, fallbacks, and cleanup.

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 result
        Discover-->>Index: return MCP definition
        Index->>Index: resolveEnvVars(environment)
        Index-->>Transport: pass resolved environment
    else parseText fails
        ConfigPaths-->>Discover: throw/error
        Discover->>Discover: fallback parseJsonc(text)
        Discover-->>Index: return parsed MCP definition (uninterpolated)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I nibble tokens in the night,

${VAR} becomes a beam of light,
Defaults hop in when needed, neat,
Escaped dollars keep their seat —
Hooray, configs hum complete!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description lacks the required 'PINEAPPLE' marker at the top as specified in the template for AI-generated contributions, and omits a formal 'Checklist' section with unchecked boxes as shown in the template. Add 'PINEAPPLE' marker at the very top of the description, and reorganize the checklist section to follow the template format with checkboxes format (- [ ]).
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 (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: resolving environment variable references in MCP server configuration, directly addressing issue #656.
Linked Issues check ✅ Passed All primary coding objectives from issue #656 are met: env-var placeholders are resolved before spawning child processes, both ${VAR} and {env:VAR} syntaxes are supported with defaults, external MCP configs are interpolated, and backward compatibility is maintained.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue #656: resolveEnvVars() addition, discover.ts modification for external config interpolation, and comprehensive test coverage are all in-scope for fixing env-var interpolation.

✏️ 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.

 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.
@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.

Addresses cubic-dev-ai review: tests were exercising a copy of the
function, not the production code. Now imports from src/mcp directly.
@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

Addresses coderabbitai review: switched discovery integration tests to
use await using tmpdir() from fixture/fixture.ts for automatic cleanup,
matching repository test standards.
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

1 participant