---
title: "Failed to Fetch Authorization Server Metadata From All Attempted URLs"
description: "The client knows which authorization server to use but cannot read its metadata, so OAuth stops before /authorize. The fix per SDK, IdP and client."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/failed-to-fetch-authorization-server-metadata"
pageType: "mcp-error"
errorString: "Failed to fetch authorization server metadata from all attempted URLs"
lastVerified: "2026-07-30"
specVersion: "2026-07-28"
---

# `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.

```bash
# 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.

**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. If this is what you see, this is not your problem.

**Clients also report this as:**

- `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`

## Fix it

**In short.** 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.

The symptom is identical across platforms; the cause is not.

### 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. 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. 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. 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. 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](https://github.com/microsoft/vscode/issues/320654)

### MCP TypeScript SDK

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

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. 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. 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. 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.

```ts
// @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](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md)

### MCP Python SDK

If your server is the MCP Python SDK

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. 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. 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. 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](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/run/authorization.md)

### FastMCP

If your server is FastMCP with a `RemoteAuthProvider`

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. 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. 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. `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](https://gofastmcp.com/servers/auth/remote-oauth)

### Keycloak

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

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. 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. 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. 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](https://www.keycloak.org/securing-apps/oidc-layers)

### Zuplo MCP Gateway

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

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. 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. 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. 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](/docs/mcp-gateway/auth/overview)

### 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 `404`s 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

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.

**Participants:** MCP client, MCP server (resource server), Authorization server (issuer has a path)

1. **MCP client → MCP server** — `GET …/oauth-protected-resource/mcp` — `the URL the 401 challenge named`
2. **MCP server → MCP client** — `200 OK` — `authorization_servers: ["https://auth.example.com/tenant1"]` _(response)_
3. **MCP client → Authorization server** — `GET /.well-known/oauth-authorization-server/tenant1` — `RFC 8414, path inserted`
4. **Authorization server → MCP client** — `404 Not Found` _(response)_
5. **MCP client → Authorization server** — `GET /.well-known/openid-configuration/tenant1` — `OpenID Connect Discovery, path inserted`
6. **Authorization server → MCP client** — `404 Not Found` _(response)_
7. **MCP client → Authorization server** — `GET /tenant1/.well-known/openid-configuration` — `OpenID Connect Discovery, path appended`
8. **Authorization server → MCP client** — `404 Not Found` — `no form left to try` _(response; this step fails — the flow stops here)_

Notes:

- **Steps 1–2** — 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.
- **Steps 3–8** — 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.

## 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. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Prevent it** — [OAuth 2.1 Authentication for MCP Servers](/learn/mcp/authentication/oauth-2-1) — Servers that must be reachable by MCP clients you do not control, or listed in a connector directory
- **Similar error** — [`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`](/learn/mcp/errors/401-without-www-authenticate) — 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.
- **Similar error** — [`Dynamic Client Registration not supported`](/learn/mcp/errors/dynamic-client-registration-not-supported) — The MCP client reached your authorization server's metadata and found no way to obtain a client ID — no registration endpoint, no Client ID Metadata Document support, and none configured in the client — so it stopped before the OAuth flow began.
- **Similar error** — [`Your MCP server returns a login page or 403 instead of 401`](/learn/mcp/errors/302-redirect-instead-of-401) — Something in front of your MCP server is enforcing authentication built for humans in browsers. Instead of the 401 an MCP client needs, it returns a login page, a redirect to one, or a bare 403 — so the client never learns how to authenticate.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
