What this is
Give an MCP server per-user permissions without deploying an authorization system.
An OAuth 2.1 resource server plus an access policy, as a library you import
rather than a proxy you deploy. It speaks MCP Streamable HTTP 2026-07-28.
You bring the authorization server. WorkOS, Stytch and Auth0 all do the job. This package never runs consent, client registration or PKCE. It verifies tokens whose audience is your public MCP URL, decides what the caller may do, and registers only the capabilities they may use.
Where it fits
Section titled “Where it fits”Reach for it when you can touch the server’s code, or wrap the builder that makes it, and the callers are people who already sign in somewhere. Dana reads cases, Alice also writes them, and that decision sits next to the handler running the query. You install a dependency, and you run nothing new.
Plenty of good tools solve the neighbouring problems. Take one of them when its row describes you:
| Approach | Example | What you take on |
|---|---|---|
| MCP gateway | agentgateway | A data-plane component to deploy and keep available |
| Gateway plus policy service | Permit | The gateway, and the policy infrastructure behind it |
| External decision point | Cerbos | A PDP to run, and a call out to it on every decision |
| Relationship-based permissions | Auth0 FGA | Richer rules than this offers, in a second authorization system |
| Framework feature | FastMCP | Convenience, tied to that framework |
| OAuth plumbing | official SDK, mcp-auth | Authentication and resource-server mechanics, with no permission model |
| Embedded dependency | mcp-authz | One package, and a policy engine that stays small by design |
You need one integration seam
Section titled “You need one integration seam”The seam you get decides the tool. Owning the source is the best case. Wrapping somebody else’s builder works too.
Look again at the middle rung. A package you install, whose tools somebody
else wrote, still works here as long as its builder hands you the server before
returning it. That hook is two lines, and it is worth asking an upstream
maintainer for. gate() (TypeScript) and
gate() (Python) show both sides of it.
Without the hook nothing in this package can help you: the SDK keeps a built server’s tool list private, so no code can filter what it never saw.
What running in-process buys you
Section titled “What running in-process buys you”A proxy knows what crosses the protocol boundary, which is a tool name and a JSON blob. A library already has your types, your handlers and your domain.
- Compile-time permission names. A typo fails the build rather than a request at 3am.
- Boot-time reconciliation. A tool no role can reach fails startup. A permission no tool requires warns. A gateway cannot run this check, because it does not learn your tool list until traffic arrives.
- Authorization context inside the handler. A proxy can decide whether Dana
may call
list_cases. Your application can also constrain which organisation’s cases she sees, in the place the query is written. - Audit that names the resource. The library cannot know that
{ id: 'C1234' }is a case. Yourauditcallback does. - A person in the loop, on the arguments.
approval: ({ force }) => forceasks a human before that one call runs, and the answer lands in the same audit event as the caller. A proxy sees{"force":true}and cannot tell you it matters. - A scope gap the client can act on. A proxy sees a refusal and can say
only that one happened. The library knows Dana holds
cases:writeand that her token lackswrite, so the challenge names that scope. She re-authorises for it instead of walking the whole consent dialog again. - Deploys where a proxy cannot. Vercel, Workers, Lambda, a Next.js route.
FastMCP and canAccess
Section titled “FastMCP and canAccess”FastMCP gates a capability with a predicate:
canAccess: requireRole('admin');canAccess: (auth) => auth?.role === 'admin' && auth?.department === 'engineering';That runs, and for one rule in one place it is the right amount of machinery.
A predicate carries no vocabulary. TypeScript cannot derive permission names
from it, startup cannot compare the tools against the roles, and each closure
agrees with the others only because you kept them in step by hand.
definePolicy hands you one object that the compiler reads and that boot
reconciles:
permission: 'cases:wrtie'failstscrather than a request at 3am.- A tool requiring
cases:deletethat no role grants refuses to boot. - An administrator edits one file instead of eleven closures.
Twenty tools sharing a vocabulary is where this pays. One tool with one rule is where it does not.
Deployment
Section titled “Deployment”You ship one MCP server process with this library inside it. Claude dials your public URL. Your authorization server runs login. TestRail or Jira keeps one service credential. The full stack, including what each box does and how catalogues differ per caller, is in Deployment.
mcp-authz is the resource server: it verifies tokens and calls
createServer(context). Consent, client registration, and PKCE belong to the
authorization server. Google Workspace signs people in but cannot mint a token
whose audience is your MCP URL. WorkOS, Stytch, and Auth0 cover both halves.