Why your MCP server works in one client and not another
•8 min read
The server is the same in both. What differs is which layers the client implements: how it connects, how it discovers the authorisation server, which of the three capabilities it lists, and whether it reads the hints on each tool.
An MCP server that works in one client and not another is almost never behaving differently. What differs is which layers the client implements: the transport it speaks, how it discovers the authorisation server, which of the three server capabilities it lists, and whether it reads the hints attached to each tool. Each of those has a distinct symptom, and identifying the symptom tells you which layer to look at without guessing. A server that connects but shows no prompts has a different fault from one that never connects at all.
What are the layers a client can differ on?
A client and a server have to agree at four separate points before a tool call happens, and a client can implement any subset of them. The transport comes first: a server offered over HTTP is unreachable to a client that only launches local processes over standard input and output. Authorisation comes second, and it is where most remote-server failures live. Capability negotiation comes third, deciding which of tools, prompts and resources the client will even ask for. Tool annotations come fourth, and they change behaviour rather than availability.
3Server capabilities negotiated independently — tools, prompts and resources. A client that does not implement one lists nothing from it.
Six symptoms, and the layer each one points at.
Symptom
Layer
What to check first
The client never connects and reports a generic failure
Authorisation discovery
Whether the protected-resource metadata is served at the path form, not only at the origin
The client connects but the tool list is empty
Capability negotiation
Whether the tools capability was declared before the transport was attached
Tools appear but prompts do not
Capability negotiation
Whether the client implements prompts at all, and whether they were registered before connect
Everything appears but the agent asks permission for every read
Tool annotations
Whether readOnlyHint is set, and whether this client reads hints
A tool result references something the client cannot open
Resource handling
Whether the referenced item is also listed, since a link from a result need not appear in a listing
It works locally and fails when deployed
Transport and proxy
Whether the public URL the server advertises matches the address the client was given
Why does authorisation discovery fail so quietly?
Authorisation discovery fails quietly because the failing request never reaches the server's transport, so nothing in the server's own logs mentions it. A client following the specification looks for the protected resource's metadata at a path-specific well-known URL. For a resource at https://example.com/mcp that address is /.well-known/oauth-protected-resource/mcp, not the bare origin form. A server that advertises only the origin form answers 404 to the path form, and every spec-following client reads that 404 as a connection failure.
Both can hand a model a block of JSON, so the choice looks cosmetic. It is not. A tool call is a decision the model makes with arguments it invents; a resource read is a fetch the client performs against an address it already holds.
Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
No card to start. Cancel from the settings screen, not from an email.
/.well-known/oauth-protected-resource/mcpThe path form a client looks for when the resource is at /mcp. Serving only the root form makes discovery fail before the transport is reached.
The reason this is easy to get wrong is that the omission has a plausible default. When a server does not declare its resource URL, an SDK will fall back to the issuer, which is the bare origin, and the metadata is then mounted at the root form only. The document's own resource field inherits the same mistake and names the origin rather than the endpoint the token is actually for. Both halves are fixed by declaring the real resource URL, and the whole failure has been written up separately as the protected-resource metadata path form.
A related divergence sits one step later, after discovery succeeds. Some clients register themselves dynamically against the authorisation server's registration endpoint; others expect to be handed a client identifier in advance. A server that only supports one of those two paths will connect for half its users and refuse the other half, and the refusal arrives as an authorisation error rather than a message about registration. When a token is involved at all, the failure usually surfaces as a 401 whose cause is not what it looks like.
Why do prompts disappear in every client at once?
When prompts disappear in every client rather than one, the cause is usually registration order on the server, not client support. A server declares its capabilities during initialisation, and an SDK typically registers the prompts capability the first time a prompt is registered. Once a transport has been attached, declared capabilities can no longer be changed, so registering prompts after connecting either throws or silently produces a server that never advertised prompts. Every client then correctly shows none, and the bug looks like a client problem across the board.
The distinguishing test is simple. If one client shows prompts and another does not, the second client does not implement prompts, and the answer is to make sure nothing essential lives only there. If no client shows prompts, the capability was never declared, and the fix is to move registration before the transport is attached. The same ordering applies to any capability an SDK declares lazily on first use.
How do tool annotations change behaviour between clients?
Tool annotations are hints rather than enforcement, so two clients can behave differently while both are correct. A hint such as readOnlyHint tells a client whether a call is safe to make without asking, which is the difference between an agent that can explore an account freely and one that has to request permission before every read. A client that reads the hints will gate writes and pass reads through; a client that ignores them will treat every tool the same way. Neither client is more dangerous than the other, because permission checks, approval gates and quotas live on the server and run regardless.
The design constraint worth knowing is that annotations are per tool, not per argument. A single tool with one enum member that writes is a write to every client that reads the hints, which means a mixed read-and-write tool has no honest annotation available. Marking it read-only tells an agent that a call which changes state is free to repeat. Marking it a write tells the same agent that several genuinely free checks need a human's permission, so it stops running the cheap checks that catch problems early. Splitting the tool is the shape that lets every call be described accurately, and it is the same reasoning that decides whether something should be a tool at all.
How do you debug it in order?
Establish whether the client connects at all. A connection failure is an authorisation or transport problem and never a tool problem.
Fetch the protected-resource metadata yourself, at the path form, and confirm it answers and names the endpoint rather than the origin.
Check whether the failing client does dynamic client registration or expects a pre-issued identifier.
Once connected, list capabilities and compare them against what the server intended to declare.
If a capability is missing everywhere, look at registration order relative to the transport attachment.
If a capability is missing in one client only, that client does not implement it, and nothing essential should depend on it.
Only then look at individual tools, and read the annotations to see how a hint-aware client will treat each one.
The ordering matters because each step invalidates the ones after it. Debugging a missing tool in a client that never completed authorisation is time spent on the wrong layer, and the two failures look similar from the outside: in both cases the user sees an integration that does nothing. Asking whether the client connected is the question that separates them, and it takes one look at the client's own connection state.
My server works over stdio but not over HTTP. What changed?
Authorisation, in almost every case. A local process launched over standard input and output usually inherits the user's environment and needs no token, while a remote server over HTTP has to authenticate every request and has to be discoverable first. The failure normally happens before the transport is reached, during discovery of the authorisation server, which is why the server's logs show nothing. Test the protected-resource metadata URL by hand before looking at anything else.
Why does one client show my prompts and another does not?
Because prompts are a separately negotiated capability and not every client implements them. Tools have the broadest support because they map directly onto the function-calling interface models already use; prompts and resources are implemented less uniformly. The practical rule is that nothing essential should live only in a capability some of your users' clients will never list. Prompts are a good home for multi-step plans and a poor home for the only path to a core action.
Do tool annotations actually stop anything?
No. They are hints that describe what a tool does, and a client is free to ignore them. Every permission check, approval gate and quota lives on the server and runs whether or not the client read a single hint, so a client that ignores annotations is no more dangerous than one that never saw them. What annotations change is how much an agent has to ask: an accurate read-only hint is what lets an agent explore an account without a permission prompt on every call.
Should a tool that mostly reads be marked read-only?
Not if any path through it writes, because annotations are per tool rather than per argument. One enum member that changes state makes the whole tool a write to every client that reads hints, and marking it read-only tells an agent that a state-changing call is safe to repeat. The honest fix is to split the writing path into its own tool, so that each tool can be described accurately rather than approximately.
The client connects but no tools appear. Where do I look?
At capability declaration order on the server, before looking at the client. Capabilities are declared during initialisation and cannot be changed once a transport is attached, so anything registered after connecting will not be advertised. If the tool list is empty in every client, that is the likely cause. If it is empty in only one client, compare the initialise response that client received against what the server intended to send.
Connecting a remote MCP server to Claude, ChatGPT or Cursor is the same URL in all three. What differs is where the URL goes, whether the client can complete a browser sign-in, and what counts as proof that it worked.
Dynamic client registration is the one part of MCP's authorization story marked SHOULD rather than MUST. What the specification actually requires, the two alternatives it names for servers that skip registration, and what implementing it costs.
McpOauthAuthorization
Why an MCP server works in one client, not another