MCP JWT Token Implementation Best Practices
JWTs give MCP servers stateless, distributed auth verification — but that same statelessness means a compromised token can't be instantly revoked. Here's how to design claims, choose a signing algorithm, and manage that tradeoff.
A JWT works for MCP authentication because it carries its own verifiable claims — any server instance can check a token's validity without a round-trip to a central session store, which matters when an MCP server runs as multiple stateless instances behind a load balancer.
Designing the Claim Set
{
"iss": "your-mcp-server.example.com",
"sub": "user-12345",
"scope": "mcp:tools:read mcp:resources:write",
"exp": 1724073600,
"iat": 1724030400,
"jti": "a1b2c3d4-unique-token-id"
}
Beyond the standard iss/sub/exp/iat claims, two design decisions matter specifically for MCP:
- Granular scopes — space-separated scope strings like
mcp:tools:readlet a server check "does this token cover THIS specific tool" rather than a binary valid/invalid check. - A
jti(JWT ID) — a unique identifier per token, which matters for the revocation problem below.
RS256 vs. HS256: Which to Sign With
RS256 (RSA, asymmetric) lets you keep the private signing key on your auth server while distributing only the public key to every service that needs to verify tokens — meaning a compromised MCP server instance can verify tokens but can't forge new ones. HS256 (HMAC, symmetric) uses the same secret to sign and verify, which is simpler for a single-service setup but means every service that can verify tokens can also mint valid ones. For any MCP deployment with more than one service verifying tokens, RS256 is the safer default.
The Real Tradeoff: Revocation
This is the part most JWT guides skip: a JWT is valid until it expires, full stop — there's no built-in way to invalidate one early the way you can delete a server-side session. If a token is compromised, it remains usable until its exp time passes, regardless of anything you do afterward. Two real mitigations exist:
- Keep expiry genuinely short (5-15 minutes) so the exposure window from a leaked token is small, paired with a separate, longer-lived refresh token used to mint new access tokens.
- Maintain a denylist keyed on
jtifor the rare case you need to kill a specific token immediately — this reintroduces a lookup, sacrificing some statelessness, but only for the exceptional revocation case rather than every request.
Practical Checklist
- Short access-token expiry (5-15 minutes), with refresh tokens handling renewal.
- RS256 signing if more than one service verifies tokens.
- Granular, tool-specific scopes rather than one blanket scope.
- A
jticlaim plus a denylist mechanism for emergency revocation. - Validate
expand scope on every single tool call, not just at initial connection.
Join the Discussion
Code Snippets (0)
No code snippets shared yet. Be the first to contribute!
Code Snippets (0)
No code snippets shared yet. Be the first to contribute!