Skip to main content

Host an MCP server

This guide shows how to protect a Model Context Protocol (MCP) server with Defakto so that both workloads and people can call it. Use it when you operate an MCP server in your trust domain and want to serve:

  • Workloads, such as AI agents and services, authenticated by their SPIFFE identities
  • People using MCP clients, such as Claude, who sign in with your company's identity provider

How it works​

MCP requires OAuth, so the MCP server needs an authorization server. Defakto's OAuth server fills that role. Your MCP server doesn't implement any OAuth flows itself. It tells clients to use Defakto, and it validates the access tokens that callers present.

Both kinds of callers end up with the same thing: a short-lived access token that Defakto issued for your MCP server's URL. Only the way they get it differs.

  • A workload presents its JWT-SVID to Defakto's token endpoint and receives an access token in which sub is the workload's SPIFFE ID. This is the JWT bearer grant (RFC 7523), which many providers call workload identity federation. No person is involved.
  • A person's MCP client, such as Claude, signs the person in by following the standard MCP authorization flow. The person signs in with your identity provider, and Defakto then issues the MCP client an access token in which sub is the person and client_id is the MCP client. This is the OAuth authorization code grant with PKCE.

People using MCP clients need no setup beyond adding your server's URL to their client, as long as their client is one that your trust domain allows. For how a workload gets a token and calls your server, see Call an MCP server from a workload.

Before you begin​

Setting up an MCP server has two parts:

  1. Set up the trust domain. Your Defakto administrators do this once for the trust domain.
  2. Set up your MCP server. Each MCP server's owner does this for their server.

Example values​

This page uses these example values. Replace them with your own as you go.

ValueExample
The address where you host your Trust Domain Servers, which also serves the OAuth serverhttps://td-server.defakto.example.com
Your MCP server's URLhttps://helpdesk.example.com/mcp
The scopes your MCP server usestickets.read, tickets.write
Your identity provider's issuer URLhttps://idp.example.com
The name you give your identity provider in the configurationcorp-idp

Part 1: Set up the trust domain​

After this setup, every MCP server in the trust domain can use Defakto as its authorization server. If your trust domain is already set up, skip to Part 2.

You need:

  • A trust domain that meets the OAuth server's requirements
  • Permission to create an application registration in your identity provider, if people will authenticate to use your MCP server from an interactive app, such as Claude

Step 1: Set the OAuth server's address​

MCP clients look up the OAuth server's endpoints in a metadata document (RFC 8414), as the MCP authorization specification requires. Defakto hosts this document for you, and the endpoints it lists run on your Trust Domain Servers.

If you host your own Trust Domain Servers, you choose the address where callers reach them, and Defakto needs that address to list the endpoints. Set it as publicBaseURL:

oauth-server.yaml
section: OAuthServer
schema: v1
spec:
endpoints:
publicBaseURL: https://td-server.defakto.example.com

Apply the configuration to your trust domain. You can apply Managed Configuration through the Defakto API, Terraform, or the Defakto CLI. See applying configuration. For example, with the CLI:

defakto config set trust-domain --id <trust-domain-id> oauth-server.yaml

If Defakto hosts your Trust Domain Servers, Defakto already knows their address, so skip this step.

Step 2 (optional): Let people sign in​

Do this step if people will use your MCP server from an interactive app, such as Claude, and authenticate with their own accounts. Workloads don't need it.

If your identity provider has a provider guide, follow it for this step.

Register Defakto in your identity provider​

People sign in through your identity provider, so Defakto needs an application registration there, the same as any web application that signs people in.

  1. In your identity provider, create an OpenID Connect web application for Defakto.
  2. Set its redirect URI to the publicBaseURL you set in step 1, followed by /oauth/upstream-callback. For example, with publicBaseURL: https://td-server.defakto.example.com, the redirect URI is https://td-server.defakto.example.com/oauth/upstream-callback. Some identity providers call this a sign-in redirect URI or callback URL.
  3. Choose how Defakto authenticates to your identity provider:
    • private_key_jwt (recommended): Defakto signs a client assertion with the trust domain's OAuth signing key, and your identity provider verifies it with Defakto's public keys. No secret is stored anywhere. To set it up:

      1. In the application's client authentication settings, choose private key JWT.
      2. In the Defakto console, open your trust domain's Settings > Trust domain URLs, and copy the OAuth JWKS Endpoint. It's also in the output of defakto trust-domain info <trust-domain>.
      3. Add the OAuth JWKS Endpoint to the application's JWKS URL setting.

      Defakto rotates its signing keys automatically, and the JWKS URL lets your identity provider pick up new keys as they're published. If your identity provider accepts only uploaded keys, not a JWKS URL, use client_secret_basic instead, because uploaded keys stop working after the next rotation.

    • client_secret_basic: Defakto sends a client secret that you store in a Kubernetes Secret. See clientAuth for how to label the Secret.

  4. Record the application's client ID.

