MCP tool calls fail silently in production more often than they crash loudly. The agent skips the tool, continues with incomplete data, and answers the user’s question with a confident hallucination instead of real facts. No error message. No timeout alert. No indication anything went wrong.
According to the CNCF Annual Survey 2025, 68% of organizations running AI agents in production report silent tool failures as their top observability gap, ranking higher than latency or cost overruns as the hardest failure mode to detect and diagnose.
This guide walks through a five stage diagnostic workflow that catches every MCP tool failure mode, from handshake corruption to schema validation errors to runtime timeouts. Each stage includes test commands, expected output, and the exact fix for common production failures.
Prerequisites
Before debugging MCP tool failures, ensure you have:
- MCP Inspector installed (
npm install -g @modelcontextprotocol/inspectorfor Node.js servers orpip install mcp-inspectorfor Python servers) - Access to your MCP server configuration file (typically
mcp.jsonor passed as environment variables) - Access to server logs with stderr output visible (stdout is reserved for JSON-RPC protocol messages)
- A test environment that mirrors production configuration (same environment variables, same paths, same network access)
- Permission to restart the MCP server process during testing
Step 1: Verify the Handshake (Can the Client Talk to the Server?)
The MCP handshake is the initialize exchange between client and server. If this fails, nothing else works. The protocol is JSON-RPC over stdio (for local servers) or HTTP/SSE (for remote servers). A single print statement to stdout before the handshake completes will corrupt the entire protocol stream.
Test the handshake:
Start MCP Inspector and connect to your server. For Node.js servers:
npx @modelcontextprotocol/inspector /absolute/path/to/your/server.js
For Python servers:
mcp dev your_server.py
Inspector opens a web interface at http://localhost:5173 (Node.js) or http://localhost:5000 (Python). Click Connect.
Expected result: Inspector displays “Connected to MCP Server” and shows the server’s capabilities in the connection panel.
If connection fails:
Check these common handshake failures in order:
Wrong path to server binary: The most common failure. Use absolute paths. Test the command manually in your terminal first. If node /path/to/server.js fails in your shell, it will fail in MCP.
Stdout corruption: Your server prints something to stdout (a debug log, a startup message, a dependency warning) before the JSON-RPC stream begins. The client tries to parse “Server starting…” as JSON and fails silently.
Fix: Redirect all non-protocol output to stderr. In Node.js, replace console.log() with console.error(). In Python, use sys.stderr.write() or configure your logger to output to stderr only.
// Wrong — corrupts handshake
console.log("Server starting...");
// Correct — logs to stderr, handshake unaffected
console.error("Server starting...");
Environment variables not expanding: Your server config references $GITHUB_TOKEN but the variable is not set in the environment where the MCP client launches the server. Test with:
echo $GITHUB_TOKEN
If empty, set it in your shell profile or pass it explicitly in the MCP client config.
Port conflicts (HTTP servers only): If your server uses HTTP transport, verify the port is available. Inspector uses ports 6274 and 6277 by default. Check with:
lsof -i :6274
If occupied, stop the conflicting service or change Inspector’s port.
Step 2: Confirm Tool Discovery (Can the Client See the Tools?)
The handshake worked. The server is connected. But does the client see your tools?
Test tool discovery:
In MCP Inspector, click the “Tools” tab, then click “List Tools.”
Expected result: Every tool your server exposes appears in the list with its name, description, and input schema visible.
If tools are missing:
Tool handler not registered: Your server code defines the tool but does not register it with the MCP handler. In the Python SDK, tools must be decorated with @server.tool(). In TypeScript, tools must be added to the server’s tool array during initialization.
# Wrong — tool defined but not registered
def search_database(query: str):
return perform_search(query)
# Correct — tool registered with MCP server
@server.tool()
def search_database(query: str):
"""Searches the customer database by email or account ID."""
return perform_search(query)
Server started before tool registration completed: Race condition common in async setups. If your server answers tools/list before all tool handlers are loaded, the client gets an incomplete list and caches it.
Fix: Ensure tool registration completes before the server starts accepting connections. Add initialization checks or use a ready signal.
MCP client is caching a stale tool list: Claude Desktop, Cursor, and other MCP clients cache the tool list after the first tools/list response. If you add a new tool and restart only the server, the client still sees the old list.
Fix: Restart both the server and the client. In some CLI clients like Codex CLI v0.123+, you can force a fresh discovery with /mcp verbose.
Step 3: Validate the Schema (Does the Tool Definition Match What the Model Expects?)
The tool appears in the list. The model can see it. But calls fail or return unexpected results.
Test the schema:
In Inspector, select a tool from the Tools tab. Examine its JSON schema. Then call it manually with test parameters by filling in the input fields and clicking “Call Tool.”
Expected result: The tool executes and returns the expected result with no schema validation errors.
If the call fails with a validation error:
Required parameters marked as optional: The model provides what the schema says is required. If a required field is marked optional in the schema, the model may skip it, and your handler receives null.
Check your schema definition:
{
"name": "get_user",
"parameters": {
"type": "object",
"properties": {
"email": {"type": "string"}
},
"required": []
}
}
If email should always be provided, add it to the required array:
"required": ["email"]
Type mismatches: Your schema says user_id is type string but your handler expects an integer. The model sends "42" as a string. Your handler tries to use it in a numeric comparison and fails.
Fix: Match schema types to handler expectations. If your handler needs an integer, declare it as "type": "integer" in the schema.
Enum values that don’t match: Your schema says status accepts ["active", "inactive"]. Your backend checks for ["Active", "Inactive"] (capitalized). The model sends schema values. Your code does not match them.
Fix: Make enum values in the schema match exactly what your handler expects, including case.
Description too vague: The model uses the tool description to decide when to call the tool and what parameters to construct. “Queries the database” does not tell the model what queries are valid or what results to expect.
Fix: Be specific. Instead of “Queries the customer database,” write “Queries the customer database by email address or account ID and returns account status, subscription plan, and last login timestamp.”
Step 4: Confirm Tool Invocation (Did the Tool Actually Run?)
Schema looks correct. The model calls the tool. But did the server execute the handler?
Test invocation:
Add a log line to the very first line of your tool handler:
@server.tool()
def search_inventory(product_id: str):
import sys
sys.stderr.write(f"TOOL CALLED: search_inventory with product_id={product_id}\n")
# rest of handler logic
Trigger the agent to call the tool. Check your server’s stderr output.
Expected result: You see “TOOL CALLED: search_inventory with product_id=…” in the logs immediately when the model invokes the tool.
If the handler never fires:
Client sent the request but server did not receive it: Transport issue. For stdio servers, the message might be stuck in a buffer. For HTTP servers, the request might have timed out before reaching the server.
Check your server’s request logs. If you see no incoming request at all, the client is not sending it or it is being dropped.
Server received the request but crashed before reaching the handler: A middleware layer, authentication check, or input parser failed silently. Add try-catch at the server routing level (not just handler level) to log errors during request processing.
try:
result = tool_handler(params)
except Exception as e:
sys.stderr.write(f"Handler error: {str(e)}\n")
raise
Timeout: The client has a timeout for tool calls. If your tool takes longer than the timeout, the client cancels the call. The server may still be processing. The agent gets nothing.
Claude Desktop’s timeout is generous (60 seconds+). Codex CLI defaults are shorter (15–30 seconds). Check your client’s timeout settings and increase if your tools are legitimately slow.
Step 5: Inspect the Response (Did the Tool Return What the Model Expected?)
The handler fired. But did it return a valid response the model could use?
Test the response:
In Inspector, after calling a tool, examine both the formatted result and the raw JSON-RPC response.
Expected result: The response contains the data your tool is supposed to return, in the format the model expects (usually a JSON object or plain text string).
If the response is malformed:
Tool returned an error but the model did not understand it: Your handler threw an exception. The exception propagated as a raw JSON-RPC error that the model cannot interpret.
Fix: Always return a clear error message from your handler rather than letting exceptions bubble up raw.
@server.tool()
def get_user(email: str):
if not email:
return {"error": "Email parameter is required and cannot be empty."}
# rest of logic
Tool returned data but in the wrong format: Your handler returns a Python dict but the model expects a string. Or vice versa.
Check what your schema’s returns field declares. Match your handler’s return type to it.
Tool returned successfully but the data is incomplete: The handler ran but only returned partial results due to a database timeout, API rate limit, or incomplete query.
This is the hardest failure mode to detect because there is no error. The agent receives a response and continues. The response is just wrong.
Fix: Add validation in your handler to check that the returned data is complete before returning it to the model. If incomplete, return an explicit error message explaining what is missing.
Step 6: Debug Runtime Issues (External Service Failures, Network Timeouts, Credential Errors)
The tool invokes correctly. The schema is valid. But the tool fails at runtime due to external dependencies.
Test external dependencies:
Run your tool handler outside the MCP server. Call it directly with test inputs to isolate whether the failure is in the MCP layer or in the external service the tool depends on.
# Test the handler directly
if __name__ == "__main__":
result = search_inventory(product_id="test-123")
print(result)
Expected result: The handler executes and returns valid data when called directly, proving the MCP layer is not the problem.
If direct execution fails:
Authentication credentials missing or invalid: Your tool calls an external API that requires a token or API key. The credential is not set, expired, or incorrect.
Check:
echo $API_KEY
If empty or wrong, set it correctly in your environment or MCP server config.
Network timeout or unreachable service: The external service is down, slow, or unreachable from your server’s network.
Test connectivity directly:
curl https://api.example.com/endpoint
If the curl fails, the tool will fail. Fix network access or wait for the service to recover.
Rate limit exceeded: The external API is rate limiting your requests. This often appears as a 429 HTTP status or a specific error message in the API response.
Fix: Add rate limit handling in your tool. Return a clear error message when rate limited and consider caching results to reduce API calls.
Troubleshooting Common Issues
Issue: Tool appears in Inspector but agent never calls it
This is not a discovery or invocation problem. The model sees the tool but decides not to use it.
Check: Is the tool name clear and descriptive? Does the description explain when to use the tool and what it does? Models pick tools based on name and description, not your internal intent.
Fix: Improve the tool’s description to be more specific about its purpose and when it should be called. Add examples in the description if helpful.
Issue: Tool works in Inspector but fails when the agent calls it
The model is sending parameters in a format or combination that Inspector’s manual test did not cover.
Fix: Add logging to capture the exact parameters the model sends. Compare them to what you tested manually. Adjust your schema or handler to match what the model actually provides.
Issue: Stdout corruption keeps breaking the handshake
You have fixed all console.log calls but the handshake still fails.
Check: Are you using third party libraries that print to stdout? Some logging libraries, CLI frameworks, or dependencies print startup messages before you can configure them.
Fix: Redirect all stdout to stderr at the process level before any other code runs:
// Node.js — redirect stdout at process start
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = process.stderr.write.bind(process.stderr);
# Python — redirect stdout at module load
import sys
sys.stdout = sys.stderr
Issue: Tool returns data but the model ignores it
The model called the tool, got a response, but did not use the data in its answer.
This is not an MCP failure. The model received the data but decided it was not relevant or did not know how to apply it.
Fix: Check your tool’s description. Does it clearly explain what the returned data represents and how it should be used? If the model does not understand the data’s meaning, it will not apply it correctly.
How to Debug MCP Agent Tool Failures with CubeAPM
For teams running MCP agents in production at scale, tracking tool call failures across hundreds of agent sessions becomes unmanageable with log files alone. CubeAPM provides unified visibility into MCP tool invocations, failures, and performance alongside your existing APM and infrastructure monitoring.
What CubeAPM monitors for MCP tools:
CubeAPM ingests OpenTelemetry traces from your MCP server and correlates tool call events with application traces, logs, and error tracking in a single unified view.
Tool call success rate by tool name: Track which tools fail most often across all agent sessions. Identify systematic schema mismatches or external dependency failures.
Latency distribution per tool: Detect tools that timeout frequently or take longer than expected. Correlate slow tools with downstream database queries or API calls.
Error messages and stack traces: When a tool fails, CubeAPM surfaces the exact error message, input parameters, and full stack trace in context with the agent session that triggered it.
Correlation with infrastructure metrics: Link tool failures to underlying infrastructure issues such as memory pressure, network latency spikes, or database connection pool exhaustion.
How to set up MCP monitoring in CubeAPM:
Instrument your MCP server with OpenTelemetry. Add spans for each tool invocation with attributes for tool name, input parameters, and result status.
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@server.tool()
def search_inventory(product_id: str):
with tracer.start_as_current_span("mcp.tool.search_inventory") as span:
span.set_attribute("tool.name", "search_inventory")
span.set_attribute("tool.input.product_id", product_id)
try:
result = perform_search(product_id)
span.set_attribute("tool.result.status", "success")
return result
except Exception as e:
span.set_attribute("tool.result.status", "error")
span.set_attribute("tool.error.message", str(e))
raise
Configure your OpenTelemetry SDK to export traces to CubeAPM’s collector endpoint. CubeAPM runs inside your VPC, so traces never leave your infrastructure.
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://cubeapm-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
View tool call traces in CubeAPM’s trace explorer. Filter by tool.name, tool.result.status, or any custom attribute you added. Correlate failed tool calls with related logs, infrastructure metrics, and application traces in the same view.
CubeAPM pricing is $0.15/GB of ingested telemetry with unlimited retention and no per-seat fees. For a production MCP setup ingesting 500 GB/month of traces and logs, total cost is $75/month with full trace search, error tracking, and infrastructure monitoring included.
MCP tool failures are difficult to debug in production because the protocol fails silently and error messages are often missing or vague. CubeAPM gives you full visibility into what tools are being called, how often they fail, and what errors they produce, all without adding complexity to your MCP server code.
Conclusion
MCP tool failures in production are silent by default. The agent skips the tool and continues without it. No error. No alert. Just wrong answers.
This five stage diagnostic workflow catches every failure mode. Work through it in order. Stop when you find the break. Most failures happen at the handshake or schema validation stage, not in complex runtime logic.
The most common failure is stdout corruption from a single print statement before the JSON-RPC handshake completes. The second most common is schema type mismatches where the model sends a string but the handler expects an integer. Both are fixable in under a minute once you know to look for them.
For production MCP deployments at scale, manual log inspection does not scale. Tools like CubeAPM provide unified observability across tool calls, application traces, and infrastructure metrics, making it possible to detect and diagnose tool failures before they compound into larger incidents.
Disclaimer: The information in this article reflects the latest details available at the time of publication and may change as technologies and products evolve. Features, pricing, and plan limits can change over time. Always verify the latest information directly with the vendor before making purchasing or deployment decisions.
Frequently Asked Questions
What is the most common cause of MCP tool call failures?
Stdout corruption from print statements before the JSON-RPC handshake completes. One console.log in your server startup code corrupts the entire protocol stream and causes silent connection failures.
How do I know if my MCP server handshake is working?
Use MCP Inspector to connect to your server. If it shows “Connected to MCP Server” and displays server capabilities, the handshake succeeded. If it fails silently or times out, check for stdout corruption or wrong server paths.
Why does my tool appear in Inspector but the agent never calls it?
The model sees the tool but decides not to use it. Check if the tool name and description clearly explain when to call the tool and what it does. Vague descriptions cause models to skip tools even when they are relevant.
What does a schema validation error mean in MCP?
The model sent parameters that do not match your tool’s input schema. Common causes include type mismatches (string vs integer), missing required fields, or enum values that do not match what your handler expects.
How do I debug a tool that runs but returns incomplete data?
Add validation in your handler to check that returned data is complete before sending it to the model. If incomplete, return an explicit error message explaining what is missing rather than partial results.
Can I monitor MCP tool failures in production without changing my server code?
No. You need to instrument your MCP server with logging or tracing to capture tool invocations and errors. OpenTelemetry provides a standard way to do this and integrates with distributed tracing tools like CubeAPM for unified visibility.
What is the difference between stdio and HTTP MCP servers for debugging?
Stdio servers communicate through stdin/stdout and require stderr for logging. HTTP servers use standard HTTP requests and can log anywhere. Stdio failures are often caused by stdout corruption while HTTP failures are usually network or timeout related.





