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 a server sitting behind auth. Here the handshake is healthy; the tool inventory is empty or unreadable. The fix is almost never "restart it," and it's rarely on the client. It's usually one specific thing the server did or didn't say.
An Empty Tool List Is a Successful Response
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.
What Actually Causes It
In rough order of how often they bite:
The server never declared the
toolscapability in theinitializeresult. 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 seecapabilities.toolsmay never calltools/listat 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: theinitializeresponse parses fine but itscapabilitiesobject is missingtoolsentirely.tools/listgenuinely 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 becauseTOOLS_ENABLEDwasn't set, or the plugin directory was empty in the container.The client is dropping tools with an invalid
inputSchema. Every tool'sinputSchemamust be a valid JSON Schema object. A tool whose schema is malformed — not an object, atypethe client doesn't accept, a$refthat 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 tool schema drift: a schema that was valid yesterday and isn't today makes a tool vanish without an error.Pagination: the tools came back, but only on a page the client didn't read.
tools/listsupports cursor pagination. A server that returns an empty first page with anextCursor, 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 whosetools/listresult 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 anotifications/tools/list_changedso 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.
How to Diagnose It Without Guessing
You can localize this in two requests, because the protocol tells you exactly where the tools were supposed to appear.
-
Read the
initializeresult and look atcapabilities.tools. If it's absent, stop — that's your bug, and it's on the server. A compliant client won't calltools/listagainst a server that didn't advertise the capability. No amount of client-side poking fixes a capability the server never announced. -
Call
tools/listdirectly and read the raw response. Ifcapabilities.toolswas present, send the request yourself. An emptytoolsarray with nonextCursormeans the server really has zero registered — look at your registration code path and the deployed environment's config. An empty array with anextCursormeans pagination; follow the cursor. A populated array means the tools exist and your client is discarding them — now check schema validity. -
Validate each tool's
inputSchema. If the array is populated but the client shows fewer tools than it contains, run eachinputSchemathrough a JSON Schema validator. The ones that fail are the ones your client is dropping. -
Check the negotiated protocol version. Confirm the version in the
initializeresult 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.
What This Means If You Operate the Server
If you run the server, an empty tool list is almost always something you can prevent at the source:
-
Declare the
toolscapability in yourinitializeresult whenever you register any tools, so clients actually ask for them. -
Validate every
inputSchemaas JSON Schema before you ship — a malformed schema doesn't error loudly, it makes a tool silently disappear from strict clients. -
Emit
notifications/tools/list_changedif tools become available after the handshake completes. - Make tool registration observable in your own logs — log the count of registered tools at startup, so a deploy that registered zero because of a missing env var is visible to you before it's invisible to a client.
- Test the deployed instance, not just local. The most common "it worked on my machine" MCP bug is a tool inventory that depends on config the production container never got.
What This Means If You Consume One
-
Check
capabilities.toolsbefore you conclude the server is broken. If it's missing, the server told your client not to ask — report that upstream rather than debugging your own config. -
Read the raw
tools/listresponse, not just the rendered list. The array and itsnextCursortell you immediately whether the tools are absent or being dropped. - Don't silently discard tools with bad schemas — surface them. A tool that fails schema validation is a finding, not a non-event; knowing it exists and is malformed is more useful than it quietly vanishing.
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.
Top comments (0)