Add your identity provider and MCP clients to the configuration​

Add your identity provider to oauth-server.yaml, with login so that people can sign in through it. Then add oauthClients, an allowlist of the MCP clients, such as Claude, that can request access tokens for your MCP server. Defakto refuses requests from MCP clients that aren't on the list:

oauth-server.yaml
section: OAuthServer
schema: v1
spec:
endpoints:
publicBaseURL: https://td-server.defakto.example.com

identityProviders:
- name: corp-idp
issuer: https://idp.example.com
login:
clientID: <client-id-from-your-identity-provider>
clientAuth:
method: private_key_jwt
scopes: [openid]

oauthClients:
- clientID: https://claude.ai/oauth/mcp-oauth-client-metadata
  • issuer is your identity provider's issuer URL.
  • login.clientID is Defakto's own client ID at your identity provider, which you recorded when you registered Defakto. Defakto uses it when it signs people in with your identity provider.
  • login.clientAuth.method is how Defakto authenticates to your identity provider, which you chose when you registered Defakto there.
  • scopes are the scopes Defakto requests from your identity provider when a person signs in. openid is required for OpenID Connect sign-in. These are different from your MCP server's scopes.
  • oauthClients is the allowlist of approved OAuth clients that identify themselves with a Client ID Metadata Document, which includes common MCP clients such as Claude. Each clientID here is the URL that the client publishes as its client ID. For details, see OAuth clients.

Depending on your identity provider, you might need additional token validation rules. For example, with Google as your identity provider, Defakto requires a rule that accepts only accounts from your company's Google Workspace domain. Otherwise, anyone with a Google account could sign in. You can also add rules of your own, such as requiring a verified email address. For details, see The ID token from sign-in.

Apply the updated configuration to your trust domain. You can apply Managed Configuration through the Defakto API, Terraform, or the Defakto CLI. See applying configuration. For example, with the CLI:

defakto config set trust-domain --id <trust-domain-id> oauth-server.yaml

Step 3 (optional): Restrict access​

Without a token policy, any workload in your trust domain can get an access token for your MCP server, and so can anyone who signs in with your identity provider (if you completed step 2). Your MCP server still decides what each caller may do, based on the identity in the token, as it would with any authorization server.

A token policy adds a central control in front of that. Callers that the policy doesn't allow never receive a token for your MCP server, so they can't call it at all, and allowed callers can be limited to fewer scopes than they ask for. For example, this policy lets only workloads whose SPIFFE IDs start with spiffe://example.com/agents/, and people signed in through any allowed MCP client, get tokens for the helpdesk MCP server, each with limited scopes. It refuses every other request:

spec:
# endpoints from step 1, and identityProviders from step 2 if you completed it
policy:
authorization:
unmatched: deny
allow:
- name: agents-call-helpdesk
match:
caller:
spiffeID: spiffe://example.com/agents/**
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://helpdesk.example.com/mcp
token:
allowedScopes: [tickets.read, tickets.write]

- name: people-call-helpdesk
match:
caller:
type: oauthClientID
user:
identityProvider: corp-idp
via: login
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://helpdesk.example.com/mcp
token:
allowedScopes: [tickets.read]
  • agents-call-helpdesk lets workloads whose SPIFFE IDs start with spiffe://example.com/agents/ get access tokens for the MCP server, with at most the tickets.read and tickets.write scopes.
  • people-call-helpdesk lets any allowed MCP client get access tokens for people who signed in with corp-idp, with at most the tickets.read scope. Leave this rule out if you skipped step 2.
  • unmatched: deny refuses everything else.

See the token policy reference for every kind of rule.

Part 2: Set up your MCP server​

You need:

  • Part 1 completed for your trust domain
  • An MCP server whose authorization settings you can change

Step 1: Point your MCP server at Defakto​

MCP clients find an MCP server's authorization server through the server's protected resource metadata, as the MCP authorization specification requires. Your MCP server names Defakto there by its OAuth issuer URL.

First, get your trust domain's OAuth issuer URL. Find the OAuth Issuer in the Defakto console, under your trust domain's Settings > Trust domain URLs, or ask your Defakto administrators for it. With the CLI:

defakto trust-domain info <trust-domain> --output json | jq -r '.oauth_issuer.issuer'

Then give that URL to your MCP server. Most MCP server SDKs have a setting for the authorization server URL, and take care of the rest. If yours doesn't, follow Authorization Server Discovery in the MCP authorization specification.

Either way, your MCP server's protected resource metadata lists the OAuth issuer URL as its authorization server. For example:

{
"resource": "https://helpdesk.example.com/mcp",
"authorization_servers": ["<OAuth issuer URL>"],
"scopes_supported": ["tickets.read", "tickets.write"],
"bearer_methods_supported": ["header"]
}

