Authorizing an MCP server

A realm can act as the authorization server for a Model Context Protocol server, so clients like Claude Code and Claude Desktop sign your users in and receive a token scoped to your MCP server — without you registering each client by hand. This page walks the whole setup and the mistakes that are easy to make.

How it fits together

Three parties, and it helps to keep them straight:

PartyWho that is
Authorization serverYour SemAuth realm. Signs users in and issues the token.
Resource serverYour MCP server. Validates the token and serves the tools.
ClientClaude Code, Claude Desktop, or another MCP client.

The client identifies itself with a Client ID Metadata Document (CIMD): its client_id is an HTTPS URL, and the realm fetches that URL to learn the client's redirect URIs. You never create the client — you decide which origins may register, and the realm does the rest.

Before you start

Step 1 — register your MCP server as an API

Do this first. A client that registers before the API exists is created without any audience it may request, and connecting will fail until you fix that client by hand.

Go to APIs → Register a resource server:

FieldValue
IdentifierA short name, e.g. acme-mcp. Cannot be changed later.
AudienceYour MCP server's canonical URI, e.g. https://mcp.acme.com/mcp.
ScopesAt least one, e.g. mcp:use. See the note on scopes below.

The audience must match your metadata's resource byte for byte. It is compared verbatim and stamped into the token's aud, so https://mcp.acme.com/mcp and https://mcp.acme.com/mcp/ are different servers as far as the realm is concerned. Getting this wrong does not fail at setup — it fails later, when your MCP server rejects a token that looks otherwise valid.

Step 2 — allow the client's origin to register

On the realm's settings page, under Client self-registration, add the hosts whose metadata documents you will accept, then turn the feature on:

claude.ai
chatgpt.com

Entries are an exact host or a subdomain wildcard (*.claude.ai). A wildcard does not match its own apex — list both if you want both. Wildcards over a top-level domain (*.com) or a shared hosting domain (*.example-host.app) are refused, because they would admit every tenant of that provider.

The allowlist is the control, not the switch. The document is fetched over HTTPS from the origin named in the client_id, so that origin is vouching for its own redirect URIs. An empty list accepts nothing, and the feature cannot be enabled without at least one host.

How the client authenticates

You do not configure this — the client's metadata document declares it, and the realm accepts two methods:

MethodWhat it means
noneA public client. PKCE proves the code was not intercepted. This is what Claude Code uses.
private_key_jwtThe client signs a short-lived assertion with a private key and publishes the public half at jwks_uri, which the realm fetches to verify it. Stronger, and what ChatGPT's connector declares.

The client_secret_* methods are refused, and not as a local restriction: a shared secret cannot be established with a client that registered itself by publishing a file, so the CIMD specification forbids them in a metadata document.

A client's jwks_uri may live on a different domain from its metadata document — that is ordinary, and the realm does not require it to appear in your allowlist. It must be HTTPS and must resolve to a public address.

Step 3 — point your MCP server at the realm

Your MCP server publishes protected-resource metadata (RFC 9728). Two fields matter:

{
  "resource": "https://mcp.acme.com/mcp",
  "authorization_servers": ["https://{issuer_id}.semauth.app"],
  "scopes_supported": ["mcp:use"]
}

Return a 401 with a WWW-Authenticate header pointing at that document when a request arrives without a token. That is how a client discovers where to send the user.

About scopes

If your metadata declares no scopes_supported and your 401 names no scope, the client has nothing to ask for. The realm then issues a token whose scope claim is empty. That is harmless if your server does not check scopes — but if it does, declare the scope in your metadata and register the same one on the API, or every request will be refused for a permission the client was never told to request.

What your users see

  1. The client opens the realm's sign-in page.
  2. The user signs in with whatever the realm allows.
  3. A consent screen names the application and the host the token will be sent to. For a desktop client that host is on the user's own machine, and the screen says so — the realm cannot verify which local program is listening.
  4. The client receives the token and calls your MCP server.

Consent is asked once per user per client, and can be withdrawn from the user's account page. Withdrawing it stops future authorizations; it does not revoke tokens already issued.

What your MCP server receives

An RS256 JWT, verifiable against the realm's /jwks.json:

ClaimValue
issThe realm's issuer URL.
audThe audience you registered — check this.
subThe user's stable identifier within the realm.
scopeThe granted scopes, intersected with what the API declares.
exp / iat / jtiOne-hour lifetime.

Validate that aud names your server and reject anything else. A token minted for another resource must not be accepted, even from the same realm.

Troubleshooting

What you seeWhat it means
audience not in client's allowed_audiences The client registered before you created the API, so it holds no audience. Register the API, then have the client re-register — or fix the existing client's audiences.
audience is not a known visible API The resource does not match any API registered by this organization. Compare it character by character, including the trailing slash.
client_id host … is not allowed by this realm The client's metadata URL is on a host you have not allowlisted.
client id metadata documents are not enabled for this realm Self-registration is off for this realm.
this realm does not accept self-registered clients The realm is an admin realm. Use an ordinary realm.
Invalid redirect_uri The callback is not among those the metadata document declares.
token_endpoint_auth_method … is not supported The document asks for a method the realm does not accept — in practice one of the client_secret_* family. Only none and private_key_jwt are available to a self-registered client.
declares private_key_jwt but no jwks_uri The document names the method without saying where its public keys are, so no assertion could be verified.
The token is opaque, not a JWT No resource reached the realm. Confirm the client sends it and that your metadata names the right authorization server.