Skip to content

fix: [#701] surface unresolved MCP env-var warnings to user via toast#806

Open
VJ-yadav wants to merge 1 commit intoAltimateAI:mainfrom
VJ-yadav:fix/surface-unresolved-mcp-env-vars
Open

fix: [#701] surface unresolved MCP env-var warnings to user via toast#806
VJ-yadav wants to merge 1 commit intoAltimateAI:mainfrom
VJ-yadav:fix/surface-unresolved-mcp-env-vars

Conversation

@VJ-yadav
Copy link
Copy Markdown
Contributor

@VJ-yadav VJ-yadav commented May 10, 2026

Summary

Closes #701.

An unresolved ${VAR} in an MCP server's env or headers block silently became \"\", the server then launched with empty credentials, and the user saw a confusing downstream error. The warning was logged via log.warn but most users never check the log.

This wires the existing log warning into the existing TUI toast mechanism so the user sees a single warning toast at startup naming each affected server, the source file, and the missing env var(s) — and pointing them at \${VAR:-default} for explicit fallbacks.

What changed

  • mcp/discover.ts — added a per-discovery accumulator. resolveServerEnvVars now records unresolved vars (deduped per block) into module state, in addition to the existing log.warn. New exports: UnresolvedEnvVarRecord type and consumeUnresolvedEnvVars() (one-shot drain). The accumulator is reset at the start of each discoverExternalMcp() call so a re-discovery does not show stale warnings.
  • mcp/index.ts — the state initializer now drains consumeUnresolvedEnvVars() alongside consumeDiscoveryResult(). If non-empty, publishes a TuiEvent.ToastShow with variant: \"warning\", grouped by server, and tracks an mcp_unresolved_env_vars telemetry event. Reuses the same Bus + Toast plumbing the discovery toast already uses (PR fix: resolve env-var references in MCP server environment config (closes #656) #666 / existing OAuth flow), so no new UI components.
  • altimate/telemetry/index.ts — added the mcp_unresolved_env_vars event type.
  • test/mcp/discover-unresolved-env.test.ts — 11 new unit tests covering capture, dedupe, multi-var, default-suffix passthrough, escape (\$\${VAR}) passthrough, set-var passthrough, env+headers separation, one-shot drain, and reset-on-new-discovery.

Scope

Limited to external MCP configs (.vscode/mcp.json, .cursor/mcp.json, .github/copilot/mcp.json, .mcp.json, ~/.claude.json, ~/.gemini/settings.json) where each server has direct attribution.

Unresolved vars in opencode.json's mcp.environment go through ConfigPaths.substitute() at file-level and are already tracked in config_env_interpolation telemetry. Surfacing those to a toast needs file→server attribution and is left as a follow-up — happy to do it in a separate PR if reviewers want both paths covered here.

Test plan

  • bun test test/mcp/discover-unresolved-env.test.ts — 11/11 pass
  • bun turbo typecheck — clean across all 5 packages
  • bun test test/mcp/ — same 29 pre-existing failures as main (all caused by the suite reading the user's real ~/.pencil/, ~/.gemini/, ~/.claude.json — unrelated to this change). My branch adds 11 passing tests on top.
  • Pre-push hook (typecheck) passes
  • Branch is fast-forward-mergeable on top of fresh main (c859b57ec)

Manual verification (toast appearance)

The new toast reuses the existing TuiEvent.ToastShow variant: \"warning\" path already used by the MCP OAuth needs-auth toast at mcp/index.ts:475 and the OAuth client-registration failure toast at mcp/index.ts:486. No new UI; same renderer, same styling. To see it in a live TUI, drop a .mcp.json like:

{
  \"mcpServers\": {
    \"snowflake\": {
      \"command\": \"snowflake-mcp\",
      \"env\": { \"SNOWFLAKE_PASSWORD\": \"\${SNOWFLAKE_PASSWORD}\" }
    }
  }
}

…with SNOWFLAKE_PASSWORD unset, and start a session. The toast appears once at MCP startup with title "MCP env vars unresolved" and a 12s duration.


Summary by cubic

Show a warning toast at MCP startup when unresolved ${VAR} references are found in server env or headers, so users can fix missing vars before servers fail. Addresses #701 by surfacing clear, grouped warnings and tracking telemetry.

  • Bug Fixes
    • Added per-discovery accumulator in mcp/discover.ts; new consumeUnresolvedEnvVars() drains it and resets each discovery.
    • mcp/index.ts shows a single warning TuiEvent.ToastShow, grouped by server with source path and var names; suggests ${VAR:-default}.
    • Added mcp_unresolved_env_vars telemetry in altimate/telemetry.
    • 11 unit tests cover capture, dedupe, multi-var, defaults/escape, env vs headers, one-shot drain, and reset.
    • Scope: external MCP configs only (.vscode/mcp.json, .cursor/mcp.json, .github/copilot/mcp.json, .mcp.json, ~/.claude.json, ~/.gemini/settings.json).

Written for commit 1a40b4f. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added warning notifications when MCP server configurations contain unresolved environment variables, displaying affected servers and missing variable names.
    • Implemented telemetry tracking for unresolved environment variable events to measure feature effectiveness.
  • Tests

    • Added comprehensive test coverage for environment variable detection and warning behavior.

Review Change Stack

… via toast

An unresolved `${VAR}` in an MCP server's env/headers block silently
became `""`, causing the server to launch with empty credentials and
fail downstream with a confusing error. The warning was logged but
most users never see the log.

This adds a per-server accumulator in `mcp/discover.ts` that captures
unresolved env-var references during discovery, plus a one-shot
`consumeUnresolvedEnvVars()` API consumed by `MCP.state` initializer,
which publishes a `TuiEvent.ToastShow` warning naming the affected
servers and missing variables and pointing users at `${VAR:-default}`
for explicit fallbacks. Adds an `mcp_unresolved_env_vars` telemetry
event to track recovery rate.

Scope: external MCP configs only (`.vscode/mcp.json`, `.cursor/mcp.json`,
`.mcp.json`, `~/.claude.json`, etc.), where each server has direct
attribution. Unresolved vars in `opencode.json`'s `mcp.environment`
go through `ConfigPaths.substitute()` at file level and are tracked
in existing telemetry; surfacing those to a toast needs file→server
attribution and is left as follow-up.
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

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 May 10, 2026

📝 Walkthrough

Walkthrough

This PR implements user-facing warnings for unresolved MCP environment-variable references. The discovery module now accumulates unresolved ${VAR} references from MCP configs, MCP initialization consumes and displays them via warning toast and telemetry, and a comprehensive test suite validates capture, deduplication, and accumulator behavior across discovery runs.

Changes

MCP Unresolved Environment Variables Warning

Layer / File(s) Summary
Event & Record Types
packages/opencode/src/mcp/discover.ts, packages/opencode/src/altimate/telemetry/index.ts
Introduces UnresolvedEnvVarRecord interface with server, source, field (env or headers), and vars array. Adds mcp_unresolved_env_vars telemetry event with timestamp, session_id, server_count, var_count, and servers array.
Discovery Accumulation
packages/opencode/src/mcp/discover.ts
resolveServerEnvVars now deduplicates unresolved ${VAR} references and accumulates records to internal state. discoverExternalMcp() resets accumulator at start of each run. New exported consumeUnresolvedEnvVars() returns records and clears state for one-shot consumption.
Warning & Telemetry Integration
packages/opencode/src/mcp/index.ts
MCP initialization consumes accumulated unresolved vars, groups by server, publishes a warning toast listing affected servers/sources/variables, and emits mcp_unresolved_env_vars telemetry with counts.
Test Coverage
packages/opencode/test/mcp/discover-unresolved-env.test.ts
Validates capture of unresolved ${VAR} from env and headers, deduplication, non-capture of ${VAR:-default} and $${VAR}, accumulator reset across runs, one-shot consumption, and correct source labeling for project-scoped configs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Whiskers twitching with delight,
Unresolved vars now see the light!
No more silent failures in the night—
Toast and telemetry shine so bright!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is comprehensive with clear sections for Summary, What Changed, Scope, and Test Plan. However, the required template header 'PINEAPPLE' for AI-generated content is missing, and the Checklist section is not properly filled out. Add 'PINEAPPLE' at the very top of the description (required for AI-generated content per template), and complete the Checklist section by marking which items were done.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.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 PR title clearly and concisely summarizes the main change: surfacing unresolved MCP environment variable warnings to users via a toast notification for issue #701.
Linked Issues check ✅ Passed The PR fully addresses issue #701's objectives: it captures unresolved ${VAR} references during MCP discovery, surfaces warnings via a user-facing toast at startup (not just logs), and tracks telemetry. All coding requirements are met across the four modified/new files.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #701: adding telemetry event, accumulating unresolved env vars, emitting warning toast, and comprehensive unit tests. The PR explicitly limits scope to external MCP configs and defers opencode.json changes for follow-up.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

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/src/mcp/index.ts (1)

190-190: ⚡ Quick win

Consider simplifying the type annotation.

The type Awaited<ReturnType<typeof import("./discover").consumeUnresolvedEnvVars>> is verbose. Since UnresolvedEnvVarRecord is already exported from ./discover, you could add a top-level import and use it directly:

import type { UnresolvedEnvVarRecord } from "./discover"

Then at line 190:

-      let unresolvedEnvVars: Awaited<ReturnType<typeof import("./discover").consumeUnresolvedEnvVars>> = []
+      let unresolvedEnvVars: UnresolvedEnvVarRecord[] = []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/mcp/index.ts` at line 190, Replace the verbose type
annotation on the unresolvedEnvVars variable by importing the exported type
UnresolvedEnvVarRecord from "./discover" and using it directly; add a top-level
line import type { UnresolvedEnvVarRecord } from "./discover" and change the
declaration for unresolvedEnvVars (the variable assigned from
consumeUnresolvedEnvVars) to use UnresolvedEnvVarRecord[] instead of
Awaited<ReturnType<typeof import("./discover").consumeUnresolvedEnvVars>> so the
code reads simply with the existing consumeUnresolvedEnvVars usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/test/mcp/discover-unresolved-env.test.ts`:
- Around line 10-28: Replace manual temp dir lifecycle by removing the top-level
tempDir variable and the beforeEach/afterEach hooks; instead in each test use
the tmpdir fixture with "await using cleanTmp = await tmpdir()" to create a
temporary directory that auto-cleans, and call consumeUnresolvedEnvVars() and
explicit deletes of UNRESOLVED_TEST_VAR_A/B/TOKEN inside each test setup as
needed; also update the test that currently creates a second temp dir (lines
~229–258) to use a nested "await using" (e.g., "await using secondTmp = await
tmpdir()") rather than mkdtemp/rm so cleanup is automatic.

---

Nitpick comments:
In `@packages/opencode/src/mcp/index.ts`:
- Line 190: Replace the verbose type annotation on the unresolvedEnvVars
variable by importing the exported type UnresolvedEnvVarRecord from "./discover"
and using it directly; add a top-level line import type { UnresolvedEnvVarRecord
} from "./discover" and change the declaration for unresolvedEnvVars (the
variable assigned from consumeUnresolvedEnvVars) to use UnresolvedEnvVarRecord[]
instead of Awaited<ReturnType<typeof
import("./discover").consumeUnresolvedEnvVars>> so the code reads simply with
the existing consumeUnresolvedEnvVars usage.
🪄 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: 5c3ca9cb-5d2b-4c9c-ba41-ec9e1075a00b

📥 Commits

Reviewing files that changed from the base of the PR and between c859b57 and 1a40b4f.

📒 Files selected for processing (4)
  • packages/opencode/src/altimate/telemetry/index.ts
  • packages/opencode/src/mcp/discover.ts
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/test/mcp/discover-unresolved-env.test.ts

Comment on lines +10 to +28
let tempDir: string

beforeEach(async () => {
tempDir = await mkdtemp(path.join(tmpdir(), "mcp-unresolved-env-"))
// Drain anything left over from prior tests so we start clean.
consumeUnresolvedEnvVars()
delete process.env["UNRESOLVED_TEST_VAR_A"]
delete process.env["UNRESOLVED_TEST_VAR_B"]
delete process.env["UNRESOLVED_TEST_TOKEN"]
})

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true })
// Empty the accumulator so a failed test doesn't poison the next one.
consumeUnresolvedEnvVars()
delete process.env["UNRESOLVED_TEST_VAR_A"]
delete process.env["UNRESOLVED_TEST_VAR_B"]
delete process.env["UNRESOLVED_TEST_TOKEN"]
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace manual temp directory management with the tmpdir fixture.

