claude-code - 💡(How to fix) Fix Stdio MCP server silently dropped when `${VAR}` substitution fails — docs promise "fail to parse" but REPL silently reports "No MCP servers configured"; `--mcp-config` spawns with unsubstituted value causing downstream auth failure [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#60513Fetched 2026-05-20 03:56:39
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
labeled ×4commented ×1

Error Message

Actual behavior in the REPL: the server is silently dropped. /mcp reports "No MCP servers configured", no log file is created, no error or warning is shown.

Expected per docs: parse error OR substitution from settings.local.json env

  • Or: hard-fail with a clear error if substitution can't resolve, in all launch paths — matching the documented "fail to parse" behavior.

Root Cause

  1. Inconsistent behavior between launch paths for the same root cause:
    • Plain claude REPL → silent drop (per #1)
    • claude --mcp-config /path/.mcp.json → server IS spawned but ${VAR} was not substituted, so the empty/literal value reaches the server. In our case (slack-mcp-server) this surfaced as invalid_auth only at the downstream service — extremely confusing root-cause-wise.
    • claude mcp list / claude -p → works correctly if the env var happens to be in the launching shell.

Code Example

mkdir -p /tmp/repro/.claude && cd /tmp/repro

cat > .mcp.json <<'EOF'
{
  "mcpServers": {
    "echo": {
      "command": "node",
      "args": ["-e", "process.stderr.write('TOKEN=' + (process.env.API_TOKEN || '<empty>') + '\\n'); process.exit(1)"],
      "env": { "API_TOKEN": "${TEST_API_TOKEN}" }
    }
  }
}
EOF

cat > .claude/settings.local.json <<'EOF'
{ "env": { "TEST_API_TOKEN": "would-be-secret" }, "enabledMcpjsonServers": ["echo"] }
EOF

unset TEST_API_TOKEN    # must NOT be in shell env

# Case A — auto-discovery
claude
# Observed in /mcp: "No MCP servers configured"
# Expected per docs: parse error OR substitution from settings.local.json env

# Case B — explicit --mcp-config
claude --mcp-config $PWD/.mcp.json
# Observed in /mcp: server attempts to spawn, fails
# Inspecting ~/Library/Caches/claude-cli-nodejs/.../mcp-logs-echo/:
#   server stderr shows TOKEN=<empty> — substitution returned empty

# Case C — token in shell env
export TEST_API_TOKEN=would-be-secret
claude    # /mcp now shows the server
RAW_BUFFERClick to expand / collapse

Three related defects affecting stdio MCP servers configured in .mcp.json with ${VAR} substitution:

  1. Silent skip contradicts documented "fail to parse". The MCP docs state:

    "If a required environment variable is not set and has no default value, Claude Code will fail to parse the config."

    Actual behavior in the REPL: the server is silently dropped. /mcp reports "No MCP servers configured", no log file is created, no error or warning is shown.

  2. Inconsistent behavior between launch paths for the same root cause:

    • Plain claude REPL → silent drop (per #1)
    • claude --mcp-config /path/.mcp.json → server IS spawned but ${VAR} was not substituted, so the empty/literal value reaches the server. In our case (slack-mcp-server) this surfaced as invalid_auth only at the downstream service — extremely confusing root-cause-wise.
    • claude mcp list / claude -p → works correctly if the env var happens to be in the launching shell.
  3. settings.local.json's env block is not a source for ${VAR} substitution in .mcp.json. The settings docs describe the env block as "Environment variables that will be applied to every session". A natural reading suggests these would be in scope for in-session ${VAR} expansion. They are not — and there is no documentation pointing to this gap. This is what led to a multi-hour debug for us: the token was in settings.local.json, every documented check passed, but the MCP server never saw it.

Reproducer

mkdir -p /tmp/repro/.claude && cd /tmp/repro

cat > .mcp.json <<'EOF'
{
  "mcpServers": {
    "echo": {
      "command": "node",
      "args": ["-e", "process.stderr.write('TOKEN=' + (process.env.API_TOKEN || '<empty>') + '\\n'); process.exit(1)"],
      "env": { "API_TOKEN": "${TEST_API_TOKEN}" }
    }
  }
}
EOF

cat > .claude/settings.local.json <<'EOF'
{ "env": { "TEST_API_TOKEN": "would-be-secret" }, "enabledMcpjsonServers": ["echo"] }
EOF

unset TEST_API_TOKEN    # must NOT be in shell env

# Case A — auto-discovery
claude
# Observed in /mcp: "No MCP servers configured"
# Expected per docs: parse error OR substitution from settings.local.json env

# Case B — explicit --mcp-config
claude --mcp-config $PWD/.mcp.json
# Observed in /mcp: server attempts to spawn, fails
# Inspecting ~/Library/Caches/claude-cli-nodejs/.../mcp-logs-echo/:
#   server stderr shows TOKEN=<empty> — substitution returned empty

# Case C — token in shell env
export TEST_API_TOKEN=would-be-secret
claude    # /mcp now shows the server

Expected behavior

Choose one (or some combination):

  • Honor the docs and settings.local.json's env block as a substitution source (most user-friendly).
  • Or: hard-fail with a clear error if substitution can't resolve, in all launch paths — matching the documented "fail to parse" behavior.
  • Document the limitation explicitly in both the MCP docs and the settings docs.
  • At minimum, eliminate the divergence between REPL / --mcp-config / mcp list for the same configuration.

Environment

  • Claude Code v2.1.138 (Homebrew cask)
  • macOS 15 / Darwin 25.4.0 / arm64
  • Plain zsh, no wrapper/alias
  • Server tested: slack-mcp-server@latest (stdio)

Cost of debugging

Reporting this took ~3.5 hours and ~15.3M tokens (≈$76 Opus 4.7 API-equivalent) across multiple Claude Code sessions. Documented placement of the secret in settings.local.json (per the "applied to every session" wording) is exactly the path a security-conscious user would take, so this defect has high cost-per-encounter.

Related issues (distinct from this one)

  • #2065 — original env-var question (closed)
  • #4276, #46889 — feature requests for substitution in settings.json
  • #6204, #51581 — HTTP transport headers (different code path)
  • #24657, #16402 — enabledMcpjsonServers in settings.local.json (different setting)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Choose one (or some combination):

  • Honor the docs and settings.local.json's env block as a substitution source (most user-friendly).
  • Or: hard-fail with a clear error if substitution can't resolve, in all launch paths — matching the documented "fail to parse" behavior.
  • Document the limitation explicitly in both the MCP docs and the settings docs.
  • At minimum, eliminate the divergence between REPL / --mcp-config / mcp list for the same configuration.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Stdio MCP server silently dropped when `${VAR}` substitution fails — docs promise "fail to parse" but REPL silently reports "No MCP servers configured"; `--mcp-config` spawns with unsubstituted value causing downstream auth failure [1 comments, 2 participants]