# Remote MCP server returns 401? How MCP OAuth works

> What a 401 from a remote MCP server means under the 2026-07-28 spec, how the client finds the login, why registration fails, and how to test it with curl.

Published 2026-10-25. Source: https://toolvet.app/blog/remote-mcp-server-401-oauth

**Short answer:** a `401` from a remote MCP server is not an outage. It is the first step of the login flow the [MCP authorization spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) defines: the server is up and wants a bearer token. The `WWW-Authenticate` header on that response points at a protected-resource metadata document (or the client looks for it at `/.well-known/oauth-protected-resource`) that names the authorization server; the client fetches that server's metadata, obtains a client ID, runs an OAuth 2.1 authorization-code flow with PKCE and a `resource` parameter, and retries with `Authorization: Bearer <token>`. When a client "can't connect", the failure is in one of those steps, and each one can be checked with a single `curl`. This post walks through them, as of 25 October 2026.

Some background: the protocol says authorization is optional and, for HTTP transports, "SHOULD conform to this specification". The MCP server plays the role of an OAuth 2.1 resource server, the MCP client is an OAuth 2.1 client, and an authorization server, which may be the same host or a separate identity provider, issues the tokens. The 2026-07-28 revision removed the `initialize` handshake, so the first request you send is already a real one, such as `tools/list`; [Remote MCP server not connecting? Test it by hand](https://toolvet.app/blog/remote-mcp-server-not-connecting) covers that request and its headers. ToolVet runs an MCP handshake against every listed server on a schedule and counts a server that asks for a login as reachable, because it answered ([how we check and score](https://toolvet.app/methodology)). So ToolVet can tell you a server is up, but not whether your login to it will work; for that, read on.

## Step 1: the 401 and its header

Send the unauthenticated request and look at the headers:

```bash
curl -si https://mcp.example.com/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'mcp-protocol-version: 2026-07-28' \
  -H 'mcp-method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"},
        "io.modelcontextprotocol/clientCapabilities":{}}}}'
```

A conforming server answers something like:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"
```

The spec's [discovery rules](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery) give the server two ways to point at its metadata, and it must use at least one: the `resource_metadata` parameter in `WWW-Authenticate`, or a well-known URI. Clients "MUST support both discovery mechanisms and use the resource metadata URL from the parsed `WWW-Authenticate` headers when present; otherwise, they MUST fall back to constructing and requesting the well-known URIs". The fallback order matters: first the path-aware form, `/.well-known/oauth-protected-resource/mcp` for a server at `/mcp`, then the root `/.well-known/oauth-protected-resource`.

The `scope` parameter, which servers SHOULD include, is the server's statement of what this operation needs. Clients "MUST treat the scopes provided in the challenge as authoritative for the current operation", and when it is absent they fall back to the `scopes_supported` list in the protected resource metadata document.

**What goes wrong here:** a bare `401` with no `WWW-Authenticate` header *and* no well-known document leaves a spec-following client with nothing to do but abort or ask the user for pre-configured values. A `401` whose body is a login page is the same problem. If you run the server, add the header; it is one line.

## Step 2: the protected resource metadata

```bash
curl -s https://mcp.example.com/.well-known/oauth-protected-resource
```

This is the [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) document the spec requires every protected MCP server to publish. It "MUST include the `authorization_servers` field containing at least one authorization server", and it carries a `resource` field, the server's canonical URI. When several authorization servers are listed, the client picks; its registrations and tokens are kept per authorization server and never reused across them.

**What goes wrong here:** the document is behind the same login it is meant to unlock, returns HTML, or lists an issuer that doesn't match what the authorization server will later call itself (step 3 rejects that).

## Step 3: the authorization server metadata

Take the issuer from `authorization_servers` and fetch its metadata. For an issuer without a path, `https://auth.example.com`, the client "MUST try" in order:

1. `https://auth.example.com/.well-known/oauth-authorization-server` (RFC 8414)
2. `https://auth.example.com/.well-known/openid-configuration` (OpenID Connect Discovery)

For an issuer with a path, `https://auth.example.com/tenant1`, the order is the RFC 8414 path-inserted form, then OIDC with path insertion, then OIDC with the path appended. A client must support both discovery standards; the authorization server needs to offer only one. Then comes a check that is easy to skip: the `issuer` value in the document "MUST be identical to the issuer identifier used to construct the well-known URL. If they differ, the client MUST NOT use the metadata." The spec's own example is a document fetched from an attacker's host that claims `"issuer": "https://honest.example"`.

The metadata tells the client what it needs next: the endpoints, whether `client_id_metadata_document_supported` is `true`, whether a `registration_endpoint` exists, and `authorization_response_iss_parameter_supported`.

## Step 4: getting a client ID

This is where "it worked with one client and not another" comes from. The [client registration](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration) page defines three mechanisms and a priority order a client should follow:

1. **Pre-registered credentials**, if the client has them for this server.
2. **Client ID Metadata Documents** (CIMD), if the authorization server advertises `client_id_metadata_document_supported`. The client's ID *is* an HTTPS URL, such as `https://app.example.com/oauth/client-metadata.json`, pointing at a JSON document with at least `client_id`, `client_name` and `redirect_uris`; the authorization server fetches it, checks that the `client_id` inside matches the URL exactly, validates the redirect URI against the list, and shows the user a consent page with the `client_name`. The spec says clients and authorization servers "SHOULD support" CIMD, and it is the recommended path when the two have no prior relationship.
3. **Dynamic Client Registration** (RFC 7591), if the metadata has a `registration_endpoint`. It is deprecated as of 2026-07-28, and "remains available for backwards compatibility with authorization servers that do not support Client ID Metadata Documents"; it is eligible for removal in the first revision released on or after 28 July 2027 per the [deprecated features registry](https://modelcontextprotocol.io/specification/2026-07-28/deprecated) ([what else 2026-07-28 deprecated](https://toolvet.app/blog/mcp-roots-sampling-logging-deprecated)).
4. **Ask the user** to type a client ID and secret, when nothing else is available.

**What goes wrong here:** the authorization server supports neither CIMD nor dynamic registration, so a client with no pre-registered ID and no UI for entering one stops with a registration error. Or registration is rejected over the redirect URI: an OpenID Connect server may refuse loopback callbacks such as `http://127.0.0.1:…` because a missing `application_type` defaults to `"web"` under OIDC; the spec now requires clients to set it appropriately, `"native"` for desktop, CLI and localhost apps. Or the server's metadata switched to a different authorization server and the client keeps presenting credentials from the old one; the spec binds credentials to the issuer that gave them and says the client "MUST re-register with the new authorization server". CIMD identifiers are portable across authorization servers; dynamic registrations are not.

## Step 5: the authorization code, with PKCE and `resource`

The client opens the browser on the authorization endpoint with a PKCE challenge, the scopes from step 1, and a `resource` parameter that "MUST identify the MCP server that the client intends to use the token with", in canonical form: `https://mcp.example.com/mcp`, no fragment, no trailing slash unless it means something. The same parameter goes on the token request. It is sent "regardless of whether authorization servers support it". Before the redirect, the client records the issuer from step 3; if the callback carries an `iss` parameter ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)), the client compares it to the recorded value by plain string comparison, no case folding, no normalisation, and rejects the response on mismatch. If the metadata said `authorization_response_iss_parameter_supported: true` and `iss` is missing, the client must reject the response too.

