Zuplo
MCP error

Failed to fetch authorization server metadata from all attempted URLs

The client knows which authorization server it is supposed to use, but none of the well-known URLs it is required to try returned a metadata document — so it never learns where `/authorize` and `/token` are, and the flow stops one step short of a login.

MCP Specification
2026-07-28

Is this you?

Take the issuer out of your own metadata document rather than from your configuration, then probe the URLs a client builds from it. That separates "the issuer string is wrong" from "the issuer is right and the document is somewhere no client looks", which are different fixes in different places.

TerminalRun this to check — it changes nothing
# 1. The issuer, byte for byte, as your MCP server publishes it.
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource/mcp \
  | jq -r '.authorization_servers[]'
# → https://auth.example.com/tenant1

# 2. The forms a client probes for that issuer, in the spec's order.
#    An issuer with no path component has only the first two.
for u in \
  https://auth.example.com/.well-known/oauth-authorization-server/tenant1 \
  https://auth.example.com/.well-known/openid-configuration/tenant1 \
  https://auth.example.com/tenant1/.well-known/openid-configuration
do
  printf '%s  ' "$u"
  curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \
    -H 'Accept: application/json' "$u"
done

# 3. For whichever answered 200, does its issuer echo step 1 exactly?
curl -sS https://auth.example.com/tenant1/.well-known/openid-configuration \
  | jq -r .issuer
This error
Every line in step 2 shows a non-200 status. Also unhealthy, and easier to miss: a 200 whose content type is text/html, which is a login page or a WAF challenge rather than a document — clients count that as a failed attempt, and VS Code reports it with 200 in the error message because it requires the body to parse as authorization server metadata first. A 415 or 403 where a browser gets 200 means the provider is rejecting the request rather than the path, so compare the headers the client sends. And if step 3 prints a string that differs from step 1 by so much as a trailing slash, the document is reachable but unusable: RFC 8414 §3.3 requires the issuer to be identical to the identifier used to build the URL, and the spec says a client that finds otherwise **MUST NOT** use the metadata.
Then the fix is the next section.
Working
At least one of the three answers 200 with application/json, and step 3 prints a string identical to step 1 — same scheme, same host, same path, same trailing slash or absence of one. The other two returning 404 is normal and not a problem: a provider is required to serve one of the two discovery mechanisms, not both at every URL shape. Which one answers does not matter, only that a conformant client would have asked for it.
Then this is not your problem — try the error reference.
5 other strings clients print for this
  • Error populating auth server metadata for <issuer>: AggregateError: Failed to fetch authorization server metadata from all attempted URLs
  • Failed to fetch authorization server metadata from <url>: <status> <body>
  • Failed to authenticate with MCP server '<name>': Failed to fetch authorization server metadata for client registration (attempted issuers: <urls>)
  • HTTP <status> trying to load OpenID provider metadata from <url>
  • failed to fetch authorization server metadata: failed to fetch metadata from any authorization server

Dotted spans such as <url> are placeholders — your client prints your own value there.

Fix it

Read the issuer your MCP server publishes in authorization_servers, then check that the issuer answers one of the three well-known URLs a client probes: /.well-known/oauth-authorization-server with the issuer's path inserted, /.well-known/openid-configuration with the path inserted, and the path-appended <issuer>/.well-known/openid-configuration. One is enough — the 2026-07-28 spec requires an authorization server to serve either RFC 8414 or OpenID Connect Discovery, and requires clients to support both. In the MCP TypeScript SDK that string is oauthMetadata.issuer, which buildOAuthProtectedResourceMetadata copies into authorization_servers verbatim; in the Python SDK it is issuer_url on AuthSettings; in FastMCP it is authorization_servers on RemoteAuthProvider. Where the provider serves the document at a URL no client will construct, serve or forward it from your own origin instead — both reference SDKs already mount /.well-known/oauth-authorization-server, and FastMCP documents the forwarding route.

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.

VS Code

