Resource server does not implement OAuth 2.0 Protected Resource Metadata.
Your server answers 401 correctly, but the response carries no pointer to protected resource metadata — so the client has nothing to discover the authorization server with, and the OAuth flow never starts.
- MCP Specification
- 2026-07-28
Is this you?
Two requests, no credentials. Read the raw challenge line, then fetch the document it should point at. This separates "the pointer is missing" from "the pointer is there and something later broke".
- This error
- A 401 with no
WWW-Authenticateat all, or one that stops atBearer realm="…"orBearer error="invalid_token"with noresource_metadata, while the second request 404s. If the well-known request answers 401 instead of 200, something in front of the server is intercepting the discovery path too. A 200 whoseresourceis not the URL you dialed is the same failure one step later — RFC 9728 requires the client to discard that document, and both reference SDKs reject anyresourcethat is not the same origin and a path prefix of the URL they requested. - Then the fix is the next section.
- Working
HTTP/1.1 401carryingWWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", scope="mcp:read", resource_metadata="https://your-server.example.com/.well-known/oauth-protected-resource/mcp", and a200 application/jsonfrom the second request whoseresourceis exactlyhttps://your-server.example.com/mcp— the URL a client dials — and whoseauthorization_serversnames at least one server. A 401 with no header is also fine as long as that document is served: a conformant client falls back to the well-known probe.- Then this is not your problem — try the error reference.
4 other strings clients print for this
- Could not fetch resource metadata
- No authorization_servers found in resource metadata <url> - Is this resource metadata configured correctly?
- Protected resource <advertised> does not match expected <dialed>
- Failed to start OAuth flow for <server>: SDK auth failed: Protected resource <advertised> does not match expected <dialed> (or origin)
Dotted spans such as <url> are placeholders — your client prints your own value there.
Fix it
Make the 401 carry resource_metadata="…" and serve the document it points at. In the MCP Python SDK one setting does both — resource_server_url on AuthSettings; the TypeScript SDK needs two, resourceMetadataUrl on requireBearerAuth for the challenge and mcpAuthMetadataRouter to mount the document; FastMCP needs the verifier wrapped in RemoteAuthProvider. Publish the external URL clients actually dial, host and path included, as that document's resource — RFC 9728 requires a client to discard metadata whose resource does not match. If your code already sets the header, something in front of the server is replacing the 401 on the way out, and that is the layer to fix.
Have your agent fix this
Copies a prompt that sends your coding agent to this page, then has it find and apply the right fix for your project.
The cause depends on your platform. Pick yours.
Where is the 401 coming from?
MCP Python SDK
If your server is the MCP Python SDK and you passed token_verifier= with an AuthSettings whose resource_server_url is None
- 1
resource_server_urlis a required field onAuthSettings, but it acceptsNone, and nothing validates that you supplied a URL.RequireAuthMiddlewareappendsresource_metadata="…"to the challenge only when itsresource_metadata_urlargument is set, and the server only passes that argument whensettings.auth.resource_server_urlis present. PassNoneand the 401 goes out asWWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required"— right status, no pointer. - 2 The same setting mounts the document.
create_protected_resource_routesis registered only whenresource_server_urlis set, so the client's well-known fallback 404s as well and discovery has nowhere left to go. - 3 Set
resource_server_urlto the public URL of the MCP endpoint itself, not the origin.build_resource_metadata_urlinserts the well-known segment between host and path, sohttps://api.example.com/mcppublishes athttps://api.example.com/.well-known/oauth-protected-resource/mcp. - 4 That value also becomes
resourcein the document, and the client checks it against the URL it dialed — same origin, and a path prefix of that URL. Behind a TLS-terminating proxy this has to be the externalhttps://URL, or you trade this error for the mismatch variant.
MCP TypeScript SDK
If your server gates the route with requireBearerAuth and omits resourceMetadataUrl
- 1
resourceMetadataUrlis optional inBearerAuthOptions. The SDK's challenge builder addserroranderror_descriptionunconditionally,scopewhenrequiredScopesis non-empty, andresource_metadataonly when that URL is set. - 2 Build the URL with
getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp'))and pass it torequireBearerAuth. The Express middleware and the web-standardfetchgate share one core, so this is the same fix on Workers, Deno, Bun and Hono. - 3 Serve what the challenge points at:
mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl })mounts/.well-known/oauth-protected-resource/mcpplus a mirror of the authorization server metadata. On afetch-handler host useoauthMetadataResponsefrom@modelcontextprotocol/server. - 4 The same challenge is emitted on
403 insufficient_scope, so settingresourceMetadataUrlonce also fixes scope step-up.
FastMCP
If you pass a bare verifier as FastMCP's auth= — JWTVerifier, IntrospectionTokenVerifier, StaticTokenVerifier, DebugTokenVerifier
- 1 This is documented behaviour, not a bug. FastMCP's token-verification docs: "
TokenVerifierfocuses exclusively on token validation without providing OAuth discovery metadata" and "Token verification operates somewhat outside the formal MCP authentication flow, which expects OAuth-style discovery." - 2 Wrap the verifier in
RemoteAuthProviderwithauthorization_serversandbase_url. That is the piece that adds discovery: "The core discovery endpoint is/.well-known/oauth-protected-resource, which tells clients that your server requires OAuth authentication and identifies the authorization servers you trust." - 3 Keep the bare verifier only where every client is provisioned with a token out of band — internal services, a fixed set of agents. It works; it just cannot bootstrap a client that has never seen your server, which is what you are hitting.
- 4
base_urlis what the publishedresourceis derived from, so it has to be the externally reachable URL of the server, not a container-local one.
AWS API Gateway (REST)
If a Lambda or Cognito authorizer on a REST API answers the 401 before your integration runs
- 1 API Gateway writes that response itself: "If API Gateway fails to process an incoming request, it returns to the client an error response without forwarding the request to the integration backend." Your MCP server never executes, so no header it would have set exists.
- 2 The response type is
UNAUTHORIZED, default status 401 — "The gateway response when the custom or Amazon Cognito authorizer failed to authenticate the caller." Customize it and map a literalWWW-Authenticatevalue, single-quoted as parameter mappings require. - 3
ACCESS_DENIED(403) is the sibling used for authorization failure. Set it too if you emitinsufficient_scopechallenges. - 4 Expose the metadata document on a route with no authorizer attached. If
/.well-known/oauth-protected-resource/...is behind the same authorizer, the client's fallback probe gets a 401 and discovery still fails. - 5 Gateway response customization is REST-API only — the AWS reference for
responseParameterssays "Supported only for REST APIs." On an HTTP API you have to emit the challenge from a proxy integration or from a component in front of API Gateway.
ingress-nginx
If ingress-nginx fronts the server and 401 is listed in custom-http-errors
- 1 Listing a status code there turns on interception — "Setting at least one code also enables proxy_intercept_errors which are required to process error_page." The client then receives the default backend's response, and "NGINX does not change the response from the custom default backend" — your challenge is gone with the rest of your 401.
- 2 Remove 401 (and 403, if you use scope challenges) from the ConfigMap
custom-http-errors, or override it on the MCP ingress with thenginx.ingress.kubernetes.io/custom-http-errorsannotation, which "will set NGINX proxy-intercept-errors, but only for the NGINX location associated with this ingress." The annotation takes a comma-separated list of codes and its value replaces the global list for that host and path, so list the codes you do want intercepted and leave 401 out. An empty annotation value is not a way to say "none": the parser rejects it as invalid content and the global list still applies. - 3 Check
hide-headersin the same ConfigMap too — it drops named headers from the upstream response before the client sees them. - 4 If you must keep custom error pages, the default backend has to emit the challenge itself. NGINX hands it
X-CodeandX-Original-URI, and "the custom backend is expected to return the correct HTTP status code instead of 200."
Azure Container Apps
If Container Apps built-in authentication (Easy Auth) is returning the 401 for you
- 1 Container Apps Easy Auth with
unauthenticatedClientAction: Return401answerswww-authenticate: Bearer realm="<app>.<region>.azurecontainerapps.io"and nothing else — noresource_metadata. It also answers/.well-known/oauth-protected-resourceitself with 401, so the client's fallback probe never reaches your container and both discovery paths are closed. - 2 There is no Container Apps setting that fixes this today. A Microsoft collaborator has confirmed the gap on an open issue in the Container Apps repo; the same issue reports that
WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES, the App Service setting that does it, appears to be silently ignored on the Container Apps sidecar, and asks Microsoft to confirm whether it is meant to work there at all. - 3 App Service and Functions have the documented counterpart: set the
WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPESapp setting to a comma-separated list of your application's scopes and App Service Authentication hosts the protected resource metadata. Microsoft labels PRM support preview. - 4 On Container Apps, exclude the MCP route and
/.well-known/oauth-protected-resourcefrom Easy Auth and emit the challenge and the document from your own code, or move the server to App Service.
Something else
Whatever component you are running, the fix has one of two shapes. Either make the 401 carry resource_metadata="…" pointing at a document you actually serve, or serve /.well-known/oauth-protected-resource/<path> and let the client's fallback probe find it — the MCP authorization spec (revision 2025-11-25) accepts either mechanism and requires clients to support both. Then find the last hop that touched the response: run the curl above against the server directly, then again through each proxy, ingress or platform gate in front of it, and change the layer where the header disappeared rather than the MCP handler. Whichever component publishes the document, its resource value has to be the external URL clients dial — host and path — or RFC 9728 requires them to discard it.
Why it happens
A client that gets a 401 does not guess where to authenticate. It reads resource_metadata out of the WWW-Authenticate challenge, fetches that RFC 9728 document, and follows authorization_servers to the authorization server; with no pointer it falls back to probing /.well-known/oauth-protected-resource/<path> and then the root, and if neither is served, discovery is over — the client reports a metadata or connection error rather than prompting anyone to log in. The pointer is optional in both reference SDKs, and omitting it drops the parameter from the challenge with no warning. The other case is a header your code did set: something in front of the server replaces the 401 before it reaches the client.
The exchange
Each step in the exchange, in order.
- MCP client
- MCP server
- Authorization server
-
MCP client to MCP server
POST /mcp no Authorization header -
The challenge is a valid 401 and machine-readable, but it carries no resource_metadata parameter, so the client has no metadata URL to follow.
MCP server to MCP client
401 Unauthorized Bearer error="invalid_token" -
With nothing in the header, a conformant client probes the path-based well-known URI and then the root. Neither is served, so discovery ends here and the authorization server is never named.
MCP client to MCP server
GET well-known fallback /.well-known/oauth-protected-resource/mcp -
MCP server to MCP client
404 Not Found no metadata document mountedFlow stops here — Authorization server not reached
What lands on you next
The challenge header is one line of one standard, and it took a day to find. There are five more standards in this flow, and then the questions stop being about connecting at all.
-
Which other discovery documents does a client expect from us?
- RFC 9728 protected resource metadata
The challenge points at a metadata document, which points at authorization server metadata, which the client then uses to register and to request scopes. A gateway serves that chain as one unit.
-
The client authenticated. Which of our tools may it call?
-
mcp-capability-filter-inboundAn authenticated client is not an authorized one. The filter allow-lists capabilities per route and blocks the rest with
MethodNotFoundbefore the request reaches your server. -
Which caller is this, next time a request looks wrong?
-
capability_invocationEach tool call resolves to a caller, a capability and an outcome with a named reason code, rather than to a status line in an access log.
-
Can this token be replayed against our other MCP route?
- RFC 8707 resource indicators
No. A gateway-issued token is bound to one route's canonical resource URI, so a token minted for one route is rejected at another.
All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.
Stop debugging auth on every MCP server
A gateway in front of your MCP servers handles discovery, audience binding, and token validation once — for every server and every client.