How to build a stateless MCP server on 2026-07-28, secured with AuthKit
A hands-on guide to deploying a spec-compliant remote MCP server on the final 2026-07-28 release, with AuthKit as the OAuth 2.1 authorization server, plus what to change if you're migrating off an earlier draft.
MCP 2026-07-28 shipped final on July 28, the largest revision to the protocol since launch. We covered what changed and why in our RC-stage explainer; this post is the hands-on companion. If you're standing up a new remote MCP server today, here's how to build it end to end: stateless transport, the new OAuth requirements, and AuthKit as your authorization server. If you're migrating an existing server off an earlier draft, skip to the last section.
The shape of the system
Two roles, cleanly separated:
- Your MCP server is the OAuth 2.1 resource server. It exposes tools and resources, and its only auth job is verifying bearer tokens.
- AuthKit is the authorization server. It owns login, consent, token issuance, and the JWKS endpoint.
Nothing in that exchange depends on a session living on a particular instance. That's the point of the new spec.
Step 1: Scaffold a stateless server
Target 2026-07-28 in a Tier 1 SDK (TypeScript, Python, Go, or C# all have it as of the final release). The key difference from earlier drafts: there's no initialize/initialized handshake to opt into anything. Protocol version and client capabilities now travel in _meta on every request, and most SDKs require you to explicitly request stateless mode rather than defaulting to it, so a client that only speaks an older revision still negotiates down cleanly.
Because there's no Mcp-Session-Id to pin a client to an instance, you can put your server behind an ordinary round-robin load balancer or run it on serverless/edge compute, with no sticky routing and no shared session store to keep in sync.
If a tool genuinely needs continuity across calls (a shopping cart, a multi-step import job), don't reach for a session. Mint an explicit handle in the tool's response (a basket_id, a job_id) and have the client pass it back as an ordinary argument on the next call. It's more visible to the model, and it survives your server restarting or a request landing on a different instance mid-flow.
Step 2: Make the server a real resource server
Before AuthKit is in the picture, your server needs to speak the protected-resource half of OAuth 2.1 correctly on its own:
Publish resource metadata.
Reject unauthenticated requests correctly. A missing or invalid token gets a 401 with a WWW-Authenticate header pointing back at that metadata endpoint, so any spec-compliant client can self-discover where to authenticate:
This much is true regardless of which authorization server you point at. Now point it at AuthKit.
Step 3: Wire up AuthKit as the authorization server
- Create a WorkOS project and enable AuthKit if you haven't already.
- Register your MCP server's URL as a Resource Indicator. In the WorkOS Dashboard, under Connect → Configuration, add
https://mcp.yourapp.com(the same value asresourcein your metadata endpoint). AuthKit will stamp tokens issued for your server with a matchingaudclaim. This is what RFC 8707 resource indicators are for, and it's what stops a token minted for one MCP server from being replayed against another. - Enable Client ID Metadata Document (CIMD). Also under Connect → Configuration. CIMD is now the spec's preferred way for MCP clients to identify themselves without a pre-registration step, and Dynamic Client Registration is deprecated in its favor, kept around only for clients that haven't caught up yet. Turn CIMD on; leave DCR enabled alongside it for now if you need to support older clients, and plan to drop it later.
- Verify tokens on every request. No session, no caching a "logged in" flag: just check the signature, issuer, and audience on each call:
That's a working, spec-compliant remote MCP server: stateless transport, discoverable auth, tokens verified per request against an authorization server you didn't have to build.
A couple of variants worth knowing about, depending on your setup:
- Already have your own login system? You don't have to migrate users to AuthKit to get this. Standalone Connect lets AuthKit issue and verify MCP tokens while your app keeps handling authentication itself.
- Want to gate individual tools by permission, not just "is this request authenticated"? That's a per-tool scopes problem, and it's worth its own read. We cover it in depth in MCP authorization patterns: per-tool scopes, consent, and least privilege. Short version: pull permissions off the verified session and check them inside each tool handler, so a tool that only needs read access never has a code path that touches a write scope.
- Client doesn't support Protected Resource Metadata yet? Some clients still look for
/.well-known/oauth-authorization-serverdirectly on your MCP server instead of following the discovery chain. Proxy it through to AuthKit's own metadata endpoint and the rest of the flow is unaffected.
Migrating an existing server
If you built against an earlier draft of the spec, here's what actually needs to change:
- Find every read or write tied to
Mcp-Session-Id. Replace session-bound state with explicit handles passed back as tool arguments, as described in Step 1. Once nothing depends on it, you can drop sticky routing at the load balancer. - Stop relying on the
initializehandshake. Capabilities now travel in_metaon each request; if your server logic assumed a one-time negotiation at connection start, move that logic to run per-request instead. - Move off Dynamic Client Registration. Turn on CIMD in the WorkOS Dashboard now. Keep DCR active in parallel until you're confident every client you support has moved over, then disable it.
- Re-check your token audience handling. If you weren't setting a Resource Indicator before, add one now (Step 3.2). Tokens without a matching
audclaim should be treated the same as any other invalid token. - Deploy multiple instances behind a plain load balancer and run your test suite. Any failure is a hidden session dependency you haven't found yet.
Where to go from here
- MCP
2026-07-28specification - What changed and why: our RC-stage explainer
- AuthKit MCP docs
- Per-tool scopes and least privilege with AuthKit
The stateless core means you're no longer designing around a session. The auth hardening means you're no longer guessing at how to secure the server. Put AuthKit in front of it and most of what's left is just building your tools.