MCP Server
Authorizer ships a built-in Model Context Protocol (MCP) server. It lets an LLM agent — Claude Desktop, Claude Code, Cursor, or any MCP-compatible host — call a curated, read-only subset of Authorizer's API as tools: identify the current user and answer fine-grained authorization questions on their behalf.
The headline use case: give an AI assistant the ability to ask "is this user allowed to
see this document?" before it retrieves or summarizes content — the same
permission-aware RAG pattern as the
with-rag-fga
example (see Real-world recipes → Permission-aware retrieval),
but driven from inside the model instead of your backend.
Two ways to run it
Remote (--mcp-enabled) | Local (authorizer mcp) | |
|---|---|---|
| Transport | Streamable HTTP at POST <url>/mcp | stdio subprocess |
| Identity | per request, from the caller's own token | one process-wide --mcp-bearer |
| Runs | inside the server you already run | a second process with its own DB pool |
| Status | use this | deprecated, removed in 2.5.0 |
The stdio subcommand still works and still prints a deprecation notice. It cannot be deployed: it starts a second copy of every provider — storage, memory store, embedded FGA engine — and serves exactly one user for the lifetime of the process.
Remote MCP server
Enable it on the server you already run:
authorizer --url https://auth.example.com --mcp-enabled # ...your other flags
--url is required with --mcp-enabled, and the server refuses to start without it.
Every token presented at /mcp is checked against this deployment's canonical resource
identifier, <url>/mcp. Without --url that identifier would be derived from request
headers, which would let a caller name the audience their own token has to match — no
check at all.
Security model
- Every request carries its own token. No ambient authority, no shared credential.
- Audience-bound tokens only. A token is accepted at
/mcponly when itsaudis exactly<url>/mcp. An ordinary login token — the kind that works at/graphql,/v1/*and gRPC — is rejected here, and an MCP token is rejected there. Neither rule has an "or" in it: a token you hand to a semi-trusted agent cannot become a full API credential. - Bearer only. No cookie, no admin secret, and no admin operation reaches this surface, so it is safe to expose to the public internet and exempt from CSRF.
- Shared middleware. Because it is mounted on the main listener, it inherits CORS, security headers, rate limiting, trusted-proxy handling, request logging and metrics.
Discovery
Authorizer is both the authorization server and the resource server here, so a client needs nothing configured beyond the URL:
-
The client calls
POST https://auth.example.com/mcpwith no token. -
Authorizer answers
401withWWW-Authenticate: Bearer realm="authorizer", resource_metadata="https://auth.example.com/.well-known/oauth-protected-resource/mcp". -
The client fetches that document (RFC 9728):
{
"resource": "https://auth.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["openid", "email", "profile", "phone", "offline_access"]
} -
It reads Authorizer's own metadata from
/.well-known/oauth-authorization-server, runs the OAuth 2.1 authorization-code flow with PKCE, and passesresource=https://auth.example.com/mcpon both the authorization and token requests (RFC 8707) so the issued token is bound to this server.
That resource value must match exactly what a user types when adding the connector,
including the path — give them https://auth.example.com/mcp, not the bare origin.
An expired token gets the same 401, which is what tells a client to refresh rather than
retry. The audience binding survives refresh, so a rotated token keeps working.
Connecting a client
Verified against a real Claude Code client, so this table says what actually happens rather than what the specs allow.
| Client | Works | How |
|---|---|---|
| Claude Code, VS Code — static token | yes, verified | Mint a token bound to <url>/mcp and pass it as a fixed header (below) |
| Claude Code — OAuth | no | Claude Code refuses: "Incompatible auth server: does not support dynamic client registration" |
| Claude.ai custom connector — pasted client ID | unverified | Anthropic documents an OAuth Client ID field under Advanced settings; not confirmed here |
| Any client that needs to self-register | no | Needs RFC 7591 DCR or a Client ID Metadata Document; Authorizer has neither yet |
Authorizer does not implement RFC 7591 dynamic client registration, and Claude Code will not fall back to anything else — it refuses the server outright rather than prompting for a client ID. Until DCR or CIMD lands, the static-token path is the supported way to connect Claude Code.
# 1. Create a service account: dashboard → Identity → Clients (note the id + secret)
# 2. Mint a token bound to the MCP resource
curl -s -X POST https://auth.example.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET \
-d scope=openid \
-d resource=https://auth.example.com/mcp
# 3. Register it
claude mcp add --transport http authorizer https://auth.example.com/mcp \
--header "Authorization: Bearer $ACCESS_TOKEN"
claude mcp list should then report ✔ Connected.
The resource parameter is the part people miss: without it the token's audience
is the client id, and /mcp rejects it. That is the audience binding working, not
a bug.
Note this token identifies the service account, not a human — profile returns
nothing useful and permission checks resolve to service_account:<client_id>. For
per-user identity you need the OAuth flow, which is why DCR/CIMD support matters
and is tracked for a future release.
Why there is no /register endpoint
Authorizer deliberately does not implement RFC 7591 dynamic client registration, and this is unlikely to change.
The MCP authorization spec (2025-11-25) demoted it. Authorization servers SHOULD support Client ID Metadata Documents and MAY support DCR, which the spec keeps only "for backwards compatibility with earlier versions of the MCP authorization spec". The client priority order it defines is: pre-registered → CIMD → DCR → prompt the user.
The industry moved the same way:
| Product | Approach |
|---|---|
| Auth0 | DCR is Enterprise-only, disabled by default, and needs tenant ACLs or a reverse proxy in front. Auth0 recommends CIMD instead for production |
| Keycloak | Has had OIDC DCR for years; ships experimental CIMD |
| Google Drive's MCP server | Rejects DCR outright (HTTP 400) |
| Anthropic | Steers directory traffic to CIMD or Anthropic-held credentials, because DCR registers a fresh client on every connection |
Auth0's stated objections — resource depletion from mass registration, security probing, unvetted misconfigured clients, audit gaps — apply with more force to a self-hosted product, where every operator would inherit an open, unauthenticated write endpoint and unbounded client-row growth.
CIMD is the planned path instead. It makes the client_id an HTTPS URL that
the authorization server fetches and validates — no write endpoint, no row
growth, no schema change. It also requires a consent screen, because CIMD makes
client identity self-asserted: the spec requires the authorization server to
display the redirect URI hostname and to warn on localhost-only clients.
Exposed tools
| Tool | Auth required | Description |
|---|---|---|
meta | no | Server feature flags & provider availability. |
profile | yes | The authenticated caller's profile. |
check_permissions | yes | Batch-evaluate (relation, object) permission checks. |
list_permissions | yes | List the objects/relations the caller can access. |
Each tool's input schema is generated from the underlying proto message, so the arguments
match the REST and GraphQL request shapes exactly. For
example, check_permissions accepts:
{
"checks": [
{ "relation": "can_view", "object": "document:1" }
],
"user": "optional-explicit-subject"
}
Local stdio server (deprecated)
Kept working for existing setups, with a deprecation notice on every run. Prefer
--mcp-enabled above.
Running the server
authorizer mcp \
--client-id=YOUR_CLIENT_ID \
--database-type=sqlite \
--database-url=auth.db \
--encryption-key=your-encryption-key \
--mcp-bearer="$USER_ACCESS_TOKEN" \
--mcp-authorizer-url=https://auth.example.com
With a SQLite/Postgres/MySQL --database-type, FGA reuses the main database
automatically — no --fga-store flag needed (see
Enabling FGA). Only pass --fga-store /
--fga-store-url when the main database is NoSQL (MongoDB, DynamoDB, …) or
you want FGA on a separate store; --fga-store takes one of sqlite,
postgres, mysql, or memory — not a URI.
The mcp command inherits the root server flags (database, JWT, client-id, --fga-store,
etc.) so it can resolve identity and run the FGA engine in-process.
MCP-specific flags
| Flag | Description | Required |
|---|---|---|
--mcp-bearer | Access token attached as Authorization: Bearer <token> on every tool call. Needed for profile/*_permissions. | for auth tools |
--mcp-authorizer-url | Public URL of your Authorizer instance, used for JWT issuer validation (e.g. https://auth.example.com). | with --mcp-bearer |
Logging goes to stderr only —
stdoutis reserved for the MCP JSON-RPC stream, so never print to it.
Connecting a host
Most MCP hosts read a JSON config that declares the command to spawn. For
Claude Desktop (claude_desktop_config.json) or Claude Code
(.mcp.json):
{
"mcpServers": {
"authorizer": {
"command": "authorizer",
"args": [
"mcp",
"--client-id", "YOUR_CLIENT_ID",
"--database-type", "sqlite",
"--database-url", "auth.db",
"--encryption-key", "your-encryption-key",
"--mcp-bearer", "USER_ACCESS_TOKEN",
"--mcp-authorizer-url", "https://auth.example.com"
]
}
}
}
Restart the host; the authorizer tools (meta, profile, check_permissions,
list_permissions) become available to the model.
Errors
When a tool call fails — bad arguments, an unauthenticated call, or a permission denial —
the server returns an MCP tool result with isError: true and the error message as text,
so the host surfaces it to the model as a recoverable failure (not a protocol abort).
Typical messages mirror the gRPC status: Unauthenticated, PermissionDenied,
FailedPrecondition (e.g. fga is not enabled).
Authorizer as the authorization server protecting your own MCP server
Everything above is about Authorizer's own MCP surface. The other direction is just as common: your MCP server, hosted anywhere, needs a real OAuth 2.1 authorization server in front of it, and Authorizer can be that AS. Same specs, different division of labour — there, you implement the resource-server half:
| Spec | What it says | Who implements it |
|---|---|---|
| OAuth 2.1 | Bearer tokens, token endpoint, client auth | Authorizer (/oauth/token, JWKS, OIDC discovery) |
| RFC 9728 (protected resource metadata) | Your resource server publishes /.well-known/oauth-protected-resource naming its resource URI and authorization_servers, pointed at from a 401 WWW-Authenticate header | your MCP server |
| RFC 8707 (resource indicators) | The client passes resource=<your MCP server URI> when requesting a token; the AS binds the token's aud to it | Authorizer binds it (see the resource parameter on the Authorization Endpoint and Token Exchange); your MCP server enforces aud on every call |
| RFC 8693 (token exchange) | An agent gets a token that says "agent X acting for user Y" — see Token Exchange & Delegation | Authorizer (grant_type=...token-exchange on /oauth/token) |
Authorizer also serves the RFC 8414 alias
/.well-known/oauth-authorization-server — the identical metadata document as
/.well-known/openid-configuration — for MCP clients that probe the OAuth-only
discovery path instead of falling back to OIDC discovery.
The with-mcp example
builds this end to end: a ~150-line Express MCP server that validates Authorizer-issued
JWTs (issuer + aud) and a client walkthrough that goes 401 → RFC 9728 discovery →
client_credentials (rejected — wrong aud) → RFC 8693 token exchange with
resource=<mcp-server-uri> → 200. Its own bonus section documents this page's
built-in stdio server too, for the "which one do I want" question.
See also
- Authorization (FGA) — the relationship model behind the permission tools.
- REST API / GraphQL API — the same operations over HTTP.
- FGA Guide — building an authorization model and writing tuples.
- OAuth2 / OIDC — the authorization-server surface used to protect a custom MCP server.
- Token Exchange & Delegation — agent-acting-for-user delegated tokens (RFC 8693).