Pablo Albaladejo

MCP · · 6 min read

A tolerant OAuth server validates your broken client

The OAuth conformance surface an MCP client must cover: RFC 9728 discovery, public clients with PKCE, form-urlencoded token requests, and why a lenient first server hides your bugs.

An OAuth server that accepts more than the specification requires will certify a client that breaks it. Your client passes against that server. It ships. Then it fails against a stricter server, and nothing points back at the first integration, because the first one still works. Below is the conformance surface an MCP client has to cover, the traps I check for inside it, and the one question about tokens that no specification can answer for you.

Tolerance is not conformance

RFC 6749 is precise about the token request. Section 4.1.3 says the client sends the parameters in the request body with the media type application/x-www-form-urlencoded. A server is free to be generous and also accept a JSON body. Plenty of them are.

That generosity is the trap. If your client sends JSON and the server takes it, your integration test passes. You now hold evidence that your client works, and the evidence is false. The defect sits in your code, unchanged, waiting for a server that reads the spec strictly.

So the first server you connect to is not a test suite. It is one sample from a large space, and it does not tell you which of your assumptions are wrong. A lenient server is more dangerous than a strict one, because a strict one fails you on day one, when you are still looking.

The fix is not more integration tests against that server. The fix is a conformance fixture that pins the specification, with one mode for each thing a real server may require. Then any new server is a point inside a space you already cover, or it tells you exactly what is missing.

The conformance surface, in the order a client meets it

MCP defines its authorization profile on top of ordinary OAuth 2.0, so the work is mostly reading the right documents in the right order.

Discovery starts with a failure. An unauthenticated call gets a 401, and the WWW-Authenticate header carries a resource_metadata pointer. RFC 9728 defines that document. It names the authorization servers for the resource and the scopes it supports. From there, RFC 8414 gives you the authorization server’s own metadata: its endpoints, its grant types, its supported auth methods.

This matters for product design, not only for code. If discovery works, the operator pastes one URL and your client finds the rest. Every field you ask a human to copy out of a vendor PDF is a field they can copy wrong.

Client identity may not exist yet. RFC 7591 lets your client register itself and receive a client_id. Read the response rather than assuming it: a server may return no client_secret at all.

The auth method is declared, so honour it. token_endpoint_auth_methods_supported may say ["none"]. That is a public client. It holds no secret and proves the request with PKCE instead. If your code path requires a secret to store credentials, or to build the token request, that server is rejected before you reach the network. The failure looks like a broken partner. It is not.

The body encoding is fixed, as above. Send form-urlencoded, always, whatever the server tolerates.

Tokens should be scoped and revocable. RFC 8707 adds a resource parameter so a token is bound to one API instead of being a general-purpose key. Some servers require it. Revocation is easy to read backwards: RFC 7009 section 2.2 says the server answers with status 200 both when it revoked the token and when the client submitted an invalid one. So a 200 is not evidence that the token existed, and a client that treats it as evidence has read something into the response that the specification does not put there.

A client that cannot redirect has a standard answer. Chat interfaces and call centres cannot always open a browser and receive a callback. That is what CIBA is for: the client starts the flow, the user approves out of band, the client polls. It is a published specification with a discoverable endpoint. Reinventing it with undocumented routes is the thing to avoid.

The discoverability test

Partners will offer you shortcuts. Some of them work, are written down, and are genuinely supported. The question is which ones become product, and there is a single test:

Is the capability discoverable from published metadata, so that any client could find it without being told?

Standard discovery documents pass. A declared auth method passes. A documented backchannel endpoint passes. Undocumented routes fail. A value your client extracts from a login page with a regular expression fails hardest of all, because a login page’s internals were never a contract. The day the partner renames a form field, you break, and they have not broken any promise.

If the answer is no, you are not integrating with a partner. You are depending on their internals. That is a decision you can still make on purpose, but it should be a decision, not an accident.

The trap no specification catches

Every rule above is machine-checkable. This one is not.

The authorization code grant serves two situations that look identical in the protocol. In one, the token represents a company, and one token for the whole tenant is right. In the other, the token represents one named person who signed in, and it must never be shared.

The wire tells you nothing about which one you have. Both flows look the same, and both hand your client a token to store. If the store has no place to record which person a token belongs to, a personal token gets written wherever the shared one goes, and the next user of that session inherits it. No schema check catches this. No conformance fixture catches it. Every specification above is satisfied.

The question that catches it is not “does this work”. It is “what does this token represent?” The answer is commercial rather than technical, so ask it before you design the storage instead of after.

What this does not solve

Reading specifications is the cheap part, and it is not the whole job. A conformance fixture proves your client handles the shapes you thought of. It says nothing about the shape you have not met yet, so it shrinks the space of surprises without closing it.

It also cannot fix the other side. If a server does not implement revocation, or its client registration expires with no programmatic way to renew it, no amount of conformance work in your own code helps. What the specifications give you there is a shared vocabulary. “Your metadata does not declare this” is a checkable statement about a document, and I would rather bring that to a partner than an opinion about their roadmap.

A checklist you can lift

A specification you follow only as far as your first partner requires is not a specification. It is that partner’s configuration, with your name on it.

FAQ

Why does an OAuth client break on the second integration and not the first?
Because servers are allowed to accept more than the specification requires. If your first server tolerates a malformed request, your client passes and you treat that as proof it works. The bug is still there. It appears on the first server that is strict, and the earlier integration keeps working, so nothing points back at it.
What does token_endpoint_auth_methods_supported: [none] mean?
It means the authorization server issues public clients: clients that hold no secret and authenticate the request with PKCE instead. It is declared in the server metadata defined by RFC 8414. A client built on the assumption that every server returns a client secret will reject that server before it reaches the network.
Does an OAuth token request have to be form-urlencoded?
Yes. RFC 6749 section 4.1.3 says the client sends the parameters in the request body using the application/x-www-form-urlencoded media type. JSON is not compliant, even when a server accepts it.
How do I decide whether a partner's integration shortcut should become product?
Ask whether the capability is discoverable from published metadata, so any client could find it without being told. Discovery documents, declared auth methods and standard endpoints pass. Undocumented routes and values scraped out of a login page fail: you are not integrating with the partner, you are depending on their internals.

SOURCES & PROOF