Disclosure: I work on Pink Agentic AI Payments at PinkWallet; it's used as the example in the last section.
Short answer: every client below can send a custom header (or a key baked into the URL) to a remote Streamable HTTP MCP server — except Claude Desktop's built-in "custom connectors," which only walks you through OAuth and has no field for a static token. If your server auths with a plain API key, either issue OAuth in front of it, or bridge it through mcp-remote for Desktop; every other client here (Claude Code, Cursor, VS Code, the OpenAI Agents SDK, LangChain) takes a headers object or CLI flag directly.
This is simple in principle — "just send an Authorization header" — and annoying in practice: every client names the field differently, some don't support headers in their GUI at all, and the client whose docs you'd expect to be definitive (Claude Desktop) doesn't support this pattern the way its config file's shape implies it should. Below is the config for each client, the exact doc quote it comes from (fetched today), and the gotcha that costs people an hour.
The comparison
| Client | Bearer header? | Key-in-URL? | Config location | Gotcha | Source |
|---|---|---|---|---|---|
| Claude Code | Yes, native | Yes, if your server accepts it |
claude mcp add --transport http ... --header (CLI) |
None observed — this is the most direct path of the six | code.claude.com/docs/en/mcp |
| Claude Desktop (custom connectors) | No documented field | Untested / not officially supported | Settings → Connectors UI | Connector setup is OAuth-oriented (sign-in flow, or an advanced "OAuth Client ID/Secret" field) — there's no place to paste a static Authorization header |
support.claude.com/…/11175166 |
| Claude Desktop (via mcp-remote) | Yes, via bridge | Yes, via bridge |
claude_desktop_config.json, command: npx mcp-remote
|
Windows/Cursor/Codex-CLI have an arg-escaping bug with spaces in headers — split the header into an env var | github.com/geelen/mcp-remote |
| Cursor | Yes, native | Yes, if your server accepts it |
mcp.json, headers object |
Header values support ${env:VAR} interpolation — use it, don't hardcode the key |
cursor.com/docs/context/mcp |
| VS Code (Copilot agent mode) | Yes, native | Yes, if your server accepts it |
mcp.json, type: "http" + headers
|
Use the inputs block (promptString, password: true) so the key doesn't sit in plaintext in the checked-in config |
code.visualstudio.com/…/mcp-configuration |
| OpenAI Agents SDK (Python) | Yes, native | Yes, if your server accepts it | MCPServerStreamableHttp(params={...}) |
params["url"] and params["headers"] are just a dict — nothing client-specific to trip over |
openai.github.io/openai-agents-python/mcp |
LangChain (langchain-mcp-adapters) |
Yes, native | Yes, if your server accepts it |
MultiServerMCPClient({...}), "transport": "http" + headers
|
Only sse and http transports carry runtime headers — stdio entries in the same client dict silently ignore a headers key |
github.com/langchain-ai/langchain-mcp-adapters |
All quotes below were re-fetched today (2026-09-30) directly from each vendor's current docs; the exact text is in devto-12-SOURCES.md.
Claude Code
claude mcp add --transport http my-server https://example-mcp.dev/mcp \
--header "Authorization: Bearer YOUR_KEY"
From the docs:
claude mcp add --transport http secure-api https://api.example.com/mcp --header "Authorization: Bearer your-token"
--header (short form -H) can be repeated for multiple headers. There's no separate "connector" concept here — a remote HTTP MCP server with a header is a first-class, one-command thing, and the one with the least friction of the six.
Claude Desktop / claude.ai (custom connectors)
This is the one that trips people up, because claude_desktop_config.json looks like it should take a url and a headers object the same way Cursor's or VS Code's config does — and it doesn't, for remote servers added through the connectors UI. What Anthropic's own support docs describe is a sign-in flow:
"you'll typically go through an OAuth authentication process to securely sign in"
with an advanced option to supply your own OAuth client:
"Optionally, click 'Advanced settings' to specify an OAuth Client ID and OAuth Client Secret for your server."
There's no documented field for pasting a static bearer token or API key into the connector UI. If your MCP server only supports a plain API key (no OAuth), you have two real options:
- Put OAuth in front of it. More work, but it's the path the client actually supports natively.
-
Bridge it with
mcp-remote, which runs as a local stdio process and does support static headers — see below. This is what most static-key servers end up recommending for Desktop today.
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-remote", "https://example-mcp.dev/mcp", "--header", "Authorization:${AUTH_HEADER}"],
"env": { "AUTH_HEADER": "Bearer YOUR_KEY" }
}
}
}
The colon-with-no-space and the split into an env var aren't stylistic — they're a documented workaround for a real bug:
"Cursor, Codex-Cli and Claude Desktop (Windows) have a bug where spaces inside
argsaren't escaped when it invokesnpx, which ends up mangling these values."
Put the header name and value together with no space around the colon, keep the actual secret in env, and it works on the platforms where the naive version silently breaks.
Cursor
{
"mcpServers": {
"my-server": {
"url": "https://example-mcp.dev/mcp",
"headers": { "Authorization": "Bearer ${env:YOUR_KEY}" }
}
}
}
Straight from Cursor's docs:
{ "mcpServers": { "remote-server": { "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer ${env:MY_SERVICE_TOKEN}" } } } }
Native url + headers, no bridge needed. The ${env:VAR} syntax means you never write the raw key into mcp.json — set the environment variable instead. The gotcha here isn't the config, it's people skipping the interpolation and committing the literal key to a repo because the docs example (fairly) shows a working string either way.
VS Code (GitHub Copilot agent mode)
{
"servers": {
"my-server": { "type": "http", "url": "https://example-mcp.dev/mcp", "headers": { "Authorization": "Bearer ${input:my_key}" } }
},
"inputs": [
{ "type": "promptString", "id": "my_key", "description": "API key", "password": true }
]
}
From the docs:
"servers": { "example": { "type": "http", "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer ${input:api-key}" } } }"VS Code prompts you for the value when the server starts for the first time" and the value is "securely stored for subsequent use."
The inputs block is the point: it's how you avoid a plaintext key sitting in mcp.json if that file is checked into a repo. type: "http" is required — this is a newer field name than some older MCP config examples floating around that only specify url.
OpenAI Agents SDK (Python)
from agents.mcp import MCPServerStreamableHttp
async with MCPServerStreamableHttp(
name="my-server",
params={
"url": "https://example-mcp.dev/mcp",
"headers": {"Authorization": f"Bearer {your_key}"},
},
) as server:
...
Straight from the SDK's own docs example:
async with MCPServerStreamableHttp( name="Streamable HTTP Python Server", params={"url": "http://localhost:8000/mcp", "headers": {"Authorization": f"Bearer {token}"}, "timeout": 10}, cache_tools_list=True, max_retry_attempts=3, ) as server:
params is a plain dict, so there's nothing client-specific to get wrong here — this is backend Python, not a GUI, so "where does the key come from" is your own secret-management problem (env var, secrets manager), same as any other server-to-server credential.
LangChain / LangGraph (langchain-mcp-adapters)
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"my-server": {
"transport": "http",
"url": "https://example-mcp.dev/mcp",
"headers": {"Authorization": f"Bearer {your_key}"},
}
})
tools = await client.get_tools()
From the README:
client = MultiServerMCPClient({"weather": {"transport": "http", "url": "http://localhost:8000/mcp", "headers": {"Authorization": "Bearer YOUR_TOKEN", "X-Custom-Header": "custom-value"}}})"Only
sseandhttptransports support runtime headers. These headers are passed with every HTTP request to the MCP server."
That last line is the gotcha worth memorizing: if you're mixing a local stdio server and a remote http one in the same MultiServerMCPClient config, a headers key on the stdio entry does nothing — headers are an HTTP-transport-only concept, and the library won't warn you.
Security notes
A few things apply across all six clients, regardless of which one you're using:
- Prefer a header over a key in the URL. A key in a URL ends up in shell history, browser history, proxy logs, and any tool that logs request lines — a header is far less likely to be logged by default. If your server supports both (as ours does, below), use the header form in anything you'll run more than once.
- Scope keys per agent, not per team. A single shared key means you can't tell which agent made a given call, and revoking access for one agent means rotating the key for everyone. Issue one key per agent identity if your server supports it.
-
Treat the config file itself as a secret.
mcp.jsonandclaude_desktop_config.jsonwith a raw key inlined are as sensitive as a.envfile — don't commit them, and use the${env:VAR}/inputsmechanisms above instead of a literal string wherever the client supports it. - Rotate keys you've pasted into a GUI at least once. If you ever typed a key directly into a client's settings UI (rather than an env var), assume it's now in that app's local storage or config file in plaintext, and rotate it if the key is meant to be long-lived.
Worked example: connecting an agent to a payments sandbox
To make this concrete, here's the same pattern against a real remote MCP server we run: the Pink Agentic AI Payments sandbox, which exposes 7 tools (get_budget, list_payees, list_rules, check_policy, request_payment, get_credential, report_receipt) and issues one API key per agent.
Pink Agentic AI Payments (by PinkWallet, early access) is the approval layer between AI agents and company money: plain-language rules, per-agent budgets and human approvals decide each payment before a single-use card or bank transfer is issued.
The sandbox's MCP server accepts both forms discussed above — Authorization: Bearer <agent_key> as a header, or the key baked into the path as /mcp/<agent_key> — so connecting it from Claude Code looks exactly like the first example in this post:
claude mcp add --transport http pink https://agentic-sandbox.pinkwallet.com/mcp \
--header "Authorization: Bearer YOUR_AGENT_KEY"
and pink.check_policy lets an agent dry-run a payment (amount, payee, purpose) and get back would_allow / would_ask / would_block plus the rule that decided it, without moving any money — useful for testing exactly this kind of client wiring before you let an agent call request_payment for real. The public sandbox is live at agentic-sandbox.pinkwallet.com (free workspace, test credentials, no money moves); its MCP endpoint is https://agentic-sandbox.pinkwallet.com/mcp.
The one-line takeaway
If a remote MCP server authenticates with a static API key, every client that lets you write a config file or run a CLI command (Claude Code, Cursor, VS Code, the OpenAI Agents SDK, LangChain) will take a headers object or flag directly. The one exception is Claude Desktop's built-in connector UI, which is OAuth-only as documented — for a plain API key there, bridge it through mcp-remote rather than waiting for a header field that isn't in the current docs.
Top comments (0)