{"title":"How Remote MCP Servers Trigger OAuth Sign-In","html_url":"https://deallocatedobjects.com/posts/how-remote-mcp-servers-trigger-oauth-sign-in","source_url":"","markdown_content":"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.\n\n### The Response That Starts It\n\nThe flow in the [MCP authorization spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) begins when the client's request gets a 401 with a `WWW-Authenticate` header pointing at your protected resource metadata:\n\n```\nHTTP/1.1 401 Unauthorized\nWWW-Authenticate: Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource/mcp\"\nAccess-Control-Expose-Headers: WWW-Authenticate\n```\n\nThe expose header is there for clients running in a browser. Without it, their JavaScript can't read the challenge.\n\nThat URL returns a small JSON document ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) naming your authorization server:\n\n```json\n{\n  \"resource\": \"https://example.com/mcp\",\n  \"authorization_servers\": [\"https://example.com\"],\n  \"bearer_methods_supported\": [\"header\"],\n  \"scopes_supported\": [\"mcp\"]\n}\n```\n\nFrom 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.\n\n### Two Callers Want Opposite Things\n\nIf 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.\n\nDirectories 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](https://callforme.tel/?ref=deallocated-post) 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.\n\n### Answer By Who's Asking\n\nSo the endpoint looks at the caller. This is the version in [Taskhold](https://taskhold.com/?ref=deallocated-post), trimmed down. Clients that sign in get challenged, and everything else can read the public parts:\n\n```ts title=\"app/api/mcp/route.ts\"\nconst ANONYMOUS_METHODS = new Set([\n  \"initialize\", \"server/discover\", \"notifications/initialized\",\n  \"tools/list\", \"ping\", \"prompts/list\", \"resources/list\",\n]);\n\nconst OAUTH_CLIENT_UA = /\\b(Claude-User|claude-ai|openai-mcp|ChatGPT|claude-code|Cursor|codex-mcp-client|Visual Studio Code|goose)\\b/i;\nconst OAUTH_CLIENT_NAME = /\\b(gemini|visual studio code|vscode|windsurf|zed)\\b/i;\n\nasync function isDiscoveryOnly(request: Request): Promise<boolean> {\n  if (request.headers.get(\"Authorization\")) return false;\n  if (OAUTH_CLIENT_UA.test(request.headers.get(\"User-Agent\") ?? \"\")) return false;\n  try {\n    const body = await request.clone().json();\n    const msgs = Array.isArray(body) ? body : [body];\n    if (msgs.some((m) => m?.method === \"initialize\" && OAUTH_CLIENT_NAME.test(String(m?.params?.clientInfo?.name ?? \"\")))) return false;\n    return msgs.length > 0 && msgs.every((m) => typeof m?.method === \"string\" && ANONYMOUS_METHODS.has(m.method));\n  } catch {\n    return false;\n  }\n}\n```\n\nThe 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.\n\nA 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.\n\n### Which Default To Pick\n\nTaskhold 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:\n\n- With a list of OAuth clients, a new client gets a 200 and may not offer sign-in until someone adds it.\n- With a list of checkers, a new checker gets a 401 and may list you as down.\n\nI'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.\n\nThere are exceptions on both sides. The CI for [awesome-remote-mcp-servers](https://github.com/punkpeye/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.\n\n### Log The 401s\n\nSome 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.\n\nI 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.\n\nYou can check your own server's answer with curl by changing the User-Agent:\n\n```bash\ncurl -si -X POST https://example.com/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -H 'User-Agent: claude-code/2.0' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1\"}}}' \\\n  | head -5\n```\n\nRun 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.","html_content":"<p>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.</p>\n<h2 id=\"the-response-that-starts-it\" data-level=\"3\">The Response That Starts It</h2>\n<p>The flow in the <a href=\"https://modelcontextprotocol.io/specification/draft/basic/authorization\">MCP authorization spec</a> begins when the client’s request gets a 401 with a <code>WWW-Authenticate</code> header pointing at your protected resource metadata:</p>\n<figure class=\"code\" data-lang=\"text\"><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span>HTTP/1.1 401 Unauthorized</span></span>\n<span class=\"line\"><span>WWW-Authenticate: Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource/mcp\"</span></span>\n<span class=\"line\"><span>Access-Control-Expose-Headers: WWW-Authenticate</span></span></code></pre></figure><p>The expose header is there for clients running in a browser. Without it, their JavaScript can’t read the challenge.</p>\n<p>That URL returns a small JSON document (<a href=\"https://datatracker.ietf.org/doc/html/rfc9728\">RFC 9728</a>) naming your authorization server:</p>\n<figure class=\"code\" data-lang=\"json\"><figcaption><span class=\"lang\">JSON</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  \"resource\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"https://example.com/mcp\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  \"authorization_servers\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: [</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"https://example.com\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  \"bearer_methods_supported\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: [</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"header\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  \"scopes_supported\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: [</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"mcp\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><p>From there the client reads your authorization server’s metadata, registers itself and opens the browser. I serve the metadata at both <code>/.well-known/oauth-protected-resource/mcp</code> and the bare <code>/.well-known/oauth-protected-resource</code>, because some clients try the root first.</p>\n<h2 id=\"two-callers-want-opposite-things\" data-level=\"3\">Two Callers Want Opposite Things</h2>\n<p>If an anonymous <code>initialize</code> returns 200, an OAuth-capable client can decide your server needs no auth. It connects, lists the tools and only fails later on <code>tools/call</code>, with no sign-in window. So those clients need the 401 on their very first request.</p>\n<p>Directories and health checkers want the opposite. They call <code>initialize</code> and <code>tools/list</code> with no credentials, and many of them read a 401 as an outage. <a href=\"https://callforme.tel/?ref=deallocated-post\">CallForMe</a> 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.</p>\n<h2 id=\"answer-by-whos-asking\" data-level=\"3\">Answer By Who’s Asking</h2>\n<p>So the endpoint looks at the caller. This is the version in <a href=\"https://taskhold.com/?ref=deallocated-post\">Taskhold</a>, trimmed down. Clients that sign in get challenged, and everything else can read the public parts:</p>\n<figure class=\"code\" data-lang=\"typescript\"><figcaption><span class=\"file\">app/api/mcp/route.ts</span><span class=\"lang\">TypeScript</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> ANONYMOUS_METHODS</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> new</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Set</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">([</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">  \"initialize\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"server/discover\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"notifications/initialized\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">  \"tools/list\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"ping\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"prompts/list\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"resources/list\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> OAUTH_CLIENT_UA</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> /</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">\\b</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">(Claude-User</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">claude-ai</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">openai-mcp</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">ChatGPT</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">claude-code</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">Cursor</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">codex-mcp-client</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">Visual Studio Code</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">goose)</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">\\b</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">i</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> OAUTH_CLIENT_NAME</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> /</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">\\b</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">(gemini</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">visual studio code</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">vscode</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">windsurf</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">zed)</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">\\b</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">i</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> isDiscoveryOnly</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#E36209;--shiki-dark:#FFAB70\">request</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Request</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Promise</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">boolean</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (request.headers.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">get</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"Authorization\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">OAUTH_CLIENT_UA</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">test</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(request.headers.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">get</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\"User-Agent\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">??</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> body</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> request.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">clone</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">().</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">json</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> msgs</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> Array.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">isArray</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(body) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> body </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> [body];</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (msgs.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">some</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"--shiki-light:#E36209;--shiki-dark:#FFAB70\">m</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> m?.method </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"initialize\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> OAUTH_CLIENT_NAME</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">test</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">String</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(m?.params?.clientInfo?.name </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">??</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)))) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> msgs.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> ></span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> msgs.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">every</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"--shiki-light:#E36209;--shiki-dark:#FFAB70\">m</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> typeof</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> m?.method </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \"string\"</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> ANONYMOUS_METHODS</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">has</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(m.method));</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">catch</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><p>The User-Agent catches most clients. Some send something generic like <code>node</code>, and those name themselves in <code>clientInfo</code> inside the <code>initialize</code> body, which is why there’s a second regex.</p>\n<p>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.</p>\n<h2 id=\"which-default-to-pick\" data-level=\"3\">Which Default To Pick</h2>\n<p>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:</p>\n<ul>\n<li>With a list of OAuth clients, a new client gets a 200 and may not offer sign-in until someone adds it.</li>\n<li>With a list of checkers, a new checker gets a 401 and may list you as down.</li>\n</ul>\n<p>I’d pick by which mistake costs you more. CallForMe also matches checkers by how they describe themselves (<code>mcp-?index</code>, <code>health-?check</code>, <code>crawler</code>), so new ones don’t sit on a 401 until I notice them.</p>\n<p>There are exceptions on both sides. The CI for <a href=\"https://github.com/punkpeye/awesome-remote-mcp-servers\">awesome-remote-mcp-servers</a> labels a server by its <code>initialize</code> reply: 200 means no auth, and a 401 with <code>resource_metadata</code> means OAuth. An OAuth server that wants the right label has to challenge that checker too, so it’s in Taskhold’s full list.</p>\n<h2 id=\"log-the-401s\" data-level=\"3\">Log The 401s</h2>\n<p>Some clients see the challenge and never finish. <code>codex mcp add --url</code> without <code>codex mcp login</code> 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 <code>/authorize</code> and kept retrying hourly.</p>\n<p>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.</p>\n<p>You can check your own server’s answer with curl by changing the User-Agent:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">curl</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -si</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -X</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> POST</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> https://example.com/mcp</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  -H</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'Content-Type: application/json'</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  -H</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'Accept: application/json, text/event-stream'</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  -H</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'User-Agent: claude-code/2.0'</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">  -d</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1\"}}}'</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> \\</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">  |</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> head</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -5</span></span></code></pre></figure><p>Run it once with that header and once with <code>User-Agent: directory-check</code>. The first should return the 401 and the second a 200, unless your server is one that challenges everyone by default.</p>","created_at":"2026-11-09T14:00:00.000Z","updated_at":"2026-11-09T14:00:00.000Z"}