If this is a warning in VS Code's MCP output channel and the server then keeps asking to register a client

  1. 1 Read it as a warning, because that is what it is. VS Code logs Error populating auth server metadata for <issuer>: …, then Using default auth metadata, and continues with endpoints it invents from the MCP server's origin — /authorize, /token and /register on that origin. Nothing serves those, so what the user reports is a repeating dynamic client registration prompt rather than a discovery failure. An open VS Code issue shows exactly that sequence for a server whose only auth is a static header.
  2. 2 Find which issuer it probed a few lines above. Using auth server metadata url: <url> means the protected resource document supplied it. No authorization_servers found in resource metadata <url> or Could not fetch resource metadata: … means it did not — and VS Code then substitutes the MCP server's own origin as the issuer, which is why the probes go to your server rather than to your identity provider.
  3. 3 Each attempt gets its own Error fetching authorization server metadata: line naming the URL and the status it returned, so the log already tells you which of the three forms was tried and what came back. Nothing in mcp.json changes this: the fix is on whichever side owns the issuer.
  4. 4 Do not expect headers you configured on the server entry to reach the discovery request. VS Code attaches them only when the metadata URL is on the MCP server's own origin — the fallback case. When the protected resource document named a separate issuer, that request goes out with Accept: application/json and nothing else.
text
2026-06-09 20:01:30.603 [warning] Could not fetch resource metadata: AggregateError: Failed to fetch resource metadata from all attempted URLs
2026-06-09 20:01:30.891 [warning] Error populating auth server metadata for https://mydomain.com: AggregateError: Failed to fetch authorization server metadata from all attempted URLs
2026-06-09 20:01:30.892 [info] Using default auth metadata
2026-06-09 20:01:49.944 [info] Received 401 status with Authorization header, retrying with new auth registration.
microsoft/vscode#320654

MCP TypeScript SDK

If your server is the MCP TypeScript SDK and clients are probing a host you did not expect

  1. 1 The authorization_servers array is not something you author. buildOAuthProtectedResourceMetadata sets authorization_servers: [options.oauthMetadata.issuer], so whatever string sits in oauthMetadata.issuer is the URL every client will build its three probes from — a placeholder, a service name that only resolves inside your network, or a development localhost all send the whole ladder somewhere a user's machine cannot reach.
  2. 2 The SDK also serves /.well-known/oauth-authorization-server on your own origin and hands back oauthMetadata verbatim — mcpAuthMetadataRouter mounts it on Express, oauthMetadataResponse answers it from a fetch handler. The SDK's own comment gives the reason: "so legacy clients that probe the resource origin still discover the AS." That route is reached only by a client that fell back to your origin; a client that read your authorization_servers goes to the issuer instead.
  3. 3 So pick which of the two you want to be true. Point issuer at the identity provider and let clients fetch from there. Or fill oauthMetadata with the document you fetched from the provider, so your own well-known route is a real answer for the clients that land on it.
  4. 4 Discovery is a cross-origin fetch for browser-resident clients. The SDK's metadata responses carry Access-Control-Allow-Origin: * because "Metadata documents must be fetchable from web-based MCP clients on any origin"; your provider's endpoint has to as well, or that class of client sees nothing there. On the client side fetchWithCorsRetry retries once without the MCP-Protocol-Version header and then returns undefined, which moves quietly to the next URL rather than raising anything you can read.
TypeScriptts
// @modelcontextprotocol/express and /server are the 2.x split packages.
// On stable @modelcontextprotocol/sdk (1.x) the same exports live under
// the /server/auth path.
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/express';

// This string becomes authorization_servers[0]. Every well-known URL a
// client probes is built from it, so it has to be the issuer as the
// provider itself publishes it — externally resolvable, trailing slash
// included or omitted to match.
const oauthMetadata = await (
  await fetch('https://auth.example.com/tenant1/.well-known/openid-configuration')
).json();

app.use(
  mcpAuthMetadataRouter({
    oauthMetadata,
    resourceServerUrl: new URL('https://api.example.com/mcp')
  })
);
MCP TypeScript SDK: Require authorization

