Answers · MCP security
How do I health-check OAuth token refresh for an MCP server connection?
Last updated
TL;DR: Test the credential, not just the server. A probe that only opens a connection, or reuses an access token that is still valid, can stay green while the refresh token behind it is already dead. Users then find out the next morning when the agent can't connect. A useful check makes one authenticated, read-only MCP request with the token your client really uses, through the same code path and token store. It also tracks when the access token expires, when the last refresh succeeded and what the last refresh error was. Two OAuth rules shape the design. RFC 6749 says that when the server issues a new refresh token, the client must discard the old one, so save the new token before using it. RFC 9700 describes refresh token rotation: if an invalidated refresh token is presented, the server can't tell attacker from client and revokes the active one. So a health checker that holds its own copy of the refresh token, or refreshes at the same time as the client, can log the user out. Treat invalid_grant on refresh as 're-authorization needed', not as a transient error to retry.
What should I check, step by step?
1. Make the check an authenticated MCP request with the client's real token (for example tools/list), not an unauthenticated ping. The MCP spec requires a 401 for invalid or expired tokens, so a 401 here is a real finding.
- Run the check through the client's own token store and refresh code, with a lock so only one process refreshes a grant at a time. Never give the checker a separate copy of the refresh token.
- Write the new refresh token to storage before you use the new access token. RFC 6749 says the client MUST discard the old refresh token when a new one is issued, and with rotation the old one may already be invalid.
- Sort failures into groups: invalid_grant from the token endpoint (the refresh token is invalid, expired or revoked: alert and ask the user to re-authorize), a 403 with insufficient_scope from the MCP server (step-up authorization, not a refresh problem), and timeouts or 5xx (transient: retry with backoff).
- Record and alert on time until the access token expires (from expires_in), time since the last successful refresh, consecutive refresh failures and any invalid_grant.
- Handle connections with no refresh token at all. The MCP spec says clients MUST NOT assume refresh tokens will be issued. For those, warn before the access token expires.
- Know your provider's limits. Google, for example, says a refresh token can stop working after six months unused, when the user revokes access, or when a user passes 100 refresh tokens for one OAuth client (the oldest is invalidated without warning). Apps with an external user type in Testing status get refresh tokens that expire in 7 days unless they request only basic profile scopes.
- If you run the authorization server, or it offers token introspection (RFC 7662) to you, use its active field to tell revoked tokens apart from network failures.
Why a green check can hide a dead refresh token
Depending on the authorization server, an access token can keep working until it expires even after the refresh token behind it is revoked. A check that only uses the cached access token passes until that access token expires, and then every call fails at once.
- RFC 9700 says authorization servers MUST detect refresh token replay for public clients, by sender-constrained refresh tokens or by rotation. With rotation, each refresh returns a new refresh token and the previous one is invalidated. If an invalidated token is presented, the server revokes the active refresh token as well, which forces a new authorization.
- That's why two processes holding the same refresh token are risky: the slower one presents a token that has just been rotated out, and the server may treat it as replay. Keep one source of truth per grant.
- RFC 9700 also says refresh tokens SHOULD expire after a period of client inactivity, and the server may revoke them on password change or logout. A connection nobody used for weeks can fail its first refresh even though nothing is wrong with your code.
What the MCP spec asks of clients and servers
The MCP authorization spec (2026-07-28) says clients that want refresh tokens MUST keep them confidential in transit and storage, SHOULD include refresh_token in their grant_types client metadata, MAY add offline_access to the scope when the authorization server lists it in scopes_supported, and MUST NOT assume refresh tokens will be issued.
- MCP servers SHOULD NOT put offline_access in the WWW-Authenticate scope or in the Protected Resource Metadata scopes_supported, because refresh tokens aren't a resource requirement.
- Servers must answer invalid or expired tokens with HTTP 401 and insufficient scope with 403 and error="insufficient_scope", so your check can tell 'refresh or re-authorize' apart from 'ask for more scope'.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Show me the public health receipts for listed MCP servers: whether each one answered its last probe, when, and how often it answered over the last 30 days.”
{
"tool": "list_mcp_health",
"arguments": {}
}- Free, no key. Rightshold's probe checks liveness only (MCP initialize and tools/list, no tool is ever called), and every probe is published as an append-only, hash-chained receipt. It doesn't test your OAuth token. Use it to tell 'the server is down' apart from 'my credential is dead'.
- get_mcp_health with a listed server's slug returns that server's fingerprint, health summary and probe receipts, newest first. Reading never probes the server.
- precall_check (free) tells you whether a server's tool definitions have changed since they were recorded before you re-authorize it.
Rightshold probes the liveness of listed MCP servers and publishes the receipts. It never holds or refreshes your OAuth tokens and can't tell you whether your grant is still valid. Not legal advice.
What are the sources?
- RFC 6749: The OAuth 2.0 Authorization Framework (Section 6 refreshing an access token, Section 5.2 invalid_grant)
- RFC 9700: Best Current Practice for OAuth 2.0 Security (Section 4.14 refresh token protection)
- Model Context Protocol specification 2026-07-28: Authorization (refresh tokens, error handling)
- RFC 6750: Bearer Token Usage (error codes)
- RFC 7662: OAuth 2.0 Token Introspection (active)
- Google for Developers: Using OAuth 2.0 to access Google APIs (refresh token expiration)
Related
- MCP OAuth: rejecting wrong-audience tokens
- How to scope OAuth tokens per task for AI agents
- How to share one MCP server across a company with per-user auth
- How to pin MCP server versions and review changes before upgrading
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
