Your MCP tools are missing because you never declared them
30 July 2026
There is a tool for this on the siteYou wrote an MCP server. It has four tools. The handlers are registered, the process starts, the client shows a green dot, and the model has no tools.
Nothing is broken. Look at your initialize response:
{
"protocolVersion": "2025-06-18",
"capabilities": {},
"serverInfo": { "name": "my-server", "version": "1.0.0" }
}
That empty object is the entire bug.
Capabilities are a permission slip, not a description
The handshake is not the client asking what you can do and the server answering. It is both sides declaring what they support, with each side then expected not to use anything the other did not declare.
A spec-following client reads capabilities: {} and concludes there are no
tools to list. So it never sends tools/list. Your handler is registered, it
works, and it is never called.
The server is correct. The client is correct. The tools do not exist.
The minimum fix is four characters:
"capabilities": { "tools": {} }
An empty object as the value means “I support tools, with no optional
sub-features”. That is entirely different from the key being absent, and the
distinction is easy to miss when both look like {} at a glance.
Why it is so hard to spot
Every layer reports success. The process is running, so the client shows connected. The handshake completed, so there is no error. The tool list is empty, which is a perfectly legal state for a server that genuinely has no tools.
It works in some clients. Implementations vary in strictness. A lenient
client that calls tools/list regardless of the declaration will find your
tools, so the server appears to work in the inspector and fails in the product,
which sends people looking at the client.
The SDKs differ in how much they do for you. Some derive capabilities from what you registered; some require you to declare them explicitly. Code copied between languages, or between SDK major versions, is where this most often arrives.
Declaring a sub-capability is a promise, not a hint
This is the second class of bug, and it is the more interesting one:
"capabilities": { "tools": { "listChanged": true } }
listChanged does not mean “my tool list might change”. It means “I will send
notifications/tools/list_changed when it does”, and the client is entitled
to act on that: cache the tool list, stop re-listing, and wait to be told.
A server that declares it and never emits the notification leaves every client on a stale list indefinitely. That is worse than not declaring it, because without the flag the client would have re-listed on its own.
The same applies to prompts.listChanged and resources.listChanged.
resources.subscribe is a separate promise about a different thing: one
resource’s contents changing, rather than which resources exist. Declaring it
commits you to implementing resources/subscribe and resources/unsubscribe,
and a client that calls one after being told it could gets a method-not-found.
Rule of thumb: declare a sub-capability only when you have written the code that honours it. The base capability costs nothing; the flags are contracts.
The direction that surprises people
Three methods travel from the server to the client:
| Method | What it asks the client to do | Client capability required |
|---|---|---|
sampling/createMessage | Run a model completion on the server’s behalf | sampling |
roots/list | Report which directories the server may work in | roots |
elicitation/create | Ask the user a question | elicitation |
A server that calls one the client did not offer gets a method-not-found back,
which reads like a broken client rather than a capability that was never
granted. Gate these calls on what the client actually declared in its
initialize request; do not assume.
sampling deserves a second look for a different reason: it is the one
capability where the server spends the user’s tokens. The protocol expects a
human approval step in front of it rather than requiring one, which means it is
the client’s job and it is easy to skip. If you are writing a client, put the
gate in. If you are writing a server, do not assume there is one.
Versions are dates, compared exactly
The other half of the handshake. Revisions are strings like 2025-06-18. There
is no range, no semver, and no compatible-with.
The client offers one; the server either echoes it or returns the newest revision it actually supports. If the client cannot speak what came back, the spec says it should close the connection rather than continue.
That sounds pedantic until you notice the differences are real. JSON-RPC
batching was valid in 2025-03-26 and removed in 2025-06-18. A client that
ignores a downgrade and keeps sending batches gets a parse error it will read as
a transport problem.
Two failure shapes:
- Server answers older than the client asked for. Negotiation working as designed. The client must now disable anything added after that revision.
- Server answers something the client never offered. A negotiation bug in the server. It should echo when supported and otherwise return its own newest.
instructions is charged on every turn
A field that is easy to over-fill:
{ "protocolVersion": "...", "capabilities": {...}, "instructions": "..." }
The instructions string goes into the model’s context for the session, on
every request, not once at the start. A long one is a standing per-turn cost
that also competes for attention with the actual task.
Detail belongs in tool descriptions, where it sits next to the thing it
describes and is read in context. Keep instructions to what the model needs
before it has looked at any tool: what this server is for, and any constraint
that applies across all of it.
What changed recently
Batching was removed in 2025-06-18. If you support multiple revisions, the
batch path has to be gated on the negotiated version rather than always
available.
Elicitation is a first-class capability. Servers can now ask the user a structured question mid-operation rather than failing and asking them to retry with more arguments. It requires the client capability, so it needs the same gating discipline as sampling.
Streamable HTTP superseded the SSE transport. Not a capability question, but
it lands in the same debugging session: a config saying "type": "sse" against
a server written since the change produces a connection that opens and never
completes a handshake, which looks like a capability problem and is not.
Fixing it on a running server
1. Print your own initialize response. Before anything else. Most of these
bugs are visible in that one message, and most people have never looked at it.
2. Declare the base capabilities for everything you implement. tools,
resources, prompts, logging, completions. Empty objects are correct and
sufficient.
3. Audit the sub-flags against your code. For each listChanged and
subscribe, find the line that emits the notification or handles the
subscription. If it does not exist, remove the flag rather than writing a
placeholder.
4. Gate server-to-client calls on the client’s declaration. Read the
initialize request’s capabilities, store it, and check before calling
sampling/createMessage, roots/list or elicitation/create.
5. Measure instructions. If it is over a few hundred tokens, move the bulk
into tool descriptions.
When an empty capabilities object is correct
A server that genuinely only serves resources should declare resources
and nothing else. Declaring tools on a server with no tools is the mirror-image
bug and produces a tools/list call that returns an empty array on every
client, forever.
A server still under construction is better off declaring only what works. An honest empty declaration is a working server with no features; an optimistic one is a server that errors when the feature is used.
If you are deliberately hiding a capability behind a flag or a licence tier, not declaring it is the correct mechanism. That is the feature working as intended rather than a bug.
The short version
- Capabilities are a permission slip. An undeclared capability is never called.
"capabilities": { "tools": {} }is the minimum for a server with tools.listChangedandsubscribeare promises to send notifications. Do not declare what you have not implemented.sampling,rootsandelicitationtravel server to client and need the client’s capability.- Protocol revisions are dates compared exactly. Batching exists in
2025-03-26and not in2025-06-18. instructionsis paid on every turn, not once.
Paste your initialize request and result into the
MCP capability negotiation checker and
it reports the negotiated revision, what each side may now call, and the
declarations that commit you to work you may not have done.