Your MCP tools are missing because you never declared them

30 July 2026

There is a tool for this on the site

You 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:

MethodWhat it asks the client to doClient capability required
sampling/createMessageRun a model completion on the server’s behalfsampling
roots/listReport which directories the server may work inroots
elicitation/createAsk the user a questionelicitation

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.
  • listChanged and subscribe are promises to send notifications. Do not declare what you have not implemented.
  • sampling, roots and elicitation travel server to client and need the client’s capability.
  • Protocol revisions are dates compared exactly. Batching exists in 2025-03-26 and not in 2025-06-18.
  • instructions is 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.