The coding guidelines require using the tmpdir function from fixture/fixture.ts with await using syntax for automatic cleanup. As per coding guidelines, this ensures proper cleanup even when tests fail and follows the project's established testing patterns.

♻️ Refactor to use tmpdir fixture

Remove the global tempDir variable and beforeEach/afterEach hooks. Instead, use await using in each test:

 import { describe, test, expect, beforeEach, afterEach } from "bun:test"
-import { mkdtemp, rm, mkdir, writeFile } from "fs/promises"
-import { tmpdir } from "os"
+import { mkdir, writeFile } from "fs/promises"
 import path from "path"
+import { tmpdir } from "../fixture/fixture"
 import { consumeUnresolvedEnvVars, discoverExternalMcp } from "../../src/mcp/discover"

-let tempDir: string
-
 beforeEach(async () => {
-  tempDir = await mkdtemp(path.join(tmpdir(), "mcp-unresolved-env-"))
   // Drain anything left over from prior tests so we start clean.
   consumeUnresolvedEnvVars()
   delete process.env["UNRESOLVED_TEST_VAR_A"]
   delete process.env["UNRESOLVED_TEST_VAR_B"]
   delete process.env["UNRESOLVED_TEST_TOKEN"]
 })

 afterEach(async () => {
-  await rm(tempDir, { recursive: true, force: true })
   // Empty the accumulator so a failed test doesn't poison the next one.
   consumeUnresolvedEnvVars()
   delete process.env["UNRESOLVED_TEST_VAR_A"]
   delete process.env["UNRESOLVED_TEST_VAR_B"]
   delete process.env["UNRESOLVED_TEST_VAR_TOKEN"]
 })

 describe("consumeUnresolvedEnvVars (issue `#701`)", () => {
   test("captures unresolved ${VAR} in a local server's env block", async () => {
+    await using tmp = await tmpdir()
-    await mkdir(path.join(tempDir, ".vscode"), { recursive: true })
+    await mkdir(path.join(tmp.path, ".vscode"), { recursive: true })
     await writeFile(
-      path.join(tempDir, ".vscode/mcp.json"),
+      path.join(tmp.path, ".vscode/mcp.json"),
       // ... rest of test
     )
-    await discoverExternalMcp(tempDir)
+    await discoverExternalMcp(tmp.path)
     // ...
   })
   // Apply the same pattern to all other tests
 })

