Return a 401 that points agents to your metadata
This check verifies that a request without a token gets a 401 whose WWW-Authenticate header
tells agents where your protected resource metadata lives.
Check id challenge. Results:
Why it matters
Section titled “Why it matters”The 401 is the start of every sign-in. An agent that gets one reads the WWW-Authenticate header
to learn that the server uses OAuth and where its metadata is. Without a Bearer challenge there,
the agent has nothing to follow, and the user sees a connection error instead of a sign-in prompt.
- Claude starts sign-in only when a request fails with
401and aWWW-Authenticateheader. It ignores the header on any other status, including200(Claude: Authentication for connectors). Withoutresource_metadata, Claude can still probe the well-known locations, but its documentation calls that a fallback that works only when your platform serves them. - ChatGPT finds your metadata either at the well-known URL or from the
resource_metadatain a401challenge (OpenAI: Authentication). - The MCP authorization specification requires clients to use
resource_metadatawhen it is present and to fall back to the well-known URLs otherwise. Other agents, such as Gemini CLI, follow it; MCP Tower has not verified them.
A server whose tools all work without a token does not need a challenge on initialize or
tools/list. It still needs one when a protected tool is called; see
call-time responses.
What MCP Tower checks
Section titled “What MCP Tower checks”- MCP Tower connects to your MCP endpoint without a token, as an agent does:
server/discover, orinitializefor servers on protocol2025-11-25, thentools/list. - If the server answers every request without asking for a token, there is no challenge to check,
and the result is
skipped. - If the server answers
401or403, MCP Tower parses itsWWW-Authenticateheader:- no header, or a scheme other than
Bearer(such asBasic):fail; - a
Bearerchallenge without theresource_metadataparameter:warn; - a
Bearerchallenge withresource_metadata:pass. An optionalscopeparameter is read as the scopes the server asks for, anderroris read on a403.
- no header, or a scheme other than
MCP Tower then fetches the metadata from the resource_metadata URL first, before the well-known
locations; see protected resource metadata.
Results
Section titled “Results”| Status | Message | Meaning | Fix |
|---|---|---|---|
| pass | The 401 response carries a Bearer challenge with resource_metadata. | Agents can start sign-in. | |
| fail | The <status> response has no Bearer challenge in WWW-Authenticate. | The 401 or 403 has no WWW-Authenticate header, or one with another scheme. | Send a Bearer challenge |
| warn | The 401 response’s Bearer challenge has no resource_metadata parameter. Agents have to guess the metadata location. | Agents fall back to the well-known URLs, which works only if you serve them. | Add resource_metadata |
| skipped | The server listed tools without a token, so there was no 401 challenge to check. | Your server is open without a token. Protected tools must still answer 401 when called. | Call-time responses |
The next step for the failure is: “Answer unauthenticated requests with 401 and WWW-Authenticate:
Bearer resource_metadata=”…/.well-known/oauth-protected-resource” so agents can find your
authorization server.” For the warning it is: “Add resource_metadata=“<path-suffixed well-known URL>” to the 401 response so agents can find your authorization server.” For an endpoint at
https://mcp.example.com/mcp, that URL is
https://mcp.example.com/.well-known/oauth-protected-resource/mcp.
How to fix
Section titled “How to fix”Send a Bearer challenge
Section titled “Send a Bearer challenge”- Find where your server rejects requests without a token. It is usually an authentication middleware in front of your MCP route.
- Make it answer
401 Unauthorized, not403,302to a login page, or200with an error in the body. - Add a
WWW-Authenticateheader with theBearerscheme and theresource_metadataparameter. Addscopewith the scopes a first sign-in needs, if you have any.
The response agents expect:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp:tools"Content-Type: application/json
{"error":"invalid_token","error_description":"Sign in to use this server"}# With auth_request in front of /mcp, a 401 from the auth service# gets the challenge before it reaches the agent.location /mcp { auth_request /_auth; error_page 401 = @challenge; proxy_pass http://origin.example.com;}
location @challenge { add_header WWW-Authenticate 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp:tools"' always; return 401 '{"error":"invalid_token"}';}const CHALLENGE = 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp:tools"';
app.use("/mcp", (req, res, next) => { if (!isValidToken(req.get("authorization"))) { return res .status(401) .set("WWW-Authenticate", CHALLENGE) .json({ error: "invalid_token" }); } next();});CHALLENGE = ( 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", ' 'scope="mcp:tools"')
@app.middleware("http")async def require_token(request: Request, call_next): if request.url.path.startswith("/mcp") and not is_valid_token( request.headers.get("authorization") ): return JSONResponse( {"error": "invalid_token"}, status_code=401, headers={"WWW-Authenticate": CHALLENGE}, ) return await call_next(request)Add resource_metadata
Section titled “Add resource_metadata”If you already send Bearer without parameters, add resource_metadata with the absolute URL of
your protected resource metadata. Use the path-suffixed URL, which names your MCP path after
/.well-known/oauth-protected-resource, and make sure the document is served there. Quote the
value, and separate parameters with a comma.
Behind MCP Tower
Section titled “Behind MCP Tower”Agents reach your server through your public hostname, mcp.example.com, while your server runs
at origin.example.com. If your server builds resource_metadata from its own host, MCP Tower
rewrites the scheme, host and port in that parameter to the public hostname and keeps the path. A
URL that already names the public hostname, or any other host, is left as it is.
Check it yourself
Section titled “Check it yourself”Send the request an agent sends first, without a token:
curl -si https://mcp.example.com/mcp \ -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"}}}'The problem is fixed when the response starts like this:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp:tools"A 200 means the server is open without a token, and the check is skipped. Then select Scan
again on the server’s page.
References
Section titled “References”- MCP authorization
2026-07-28: Protected resource metadata discovery requirements and scope selection strategy - RFC 9728 §5.1: the
resource_metadataparameter inWWW-Authenticate - RFC 6750 §3: the
Bearerchallenge - Claude: Authentication for connectors, “Serve discovery metadata”
- OpenAI: Authentication, “Host protected resource metadata on your MCP server”