Authentication
MCP traffic on Catalyst crosses two authentication boundaries, and Catalyst handles each one separately:
- Caller → Catalyst — the agent authenticates to Catalyst's MCP proxy endpoint with its App ID API token.
- Catalyst → upstream MCP server — Catalyst authenticates to the real server using credentials configured on the
MCPServerconnection.
The agent only ever holds its own Catalyst token. The upstream server's credentials live on the connection and in your project's secret store — never in agent code, prompts, or logs.
Caller to Catalyst
Every request to <DAPR_HTTP_ENDPOINT>/v1.0/diagrid/mcp/<mcp-server-name> must carry the calling App ID's API token in the dapr-api-token header:
headers = {"dapr-api-token": DAPR_API_TOKEN}
diagrid dev run injects DAPR_API_TOKEN for each app during local development; for a hosted project, read the App ID's token from the Catalyst console. Catalyst validates the token, then applies the access policy for the target server before forwarding the call. This is the same identity Catalyst uses for every other call the App ID makes.
Catalyst to the upstream server
Configure the upstream credential once on the MCPServer connection. Catalyst attaches it to each forwarded request and strips the caller's dapr-api-token so it never leaks upstream. Catalyst supports three authentication shapes, listed strongest first.
SPIFFE JWT (recommended)
SPIFFE JWT is the strongest way to authenticate Catalyst to your MCP server, and the recommended choice whenever your server supports it: it is secretless and provides true zero trust authentication. No credential is ever provisioned or stored — Catalyst presents a short-lived, audience-scoped JWT-SVID minted from its own workload identity and rotated automatically, and your server verifies it against Catalyst's OIDC issuer.
This approach is supported by any OIDC-capable server: the server validates the token against the issuer's discovery URL, confirms the expected audience, and authorizes the subject (sub). You control each of these values, so the same standard OIDC verification a server already performs is sufficient to authenticate Catalyst.
Configure spec.endpoint.streamableHTTP.auth.spiffe.jwt with the header to carry the token and the expected audience:
spec:
endpoint:
streamableHTTP:
url: https://mcp.example.com/mcp
auth:
spiffe:
jwt:
header: Authorization
headerValuePrefix: "Bearer "
audience: https://mcp.example.com
Verify the SPIFFE JWT on your server
Catalyst acts as the identity provider here: the calling sidecar attaches a short-lived JWT-SVID for the MCPServer's backing App ID to requests it forwards to your URL. No secret is provisioned — the credential is a short-lived, audience-scoped, signed assertion of the caller's workload identity, minted from Catalyst's own workload identity and rotated automatically. Your server's job is to validate it. The token is a standard OIDC-discoverable RS256 JWT, so any JWT library works — you need no SPIFFE-specific tooling. Treat a request as authenticated from Catalyst only when all of these hold:
| Claim | What to require | Value |
|---|---|---|
| signature | Verified against the issuer's JWKS | RS256, keys from the issuer's jwks_uri |
iss | Equals your region's Catalyst issuer | e.g. https://oidc.r1.diagrid.io — see below |
aud | Contains the audience you configured | [ <your audience>, <region trust domain> ] |
exp / nbf / iat | Token is currently valid | short-lived; rotated automatically |
sub | Identifies the calling App ID | spiffe://<region-trust-domain>/ns/prj-<project-id>/<app-id> |
Find the issuer, then discover the signing keys. The iss is your region's OIDC issuer — the data-plane value, not the control-plane API host. Read it from the region status:
diagrid region get <region> -o yaml
# status.endpoints.oidc: https://oidc.r1.diagrid.io
Point your JWT library at that issuer's discovery document rather than hardcoding a key URL:
https://oidc.r1.diagrid.io/.well-known/openid-configuration
It returns the jwks_uri (https://oidc.r1.diagrid.io/jwks.json) and the signing algorithm (RS256). Most JWT libraries take the issuer, perform this discovery, then fetch, cache, and rotate the keys for you — Catalyst rotates its signing keys, so do not pin a single key. The value is also always present as the iss claim on a token your server receives.
The audience is the value you control end-to-end. Whatever you set in auth.spiffe.jwt.audience is stamped into the token's aud — alongside the region trust domain, so aud is multi-valued. Choose a string that names your server (mcp://payments, https://mcp.example.com) and require that your value is present in aud. This is what stops a token minted for a different server from being replayed against yours; most JWT libraries do this membership check when you pass an expected audience.
Pinning sub is recommended hardening. The sub is the SPIFFE ID of the App ID backing the MCPServer (same name as the connection), in the form spiffe://<region-trust-domain>/ns/prj-<project-id>/<app-id>. Read the exact value with:
diagrid appid get <mcpserver-name> --project <project> -o yaml
# status.spiffeId: spiffe://<cluster>.<region>.global.public.diagrid.io/ns/prj-<project-id>/<app-id>
Verifying the signature, iss, aud, and expiry establishes authenticity; validating sub restricts which Catalyst identity you accept.
You don't have to pin an exact sub. Because the SPIFFE ID is hierarchical, your server can glob-match it to accept a whole project or a set of App IDs by naming convention:
- Every App ID in a project —
spiffe://<region-trust-domain>/ns/prj-<project-id>/* - App IDs sharing a prefix —
spiffe://<region-trust-domain>/ns/prj-<project-id>/mcp-server-*
This matching is your server's own authorization logic against the sub claim, not Catalyst configuration — your server matches the claim against a glob pattern rather than an exact string.
The token arrives in the header you named in the spec (Authorization above), with any headerValuePrefix (Bearer ) stripped before parsing.
OAuth 2.0 client credentials
For servers that issue tokens through the OAuth 2.0 client-credentials grant, configure the issuer and client on spec.endpoint.streamableHTTP.auth.oauth2. Catalyst acquires and refreshes the token and attaches it to each call.
spec:
endpoint:
streamableHTTP:
url: https://mcp.example.com/mcp
auth:
oauth2:
issuer: https://auth.example.com
audience: https://mcp.example.com
clientID: my-client-id
scopes: ["mcp:read", "mcp:write"]
clientSecret: my-client-secret
Static headers
The most common shape — an API key or bearer token sent as a request header. Provide the full header value, including any Bearer prefix:
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata:
name: my-mcp
spec:
endpoint:
streamableHTTP:
url: https://mcp.example.com/mcp
headers:
- name: Authorization
value: "Bearer <token>"
When you provide a credential — through diagrid mcpserver create --header, the interactive prompt, the console, or a plaintext value in a spec — Catalyst extracts it into your project's secret store automatically; it never lands in the control-plane database as plaintext.
The built-in catalog entries come with the right shape pre-filled for each provider — header-based for most, OAuth 2.0 for Pendo and Snowflake, SPIFFE for Cloudflare — so you only supply the credential.
What's next
- Control tool access — layer per-tool authorization on top of authentication.
- Add an MCP server — set the upstream credential when you register a connection.
- Security and trust posture — the full identity and trust model.