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
- Build the token request as
application/x-www-form-urlencoded, whatever your first server accepts. - Add one fixture mode per server variation: public client with no secret,
client_secret_basic, mandatoryresource, form-urlencoded only. - Read
token_endpoint_auth_methods_supportedand follow it. Treat a missing capability as something to degrade on, never to fail on. - Start discovery from the
401and itsresource_metadatapointer, so the operator pastes one URL. - Answer 200 when revocation is asked for an invalid token, as RFC 7009 requires, and never read a 200 as proof the token was real.
- Before storing a token, write down what it represents. If it represents a person, the store has to be able to say which person.
- Never ask an operator a question the server can answer.
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.
Related writing
- Evaluating LLM outputs in production — the same argument for model output: fixtures you control, and a gate that fails the build instead of your users.
- Shipping production software with AI agents — the operating model that makes a conformance fixture worth writing, because agents ship faster than review does.
- Observability for LLM pipelines — what to instrument when the failure is silent rather than loud, which is the case for every trap above.
- A 90% pass rate is not a gate — the same idea for models: a hard requirement you cannot average away.