Call an MCP server from a workload
This guide shows how a workload in your trust domain, such as an AI agent or a service, calls an MCP server using its SPIFFE identity. The workload needs no client secret or API key.
How it works
The MCP server accepts OAuth access tokens, not SVIDs, so your workload exchanges its JWT-SVID for an access token from your trust domain's OAuth server. To do this, the workload follows the standard MCP authorization flow:
- The workload asks the MCP server which authorization server it uses, and finds your trust domain's OAuth server (standard MCP authorization server discovery).
- The workload gets a JWT-SVID, as it gets JWT-SVIDs today, with its audience set to your trust domain's OAuth issuer URL.
- The workload sends the JWT-SVID to the OAuth server's token endpoint and names the MCP server it wants to call (the OAuth JWT bearer grant, which many providers call workload identity federation).
- The OAuth server returns a short-lived access token for that MCP server.
- The workload calls the MCP server with the access token (standard MCP authorization).
Before you begin
You need:
- A trust domain that meets the OAuth server's requirements
- An MCP server set up to accept Defakto access tokens. See Host an MCP server.
- A workload that can get JWT-SVIDs, and can reach both the MCP server and your Trust Domain Servers
Example values
This page uses these example values. Replace them with your own as you go.
| Value | Example |
|---|---|
| The MCP server's URL | https://helpdesk.example.com/mcp |
| The OAuth server's token endpoint, which you discover in step 1 | https://td-server.defakto.example.com/oauth/token |
| The scopes your workload needs | tickets.read |
Check your MCP client SDK first
Almost everything in this flow is standard: MCP's discovery of the authorization server, the OAuth JWT bearer grant, and renewing tokens before they expire. Your MCP client SDK may handle some or all of it, either by supporting the workload identity federation pattern natively or through a token provider hook. Check your SDK's documentation before writing your own code.
If your SDK doesn't support the pattern, follow these steps.
Step 1: Discover the OAuth server
Find the MCP server's authorization server the same way any MCP client does, as described in the MCP authorization specification:
-
Send a request to the MCP server without an
Authorizationheader. The MCP server responds with HTTP401and aWWW-Authenticateheader that points to its protected resource metadata:WWW-Authenticate: Bearer resource_metadata="https://helpdesk.example.com/.well-known/oauth-protected-resource/mcp" -
Fetch the protected resource metadata. Its
authorization_serversfield lists your trust domain's OAuth issuer URL:{"resource": "https://helpdesk.example.com/mcp","authorization_servers": ["<OAuth issuer URL>"]} -
Fetch the OAuth server's authorization server metadata for that issuer, as MCP clients do, and read its
issuerandtoken_endpointfields:{"issuer": "<OAuth issuer URL>","token_endpoint": "https://td-server.defakto.example.com/oauth/token"}
Keep issuer for step 2 and token_endpoint for step 3. These values rarely change, so you can cache them.
Step 2: Get a JWT-SVID for the OAuth server
Get a JWT-SVID the usual way, through the SPIFFE Workload API or, in serverless runtimes, the Defakto serverless SDK. Set its audience to the issuer from step 1, not the MCP server's URL.
Step 3: Exchange the JWT-SVID for an access token
Request an access token from the token_endpoint you discovered in step 1, using the OAuth JWT bearer grant (RFC 7523).
POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<JWT-SVID from step 2>
&resource=<MCP server URL>
&scope=<scopes, separated by spaces>
The body is shown on separate lines for readability. Send it as a single URL-encoded form body.
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | Exactly urn:ietf:params:oauth:grant-type:jwt-bearer |
assertion | Yes | The JWT-SVID from step 2 |
resource | Yes | The MCP server's URL, such as https://helpdesk.example.com/mcp. Send the same string the MCP server uses as its own URL. |
scope | No | The scopes your workload needs, separated by spaces, such as tickets.read. Without it, the access token has no scopes. |
A successful response has HTTP status 200 and a JSON body:
{
"access_token": "<access token>",
"token_type": "Bearer",
"expires_in": 300,
"scope": "tickets.read"
}
| Field | Meaning |
|---|---|
access_token | The access token to send to the MCP server |
token_type | Bearer |
expires_in | Seconds until the access token expires |
scope | The scopes granted, which can be fewer than you requested. Left out when the access token has no scopes. |
A failed request has HTTP status 400 or 401, and a JSON body with an OAuth error code, such as:
{
"error": "invalid_grant",
"error_description": "<details>"
}
See Troubleshooting for common error codes.
Step 4: Call the MCP server
Send the access token in the Authorization header of every request to the MCP server:
Authorization: Bearer <access token>
Reuse the access token until shortly before it expires, as given by expires_in. Then repeat steps 2 and 3 to get a new one. Request a new JWT-SVID each time, rather than reusing the one from the previous exchange, because JWT-SVIDs are also short-lived, and the access token never outlives the JWT-SVID it was exchanged from.
Troubleshooting
To see why a request failed, open your trust domain in the Defakto console, and go to Issuance > Token Exchanges.
The MCP server doesn't return a 401 with a WWW-Authenticate header. The MCP server isn't set up to advertise its authorization server. Ask the MCP server's owner to set it up as described in Host an MCP server.
The token endpoint returns invalid_grant. The OAuth server rejected the JWT-SVID. The most common cause is the wrong audience. The JWT-SVID's audience must be the issuer you discovered in step 1, not the MCP server's URL. An expired JWT-SVID causes the same error, so request a fresh one for each exchange.
The token endpoint returns invalid_request. A required parameter is missing or repeated. Check that the request includes grant_type, assertion, and exactly one resource.
The token endpoint refuses the request, but the JWT-SVID is correct. Your trust domain's token policy may not allow your workload to get tokens for this MCP server, or for the scopes it asked for. Ask your Defakto administrators to check the policy. The OAuth server's audit event for the request shows why it was refused.
The MCP server rejects the access token. Check that the resource you sent is exactly the MCP server's URL, including any trailing slash, because the MCP server compares it with the token's aud claim.
The MCP server accepts the token but refuses some operations. The token may carry fewer scopes than you requested. When your trust domain's token policy allows only some of the requested scopes, Defakto issues a token with just those, without an error. Your Defakto administrators can check allowedScopes in the policy rules that match your workload.