Your MCP Server Connects but Shows No Tools A developer troubleshooting MCP servers that connect but show no tools identifies the root cause as a valid but empty tools/list response, not a transport failure. The most common culprit is the server failing to declare the tools capability in the initialize result, causing spec-compliant clients to skip the tools/list call entirely. Other causes include conditional tool registration, invalid inputSchema entries, and pagination issues. 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 https://merlonix.com/blog/why-mcp-initialize-fails/ 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 https://merlonix.com/blog/streamable-http-vs-sse-mcp-transports/ , 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 https://merlonix.com/blog/how-to-health-check-a-remote-mcp-server/ 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 result inputSchema as JSON Schema before you ship notifications/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 https://merlonix.com/tools/mcp-health/ 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 https://merlonix.com/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 https://merlonix.com/for/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.