anthropics / anthropics/claude-code

[BUG] Plugin HTTP MCP ${VAR} header expansion resolves to empty in 2.1.260 (works in 2.1.247 / 2.1.223) — bearer token never sent

Open
#93,595 0 comments 0 reactions 0 assignees View on GitHub
area:mcp area:plugins bug has repro platform:windows regression
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

✨ The following was generated via Claude, mistakes do happen.

### Preflight Checklist

- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code

### What's Wrong?

A plugin-defined HTTP MCP server whose `Authorization` header uses `${VAR}` substitution fails to connect on **2.1.260** with HTTP 401, because the header is sent with no token in it. The identical plugin, environment variable, shell and config connect successfully on **2.1.247** and **2.1.223**.

This bites desktop-app users hardest, because the Windows desktop app bundles its own CLI under `%APPDATA%\Claude\claude-code\\` — currently 2.1.260 — while a separate npm-global install on `PATH` may be on a working version. The result looks like a desktop-vs-terminal inconsistency, but it is not: **2.1.260 reproduces the failure when launched directly from a plain shell**, with the desktop app out of the picture entirely.

Plugin manifest under test (redacted plugin name; any plugin with the same shape will do):

```json
"mcpServers": {
"sonarqube": {
"type": "http",
"url": "https://api.sonarcloud.io/mcp",
"headers": {
"Authorization": "Bearer ${SONAR_TOKEN}",
"SONARQUBE_ORG": "example-org"
}
}
}
```

**Version bisect — one machine, one shell, one config, one env var:**

| CLI build | Origin | `claude mcp list` result |
| --- | --- | --- |
| 2.1.223 | npm-global on `PATH` | ✔ Connected |
| 2.1.247 | older desktop-bundled build, still on disk | ✔ Connected |
| **2.1.260** | current desktop-bundled build | ✘ Failed to connect — HTTP 401 |

All three were invoked from the same Bash session, in which `SONAR_TOKEN` is confirmed present. That isolates the variable to the CLI binary.

**Evidence the token is valid and the header arrives empty.** Three direct POSTs to the same endpoint from that same shell:

| `Authorization` value | HTTP |
| --- | --- |
| `Bearer $SONAR_TOKEN` (expanded by the shell) | **200** |
| `Bearer ${SONAR_TOKEN}` (literal, unexpanded) | 403 |
| `Bearer ` (empty) | **401** |

The 401 returned by 2.1.260 matches the *empty* case, not the unexpanded-literal case — so this is not the trailing-brace corruption described in #84367. Note the limitation: the server returns the same 401 whether the header is empty or absent, and `claude mcp get` redacts the value on both builds, so I cannot distinguish "expanded to empty string" from "header dropped" from the client side. Either way the token does not leave the machine.

### What Should Happen?

`${SONAR_TOKEN}` in a plugin manifest's `mcpServers.*.headers` should resolve from the CLI process environment on 2.1.260 exactly as it does on 2.1.247 and 2.1.223, and the server should connect.

### Error Messages/Logs

```
plugin:example-plugin:sonarqube: https://api.sonarcloud.io/mcp (HTTP) - ✘ Failed to connect —
Server rejected the configured Authorization header (HTTP 401). Check that the token is valid
for this MCP endpoint — OAuth fallback is disabled when headers.Authorization is set.
Error detail: Streamable HTTP error: Error POSTing to endpoint:
{"jsonrpc":"2.0","id":null,"error":{"code":-32000,"message":"SonarQube token required.
Provide via Authorization: Bearer header."}}
```

### Steps to Reproduce

1. Install a plugin whose manifest declares a `type: "http"` MCP server with `"Authorization": "Bearer ${SOME_TOKEN}"` in `headers`.
2. Set `SOME_TOKEN` as a user-scope environment variable to a value the endpoint accepts, and open a fresh shell so it is inherited.
3. Confirm the credential itself works from that shell, independently of Claude Code:
`curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer $SOME_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"diag","version":"1"}}}'`
→ 200
4. From that same shell, run `claude mcp list` using a 2.1.247 or 2.1.223 binary → server reports **Connected**.
5. From that same shell, run `claude mcp list` using the 2.1.260 binary (on Windows: `%APPDATA%\Claude\claude-code\2.1.260\claude.exe`) → server reports **Failed to connect (HTTP 401)**.

No config, plugin version, token or environment change between steps 4 and 5 — only the binary.

### Claude Model

Opus (claude-opus-5) — not model-dependent; `claude mcp list` reproduces it with no model involved.

### Is this a regression?

Yes

### Last Working Version

2.1.247

### Claude Code Version

2.1.260

### Platform

Claude Max subscription (desktop app + npm-global CLI)

### Operating System

Windows 11 Pro 10.0.26200

### Terminal/Shell

Claude desktop app (bundled CLI) and Git Bash / cmd

### Additional Information

- Regression is bracketed to **2.1.248 – 2.1.260**; 2.1.247 is the last build confirmed working here.
- The plugin manifest is byte-identical across every cached version of that plugin tested, and there is no competing `mcpServers` entry in `~/.claude.json`, so config drift is ruled out.
- No profile script (`.bashrc`, `.bash_profile`, `.profile`, PowerShell `$PROFILE`) defines the variable — it is inherited from the process environment in every case.
- Possibly related but distinct: #84367 (trailing brace corrupts the header → HTTP 400, on 2.1.222 which works here), #84314 (non-deterministic `${VAR}` expansion for MCP `env` on Linux), #90074 (Windows desktop sanitizes the stdio MCP environment — ruled out here, since 2.1.260 fails with the variable present in its own environment), #91307 (Gitea MCP auth differs Desktop vs Terminal on macOS — that one is framed as host-app state, whereas this reproduces from a plain shell).
- Practical impact: every plugin-distributed HTTP MCP server that authenticates via `${VAR}` headers is silently unusable for desktop-app users on 2.1.260, and the error text ("Check that the token is valid") points investigation at the credential rather than at the client.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the plugin manifest's mcpServers.*.headers configuration and reproduce with claude mcp list using the 2.1.247 and 2.1.260 binaries, keeping the shell, environment variable, and endpoint unchanged. Trace the HTTP MCP header expansion path; done means ${SOME_TOKEN} sends the token and the server reports Connected on 2.1.260, without regressing the working versions.

Written by the indexing model from the issue text.

Assessment

Domain
api, cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.