MCP Python SDK

If your server is the MCP Python SDK

  1. 1 There is no route you forgot to mount here. issuer_url on AuthSettings is documented as "OAuth authorization server URL that issues tokens for this resource server", and it is the value that lands in authorization_servers. The SDK's authorization guide is explicit about where its responsibility ends: it publishes protected resource metadata and the 401 that points at it, and "That is the entire discovery story." No authorization server metadata document is served from your app.
  2. 2 So this error is about the string and about whether that host answers. validate_issuer_url rejects a fragment, a query string and any non-HTTPS scheme outside loopback at startup, so those never get this far. What it cannot check is reachability, and it cannot check that the provider serves a document at a URL a client will construct.
  3. 3 Get the string exactly right, trailing slash included. AuthSettings sets url_preserve_empty_path=True for that reason, with the SDK's own comment stating it: "RFC 8414/9207 issuer comparison is exact string comparison, so a spurious trailing slash would break it." A slash you did not intend changes both the URLs the client builds and the echo check it applies to what comes back.
  4. 4 Behind a proxy or in a container, issuer_url is the provider's external URL. A client resolves it from the user's machine, not from inside your network.
python
# mcp 2.x. On the 1.x stable line the class is FastMCP; these AuthSettings
# fields are the same.
from mcp.server import MCPServer
from mcp.server.auth.settings import AuthSettings
from pydantic import AnyHttpUrl

mcp = MCPServer(
    "Notes",
    token_verifier=MyTokenVerifier(),
    auth=AuthSettings(
        # Published as authorization_servers[0]. The client builds every
        # well-known probe from this exact string.
        issuer_url=AnyHttpUrl("https://auth.example.com/tenant1"),
        resource_server_url=AnyHttpUrl("https://api.example.com/mcp"),
        required_scopes=["notes:read"],
    ),
)
MCP Python SDK: Authorization

FastMCP

If your server is FastMCP with a RemoteAuthProvider

  1. 1 authorization_servers is the list clients probe — FastMCP's docs put it as "tells MCP clients which identity providers you trust." What the server itself serves is one document: "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." Authorization server metadata is not on that list, so the client goes to the provider for it.
  2. 2 Where the provider will not serve a document a client can reach, FastMCP documents forwarding as a supported pattern rather than a hack: override get_routes, call super().get_routes(), and append a route that returns the provider's document. The docs describe it as "providing authorization server metadata forwarding, which allows MCP clients to discover your identity provider's capabilities through your MCP server rather than contacting the identity provider directly."
  3. 3 Forwarding only helps clients that probe your origin. A client reads authorization_servers first, so if that still names the provider, your forwarded route is never requested. Either put your own base URL in authorization_servers, or fix the provider-side URL and leave forwarding out of it.
  4. 4 base_url is the server's base, not the MCP endpoint: the docs say for a server reachable at https://api.yourcompany.com/mcp, use https://api.yourcompany.com. It has to be externally reachable, since it is what identifies the resource in the document clients read first.
python
import httpx
from starlette.responses import JSONResponse
from starlette.routing import Route

from fastmcp import FastMCP
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier
from pydantic import AnyHttpUrl


class CompanyAuthProvider(RemoteAuthProvider):
    def __init__(self):
        super().__init__(
            token_verifier=JWTVerifier(
                jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
                issuer="https://auth.yourcompany.com",
                audience="mcp-production-api",
            ),
            authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
            base_url="https://api.yourcompany.com",
        )

    def get_routes(self) -> list[Route]:
        routes = super().get_routes()

        async def authorization_server_metadata(request):
            async with httpx.AsyncClient() as client:
                response = await client.get(
                    "https://auth.yourcompany.com/.well-known/oauth-authorization-server"
                )
                response.raise_for_status()
                return JSONResponse(response.json())

        routes.append(
            Route(
                "/.well-known/oauth-authorization-server",
                authorization_server_metadata,
            )
        )
        return routes


mcp = FastMCP(name="Company API", auth=CompanyAuthProvider())
FastMCP: Remote OAuth

