Skip to content

Build1 publisher2 min readPublished

Claude Code's .mcp.json expansion passes bare $VAR and unset ${VAR} to servers as raw text

Claude Code 2.1.278 left a bare $VAR unexpanded in all five .mcp.json fields a direct test probed, and sent an unset ${VAR} to the server as literal text. A shared config can hand a server a placeholder where a token belongs.

The Engineer · Build desk

Illustration accompanying Claude Code's .mcp.json expansion passes bare $VAR and unset ${VAR} to servers as raw text

What happened

  • Claude Code's MCP docs, fetched on 2026-09-22, list two expansion forms, ${VAR} and ${VAR:-default}, applied across command, args, env, url and headers.
  • Both documented forms expanded as described in every field the test covered, on stdio server entries and on an HTTP server entry.
  • A nested default of the form ${A:-${B}} expanded only in headers, one of the two fields tested on the HTTP server entry.
  • For an unset variable with no default, the docs say the config still loads and Claude Code shows a missing-variable warning in claude mcp list output.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Any teammate whose shell lacks a referenced variable launches the shared server holding placeholder text where a credential or config value belongs.
  • constraint A fallback chain such as ${A:-${B}} is unsafe to copy between fields on 2.1.278, since it resolves in headers and stays unresolved in the other four.
  • decision Teams sharing one .mcp.json have a convention to settle: braces on every reference, an explicit :-default where a fallback beats a placeholder, and a per-machine check of claude mcp list.

The author of the dev.to post said that instead of guessing, they wrote a dependency-free stdio MCP server that reports its own process.argv and process.env, wired it into a .mcp.json several times over, and let Claude Code launch it [14]. The server answers initialize, ping, tools/list and tools/call over newline-delimited JSON-RPC and exposes one tool, echo_env [10]. At startup it appends a snapshot to spawn-log.jsonl, so a record of what the process received exists even when no model turn happens [11]. The run used a fresh mktemp -d directory, with no project instructions or memory files in play [12].

The config registers that one script seven times [13]. One entry carries all six forms in args and again as env keys. The other six differ only in command, because command is a single string and holds one form at a time [13]. Writing seven registrations is tedious, and it is the right call: each value is read back from the process itself, and the spawn log does not wait for a model to decide to call echo_env [11].

The docs list only the braced forms and do not mention $VAR without braces, or nesting [9]. In the test, an env key set to "$PROBE_SET" reached the server as the text $PROBE_SET [18][17]. The same happened in every other field, on the stdio entries and the HTTP one [4][1]. The server still launches, holding a variable's name where its value should be [17].

The unset case matched the docs [5][8]. The placeholder is a non-empty string. A server that only checks whether its token variable exists will find one, so the failure moves to whichever request first uses the token. I would not expect a rejected API call inside the server to send anyone to claude mcp list looking for that warning.

The author attributes the headers-only nesting result to headers being expanded twice [6]. On that account, the first pass would apply the outer default and leave ${PROBE_SET} in the string, and the second pass would resolve it [6].

Every result here comes from Claude Code 2.1.278 [2]. For these results to hold on another install, that install needs the same expander. The two braced forms, the five fields and the unset behavior are written into the docs [7][8]. The bare and nested results are observations of one release, and a later release could change either one without contradicting the page [9].

What to watch

  • An update to the Claude Code MCP docs that covers bare $VAR or nested defaults, turning observed 2.1.278 behavior into stated behavior.
  • A Claude Code release after 2.1.278 that expands nested defaults in all five fields, or in none, ending the headers-only split.
  • Whether Claude Code starts surfacing the missing-variable warning at server launch as well as in claude mcp list.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories