DEV Community

Cover image for The remote MCP client config matrix nobody documents (and the three ways type fails silently)
TuringCorp
TuringCorp

Posted on

The remote MCP client config matrix nobody documents (and the three ways type fails silently)

The remote MCP client config matrix nobody documents (and the three ways type fails silently)

I maintain a remote MCP server that authenticates with a static Authorization: Bearer header. No OAuth, no device
flow, no browser handoff. That is the boring case, and it turned out to be the one where every client has its own
opinion.

Over a month of connecting the same endpoint to Cursor, Windsurf, Claude Desktop, Claude Code, Cline, VS Code and
Codex, plus a two-line bridge for stdio-only hosts, I collected one table. It is below, and it exists because
the same HTTP transport has four different names depending on which client is reading your JSON.

The interesting part is not that the names differ. It is that a wrong name fails in three completely different
ways, and two of them do not look like configuration errors at all.

First, isolate transport from credential at the HTTP level

Before touching any client, get one answer: does the endpoint work with a curl? If it does, everything after that
is client configuration, and you should stop debugging the server.

export MCP_URL='https://mcp.turingcorp.net/mcp'

# A) discovery, no credential. Open by design. 200 here = transport is fine.
curl -s -o /dev/null -w 'open   %{http_code}\n' -X POST "$MCP_URL" \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# B) a deliberately wrong credential, on the calling path. Must be 401.
curl -s -o /dev/null -w 'wrong  %{http_code}\n' -X POST "$MCP_URL" \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'authorization: Bearer definitely-not-a-real-pass' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"<your-tool>","arguments":{}}}'

# C) the real credential, same call. The only line that proves end-to-end.
curl -s -o /dev/null -w 'real   %{http_code}\n' -X POST "$MCP_URL" \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "authorization: Bearer $AGENT_PASS" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"<your-tool>","arguments":{}}}'
Enter fullscreen mode Exit fullscreen mode

Read the three results as a decision table, not as a pass/fail:

A (open) B (wrong pass) C (real pass) What it means
not 200 — — transport problem: wrong path, wrong type, proxy, or a client-side bridge eating the request
200 200 200 the call is not protected on the server, or you are hitting a different route than you think
200 401 401 the credential is genuinely wrong or expired. This is the only case where re-issuing is the right move
200 401 200 server and credential are both correct. If the client still fails, the bug is in the client config

And one trap worth burning in: tools/list is not a credential test. Run line A with a garbage header and
you will still get 200 with the full tool list, because the method is open and the header is never consulted.
An "auth check" written as a listing call passes no matter what you put in it. Probe the calling path.

The matrix

Same endpoint, same key, seven clients, four distinct transport names. The parent key differs too.

Client Config location Parent key Transport field Value
Claude Code CLI — --transport http
Cursor / Windsurf / Claude Desktop JSON settings mcpServers type http
Cline cline_mcp_settings.json mcpServers type streamableHttp
VS Code .vscode/mcp.json servers type http
Codex ~/.codex/config.toml [mcp_servers.<name>] url + http_headers table keys
stdio-only hosts JSON settings mcpServers command + args npx mcp-remote

The header itself is unremarkable everywhere, so here is the whole thing for the JSON crowd:

{
  "mcpServers": {
    "MyServer": {
      "type": "http",
      "url": "https://mcp.turingcorp.net/mcp",
      "headers": { "Authorization": "Bearer <your pass>" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cline needs exactly one character of difference, and it is camelCase with no hyphen:

{
  "mcpServers": {
    "MyServer": {
      "type": "streamableHttp",
      "url": "https://your-server.example.com/mcp",
      "headers": { "Authorization": "Bearer <your pass>" },
      "disabled": false,
      "autoApprove": [],
      "timeout": 300
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

VS Code is the same body under servers, not mcpServers. Codex keeps the credential as a TOML table so the UI
never has to know about it:

[mcp_servers.MyServer]
url = "https://your-server.example.com/mcp"
http_headers = { Authorization = "Bearer <your pass>" }
tool_timeout_sec = 300
Enter fullscreen mode Exit fullscreen mode

The three ways a wrong type fails

This is the part I would have wanted to read a month ago, because only one of the three looks like a typo.

1. Refused, loudly. The client validates the transport field against a closed set and tells you the value is
invalid. Cheap. You fix it in ten seconds. Every client should be this one.

2. Silently treated as stdio. Omit type and leave a bare url, and at least one client family reads the
entry as a local stdio server, then either launches nothing or reports a spawn failure. The config looks right,
the error message points at a nonexistent process, and the URL is never dialed. If your client says "server
exited" for a remote endpoint, check that type is present before you check anything else.

3. Connected, then 401 on the first call. The worst one. The client negotiates SSE instead of streamable
HTTP, a server that does not speak SSE answers 405 (or the client falls back until something half-works), the
liveness indicator goes green off discovery alone, and the first real call fails. Cline does exactly this when
type is missing or spelled streamable-http: it falls back to SSE, and because my endpoint does not serve SSE
the connection dies with 405 that reads like a server fault rather than a one-word config fix.

The unifying question behind all three: is type present, and does it say the word this particular client
uses?
There is no cross-client spelling. The word is part of the client's contract, and it belongs in the
config file, not in a prompt, not in a README you copy from a different client.

The stdio bridge has its own header trap

For hosts that only speak stdio, the standard bridge works, but the credential has to arrive through --header:

{
  "mcpServers": {
    "MyServer": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://your-server.example.com/mcp",
               "--header", "Authorization:${MY_PASS}"],
      "env": { "MY_PASS": "Bearer <your pass>" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Two details that cost me an evening each. First, mcp-remote does not read an AUTHORIZATION environment
variable, so a config that only sets one will connect, list tools, and then fail on the first call with 401 -
the discovery/execution split again, this time wearing a costume. Second, the missing space after
Authorization: is deliberate: some hosts mangle spaces inside args, so the header is assembled from an env
var and a no-space prefix.

Two settings that fail in the quiet direction

Neither of these produces an error message on the client side.

Setting Wrong value looks like Correct value
call timeout a decide-style call cut off at ~60 s, then a "server error" 300 s or higher, in the units the client documents (Cline: seconds)
retrieve-after-timeout a second, separately billed call fetch by job_id with the same credential, usually 7 days of retention

If your MCP server does minutes-long work, put the timeout in the config and write the retrieve path down next to
it, because the failure mode is "the client gave up waiting", which is indistinguishable from "the server
failed" unless you go look.

Copy the table, not the blog post

Generalize it to your own server in four steps.

  1. Fill the matrix for the clients you actually support, with the exact transport word each one wants.
  2. Paste the three curls above into your install docs as the first diagnostic, so users can segment transport from credential before they open an issue.
  3. State plainly which methods are unauthenticated. tools/list open plus tools/call protected is a defensible default for discovery - but it makes "connected" a meaningless signal, and saying so up front saves a class of support tickets.
  4. Put the timeout and the retrieve-by-id path in the same section as the config, not in a separate performance page nobody reads.

I run this matrix against a decision endpoint at https://mcp.turingcorp.net/mcp; the same table applies to any
remote MCP server with a static Bearer header. If your client is not in the table, the curl block still tells you
which of the three failure classes you are in, which is usually the whole diagnosis.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.