MCP Server Connected but No Tools Showing? 7 Fixes (+ "Tool Not Found")
Nikhil Tiwari
MCP Playground
π TL;DR
- Look at the raw
tools/listresponse first. It tells you whether the server sent no tools or the client hid them. - Empty list from the server: usually a stale build, tools registered after the server connected, or a Python module that was never imported.
- No
toolscapability ininitialize: the client won't even ask for tools. - Tools listed by the server but missing in the client: a toolset or read-only flag, a tool switched off in the client, or a name or schema the client rejects.
- "Tool not found" means the name the client sent doesn't match a registered tool: a renamed tool, a stale client list, or a prefixed name.
Table of Contents
- Which problem do you have?
- Check the raw tools/list response first
- 1. The client is running an old build or another config
- 2. The server never declared the tools capability
- 3. Tools are registered after the server connects
- 4. Python: the tools module was never imported
- 5. Toolsets, read-only mode and switched-off tools
- 6. The client drops tools it can't use
- 7. Tools that arrive later: list_changed and pagination
- Fixing "Tool not found" errors
- FAQ
Your MCP server shows a green dot. The client says it's connected, but no tools are showing, or the model tries a tool and gets "Tool not found".
This one is frustrating because nothing crashed. There's no stack trace to search for, and restarting the client changes nothing.
I'll show you how to read the one response that settles it: tools/list. Then I'll walk through the seven causes I see most often, with the exact error text from the official SDKs.
If the server doesn't start at all, this is the wrong post. Start with fixing spawn npx ENOENT or why a server works in the terminal but not in Claude Desktop. New to the protocol? Read what the Model Context Protocol is first.
Which problem do you have?
"No tools" covers three different failures. Find yours before changing any code.
| What you see | What it means | Go to |
|---|---|---|
| Server connected, 0 tools | The server sent an empty list, or the client never asked | Fixes 1β4, 7 |
| Some tools missing | Hidden by a flag or token scope, switched off, or dropped by the client | Fixes 5β6 |
| Tools listed, call says "not found" | The called name doesn't match a registered tool | Tool not found |
Check the raw tools/list response first
Every MCP client does the same two steps. It sends initialize, reads the server's capabilities, then sends tools/list if the server says it has tools.
You need to see both responses as raw JSON-RPC. The client's tool picker can't tell you whether the server or the client lost the tools.
A healthy initialize result includes a tools key (the MCP tools spec says servers with tools MUST declare it):
"capabilities": {
"tools": { "listChanged": true }
}
And a healthy tools/list result has at least one entry:
{ "tools": [ { "name": "get_weather", "description": "...", "inputSchema": { "type": "object", ... } } ] }
Now read the result:
- No
toolscapability: the client skipstools/list. Go to fix 2. "tools": []: the server registered nothing by the time it answered. Go to fixes 1, 3 and 4.- Tools in the response, none in the client: the client hid them. Go to fixes 5 and 6.
- A
nextCursorfield: there are more pages. Go to fix 7.
Where to see the raw messages
- Claude Desktop:
~/Library/Logs/Claude/mcp-server-NAME.logon a Mac,%APPDATA%\Claude\logs\on Windows. It logs the messages and the server's stderr. - Cursor: Cmd/Ctrl+Shift+U opens the Output panel. Pick the MCP channel from the dropdown (Cursor MCP docs).
- Claude Code:
/mcplists each server, its status and its tools. - A remote server: paste the URL into the free MCP tester β and it lists tools in the browser.
For a local stdio server, MCP Playground Desktop shows every JSON-RPC message in its bottom JSON-RPC panel, next to a Server stderr tab. More on that at the end.
1. The client is running an old build or another config
This is the most common cause, and the least interesting. The client is not running the code you just edited.
- TypeScript not rebuilt. Your config points at
build/index.js, but you editedsrc/index.ts. Runnpm run build, ortsc --watchwhile you work. - Cached npx package.
npx -y some-servercan reuse a cached copy. Pin a version, such assome-server@1.4.0, to know what runs. - Wrong config file. Claude Desktop, Cursor, VS Code and Claude Code each read their own file, and Cursor and Claude Code also have project-level ones.
- Not fully restarted. Closing the window isn't quitting. Use βQ on a Mac, or Quit from the Windows system tray.
A quick proof: print a version line to stderr at startup, and check that the log shows it. If the line isn't there, the client is running something else.
console.error("my-server starting, build", new Date().toISOString());
Use console.error, not console.log. On stdio, stdout is the protocol channel (see why stdout breaks stdio servers).
2. The server never declared the tools capability
A client only sends tools/list if the server declared tools in initialize. No capability, no request, no tools. The server never even sees the question.
With the high-level APIs you rarely hit this. The TypeScript SDK's McpServer declares tools the first time you register a tool, and Python's FastMCP declares it for you.
The low-level Server class is different. You list capabilities yourself, and the TypeScript SDK refuses a tools handler without them:
Error: Server does not support tools (required for tools/list)
That error crashes the server at startup, so you'd see it in the log. The fix is to declare the capability in the constructor:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const server = new Server(
{ name: "my-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
Hand-written servers and other-language SDKs can skip the check. They answer tools/list fine, but the capability is missing, so clients never call it. Check the initialize result.
3. Tools are registered after the server connects
The client asks for tools right after initialize. Anything you register later can miss that moment.
TypeScript: register before connect()
This pattern looks harmless. It loads config, connects, then registers tools when a slow call finishes:
const server = new McpServer({ name: "my-server", version: "1.0.0" });
await server.connect(new StdioServerTransport());
const schema = await loadSchemaFromDb(); // slow
server.registerTool("query", { inputSchema: { sql: z.string() } }, handler);
In the TypeScript SDK (v1.32), the first registerTool also declares the tools capability. Doing that after connect() throws:
Error: Cannot register capabilities after connecting to transport
By then the client has already connected and seen no tools capability. Here the process then dies. Inside a try/catch or an un-awaited promise, it keeps running with zero tools, and the error only shows in stderr.
Register every tool first, then call connect() last.
Need tools that depend on runtime data? Register at least one tool before connecting. After that, the SDK sends notifications/tools/list_changed for each new tool (fix 7).
Python: code after mcp.run() never runs
mcp.run() blocks until the server exits. A @mcp.tool() below it is never registered.
mcp = FastMCP("my-server")
mcp.run() # blocks here
@mcp.tool() # never reached
def search(q: str) -> str: ...
Keep mcp.run() at the very bottom, inside if __name__ == "__main__":.
4. Python: the tools module was never imported
A decorator only runs when Python imports the file that holds it. Split your tools into tools.py, never import it, and the list is empty.
The fix seems obvious: import it in server.py. But there's a trap that catches a lot of people:
# server.py (you run: python server.py)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
import tools
if __name__ == "__main__":
mcp.run()
# tools.py
from server import mcp # imports server.py AGAIN, as a new module
@mcp.tool()
def search(q: str) -> str: ...
When you run python server.py, that file is the module __main__. from server import mcp loads it a second time under the name server. That creates a second FastMCP instance, and your tools register on it.
The instance that runs, in __main__, has no tools. There's no error.
Fix it by putting the instance in its own module that both files import:
# app.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
# tools.py
from app import mcp
@mcp.tool()
def search(q: str) -> str: ...
# server.py
from app import mcp
import tools # noqa: F401 registers the tools
if __name__ == "__main__":
mcp.run()
The full setup, with uv run --directory so the imports resolve, is in testing a Python MCP server locally with uv.
One more Python note: FastMCP logs Tool already exists: NAME when two functions share a name. The first one wins. If a tool behaves like an older version, check stderr for that warning.
5. Toolsets, read-only mode and switched-off tools
Many servers decide which tools to list from their config. The tool exists in the code, but your settings filter it out.
The official GitHub MCP server is a good example. With no toolsets set, it only loads context, repos, issues, pull_requests and users. Actions and code security tools don't appear until you ask:
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here",
"GITHUB_TOOLSETS": "default,actions,code_security"
}
Its --read-only flag also skips every write tool, even ones you name. Other servers follow the same idea. Check the README for:
- Toolset or feature flags, like
GITHUB_TOOLSETSor--tools. - Read-only or safe modes that drop write tools.
- Tools gated on credentials. Some servers only list tools the token can use. If the key is missing, you can get an empty list instead of an error (see env variables not reaching your server).
Then check the client. Claude Desktop, Cursor and VS Code all let you switch servers or tools off in their MCP or tools settings, and a switched-off tool isn't sent to the model.
On the server side, the TypeScript SDK can hide a tool with tool.disable(). A disabled tool is left out of tools/list, and calling it returns Tool NAME disabled.
6. The client drops tools it can't use
Sometimes the server's response is fine, and the tool still never reaches the model. The client, or the model API behind it, rejected the definition.
The spec sets the rules most clients enforce:
- Names: 1 to 128 characters, only
AβZ,aβz,0β9,_,-and.. No spaces or other symbols. - Unique names within a server.
inputSchemamust be a JSON Schema object, nevernull. For a tool with no arguments, use{ "type": "object", "additionalProperties": false }.
Model APIs add their own limits on top. Clients prefix tool names with the server name, which uses up characters. A long server name plus a long tool name can push past a model's limit. Keep both short.
Schemas are the other trap. An array with no items, or unusual keywords, can be accepted by one model and rejected by another. When a client drops a tool, it usually says so in its MCP log, not in the chat.
If you want to know which models accept your schemas, run the same server across several. That's covered in Agent Studio on desktop, and tool description quality covers getting models to pick the right tool.
7. Tools that arrive later: list_changed and pagination
Some servers load tools after startup: from a plugin folder, an OpenAPI file, or a remote catalog. The first tools/list comes back empty or short, and the rest arrive later.
The server should then send:
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }
That only works if the server declared "listChanged": true, and if the client re-fetches when told. If the client ignores it, you keep the empty list until you reconnect.
Two fixes, in order of preference:
- Load everything before you connect. A few hundred milliseconds of startup is better than an empty tool list.
- If tools really are dynamic, send
list_changedand test with a client that re-fetches. Use the client's refresh or reconnect button to confirm the tools appear.
tools/list also supports pagination. A server with many tools can return a page plus nextCursor. A client that doesn't follow the cursor only shows the first page.
If you wrote the server, return all tools in one page unless you have hundreds. If you didn't, look for nextCursor in the raw response before blaming the server.
Fixing "Tool not found" errors
Here the tools are listed. The model calls one, and the server says it doesn't exist. The wording depends on the SDK:
| Server | Error text |
|---|---|
TypeScript SDK (McpServer) | MCP error -32602: Tool NAME not found, returned as a tool result with isError: true |
Python SDK (FastMCP) | Unknown tool: NAME, returned as a tool result with isError: true |
| Spec example | JSON-RPC error -32602, "Unknown tool: NAME" |
Read the NAME in the error carefully. It shows exactly what the client sent, and that usually gives the cause away:
- A renamed tool. You renamed
searchtosearch_docs, but the client kept its old list. Restart or refresh the server in the client. - A prefixed name sent to the server. Clients show tools with the server's name added, like
mcp__github__create_issuein Claude Code. If that full name reaches your server, the client or your own agent code didn't strip the prefix. - A made-up name. The model guessed a tool that sounds right, like
get_issuewhen you haveissue_read. Clear descriptions help; so does fewer, well-named tools. - Case or separators. Names are case-sensitive.
createIssueandcreate_issueare different tools. - A disabled tool in the TypeScript SDK returns
Tool NAME disabledinstead.
The quickest test is to call the tool yourself with the exact name from tools/list, outside any model. If that works, the problem is in the client or the model, not your server.
Test it outside the client
Every fix above starts with the same question: what did the server actually send? MCP Playground Desktop answers it for local stdio servers:
- A Tools tab with a count, filled from
tools/list. If it says No tools, the server returned none. - A Server tab showing the server's version, protocol version and declared capabilities. If
toolsisn't declared, the app doesn't ask for tools, just like a client. - A JSON-RPC panel with every message, and a Server stderr tab for your startup line,
Tool already existswarnings and stack traces. - A Refresh button that runs
tools/listagain, for servers that load tools late. - Import from Claude Desktop, Cursor, VS Code, Windsurf and Claude Code, so you test the exact command your client runs.
Call a tool by hand there, then try it with a model in Agent Studio. Get MCP Playground Desktop for Mac and Windows β
FAQ
Why does my MCP server connect but show no tools?+
What does "Cannot register capabilities after connecting to transport" mean?+
Why are my FastMCP tools not showing up?+
Why are some GitHub MCP server tools missing?+
How do I fix "Tool not found" in MCP?+
My server adds tools at runtime. Why doesn't the client see them?+
Conclusion
An MCP server with no tools showing is almost never a mystery once you see the raw initialize and tools/list responses. They tell you whether the server sent nothing or the client hid it.
From there it's usually a stale build, tools registered too late, a Python import trap, or a toolset flag. "Tool not found" comes down to the exact name the client sent.
Written by Nikhil Tiwari
15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.
Free MCP Tools (no install)
Build, compare & ship MCP agents
Connect any MCP server, run evals on it, compare 90+ models side-by-side, deploy hosted servers, and save reusable agents you can export as an API β all in your browser.