Keycloak

If the issuer in authorization_servers is a Keycloak realm, or any provider whose issuer URL carries a path

  1. 1 A realm issuer always has a path — https://keycloak.example.com/realms/ <realm> — and Keycloak documents exactly one discovery endpoint for it: /realms/{realm-name}/.well-known/openid-configuration. That is the path-appended OpenID Connect form, which is the third and last URL in the order a client tries. The two path-insertion forms ahead of it are not endpoints Keycloak documents, so a conformant client sees two failures before its one success. That is expected behaviour, not a misconfiguration.
  2. 2 Which makes Keycloak a reliable detector of a client that stops early. A filed case: Bifrost's MCP OAuth client tries the two path-insertion forms and the two bare-origin forms and never the path-appended one, so an issuer with a path fails there while the document sits at a URL the client never requested. Reproduced against Replit's issuer https://replit.com/oidc, where the RFC 8414 path-insertion URL returns 403 and https://replit.com/oidc/.well-known/openid-configuration returns 200 with valid metadata.
  3. 3 Confirm the realm URL you published matches the realm's own issuer character for character. Keycloak echoes its configured hostname in that document, and RFC 8414 §3.3 makes a client discard metadata whose issuer differs from the identifier it used to build the URL — so a realm behind a proxy on a different hostname fails the check even once the fetch succeeds.
  4. 4 If the client is the component you cannot change, serve the document from somewhere the client will look: forward it from your MCP server's origin, or put an authorization server in front of the route that publishes its own metadata at the first URL in the ladder.
Keycloak: OpenID Connect discovery endpoint

Zuplo MCP Gateway

If you would rather not depend on every MCP client agreeing with your provider's well-known layout

  1. 1 For its MCP routes the gateway is both the OAuth 2.1 resource server and the authorization server, so it publishes both documents itself: RFC 9728 protected resource metadata at /.well-known/oauth-protected-resource/{routePath}, and RFC 8414 authorization server metadata at /.well-known/oauth-authorization-server gateway-wide and at /.well-known/oauth-authorization-server/{routePath} per route, which rebinds issuer. The per-route form is the RFC 8414 path-insertion URL, which is the first entry in the ladder — so a client's first probe is the one that answers.
  2. 2 The identity provider is still yours. mcp-oauth-inbound delegates browser login to an OIDC provider you configure and then issues the gateway's own bearer token, so the provider's well-known layout stops being something every MCP client has to agree with: only the gateway talks to it.
  3. 3 The well-known endpoints answer with Access-Control-Allow-Origin: *, which is what browser-resident clients need to read them cross-origin at all — the failure mode that produces no status code to read on the client side.
  4. 4 One caveat worth knowing before you rely on this, and it is documented: the gateway derives its issuer from the incoming request origin, honouring Host then X-Forwarded-Host. Front it with a custom domain whose proxy strips or rewrites those and it advertises an issuer that does not match the URL the client connected to — the same rejection, one layer further along.
Zuplo MCP Gateway: authorization

Something else

Whatever the components are, this error has one shape: a URL was built and nothing usable came back, and only two things can be changed. Change the issuer, so the URLs a client builds land where the document already is. Or change what answers those URLs, by serving the document from a place a client will ask. Run the curl above to decide which, and read the status codes rather than the client's message: three 404s mean the issuer is pointing somewhere with no metadata, one 200 that a client still rejected means the document is there and its issuer does not echo, and a 200 text/html or a 415 means something in front of the provider is answering instead of the provider. Do not treat a provider as broken for serving only one of the two mechanisms — the 2026-07-28 spec requires an authorization server to provide *at least one* of RFC 8414 or OpenID Connect Discovery, and puts the burden of supporting both on the client.

Why it happens

