Make the MCP server accept the tokens it is issued
This check verifies that your MCP server accepts the access token your authorization server just issued, for the same requests an agent sends right after sign-in.
Check id token_accepted (token accepted). Results:
Why it matters
Section titled “Why it matters”A token that your own server refuses is the most confusing failure a user can hit: sign-in succeeds, the consent screen closes, and the agent still cannot use the server.
- Claude treats a
401on a request that carries a token as a reason to refresh the token and try again (Claude: Token refresh). A server that refuses every fresh token sends the user back to sign-in over and over. - ChatGPT expects your server to check the token’s issuer, audience, expiry and scopes on every
request, and to reject a token without the expected audience with a
WWW-Authenticatechallenge (OpenAI: Echo the resource parameter). - The MCP authorization specification requires servers to accept only tokens issued for them as
the audience, and to answer invalid or expired tokens with
401(Token handling).
The usual cause is a mismatch between what the authorization server puts in the token and what the server checks: a different audience, a missing scope, or a different issuer.
What MCP Tower checks
Section titled “What MCP Tower checks”- After a sign-in, MCP Tower scans again as the signed-in user. It sends the token in an
Authorization: Bearerheader onserver/discover(orinitializefor protocol2025-11-25) andtools/list, as an agent does. - If both succeed, the result is
pass. - If the server answers
403, the result isfail, and the message names theerrorfrom theWWW-Authenticateheader, usuallyinsufficient_scope. The next step names the header’sscope. - If the server answers
401to the fresh token, the result isfail. The token was requested for the resource identifier in your protected resource metadata, so the next step names that URL as the audience to accept. - Any other failure of the signed-in requests is
failwith its reason. - Without a token there is nothing to check, and the result is
skipped. A token that could not be refreshed is alsoskipped; see token refresh.
Results
Section titled “Results”| Status | Message | Meaning | Fix |
|---|---|---|---|
| pass | The server accepted the token for discovery and tools/list. | Signed-in agents can use the server. | |
| fail | The server answered 403 <error> with the token. | The token is valid but lacks a scope. Next step: “Sign in again with the scope <scope>, or check that the provider grants it.” | Grant or require the right scope |
| fail | The server refused a fresh token with 401. | The server does not accept the token at all. Next step: “Check that the server accepts tokens with the audience <resource>.” | Accept the audience |
| fail | The signed-in pass failed: <reason>. | The signed-in requests failed for another reason. Next step: “Scan again; if it repeats, check the server’s logs for this request.” | Other failures |
| skipped | No token yet: sign in to scan as a signed-in user. | Nobody has signed in for this server yet. Select Sign in to scan. | |
| skipped | No usable token. | The stored token expired and could not be refreshed. Sign in again. | Token refresh |
<reason> is one of: dns_error, connect_error, tls_error, timeout, address_blocked,
http_error (an unexpected status such as 500), not_mcp (the response is not JSON-RPC),
unsupported_transport, protocol_error (an error from server/discover, initialize or
tools/list) or too_large.
How to fix
Section titled “How to fix”The server rejects the audience
Section titled “The server rejects the audience”- Decode a token your authorization server issued (for a JWT, its
audclaim) or look it up with token introspection. Note its audience and issuer. - Compare the audience with
resourcein your protected resource metadata, for examplehttps://mcp.example.com/mcp. Agents send that value as theresourceparameter, so tokens carry it as their audience when the authorization server supports RFC 8707. - Make your server accept exactly that audience, and the issuer listed in
authorization_servers. Answer a wrong or expired token with401and aBearerchallenge.
# nginx delegates validation; the auth service checks aud, iss and exp.location /mcp { auth_request /_auth; proxy_pass http://origin.example.com;}
location = /_auth { internal; proxy_pass http://origin.example.com/validate?audience=https://mcp.example.com/mcp; proxy_pass_request_body off; proxy_set_header Content-Length "";}import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/jwks"));
export async function verifyToken(header) { const token = header?.replace(/^Bearer /, ""); const { payload } = await jwtVerify(token, JWKS, { issuer: "https://auth.example.com", audience: "https://mcp.example.com/mcp", }); return payload;}import jwt
jwks = jwt.PyJWKClient("https://auth.example.com/jwks")
def verify_token(header: str) -> dict: token = header.removeprefix("Bearer ") key = jwks.get_signing_key_from_jwt(token) return jwt.decode( token, key.key, algorithms=["RS256"], issuer="https://auth.example.com", audience="https://mcp.example.com/mcp", )The token lacks a scope
Section titled “The token lacks a scope”A 403 with error="insufficient_scope" means the server wants a scope the token does not hold.
Either the scope was never requested, or the authorization server did not grant it.
- Check which scopes
initialize,server/discoverandtools/listrequire. These requests run before the user picks a tool, so they should need only the scope in your401challenge, or none. - Put that scope in the
scopeparameter of your401challenge, so agents request it at the first sign-in. - When a
403is right, name every scope the request needs in a single challenge.
HTTP/1.1 403 ForbiddenWWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:tools", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"Then sign in again from the server’s page in MCP Tower, or check tool scopes to see which scopes the authorization server granted.
The signed-in requests fail for another reason
Section titled “The signed-in requests fail for another reason”A network reason (dns_error, timeout) or http_error on the signed-in pass only, while the
anonymous pass works, usually means a separate code path for authenticated requests: a proxy that
forwards Authorization to another backend, a token check that calls a slow service, or a crash
when the token is parsed. Look up the request in your server’s logs at the time of the scan.
Behind MCP Tower
Section titled “Behind MCP Tower”Agents request tokens for the public URL, for example https://mcp.example.com/mcp, not for your
origin host. MCP Tower forwards the Authorization header unchanged, so your origin receives tokens
whose audience names the public hostname, and must accept them. See
public audience.
Check it yourself
Section titled “Check it yourself”Sign in once with your own client to get a token, then send the two requests an agent sends:
curl -si https://mcp.example.com/mcp \ -H 'Authorization: Bearer <access-token>' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
curl -si https://mcp.example.com/mcp \ -H 'Authorization: Bearer <access-token>' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2025-11-25' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'The problem is fixed when both answer HTTP/1.1 200 OK with a JSON-RPC result. A refused token
looks like this:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", error="invalid_token"Then select Scan again on the server’s page.
References
Section titled “References”- MCP authorization
2026-07-28: Token handling and runtime insufficient scope errors - RFC 6750 §3.1: the
invalid_tokenandinsufficient_scopeerror codes - RFC 8707 §2: the
resourceparameter and the token’s audience - Claude: Token refresh
- OpenAI: Echo the resource parameter throughout the OAuth flow