Typos fail the build
Permission names come from the policy literal, so permission: 'cases:wrtie' is a compile error in
TypeScript rather than a 403 at 3am.
An OAuth 2.1 resource server plus an access policy, shipped as a library you import. You bring the authorization server. This verifies tokens whose audience is your public MCP URL, works out what the caller may do, and registers only the capabilities they may use.
const policy = definePolicy({ roles: { reader: ['cases:read'], editor: ['cases:read', 'cases:write'], }, rules: [ { match: { domain: 'acme.com' }, role: 'reader' }, { match: { email: 'alice@acme.com' }, role: 'editor' }, ],});
const { tool, server } = authz(policy);
const createServer = server( [ tool('get_case', { permission: 'cases:read', inputSchema }, getCase), tool('update_case', { permission: 'cases:write', inputSchema }, updateCase), ], { name: 'acme', version: '1.0.0' },);Dana calls tools/list and sees get_case. Alice sees both. update_case is
not hidden from Dana. It was never registered for her request.
policy = define_policy({ "roles": { "reader": ["cases:read"], "editor": ["cases:read", "cases:write"], }, "rules": [ {"match": {"domain": "acme.com"}, "role": "reader"}, {"match": {"email": "alice@acme.com"}, "role": "editor"}, ],})
server = AuthorizedMCPServer("cases", policy=policy, token_verifier=..., auth=...)
@server.tool(permission="cases:read")def get_case(case_id: str) -> dict[str, str]: ...
@server.tool(permission="cases:write")def update_case(case_id: str, title: str) -> dict[str, str]: ...Dana calls tools/list and sees get_case. Alice sees both. Calling
update_case by name without the permission earns
forbidden / policy_denied rather than a silent empty result.
Typos fail the build
Permission names come from the policy literal, so permission: 'cases:wrtie' is a compile error in
TypeScript rather than a 403 at 3am.
Startup catches dead rules
A capability that no role can reach stops the server booting. A gateway cannot run that check, because it does not learn your tool list until traffic arrives.
The principal reaches your handler
A proxy can decide whether Dana may call list_cases. Your handler can also scope the query to her
organisation, in the one place that knows how.
Nothing new to operate
No sidecar, no policy service, no extra hop. It deploys wherever your server already deploys, including Vercel, Workers and Lambda.
npm install mcp-authz @modelcontextprotocol/serverpip install mcp-authz # imports as mcp_authzBoth packages run the same versioned conformance fixtures, so a policy decision means the same thing in either language. Their releases are independent.