# Your MCP Server Connects but Shows No Tools

> Source: <https://dev.to/merlonix/your-mcp-server-connects-but-shows-no-tools-1fo6>
> Published: 2026-08-24 16:17:12+00:00

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.
