headersHelper in plugin .mcp.json still ignores ${CLAUDE_PLUGIN_ROOT} (repro on 2.1.181/2.1.187; root cause + fingerprint)

Open 💬 0 comments Opened Jun 29, 2026 by agruene

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:

  • stdio branch: substitutes command, args, and injects CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA into the child env.
  • http / sse / ws branch: substitutes only url and headers. headersHelper is 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):

  1. Expand ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} in headersHelper, the same as command/args/url/headers, or
  2. Inject CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA into the env of the headersHelper exec (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, headersheadersHelper 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

View original on GitHub ↗