Skip to main content
Version: 2.x (Latest)

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)
TransportStreamable HTTP at POST <url>/mcpstdio subprocess
Identityper request, from the caller's own tokenone process-wide --mcp-bearer
Runsinside the server you already runa second process with its own DB pool
Statususe thisdeprecated, 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 /mcp only when its aud is 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:

  1. The client calls POST https://auth.example.com/mcp with no token.

  2. Authorizer answers 401 with WWW-Authenticate: Bearer realm="authorizer", resource_metadata="https://auth.example.com/.well-known/oauth-protected-resource/mcp".

  3. 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"]
    }
  4. It reads Authorizer's own metadata from /.well-known/oauth-authorization-server, runs the OAuth 2.1 authorization-code flow with PKCE, and passes resource=https://auth.example.com/mcp on 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.

ClientWorksHow
Claude Code, VS Code — static tokenyes, verifiedMint a token bound to <url>/mcp and pass it as a fixed header (below)
Claude Code — OAuthnoClaude Code refuses: "Incompatible auth server: does not support dynamic client registration"
Claude.ai custom connector — pasted client IDunverifiedAnthropic documents an OAuth Client ID field under Advanced settings; not confirmed here
Any client that needs to self-registernoNeeds 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:

ProductApproach
Auth0DCR is Enterprise-only, disabled by default, and needs tenant ACLs or a reverse proxy in front. Auth0 recommends CIMD instead for production
KeycloakHas had OIDC DCR for years; ships experimental CIMD
Google Drive's MCP serverRejects DCR outright (HTTP 400)
AnthropicSteers 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

ToolAuth requiredDescription
metanoServer feature flags & provider availability.
profileyesThe authenticated caller's profile.
check_permissionsyesBatch-evaluate (relation, object) permission checks.
list_permissionsyesList 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

FlagDescriptionRequired
--mcp-bearerAccess token attached as Authorization: Bearer <token> on every tool call. Needed for profile/*_permissions.for auth tools
--mcp-authorizer-urlPublic URL of your Authorizer instance, used for JWT issuer validation (e.g. https://auth.example.com).with --mcp-bearer

Logging goes to stderr only — stdout is 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:

SpecWhat it saysWho implements it
OAuth 2.1Bearer tokens, token endpoint, client authAuthorizer (/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 headeryour 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 itAuthorizer 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 & DelegationAuthorizer (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