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

Resolved 💬 3 comments Opened May 19, 2026 by jannikfstone Closed May 23, 2026

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.

  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.
  1. 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)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