**What goes wrong here:** the user sees the consent page and the client still ends with an error. One cause is a `resource` value that isn't the one the resource server expects in the token's audience, so step 6 fails; another is a callback `iss` that differs from the issuer in the metadata (a proxy, a trailing slash, a different hostname for the same identity provider).

## Step 6: the token, and the 403 that isn't a 401

The access token goes in the `Authorization: Bearer` header on every request; it must not go in the query string. The server must validate that the token was issued for it as audience and must answer `401` for an invalid or expired token. The spec's error table, in short:

| Status | Meaning |
|---|---|
| `401` | Authorization required, or the token is invalid or expired |
| `403` | Invalid scopes or insufficient permissions |
| `400` | Malformed authorization request |

A `403` at runtime should come with `WWW-Authenticate: Bearer error="insufficient_scope", scope="files:write", resource_metadata="…"`. The spec's step-up flow: take the union of the scopes you previously requested and the ones in the challenge, re-authorize, retry the original request "no more than a few times", then treat it as permanent. Servers "SHOULD include all scopes required for the current operation in a single challenge"; one missing scope per round trip means repeated consent screens.

Three rules in the token-handling section close the confused-deputy and token-passthrough cases, two for servers and one for clients. An MCP server "MUST only accept tokens that are valid for use with their own resources" and "MUST NOT accept or transit any other tokens": forwarding the client's token to a downstream API, or accepting a token minted for some other API, is the confused-deputy case. And a client "MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server", so an API key for another service pasted into the `Authorization` header is a client bug, however the server reacts to it.

## Checklist

If you run a remote MCP server behind a login: return `401` with `WWW-Authenticate: Bearer resource_metadata="…"` and a `scope`; publish `/.well-known/oauth-protected-resource` with `resource` and `authorization_servers`; make sure your authorization server publishes metadata whose `issuer` matches its URL exactly and supports CIMD, because dynamic registration is on its way out; validate the token's audience against your canonical URI; answer missing scopes with `403` and `insufficient_scope`, every needed scope in one challenge.

If you build a client or an agent: parse `WWW-Authenticate` first, fall back to the two well-known paths in order, try both discovery documents, reject metadata whose `issuer` doesn't match, send `resource` on both requests, check `iss` on the callback, keep registrations and tokens per issuer, and treat `401` as "log in" and `403` as "ask for more scope", never as "the server is down". Whether the server is up is a question ToolVet already answers, on every listing in the [directory](https://toolvet.app/browse) and on the [ToolVet MCP server](https://toolvet.app/agents).

## Frequently asked questions

### Does a 401 mean the remote MCP server is down?

No. Under the MCP authorization spec a `401` is how a protected server starts the OAuth flow: it is up, and the `WWW-Authenticate` header (or the `/.well-known/oauth-protected-resource` document) tells the client where to log in. ToolVet counts a server that asks for a login as reachable for the same reason.

### Why does my MCP client say it can't register with the server?

Usually because the authorization server offers neither automatic registration mechanism: Client ID Metadata Documents (advertised by `client_id_metadata_document_supported: true`) or a `registration_endpoint` for dynamic registration. The remaining options are a pre-registered client ID or typing one in. The other common cause is a registration rejected over the redirect URI (see step 4). Dynamic registration is deprecated since 2026-07-28, so new servers should support CIMD.

### What is a Client ID Metadata Document in MCP?

A JSON file the client hosts at an HTTPS URL, and that URL is the `client_id`. It must contain at least `client_id`, `client_name` and `redirect_uris`, with `client_id` equal to the URL. The authorization server fetches it on demand, validates the redirect URI against it and shows the `client_name` on the consent page, so no prior registration is needed.

### What is the resource parameter for in MCP OAuth?

It is RFC 8707's resource indicator: the canonical URI of the MCP server the token is for, sent on both the authorization and the token request. The server then checks that the token's audience is itself, which stops a token issued for one server from being replayed at another.

Everything quoted above is from the MCP specification revision 2026-07-28 as read on 25 October 2026. [How we check and score](https://toolvet.app/methodology) explains how ToolVet tests remote MCP servers and why a login request counts as a reachable server.
