headersHelper in plugin .mcp.json still ignores ${CLAUDE_PLUGIN_ROOT} (repro on 2.1.181/2.1.187; root cause + fingerprint)
Summary
headersHelper in a plugin .mcp.json still does not expand ${CLAUDE_PLUGIN_ROOT}, and CLAUDE_PLUGIN_ROOT is not present in the helper's environment either. This was previously reported in #47789 (closed as stale/not planned) and #68229 / #69083 (both closed as duplicates funnelling back to #47789). The bot on #47789 invited a new issue if still relevant — it is still relevant and still reproduces on v2.1.181 and v2.1.187. This issue adds (1) current-version confirmation, (2) the root cause in the bundled binary, and (3) a reliable failure fingerprint.
Root cause
The plugin-root substitution applier branches per transport type:
stdiobranch: substitutescommand,args, and injectsCLAUDE_PLUGIN_ROOT/CLAUDE_PLUGIN_DATAinto the childenv.http/sse/wsbranch: substitutes onlyurlandheaders.headersHelperis never passed through the substitution function.
The literal string ${CLAUDE_PLUGIN_ROOT}/scripts/...sh is therefore handed verbatim to the helper exec. That exec runs the command with {shell:true} and an env of {...process.env, CLAUDE_CODE_MCP_SERVER_NAME, CLAUDE_CODE_MCP_SERVER_URL} — CLAUDE_PLUGIN_ROOT is not added there, and it is not in the parent claude process env either (it is only injected for stdio MCP children). The shell expands the unset variable to empty → tries to run /scripts/...sh → exit 127 → headersHelper ... did not return a valid value → fallback to SDK OAuth + dynamic client registration.
For a server whose auth server rejects DCR, this surfaces as the confusing terminal error:
Incompatible auth server: does not support dynamic client registration.
…which gives no hint that the real problem is an unexpanded path.
Failure fingerprint (useful for triage)
A successful headersHelper run mints a token over the network and takes ~330ms. In the failing case the debug log shows Executing headersHelper immediately followed — within single-digit milliseconds (~9ms) — by did not return a valid value. That gap is too short for the script to have run at all; it is the fingerprint of "script never found" rather than "script failed".
Debug log location:~/Library/Caches/claude-cli-nodejs/<proj-slug>/mcp-logs-<plugin-server-slug>/<ts>.jsonl
Steps to reproduce
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://example.com/api/2.0/mcp/sql",
"headersHelper": "${CLAUDE_PLUGIN_ROOT}/scripts/get-token.sh"
}
}
}
Install via marketplace, start Claude Code → server fails to connect.
Minimal repro of the mechanism (no plugin needed):
# Reproduces the failure: unset var expands to empty, exit 127
( unset CLAUDE_PLUGIN_ROOT; bash -c '${CLAUDE_PLUGIN_ROOT}/scripts/get-token.sh' )
# Proves the script itself is fine when the var is set
CLAUDE_PLUGIN_ROOT=<plugin-dir> bash -c '${CLAUDE_PLUGIN_ROOT}/scripts/get-token.sh'
Expected behavior
Either (matching what already works for stdio):
- Expand
${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_DATA}inheadersHelper, the same ascommand/args/url/headers, or - Inject
CLAUDE_PLUGIN_ROOT/CLAUDE_PLUGIN_DATAinto the env of theheadersHelperexec (same as is done for stdio MCP children and hook processes).
A secondary improvement: when headersHelper returns no value, fall back with a clearer diagnostic instead of an "Incompatible auth server / DCR" error that points away from the real cause.
Workaround
Export CLAUDE_PLUGIN_ROOT in the shell before launching claude so it lands in process.env and the shell can expand it. Resolving the path dynamically from ~/.claude/plugins/installed_plugins.json keeps it working across plugin version bumps. Editing the plugin's headersHelper to an absolute path also works but breaks on update.
Docs inconsistency (unchanged from prior reports)
The plugins reference says the variables are "substituted inline anywhere they appear in … MCP or LSP server configs", but the MCP env-var expansion docs list only command, args, env, url, headers — headersHelper is missing. The two contradict each other.
Environment
- Claude Code: confirmed on v2.1.181 (Homebrew CLI) and v2.1.187 (Claude Desktop bundled)
- Platform: macOS (darwin 25.5.0)
- Plugin installed via marketplace, HTTP transport, OAuth
headersHelper
Related (all closed)
- #47789 — original report, closed as stale / not planned (bot invited a new issue)
- #68229 — closed as duplicate of #47789
- #69083 — closed as duplicate of #68229