How to build an MCP app on the 2026-07-28 spec with WorkOS AuthKit
A hands on build of an interactive MCP app, gated by WorkOS AuthKit with permissions checked per tool, deployed against the finalized 2026-07-28 spec.
MCP 2026-07-28 shipped as final on July 28, 2026. It moves the protocol to a stateless core, hardens authorization to match production OAuth 2.1 and OIDC deployments, and formally graduates its first extensions, MCP Apps and Tasks, into a versioned extensions framework.
MCP Apps itself isn't new. It's been live since January, and Claude, ChatGPT, Goose, and VS Code have all supported it for months. What's new is that it's now sitting on a finalized, stateless transport with hardened auth requirements underneath it. Most of the MCP Apps examples floating around were built earlier, on the release candidate or on the old session based transport, and most of them skip auth entirely or fake it with a hardcoded token. This post builds one for real: an interactive app that renders inside Claude, backed by a stateless server built to the final spec, gated by WorkOS AuthKit with permissions enforced per tool, not just per server.
What actually changed on July 28
Three things matter for this build:
- The protocol is stateless now. The
initializehandshake and theMcp-Session-Idheader are both gone. There's no protocol level session to pin a client to a server instance, so any instance can answer any request. That's good news for deployment: a plain round robin load balancer works, with no sticky routing and no shared session store. - Extensions are formal. MCP Apps and Tasks now ship under a versioned extensions framework with their own identifiers, instead of being bolted onto the core spec. Clients and servers negotiate which extensions they support during setup.
- Auth is closer to real OAuth. MCP servers are now unambiguously OAuth 2.1 resource servers. They're expected to implement Protected Resource Metadata (RFC 9728) so clients can discover the right authorization server automatically, and clients are expected to use Resource Indicators (RFC 8707) so a token minted for one MCP server can't be replayed against another.
None of that requires you to hand roll header parsing. The point of building against the finalized SDKs is that they handle the stateless transport for you, so the rest of this post focuses on what you still have to write yourself: the tools, the UI, and the auth.
What we're building
A small internal tool: an expense approval board.
view_expensesreturns an interactive table of pending expenses. Anyone on the team can call it.approve_expenseapproves one. Only people holding an approver permission can call it.
The UI renders inline in the conversation. The approve button only works for people who actually hold the approver scope, and that isn't cosmetic. The server checks on every call, not just when the button is drawn.
Step 1: A stateless server
With the session model gone, there's no server side session object to lean on for state that spans calls. If your app needs to remember something between calls, like a filter or a draft, the pattern is to mint an explicit handle and have the model pass it back as an ordinary argument, not to rely on the server remembering anything. Our app doesn't need that: expense data lives in a database, keyed by the organization id from the caller's token, not by any notion of session.
Step 2: Define the tools and the UI
Each tool that has a UI points to it with _meta.ui.resourceUri. The UI itself is served as a resource under the ui:// scheme, and the host renders it in a sandboxed iframe.
The UI resource itself is a small HTML page that talks back to the host over JSON-RPC:
Notice the canApprove flag coming back from the server. The server decides who sees a working approve button, not the client. The UI can hide or disable the button based on that flag, but the actual enforcement happens on the tool call itself, which is the next step.
Step 3: Put AuthKit in front of it
The MCP server is the OAuth 2.1 resource server. AuthKit is the authorization server. The server needs to verify bearer tokens and expose a metadata endpoint so clients can discover AuthKit automatically.
This shape doesn't change based on the transport underneath. AuthKit is the authorization server for the resource, not for the transport, so this looks the same whether the app is stateless or not. If anything, the stateless core is a cleaner fit for it: each request proves itself with its own token, and there's no session context to keep in sync.
Step 4: Make the scopes real, per tool
This is the part most MCP Apps examples skip.
AuthKit doesn't ship a dedicated "scope per MCP tool" primitive out of the box. What you actually do is create a permission in AuthKit, something like expenses:approve, assign it to the right people through AuthKit's role based access control, and check for it inside each tool handler by name. "Access to the server" isn't a permission worth having. "Can call approve_expense" is.
Wire that check into two places: inside the handler for the gated tool, which is the real enforcement, and inside the response for view_expenses, so the UI can hide the approve button for people who'd just get denied anyway.
Denying inside the handler is the actual security boundary. Hiding the button is just good manners. Someone could call approve_expense directly, skipping the UI entirely, and they'd still get rejected.
Step 5: Deploy it stateless
Because there's no session to pin a client to an instance, this deploys like any other HTTP service: multiple instances behind an ordinary load balancer, no sticky routing, no shared session store. If you want a quick sanity check that you actually removed every hidden session dependency, deploy two instances and run your test suite against both. Anything that fails only when it lands on the "wrong" instance had a session dependency you missed.
Step 6: Try it in Claude
Add the server as a connector, then ask something like "show me pending expenses." The board renders inline. Someone without the approver permission sees the same table with the approve action disabled. Someone with it can approve directly from the conversation, and the action shows up as an audited tool call, not a hidden side effect.
What's actually new here
Plenty of writeups cover the UI side of MCP Apps well: the official quickstart and build guide, a couple of community tutorials, sample repos with dashboards and PDF viewers. Plenty of other writeups, including WorkOS's own, cover per-tool scopes as a concept. What's less common is putting both pieces on the same app, with the auth enforced at the handler rather than assumed, and built against the finalized stateless spec rather than the release candidate or the older session model. That combination, not MCP Apps by itself, is the part worth writing up.