SEP-2742: Declaring Authentication Methods for Remote MCP Servers#2742
SEP-2742: Declaring Authentication Methods for Remote MCP Servers#2742tobinsouth wants to merge 2 commits into
Conversation
Adds an auth block to the Remote shape shared by server.json and Server Cards (SEP-2127), declaring authentication posture, supported methods (OAuth and static headers), OAuth registration mode, client credential type, and out-of-band Authorization Server endpoints for servers without RFC 8414 discovery. Also adds pattern and maxLength validation hints to the existing Input shape. Cross-references SEP-2633 (mcp.json) for the corresponding resolved client-side fields.
nbarbettini
left a comment
There was a problem hiding this comment.
I fully support fleshing out server cards and pinning down the relationship between server cards and mcp.json. Flagging some spec-related places where I was confused or I think there is ambiguity that needs to be sorted out.
|
|
||
| 1. **A server that supports OAuth has no way to say so in its Server Card.** The `Remote` shape in SEP-2127 (and the underlying `server.schema.json`) declares headers via `KeyValueInput` but has no field for OAuth at all. A registry, IDE, or marketplace ingesting a Server Card cannot tell whether the server expects an OAuth flow, an API key, or nothing. | ||
|
|
||
| 2. **A server that supports both OAuth and a static API key can declare neither as a choice.** `headers[]` is a flat list. There is no way to say "use OAuth, or alternatively send an API key" — common for servers whose enterprise customers prefer key-based service accounts while individual users prefer OAuth. |
There was a problem hiding this comment.
common for servers whose enterprise customers prefer key-based service accounts while individual users prefer OAuth
A flag here for discussion - is this a pattern we want to encourage? Authorization says HTTP servers SHOULD implement OAuth 2.1, and later says authorization servers MUST implement OAuth 2.1. From that, we can argue that preferring key-based service accounts is certainly a choice a server developer can make, but it is out of compliance with the spec.
|
|
||
| 2. **A server that supports both OAuth and a static API key can declare neither as a choice.** `headers[]` is a flat list. There is no way to say "use OAuth, or alternatively send an API key" — common for servers whose enterprise customers prefer key-based service accounts while individual users prefer OAuth. | ||
|
|
||
| 3. **A confidential OAuth client is unrepresentable.** `mcp.json`'s `oauth` object has no field for a client secret, a private key, or a client certificate. Servers whose Authorization Servers require `client_secret_post`, `client_secret_basic`, `private_key_jwt` (RFC 7523), or `tls_client_auth` (RFC 8705) cannot be expressed. The configuration falls back to a hand-rolled `Authorization` header, which abandons the OAuth flow entirely. |
There was a problem hiding this comment.
Isn't client_secret_post, etc. discoverable via AS metadata?
|
|
||
| 3. **A confidential OAuth client is unrepresentable.** `mcp.json`'s `oauth` object has no field for a client secret, a private key, or a client certificate. Servers whose Authorization Servers require `client_secret_post`, `client_secret_basic`, `private_key_jwt` (RFC 7523), or `tls_client_auth` (RFC 8705) cannot be expressed. The configuration falls back to a hand-rolled `Authorization` header, which abandons the OAuth flow entirely. | ||
|
|
||
| 4. **Servers with non-standard discovery cannot be configured.** RFC 9728 / RFC 8414 discovery is the standard, but a meaningful fraction of real-world Authorization Servers do not publish `.well-known/oauth-authorization-server` or publish it incorrectly. Snowflake's Custom OAuth security integration, for example, exposes its endpoints at `/oauth/authorize` and `/oauth/token-request` and publishes no discovery document. There is no escape hatch in any of the configuration formats. |
There was a problem hiding this comment.
Maybe I'm a spec-purist, but I'm wary of providing this escape hatch. Saying "it's okay if you don't follow the spec, we'll try to make it work anyway" means that the spec loses significant leverage.
|
|
||
| 5. **Client ID Metadata Documents are unrepresentable.** [The CIMD Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) defines a registration model where the `client_id` _is_ an HTTPS URL pointing to a metadata document — no DCR, no per-server pre-registration. A `clientId` field that contains an HTTPS URL cannot be distinguished from a static client identifier that happens to look like a URL. The connection behavior is completely different. | ||
|
|
||
| 6. **Servers that accept anonymous requests but offer authenticated tools have no way to say so.** Many servers serve `initialize` and `tools/list` without authentication but require it for some `tools/call` operations, returning 401 lazily. A client that doesn't know this pre-authenticates unnecessarily; a client that doesn't know auth is _required_ wastes a round-trip on a guaranteed 401. |
There was a problem hiding this comment.
I can see the desire for expressing this, but IMO it is a bigger conversation than just whether it can be expressed in a server card. Currently the Authorization spec has no carve-out for "some things require authorization, and some things don't".
|
|
||
| 4. **Servers with non-standard discovery cannot be configured.** RFC 9728 / RFC 8414 discovery is the standard, but a meaningful fraction of real-world Authorization Servers do not publish `.well-known/oauth-authorization-server` or publish it incorrectly. Snowflake's Custom OAuth security integration, for example, exposes its endpoints at `/oauth/authorize` and `/oauth/token-request` and publishes no discovery document. There is no escape hatch in any of the configuration formats. | ||
|
|
||
| 5. **Client ID Metadata Documents are unrepresentable.** [The CIMD Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) defines a registration model where the `client_id` _is_ an HTTPS URL pointing to a metadata document — no DCR, no per-server pre-registration. A `clientId` field that contains an HTTPS URL cannot be distinguished from a static client identifier that happens to look like a URL. The connection behavior is completely different. |
There was a problem hiding this comment.
CIMD support is also discoverable via metadata, right? What additional benefit is there in pre-declaring it?
| | --- | --- | --- | --- | | ||
| | `type` | `"oauth"` | Yes | Discriminator. | | ||
| | `registration` | `"dynamic"` \| `"client-metadata"` \| `"static"` | No (default `"dynamic"`) | How the OAuth client is established. See [Registration modes](#registration-modes). | | ||
| | `clientId` | `string` | When `registration` is `"static"` and the publisher provides a shared client ID | The OAuth client identifier. See [Static registration and `clientId`](#static-registration-and-clientid). | |
There was a problem hiding this comment.
I'm trying to think of a scenario where the resource server publishes a shared client ID. It seems odd to me. What do you have in mind?
There was a problem hiding this comment.
Oh, now I see the static registration section below. It feels like an OAuth antipattern to me.
|
|
||
| - **`dynamic`** — The client registers itself with the Authorization Server at runtime using RFC 7591 Dynamic Client Registration. No `clientId` is declared. The Authorization Server may issue a `client_secret` in the registration response, which the client stores. This is the default and the recommended mode for new servers, per the MCP authorization specification. | ||
|
|
||
| - **`client-metadata`** — The client identifier is an HTTPS URL pointing to a Client ID Metadata Document, per [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). There is no registration step and no per-server state. The client supplies its own metadata document URL as `client_id`. No `clientId` is declared in the server's `auth` block (the URL belongs to the _client_, not the server). _Note: this mode depends on the MCP authorization specification adopting CIMD support; it is included here so the configuration format does not need a breaking change when it does._ |
There was a problem hiding this comment.
| - **`client-metadata`** — The client identifier is an HTTPS URL pointing to a Client ID Metadata Document, per [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). There is no registration step and no per-server state. The client supplies its own metadata document URL as `client_id`. No `clientId` is declared in the server's `auth` block (the URL belongs to the _client_, not the server). _Note: this mode depends on the MCP authorization specification adopting CIMD support; it is included here so the configuration format does not need a breaking change when it does._ | |
| - **`client-metadata`** — The client identifier is an HTTPS URL pointing to a Client ID Metadata Document, per [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). There is no registration step and no per-server state. The client supplies its own metadata document URL as `client_id`. No `clientId` is declared in the server's `auth` block (the URL belongs to the _client_, not the server). |
| #### Optional auth — authenticate only for write tools | ||
|
|
||
| ```json | ||
| { | ||
| "type": "streamable-http", | ||
| "url": "https://mcp.docs.example.com/mcp", | ||
| "auth": { | ||
| "posture": "optional", | ||
| "methods": [ | ||
| { "type": "oauth", "registration": "dynamic" } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The client connects without credentials, lists tools, and offers authentication when a tool returns 401. |
There was a problem hiding this comment.
What information does this convey to the client that it isn't possible to convey today? Or said another way: knowing this info, how should the client behave differently than today?
My read is that the client will still follow the pattern of "try a request, and if it returns 401 with WWW-Authenticate, perform an authorization flow" which is what clients already do today.
|
|
||
| ### Why posture is three-valued, not boolean | ||
|
|
||
| `required` and `none` are obvious. `optional` matters because the MCP authorization specification's lazy-401 model means many servers can legitimately serve `initialize` and `tools/list` to anonymous clients while gating individual tools. A boolean conflates "you must authenticate before connecting" with "you can authenticate if you want a better experience." Clients need to distinguish them: a client that doesn't know auth is required wastes a round-trip; a client that doesn't know auth is optional pre-authenticates everyone. |
There was a problem hiding this comment.
a client that doesn't know auth is optional pre-authenticates everyone.
Does it though? To my knowledge, MCP clients don't pre-authenticate today.
|
|
||
| ### Why declare a menu, not a choice | ||
|
|
||
| A Server Card describes what a server supports; an `mcp.json` describes what one consumer chose. Putting `auth.methods` on the publisher-side document keeps the layering consistent with how SEP-2127 and SEP-2633 already split concerns — `server.json` is "highly configurable," `mcp.json` is "fully resolved." Without a menu, a server that supports both OAuth and an API key must publish two Server Cards. |
There was a problem hiding this comment.
A Server Card describes what a server supports; an
mcp.jsondescribes what one consumer chose.
I like this framing! 👍
Summary
This SEP adds an
authblock to theRemoteshape shared byserver.jsonand Server Cards (SEP-2127), so a remote MCP server can declare which authentication methods it supports — and what an OAuth client needs that RFC 8414 / RFC 9728 discovery cannot provide — before the first MCP request.It declares:
none/optional/required, so clients know whether to authenticate beforeinitialize, lazily on 401, or not at all.dynamic(RFC 7591 DCR),client-metadata(CIMD I-D), orstatic(pre-registered, with the client ID either publisher-shared or consumer-supplied).none,client_secret,private_key_jwt,tls_client_auth— so confidential clients are representable..well-known/oauth-authorization-server, with strict same-origin and validation constraints to prevent IdP Mix-Up.Input.pattern/Input.maxLength— two small validation hints for configuration UIs, additive to the existingInputshape.Everything that is discoverable from RFC 8414 / RFC 9728 (token endpoint auth method, scopes, signing algorithms, registration endpoints) is deliberately excluded — the
authblock carries only facts no protocol channel can answer.Why now
SEP-2127 (Server Cards) declares headers and URL templates but has no OAuth declaration of any kind. SEP-2633 (mcp.json) has an
oauthobject with three fields (clientId,scopes,redirectUri) — not enough to express confidential clients, CIMD registration, broken-discovery servers, or "register your own client." Without this gap closed, server publishers who want to be auto-configurable still need per-client README snippets, which is the problem SEP-2633 set out to solve.The SEP also recommends a small set of corresponding additions to the
mcp.jsonoauthobject (registration mode, client credential, endpoints, posture, and aserverCardreference) — those will be raised as review feedback on #2633 separately.Relationship to existing work
Remoteshape that Server Cards inherit fromserver.json.registration: "dynamic",posture: "required") matches the spec's recommended DCR + PKCE flow exactly. Servers that comply with the auth spec need declare nothing.Looking for a sponsor
This sits at the intersection of the Server Card WG and the Auth IG. Tagging @dsp-ant, @SamMorrowDrums, and @tadasant for thoughts on whether this is the right shape and where it should live (this SEP, a follow-up to #2127, or an extension). Happy to rework the layering if the WG prefers a different split.