Build1 publisher2 min readPublished
A remote MCP server spends five of its six routes on getting the client signed in
The tool logic is one route handler. The other five routes stand up an OAuth authorization server and two well-known documents, and the 401 that points at them decides whether Claude ever shows a sign-in page.
The Engineer · Build desk

What happened
- A developer writing on dev.to documents shipping a remote MCP server for Deoochform, a form builder where the assistant builds the form, so the MCP endpoint is the product surface rather than a side feature.
- A client that has never seen the server walks all four surfaces in order without being told to, which is what turns a pasted URL in Claude into a browser sign-in rather than a token prompt.
- The official SDK's WebStandardStreamableHTTPServerTransport speaks Web-standard Request and Response objects, so one handler function is exported as GET, POST and DELETE from an App Router route.
- The server object is built per request with the resolved actor baked in, so each tool closes over the caller's identity instead of taking a user argument, at a cost the author puts at one allocation per request.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost The bill for remote MCP lands as five thin routes over the session system you already run, so the spend is engineering attention on discovery documents rather than a contract with an identity vendor.
- constraint Adding a second MCP endpoint stops being a route alias, because each one drags its own nested metadata document along, so versioning your tools also versions your discovery surface.
- decision Anyone holding a module-level MCP server whose tools take a user id argument now has a refactor to price against the per-request alternative that makes cross-tenant reads unreachable by construction.
- exposure A broken integration reaches you as a user saying the client cannot connect, because the failed preflight means no request ever arrives to be logged or alerted on.
Walk the chain in the order a cold client walks it. A client POSTs to /api/mcp holding nothing. Back comes a 401 carrying `WWW-Authenticate: Bearer resource_metadata=` pointing at /.well-known/oauth-protected-resource/api/mcp, which is RFC 9728 and which the author calls the trigger for the entire browser sign-in flow [5]. That document answers with two fields, `resource` and `authorization_servers` [8]. The client follows `authorization_servers` to /.well-known/oauth-authorization-server and reads `issuer`, `authorization_endpoint`, `token_endpoint` and `registration_endpoint` [9]. Each step's input is the step before it. Omit the header and, according to the post, the client concludes your server is broken rather than password-protected [6].
Six paths, not four surfaces, is the number that predicts the work: the MCP endpoint, two well-known documents, /authorize, /token, /register. Of the six, one carries tool traffic, and the other five exist to put a token in the client's hands [16].
The expensive one to debug is the one with no server-side symptom. `Access-Control-Expose-Headers` has to name `mcp-session-id` and `mcp-protocol-version`; leave them out and the browser withholds from the client the protocol headers your server correctly sent [15]. The response reaches the browser with those headers intact, but the browser's CORS policy stops the client's JavaScript from reading them.
Statelessness in this design is conditional, and the post names the condition: `sessionIdGenerator: undefined`, because consecutive requests on Vercel or any serverless host land on different instances, so a session has nowhere to live [12]. That is a fact about the hosting environment, not about MCP itself. A single long-lived Node process behind sticky routing breaks the premise, and stateless goes back to being a preference you can defend rather than the only thing that works.
The spec and the field report come apart here, and it matters which one you are copying. The metadata pointer on the 401 is RFC 9728 [5], so it holds for any client that implements the RFC. What Claude and ChatGPT actually do when it is missing is one developer's observation, from one stack: Next.js 16 App Router with no framework beyond the official SDK [2]. Everything in the write-up about client behaviour was measured against those two clients, at whatever version they were that week, and nothing in the post claims otherwise. Which leaves one acceptance test worth running before you ship: a browser sign-in driven by a client that holds no token yet, on a server it has never seen.
What to watch
- Whether Claude and ChatGPT will accept a preregistered client id or insist on hitting registration_endpoint, which decides how much of /register you have to build.
- Any rename or default change in the SDK's transport options, since the stateless behaviour rests entirely on sessionIdGenerator: undefined.
- Whether the wildcard Access-Control-Allow-Origin in this CORS block survives contact with clients or reviewers that expect an origin allowlist on a token-bearing endpoint.