The connection turns green. The client says it's connected. And then the tool list is empty — the agent has nothing to call, the palette is blank, and nothing you wrote is reachable. This is one of the most disorienting MCP failures precisely because it doesn't look like a failure. A refused connection or a 401
at least tells you where to look. An empty tool list says everything worked, which is the one thing that isn't true.
It's worth being precise about what this state actually is. The transport connected, the initialize
handshake completed and parsed, and the client asked the server what it can do — and the answer was "nothing," or close to it. That is a different bug from a server that never finishes initialize and a different bug from
Start from the reframing, because it changes where you look. When a client shows no tools, it has almost always received a perfectly valid tools/list
response — one whose tools
array happens to be empty, or whose entries the client discarded. The server said "here are my tools: none." That is a 200. Nothing in the transport or the protocol is broken. So the debugging question is not "why did the request fail," it's "why is the correct answer empty — and is that empty coming from the server, or from the client throwing entries away?"
Those are the two halves of every no-tools case: the server genuinely advertised zero tools, or the server advertised tools and the client dropped them. Everything below sorts into one of those two piles, and telling which pile you're in is most of the work.
In rough order of how often they bite:
The server never declared the tools capability in the initialize result. This is the single most common cause and the most misleading, because the connection still succeeds. In the MCP handshake the server announces its capabilities; a spec-compliant client that doesn't see
capabilities.tools
may never call tools/list
at all. You get a healthy connection to a server that, as far as the client is concerned, has no tools to offer — not because the list is empty, but because the client was told not to ask. A giveaway: the initialize
response parses fine but its capabilities
object is missing tools
entirely.** tools/list genuinely returns an empty array.** The capability is declared, the client asks, and the server honestly answers with zero tools. This happens when tool registration is conditional — gated behind an environment variable, a feature flag, a config file the deployed instance never loaded, or a code path that only runs in one environment. The server you tested locally had tools; the one you deployed registered none because
TOOLS_ENABLED
wasn't set, or the plugin directory was empty in the container.The client is dropping tools with an invalid inputSchema. Every tool's
inputSchema
must be a valid JSON Schema object. A tool whose schema is malformed — not an object, a type
the client doesn't accept, a $ref
that doesn't resolve — can be silently filtered out by a strict client rather than shown as broken. Advertise five tools where three have bad schemas and the client may show two, or none. This is the same class of problem as Pagination: the tools came back, but only on a page the client didn't read. tools/list
supports cursor pagination. A server that returns an empty first page with a nextCursor
, or a client that requests one page and stops, can leave real tools unread. Less common than the capability miss, but it produces the identical symptom and is easy to miss because the raw response does contain a cursor if you look.
A protocol-version mismatch changed the shape. The client and server negotiate a protocol revision during initialize
. If the server answers a version whose tools/list
result shape differs from what the client expects, the client can parse the envelope, find nothing where it expects the array, and render an empty list. This overlaps with SSE-vs-streamable-HTTP transport confusion, where the response comes back on a channel the client isn't reading — same empty-list symptom, different root cause.
Tools registered after initialize, with no change notification. A server that loads tools lazily and finishes the handshake before they're ready must emit a
notifications/tools/list_changed
so the client re-fetches. Skip that notification and the client keeps the empty list it got at connect time, forever, even though the server now has tools.Auth scope exposes zero tools. An authenticated server can gate which tools a given token sees. A valid token with the wrong scope completes the handshake and gets an empty — or thin — tool list, which reads as "no tools" when it's really "no tools for you."
Only two of those — conditional registration and the missing notification — are really about tools not existing. The rest are about a tool inventory that exists and doesn't make it to the client intact.
You can localize this in two requests, because the protocol tells you exactly where the tools were supposed to appear.
initialize
result and look at capabilities.tools
.tools/list
against a server that didn't advertise the capability. No amount of client-side poking fixes a capability the server never announced.tools/list
directly and read the raw response.capabilities.tools
was present, send the request yourself. An empty tools
array with no nextCursor
means the server really has zero registered — look at your registration code path and the deployed environment's config. An empty array nextCursor
means pagination; follow the cursor. A populated array means the tools exist and your client is discarding them — now check schema validity.inputSchema
.inputSchema
through a JSON Schema validator. The ones that fail are the ones your client is dropping.initialize
result is one your client fully supports, and that you're reading the response on the same transport the server is answering on.The MCP health-check walkthrough has the exact initialize
and tools/list
requests to send by hand. What a one-shot manual probe can't tell you is whether today's empty list is new — a server that advertised eight tools last week and zero today is a regression worth paging on; a server that has always shown zero is a config that was never finished. That distinction needs a check with memory.
If you run the server, an empty tool list is almost always something you can prevent at the source:
tools
capability in your initialize
resultinputSchema
as JSON Schema before you shipnotifications/tools/list_changed
capabilities.tools
before you conclude the server is broken.tools/list
response, not just the rendered list.nextCursor
tell you immediately whether the tools are absent or being dropped.An MCP server that connects and shows no tools is not down and not gated — it's up and empty, which is its own diagnosis. The tool inventory either wasn't advertised, wasn't registered in the environment you deployed, or didn't survive the trip to the client intact. Merlonix's free MCP health checker reads the initialize
capabilities and the tools/list
inventory in one shot and tells you which of those it is — capability present or missing, tool count, and whether the schemas behind them are valid — instead of leaving you staring at a green light and a blank list. The MCP directory shows how live servers present their tool surface from the outside, and if you build or operate MCP servers for a living, MCP server developers gathers the rest of the toolchain in one place.
A blank tool list is the server answering a question honestly. The debugging is figuring out why the honest answer is nothing — and whether that's the server's doing or your client's.