Jake Marsh When someone pastes your MCP server’s URL into Claude or ChatGPT, a sign-in window should open. What makes that happen is one HTTP response from your server. I run a few remote MCP servers with OAuth, and the response those clients need is the same one that gets a server marked as down in directories.
The Response That Starts It# #
The flow in the MCP authorization spec begins when the client’s request gets a 401 with a WWW-Authenticate header pointing at your protected resource metadata:
The expose header is there for clients running in a browser. Without it, their JavaScript can’t read the challenge.
That URL returns a small JSON document ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) naming your authorization server:
From there the client reads your authorization server’s metadata, registers itself and opens the browser. I serve the metadata at both `/.well-known/oauth-protected-resource/mcp` and the bare `/.well-known/oauth-protected-resource`, because some clients try the root first.
## Two Callers Want Opposite Things[#](#two-callers-want-opposite-things)
If an anonymous `initialize` returns 200, an OAuth-capable client can decide your server needs no auth. It connects, lists the tools and only fails later on `tools/call`, with no sign-in window. So those clients need the 401 on their very first request.
Directories and health checkers want the opposite. They call initialize and tools/list with no credentials, and many of them read a 401 as an outage. CallForMe logged about 700 of those 401s from checkers in two days, and one directory listed it as “Unhealthy, 0.0% uptime over 32 days” while it was running fine.
Answer By Who’s Asking# #
So the endpoint looks at the caller. This is the version in Taskhold, trimmed down. Clients that sign in get challenged, and everything else can read the public parts:
The User-Agent catches most clients. Some send something generic like node, and those name themselves in clientInfo inside the initialize body, which is why there’s a second regex.
A spoofed User-Agent gets a caller nothing here, since the list only decides who sees a challenge. Every tool that touches data still needs a token, and anonymous requests run against a backend that throws on any call.
Which Default To Pick# #
Taskhold defaults to anonymous and challenges a list of known OAuth clients. CallForMe does the inverse: it defaults to the 401 and opens up for a list of known checkers. Your choice decides who gets the wrong answer when a caller you’ve never seen shows up:
- With a list of OAuth clients, a new client gets a 200 and may not offer sign-in until someone adds it.
- With a list of checkers, a new checker gets a 401 and may list you as down.
I’d pick by which mistake costs you more. CallForMe also matches checkers by how they describe themselves (mcp-?index, health-?check, crawler), so new ones don’t sit on a 401 until I notice them.
There are exceptions on both sides. The CI for awesome-remote-mcp-servers labels a server by its initialize reply: 200 means no auth, and a 401 with resource_metadata means OAuth. An OAuth server that wants the right label has to challenge that checker too, so it’s in Taskhold’s full list.
Log The 401s# #
Some clients see the challenge and never finish. codex mcp add --url without codex mcp login hit a 401 on every session: in early November my logs showed 7 IPs stuck in one day and no completed Codex sign-ins in 3 days. CallForMe now lets unauthenticated Codex sessions into a public mode that needs no sign-in, and signed-in ones keep their token. Another runtime registered with the OAuth server, never called /authorize and kept retrying hourly.
I only found these by logging each 401 with its User-Agent, limited to once per IP and client every 6 hours. Comparing that against completed sign-ins shows which clients get stuck.
You can check your own server’s answer with curl by changing the User-Agent:
Run it once with that header and once with User-Agent: directory-check. The first should return the 401 and the second a 200, unless your server is one that challenges everyone by default.