When you connect an MCP server to Claude Code, there's a failure mode nobody warns you about: the tools load, the connection handshake succeeds, and Claude still never calls them. It just uses Bash or grep or works out its own approach.
I've hit this probably six times now. The root cause is almost always the tool description, parameter names, or return format — not the implementation.
Why Claude routes around your tools
Claude doesn't pick tools randomly. It reads the schema and decides whether calling your tool is clearer than doing it directly. A vague description and it won't recognize when to use it. An unpredictable return format and it'll try once, fail silently, and fall back to Bash.
The description problem
{
"name": "get_customer",
"description": "Gets customer data from the database"
}
vs:
{
"name": "get_customer",
"description": "Retrieve a customer record by ID. Use when you need billing info, account status, or contact details. Returns name, email, plan, and created_at."
}
The second version tells Claude what triggers this tool, what the inputs should be, and what it gets back. That's the difference between reliable routing and Claude deciding to query the DB itself.
Specific parameter names
id and type as parameter names are almost useless. Claude has to guess from context what they mean. customer_id, record_type, start_date are obvious. Claude reads the parameter names when it's deciding how to form a call — generic names make it guess wrong or skip the tool entirely.
One tool, one job
A manage_records tool that takes an action parameter and handles create/read/update/delete will often be ignored. Four explicit tools with obvious names routes correctly every time. More tools isn't overhead — it's clarity.
Verifying Claude is actually calling your code
Add one line to each handler:
async function get_customer({ customer_id }: { customer_id: string }) {
console.error(`[MCP] get_customer called: ${customer_id}`)
// ...
}
Use console.error, not console.log. MCP servers communicate on stdout, so stderr is where debug output should go. You'll see immediately whether Claude is calling your tools or quietly working around them.
The silent error problem
Worst case: your tool throws, Claude gets an empty response, and keeps going without mentioning it. Your logs show the call happened. The result never made it through.
try {
const result = await doWork(args)
return { content: [{ type: 'text', text: JSON.stringify(result) }] }
} catch (err) {
return {
content: [{ type: 'text', text: `ERROR: ${err.message}` }],
isError: true
}
}
With isError: true, Claude reasons about what went wrong and tries something else. Without it, silence.
I put these patterns into a starter kit — TypeScript template, 4 working examples (file system, database, web scraper, REST wrapper), debugging checklist. builtbyzac.com/mcp-kit.html if you're building one. $49.
Top comments (0)