Skip to content

50% off your first year. Join before 2027. Claim →

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:

pass fail skipped

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 401 on 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-Authenticate challenge (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.

  1. After a sign-in, MCP Tower scans again as the signed-in user. It sends the token in an Authorization: Bearer header on server/discover (or initialize for protocol 2025-11-25) and tools/list, as an agent does.
  2. If both succeed, the result is pass.
  3. If the server answers 403, the result is fail, and the message names the error from the WWW-Authenticate header, usually insufficient_scope. The next step names the header’s scope.
  4. If the server answers 401 to the fresh token, the result is fail. 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.
  5. Any other failure of the signed-in requests is fail with its reason.
  6. Without a token there is nothing to check, and the result is skipped. A token that could not be refreshed is also skipped; see token refresh.
StatusMessageMeaningFix
passThe server accepted the token for discovery and tools/list.Signed-in agents can use the server.
failThe 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
failThe 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
failThe 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
skippedNo token yet: sign in to scan as a signed-in user.Nobody has signed in for this server yet. Select Sign in to scan.
skippedNo 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.

  1. Decode a token your authorization server issued (for a JWT, its aud claim) or look it up with token introspection. Note its audience and issuer.
  2. Compare the audience with resource in your protected resource metadata, for example https://mcp.example.com/mcp. Agents send that value as the resource parameter, so tokens carry it as their audience when the authorization server supports RFC 8707.
  3. Make your server accept exactly that audience, and the issuer listed in authorization_servers. Answer a wrong or expired token with 401 and a Bearer challenge.
nginx.conf
# 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 "";
}

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.

  1. Check which scopes initialize, server/discover and tools/list require. These requests run before the user picks a tool, so they should need only the scope in your 401 challenge, or none.
  2. Put that scope in the scope parameter of your 401 challenge, so agents request it at the first sign-in.
  3. When a 403 is right, name every scope the request needs in a single challenge.
403 response
HTTP/1.1 403 Forbidden
WWW-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.

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.

Sign in once with your own client to get a token, then send the two requests an agent sends:

Terminal window
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 Unauthorized
WWW-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.