cd /news/agent-protocols/your-remote-mcp-server-is-connected-… · home › topics › agent-protocols › article
[ARTICLE · art-148787] src=dev.to ↗ pub= topic=agent-protocols verified=true sentiment=· neutral

Your remote MCP server is "connected" and still returns 401

A developer explains that remote MCP servers can report as "connected" while still returning 401 on tool calls, because discovery methods like initialize and tools/list are deliberately open while tools/call requires a credential. The writeup provides a three-step curl diagnostic that separates connectivity from authorization, warning that tools/list returns 200 regardless of the credential supplied and therefore cannot serve as an auth check.

by read7 min views2 publishedOct 10, 2026

There is a specific kind of bug report that shows up in every remote MCP rollout, and it always sounds the same:

I added the server. It shows as connected. It lists tools. Then the first real call fails with 401.

Nothing is broken. The server is fine, the client is fine, and the credential is fine. What is wrong is the mental model — the one most of us carried over from local stdio servers, where "connected" meant "ready."

For a remote MCP server, reachability and authorization are two different things, and the first one does not imply the second. Discovery is deliberately open. Execution is not.

A hosted MCP endpoint usually exposes the discovery half of the protocol without any credential at all, because that is what directory crawlers, liveness probes and humans-with-a-browser need:

What the client does Credential needed Typical result without one
initialize no 200
tools/list no 200 + the tool list
tools/call yes 401 with WWW-Authenticate

That asymmetry is what makes the failure feel like a client bug. The client's "connected" indicator is driven by initialize and tools/list, both of which succeed with no credential at all. The lights are green, and the thing still cannot do any work.

So the first diagnostic question is never "is it connected?" It is: "which half of the protocol is failing?" If listing works and calling 401s, you are missing a credential, not a connection.

The symmetric mistake is to conclude the token is expired and go re-issue a pass. Before you do that, check two things, because both produce an identical-looking 401:

Bearer <pass>, with the word and the space. A client config that accepts headers as an opaque map will happily send a bare pass, and the server will correctly reject it. The exact spelling is part of the contract, and it belongs in the config, not in the model's prompt.401 returned to npx command, seeing a 401, and reading it as "auth is required, therefore my auth is working." It is a precise way to learn nothing. Here is the trap, and it is worth stating precisely because I walked into it while writing this: tools/list is not a credential test at all. I ran a tools/list with a deliberately garbage pass, expecting 401, and got 200 with the full tool list — because the method is open, so the header is simply not consulted. If your "auth check" is a listing call, it will pass no matter what you put in the header.

The credential only gets exercised where it is actually needed: on tools/call. So the three states have to be probed on the calling path, not the listing path.

Set the pass in the environment, never on the command line (it lands in shell history and in transcripts):

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"}'

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":{}}}'

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":{}}}'

Three lines, three different questions, and only the third one is a pass/fail about your setup. If line 1 is not 200, you have a transport problem. If line 2 returns 200, the call is not actually protected on the server (or you are hitting a different path than you think) — that is a finding about the server, not about your config. If line 2 is 401 and line 3 is also 401, the pass is genuinely wrong or expired, and that is the only case where re-rolling is the right move.

Bake the same three states into your own test suite rather than into a wiki page. The property you are asserting is not "the server works" — it is "a credential is required on the calling path, and our client config supplies a working one."

Remote MCP over HTTP has more than one dialect in the wild, and clients spell them differently. Whoever writes the config has to match the client, not the spec summary:

Client-side difference What to write Failure when you get it wrong
Key that holds the server map mcpServers in most JSON-config clients,servers in VS Code'smcp.json Server silently ignored — "connected" never happens, or the wrong entry is loaded
Transport value http in most clients; one popular one wants**streamableHttp** in camelCase Client falls back to SSE, which the server does not speak ⇒ 405 , and the panel reports a connection problem
Header-in- args bridges mcp-remote takes headersonly from--header ; it does not read anAUTHORIZATION environment variable Config looks right, tools/list works, first call 401 — the exact symptom from the top of this post
Timeouts client-side, in seconds for one client and milliseconds for another decide -style calls that legitimately take minutes get cut at 60 s and read as failures

Notice how many of these produce the same user-visible symptom. That is the trap: one error message, four unrelated causes, and a strong pull toward "the server is flaky."

The way out is to make the config matrix explicit for your own server and check it into your repo next to the code. Every row above is a thing a reader can copy; every one of them is something we got wrong once before it went into the matrix.

One more design point worth stating plainly, because it affects the calling pattern as much as the config.

Put the pass in the host's config. Never accept it as a tool argument. A credential that is an argument is a credential that ends up in prompts, in transcripts and in whatever log pipeline those feed. This is also why retrieval-style tools are best designed as tool calls rather than REST endpoints: the host attaches the header, the agent never holds the value, and a cut-off call can be fetched back without anyone copy-pasting a token.

If you take one thing from this post, take the three-line check. "Connected" is a claim about tools/list. Whether your agent can actually call anything is a claim about tools/call — and the only honest way to know is to test the three states separately, on purpose, once.

The same three states, plus a worked example of asserting on a tool's contract instead of its answer, are in our public cookbook: https://mcp.turingcorp.net/ links to the repo, and the relevant sections are docs/COOKBOOK.md §7–§10. The seven-field config matrix lives in llms-install.md in the same repository, and it is the document that came out of every mistake described above.

Copy this into your own runbook and fill in the right-hand column for your server:

Symptom Most likely cause First thing to check
Panel says connected, first call 401 credential not attached by the client config the headers block, including theBearer prefix
405 right after adding the server transport dialect mismatch (SSE fallback) the type field for that specific client
Server entry missing from the client UI wrong config key ( servers vsmcpServers ) the JSON key, not the URL
Call dies at ~60 s, no error body client tool-call timeout, not a server fault timeout unit and value; raise it
Works with curl , fails in the host header bridge ( mcp-remote and friends) ignores env vars move the header into --header
401 on retrieval of an earlier call retrieval attempted over REST without a host-attached credential use the retrieval tool call instead
── more in #agent-protocols 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-remote-mcp-serv…] indexed:0 read:7min 2026-10-10 · —