OpenAPI mode
mcp_authz.openapi gates an HTTP API the way gate gates an MCP server: the
served OpenAPI document is cut to the caller, and the request is checked whether
or not they ever read it.
from mcp_authz import JwtVerifierfrom mcp_authz.openapi import OpenApiAuthorizationMiddleware, record_operations, to_permissions_module
app = OpenApiAuthorizationMiddleware( api, # any ASGI app: FastAPI, Starlette, Litestar, Django spec=spec, permissions=PERMISSIONS, # {"getCase": "cases:read", "deleteCase": "cases:delete", ...} policy=policy, token_verifier=JwtVerifier( issuer="https://auth.acme.com", jwks_uri="https://auth.acme.com/.well-known/jwks.json", resource="https://api.acme.com", ), resource_server_url="https://api.acme.com", authorization_servers=["https://auth.acme.com"], emitter="cases-api", on_audit=lambda event: log.info("audit", extra=event.to_dict()),)It is ASGI middleware rather than a framework plugin, so the API underneath keeps whatever it already uses.
Build the map from the document
Section titled “Build the map from the document”print(to_permissions_module(record_operations(spec)))Every operation is priced TODO:unassigned, which no role grants, so
construction fails until a person has decided what each one costs. An operation
with no operationId is refused rather than named for you: a generated fallback
changes under a path rename, and the map then silently stops matching the route
it was written for.
What it refuses
Section titled “What it refuses”- A route the document does not describe, with a 404 that says so. Routes you deliberately keep out of the spec belong outside this middleware rather than behind it.
- An operation the map does not price, at construction.
- A map entry naming an operation the document no longer has, at construction.
- A permission no role grants, at construction.
Events are the ones described in Audit and
decisions, with kind="operation", the
operationId as name and the concrete path as resource. The phase follows
the response status: what an auditor is asking is whether the action happened,
and over HTTP that is the status rather than whether a coroutine raised.