Add OAuth (RFC 9728) auth to your MCP server
What we checked
We probed your declared endpoint to see whether (and how) it enforces authorization, following the MCP authorization spec. The outcome decides the signal:
Why it matters
A remote MCP server is a third party you hand context and tool-calls to. If it exposes sensitive data or actions with no authorization, anyone who learns the URL can drive it. OAuth 2.0 gives each client a scoped, revocable token instead of a shared secret, and RFC 9728 lets a client discover how to get one without any out-of-band configuration.
The metadata document is the missing half of a bare challenge: a 401
alone tells a client it needs a token, but not where to obtain it. RFC 9728
points the client at your authorization server so the flow can complete
automatically.
How to fix it
Two pieces are needed: a challenge that references your metadata, and the metadata document itself.
First, serve the Protected Resource Metadata at
/.well-known/oauth-protected-resource. It names the resource and the
authorization server(s) a client should use:
{
"resource": "https://mcp.example.com",
"authorization_servers": ["https://auth.example.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read", "mcp:write"]
} Then reject unauthenticated requests with a 401 whose WWW-Authenticate
header points back at that metadata. The snippets below are deliberately
minimal and conceptual; check your SDK version’s docs, since recent releases of
the official SDKs ship built-in authorization helpers (token verifiers, a
metadata route) that wire most of this for you.
// Conceptual Express middleware. Verify the bearer token your
// authorization server issued; on failure, point the client at the
// RFC 9728 metadata so it can start the OAuth flow.
const METADATA_URL =
"https://mcp.example.com/.well-known/oauth-protected-resource";
app.use((req, res, next) => {
const token = (req.headers.authorization ?? "").replace(/^Bearer /, "");
if (!isValidToken(token)) {
res
.status(401)
.set("WWW-Authenticate", `Bearer resource_metadata="${METADATA_URL}"`)
.end();
return;
}
next();
});
// Validate tokens against your authorization server (JWKS / introspection).
// The official SDK exposes auth helpers - prefer those over hand-rolling. # Conceptual ASGI middleware. On a missing/invalid token, return a 401
# whose WWW-Authenticate header references the RFC 9728 metadata document.
METADATA_URL = "https://mcp.example.com/.well-known/oauth-protected-resource"
async def require_auth(request, call_next):
token = request.headers.get("authorization", "").removeprefix("Bearer ")
if not is_valid_token(token):
return Response(
status_code=401,
headers={"WWW-Authenticate": f'Bearer resource_metadata="{METADATA_URL}"'},
)
return await call_next(request)
# The official 'mcp' SDK ships authorization support (token verifier + the
# protected-resource metadata route) - prefer it over hand-rolling validation. How we re-check
We re-probe your endpoint on the next crawl. Once we see a 401 challenge that
references a reachable, valid RFC 9728 metadata document naming your
authorization server, the check records full OAuth discovery on the following
score refresh.
Give this to your AI
Paste this into Claude Code (or any coding agent) from inside your server's repository. It states the failing signal, the outcome we re-check for, and the format the fix has to take.
Context: this repository (or its infrastructure) runs a remote MCP server at an HTTPS endpoint. VerifyMCP's authentication check failed, which means the endpoint either accepts tool calls with no authorization check, or challenges with a bare `401` that doesn't tell a client where to obtain a token. Goal: unauthenticated requests get a `401` whose `WWW-Authenticate` header points at a reachable RFC 9728 Protected Resource Metadata document, so the next crawl records the check as passing. Do this: 1. First decide whether this server needs auth at all: if everything it exposes is public, read-only data, an open endpoint is an accepted state per the guide, stop here and tell me instead of adding auth. You do NOT need to gate discovery. VerifyMCP probes `tools/call` as well as `initialize`, so a server that serves `tools/list` anonymously and requires a token to CALL a tool earns full marks. Keeping the tool list public is fine and costs nothing. 2. If it exposes private data, writes, or side effects, serve Protected Resource Metadata at `/.well-known/oauth-protected-resource`, naming the `resource` and the `authorization_servers` a client should use. 3. Reject requests without a valid bearer token with a `401` whose `WWW-Authenticate` header includes `resource_metadata` pointing at that document, and verify tokens against the authorization server (JWKS or introspection); prefer the SDK's built-in auth helpers. 4. If any tool declares `annotations.destructiveHint: true`, treat it as the priority: it must require a valid token to be CALLED, since leaving it open is the one authorization state we fail outright. Rules: - Adding auth changes who can call this server. Confirm the auth requirement and the chosen authorization server with the human before shipping; a botched rollout can lock out legitimate clients. - Never try to pass the check by publishing metadata that points at an authorization server that doesn't actually verify tokens. - Don't add auth to a server that's genuinely public and read-only, that's a legitimate pass, not a gap to close. Report back: the files changed, the metadata URL, and the curl command showing a token-less request now gets a `401` with `WWW-Authenticate` referencing that metadata. Reference: https://verifymcp.io/docs/remote/authentication