Step 2: Validate access tokens​

Your MCP server must validate the access token on every request. Most MCP server SDKs and platforms do this for you, because token validation is a standard part of MCP authorization, and you only configure the OAuth issuer URL and your MCP server's URL. If you validate tokens yourself, follow RFC 9068, using your trust domain's OAuth issuer, the keys at its OAuth JWKS Endpoint, and your MCP server's URL as the audience. See Validating an access token.

What your MCP server receives​

The access token tells your MCP server who is calling. A request from a workload carries a token like this:

{
"iss": "<OAuth issuer URL>",
"sub": "spiffe://example.com/agents/triage",
"aud": "https://helpdesk.example.com/mcp",
"client_id": "spiffe://example.com/agents/triage",
"scope": "tickets.read tickets.write",
"jti": "e311bd50311d5c936825e3e7b422a9ba",
"iat": 1792598475,
"exp": 1792598775
}

A request from a person who signed in through Claude carries a token like this:

{
"iss": "<OAuth issuer URL>",
"sub": "alice-8f3k2",
"aud": "https://helpdesk.example.com/mcp",
"client_id": "https://claude.ai/oauth/mcp-oauth-client-metadata",
"scope": "tickets.read",
"jti": "5b7e0c2a9d4f4e18a3c6b1d2e8f09a7c",
"iat": 1792598500,
"exp": 1792598800
}
ClaimFrom a workloadFrom a person
subThe workload's SPIFFE IDThe person's identifier from the identity provider, by default the sub of their ID token. It's never a SPIFFE ID.
client_idThe workload's SPIFFE IDThe MCP client's Client ID Metadata Document URL
scopeThe scopes granted to the workload, which can be fewer than it requestedThe scopes granted to the MCP client, which can be fewer than it requested

Use sub to decide what the caller may do. For a person, sub is the same identifier that your identity provider uses, so you can look the person up in your own systems. See issued tokens for every claim.

Test your setup (optional)​

Setup is complete. These quick checks are optional, and confirm that MCP clients can find Defakto, and that callers can get tokens for your MCP server. If a check fails, see Troubleshooting.

  1. Check discovery. Fetch the OAuth server's metadata, and confirm that authorization_endpoint and token_endpoint use your publicBaseURL. If you've just applied the configuration, the metadata can take a few minutes, and up to an hour, to show it. Find the OAuth Authorization Server Metadata URL in the Defakto console, under your trust domain's Settings > Trust domain URLs:

    curl "<OAuth Authorization Server Metadata URL>"
  2. Test a workload caller. From a workload whose SPIFFE ID starts with spiffe://example.com/agents/, request a token with defakto-dbg:

    defakto-dbg oauth-token \
    --token-endpoint https://td-server.defakto.example.com/oauth/token \
    --resource https://helpdesk.example.com/mcp
  3. Test a person. In Claude, add the MCP server as a custom connector with the URL https://helpdesk.example.com/mcp, then connect to it. Your browser opens your identity provider's sign-in page. After you sign in, Claude can call the MCP server's tools.

Troubleshooting​

The identity provider reports a redirect URI mismatch. The redirect URI registered in Part 1 must be exactly <publicBaseURL>/oauth/upstream-callback, with the same scheme, host, and path as endpoints.publicBaseURL.

A person signs in, but the MCP client then reports an authorization error. If you've added a token policy, the request may not match the rule you expected. Check each field in the rule against the request. For example, if the rule matches on audience, the value must be exactly the URL the MCP client sent, including any trailing slash. The OAuth server's audit event for the request shows which rules matched.

Callers get tokens, but your MCP server refuses some of their operations. If you've added a token policy, its allowedScopes may grant fewer scopes than callers ask for. Defakto then issues tokens with only the allowed scopes, and your MCP server refuses operations that need the others. Check allowedScopes in the rules that match those callers.

A workload receives invalid_grant. The most common cause is a JWT-SVID with the wrong audience. The JWT-SVID's audience must be the OAuth issuer, not the MCP server. See Call an MCP server from a workload.

The MCP client never opens a sign-in page. The MCP client can't find the authorization server. Check that the MCP server returns 401 with a WWW-Authenticate header, and that its protected resource metadata lists the OAuth issuer URL.

The browser shows "Sign-in could not be completed", with the reference unknown-client. The MCP client isn't in oauthClients. Check that its client ID is listed exactly as the client publishes it, character by character. To find the client ID the MCP client sent, look in the Trust Domain Server's logs for oauth authorize: client not admitted. See OAuth clients.

The MCP server rejects valid-looking tokens. Check that it compares iss with the OAuth issuer, not your JWT-SVID issuer, and that it compares aud with the same URL that callers request.