Apply this pattern to all 11 tests. The test at lines 229-258 that creates a second temp directory should use a nested await using cleanTmp = await tmpdir() instead of manual mkdtemp/rm.

As per coding guidelines: "Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup in test files. 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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/mcp/discover-unresolved-env.test.ts` around lines 10 -
28, Replace manual temp dir lifecycle by removing the top-level tempDir variable
and the beforeEach/afterEach hooks; instead in each test use the tmpdir fixture
with "await using cleanTmp = await tmpdir()" to create a temporary directory
that auto-cleans, and call consumeUnresolvedEnvVars() and explicit deletes of
UNRESOLVED_TEST_VAR_A/B/TOKEN inside each test setup as needed; also update the
test that currently creates a second temp dir (lines ~229–258) to use a nested
"await using" (e.g., "await using secondTmp = await tmpdir()") rather than
mkdtemp/rm so cleanup is automatic.

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.

No issues found across 4 files

@dev-punia-altimate
Copy link
Copy Markdown

❌ Tests — Failures Detected

TypeScript — 15 failure(s)

  • baseline [0.55ms]
  • baseline [0.37ms]
  • baseline [0.13ms]
  • baseline [0.29ms]
  • connection_refused [0.26ms]
  • timeout [0.06ms]
  • permission_denied [0.04ms]
  • parse_error [0.04ms]
  • oom [0.04ms]
  • network_error [0.04ms]
  • auth_failure [0.03ms]
  • rate_limit [0.04ms]
  • internal_error [0.03ms]
  • empty_error [0.03ms]
  • connection_refused [0.04ms]

Next Step

Please address the failing cases above and re-run verification.

cc @VJ-yadav

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.

fix: surface unresolved MCP env-var warnings to user, not just log

2 participants