Nothing here is guesswork on the client's part: it takes the first entry of authorization_servers out of your protected resource metadata document and builds well-known URLs from it. For an issuer whose URL carries a path, the 2026-07-28 spec requires three, in this order — RFC 8414 with the path inserted between origin and well-known suffix, OpenID Connect Discovery with the path inserted, then OpenID Connect Discovery with the suffix appended after the path. For an issuer with no path there are two. The spec says nothing about what a client does when all of them fail, so each one improvises, and that is why the same cause surfaces as five different strings. Three things put a client in that position. The issuer string is wrong — a placeholder, a container-internal hostname, a localhost left from development, or a trailing slash that changes every URL built from it. The issuer is right but the provider serves its document at a form outside the ladder: Keycloak documents only the path-appended /realms/{realm-name}/.well-known/openid-configuration, and an Amazon Cognito user pool serves OpenID Connect discovery on cognito-idp.<region> .amazonaws.com, a different host from the rest of the pool's endpoints. Or there was no protected resource document to read at all, in which case the client falls back to the MCP server's own origin as the issuer and probes your server instead of your provider — the MCP Inspector collapses that fallback to the bare origin and drops the mount path with it. A fourth case looks like a network problem and is not. A 200 that is not a metadata document counts as a failed attempt: an SSO interstitial, a WAF challenge page, or JSON with no issuer field. VS Code reports those with status 200 in the same message, because it requires the body to parse as authorization server metadata before it counts the URL as answered. So does a provider that rejects the request rather than the path — one MCP Inspector report has an authorization server returning 415 Unsupported Media Type because the client sent Content-Type: application/json on the discovery GET.

The exchange

Each step in the exchange, in order.

  • MCP client
  • MCP server resource server
  • Authorization server issuer has a path
  1. Everything ahead of this error worked. The 401 carried a resource_metadata pointer, the document was served, and it named an authorization server — so the client has an issuer and is not guessing.

    MCP client to MCP server

    GET …/oauth-protected-resource/mcp the URL the 401 challenge named
  2. MCP server to MCP client

    200 OK authorization_servers: ["https://auth.example.com/tenant1"]
  3. Three well-known forms, in the order the 2026-07-28 spec requires for an issuer whose URL carries a path. Any one of them answering with a metadata document ends the ladder; a provider only has to serve one.

    MCP client to Authorization server

    GET /.well-known/oauth-authorization-server/tenant1 RFC 8414, path inserted
  4. Authorization server to MCP client

    404 Not Found
  5. MCP client to Authorization server

    GET /.well-known/openid-configuration/tenant1 OpenID Connect Discovery, path inserted
  6. Authorization server to MCP client

    404 Not Found
  7. MCP client to Authorization server

    GET /tenant1/.well-known/openid-configuration OpenID Connect Discovery, path appended
  8. Authorization server to MCP client

    404 Not Found no form left to try

    Flow stops here

The MCP client fetches the server's protected resource metadata, which names an authorization server whose URL carries a path. The client then probes the three well-known metadata URLs the spec requires for such an issuer — RFC 8414 with the path inserted, OpenID Connect Discovery with the path inserted, and OpenID Connect Discovery appended after the path. All three return 404, so the client never learns the authorization and token endpoints and the OAuth flow stops before any browser opens.
Zuplo

What lands on you next

Two documents and five well-known URLs stood between a client and a login screen. Past them the questions are no longer about reaching anything.

Discovery works. How does a client we have never seen get a client_id?

Client ID Metadata Documents

The client presents an HTTPS URL as its client_id and the authorization server fetches the metadata from it, so no client has to be pre-registered by hand before it can connect.

Two routes need two different identity providers. Can one gateway do that?

mcp-oauth-inbound

Not in one project — the runtime rejects a second MCP OAuth policy. Routes share the gateway's authorization server, and the provider behind it is a single configuration.

The user consented. What does the API behind the server receive?

mcp-token-exchange-inbound

Not the client's token: the spec forbids passthrough with a MUST NOT. The inbound credential is dropped and an independent upstream one is minted per user or per gateway.

A token arrived and we rejected it. Where is that written down?

mcp_auth_downstream_token_validated

Each validation of a client-presented token emits its own event, so a rejection resolves to a named outcome rather than to a 401 in an access log.

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.