Skip to content

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

Respond correctly when a protected tool is called

This check reminds you what a protected tool must return when it is called without a token, or with a token that lacks a scope, because a scan cannot call your tools to find out.

Check id call_time_guidance (call-time guidance). Results:

info skipped

On a server that lets agents list tools anonymously, sign-in starts when the user calls a protected tool. What your server answers to that call decides whether the user gets a sign-in prompt or an error message, and the two largest agents act on different signals.

  • Claude starts sign-in only when the HTTP request for the tools/call fails with 401 and a WWW-Authenticate header. A 200 that carries a tool result with isError: true is passed to the model as text, with no prompt. A tool handler cannot produce the 401, because the MCP SDK wraps whatever it returns in a 200, so the check has to run before the request reaches the SDK (Claude: Lazy authentication). After sign-in, Claude retries the same call.
  • ChatGPT shows its sign-in prompt for a tool when the tool declares oauth2 in securitySchemes and the call returns an error result with isError: true and _meta["mcp/www_authenticate"], a Bearer challenge that contains both error and error_description (OpenAI: Authentication).
  • The MCP authorization specification defines the HTTP signals: 401 when a token is missing or invalid, and 403 with error="insufficient_scope" when the token needs more scope. Other agents are expected to follow it; MCP Tower has not verified them.

The two signals do not fit in one response: a 401 has no tool result, and a tool result arrives in a 200. Neither agent’s documentation describes a single response that triggers both prompts, so MCP Tower does not suggest one. See Choose the response below.

A scan reads tools/list and never calls a tool, so it cannot see what a call returns. The check reports info when any tool needs, or may need, sign-in:

  • it declares oauth2 in securitySchemes, alone or with noauth;
  • it declares scopes in authorization.scopes;
  • it appears only after sign-in, or the whole server needs a token;
  • it declares nothing on a server that offers OAuth while listing tools anonymously, so it may still enforce sign-in when called.

When every tool declares only noauth, or the server shows no sign of OAuth, the check is skipped. It never fails or warns, and it has a docs link even though it is info.

StatusMessageMeaningFix
infoSome tools need, or may need, sign-in. The scan cannot verify what happens when they are called.Check your server’s call-time responses yourself.Answer without a token, Answer without enough scope
skippedNo tool needs sign-in.Every tool works without a token.

The next step shown with info is: “On a protected call without a token: return HTTP 401 with WWW-Authenticate (Claude) and a tool error with _meta[‘mcp/www_authenticate’] (ChatGPT). For missing scope: 403 insufficient_scope listing every needed scope.”

  1. In your HTTP handler, before the MCP SDK runs, parse the JSON-RPC body. If it is a tools/call for a protected tool and carries no valid token, stop there.
  2. Answer 401 Unauthorized with WWW-Authenticate: Bearer and the resource_metadata URL, plus scope with the scopes the tool needs.
  3. Let initialize, tools/list and calls to public tools through without a token.
tools/call for a protected tool, no token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Sign in to see your orders", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="orders:read"

nginx cannot see which tool a JSON-RPC body calls, so it cannot make this decision. Do it in your server code, as in the other tabs, and make sure nginx passes the 401 and its WWW-Authenticate header through unchanged.

When a signed-in user calls a tool their token cannot use, answer 403 Forbidden with error="insufficient_scope" and a scope parameter that lists every scope the operation needs, not only the missing one. The MCP specification asks for all required scopes in a single challenge, and Claude recommends listing the scopes the user still needs, because scopes from an earlier step-up are not reliably carried forward (Claude: Ask for more scope with 403). This is step-up.

tools/call with a token that lacks orders:write
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:read orders:write", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"

The sources describe the HTTP 401 as the signal Claude and the MCP specification act on, and the _meta tool error as the signal ChatGPT acts on. One call can return only one of them. Pick the response for the agents your users rely on, and test it in each. For ChatGPT, the tool error looks like this:

tools/call result for ChatGPT
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"isError": true,
"content": [{ "type": "text", "text": "Sign in to see your orders." }],
"_meta": {
"mcp/www_authenticate": [
"Bearer resource_metadata=\"https://mcp.example.com/.well-known/oauth-protected-resource/mcp\", error=\"invalid_token\", error_description=\"Sign in to see your orders\""
]
}
}
}

Call a protected tool without a token:

Terminal window
curl -si https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_order","arguments":{"id":"1"}}}'

For Claude, the problem is fixed when the response starts with HTTP/1.1 401 Unauthorized and a WWW-Authenticate: Bearer header with resource_metadata. A 200 with isError: true means the refusal came from the tool handler. Then send the same call with -H 'Authorization: Bearer <access-token>' for a token without the tool’s scope, and look for 403 with error="insufficient_scope" and every needed scope. Public tools should still answer 200 without a token. Then select Scan again on the server’s page; the result stays info.