Configuration & Environment
MCP servers that verify OAuth tokens but do not expose `/.well-known/oauth-protected-resource` violate the MCP Authorization Specification and prevent clients from discovering which authorization servers issued the tokens they should accept.
This is a weak authentication-discovery defect (CWE-1390) where the server enforces JWT or OAuth bearer authentication on tool calls but never publishes a Protected Resource Metadata (PRM) document at `/.well-known/oauth-protected-resource`. RFC 9728 and the MCP Authorization Specification (2025-11-25) make PRM mandatory: clients use it to learn the resource identifier and the list of authorization servers whose tokens this resource accepts. Without it, clients cannot verify the audience claim of incoming tokens against the resource they intended to call, opening token-confusion and audience-confusion attacks.
MCP servers are routinely accessed by multiple authorization servers in agent ecosystems — a single tool surface might be reachable through Claude.ai, a partner orchestrator, and a self-hosted IDE. The MCP spec makes PRM the single source of truth for which authorization servers a server trusts; without it, clients have no way to verify they are not being routed to a token-issuing impostor. Because MCP servers expose tools that perform high-impact actions on user data, audience-confusion attacks are particularly damaging.
import jwt |
from fastmcp import FastMCP |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
def list_files() -> list[str]: |
return ["a.txt", "b.txt"] |
# VULNERABLE: JWT enforced, but no PRM endpoint published. |
# Clients cannot discover which auth servers this resource trusts. |
def auth(token: str): |
return jwt.decode(token, "secret", algorithms=["HS256"]) |
from fastapi import FastAPI |
import jwt |
from fastmcp import FastMCP |
mcp = FastMCP("my-mcp") |
app = FastAPI() |
@app.get("/.well-known/oauth-protected-resource") |
def prm(): |
return { |
"resource": "https://my-mcp.example.com", |
"authorization_servers": ["https://auth.example.com"], |
"bearer_methods_supported": ["header"], |
} |
@mcp.tool() |
def list_files() -> list[str]: |
return ["a.txt", "b.txt"] |
def auth(token: str): |
return jwt.decode(token, "secret", algorithms=["HS256"]) |
MCPSafe fires when ALL three conditions hold: (1) the file registers MCP tools; (2) the file configures OAuth or JWT verification (`jwt.decode`, `jwt.verify`, `OAuth2*Bearer`, `authorization_endpoint`, `jsonwebtoken`); (3) the file does NOT contain the literal string `.well-known/oauth-protected-resource` or `.well-known/oauth-authorization-server`. The v1 detection is single-file: if the PRM route is served from a sibling module, an API gateway rule, or a reverse proxy, the rule will still fire — use a `# nosem` annotation in that case. The fix is to add the metadata endpoint to the MCP server module itself so the discovery contract is reviewable in source.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now