Skip to main content
RapidDev - Software Development Agency
mcp-tutorial

How to fix MCP tool execution errors

MCP tool execution errors occur when a tool handler throws an unhandled exception, returns an invalid format, or fails input validation. The tool returns isError: true in the response, and the host displays an error message to the user. Fix this by wrapping all tool handlers in try/catch blocks, validating inputs with Zod schemas, and returning proper content arrays with descriptive error messages instead of throwing exceptions.

What you'll learn

  • What causes MCP tool execution errors and the isError response format
  • How to wrap tool handlers in proper error handling
  • How to validate inputs with Zod schemas to prevent bad data
  • How to return helpful error messages that guide the AI toward a fix
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read15-20 minMCP SDK 1.0+, TypeScript/PythonMarch 2026RapidDev Engineering Team
TL;DR

MCP tool execution errors occur when a tool handler throws an unhandled exception, returns an invalid format, or fails input validation. The tool returns isError: true in the response, and the host displays an error message to the user. Fix this by wrapping all tool handlers in try/catch blocks, validating inputs with Zod schemas, and returning proper content arrays with descriptive error messages instead of throwing exceptions.

Fixing MCP Tool Execution Errors

When an MCP tool runs but fails, the server returns a response with isError: true and a content array describing the error. If the tool throws an unhandled exception, the SDK catches it and returns a generic error. Both situations degrade the AI's ability to use your tools effectively. This tutorial shows you how to build robust tool handlers that fail gracefully with helpful error messages.

Prerequisites

  • An MCP server with tools that are returning errors
  • Basic understanding of MCP tool handlers
  • Familiarity with TypeScript or Python error handling

Step-by-step guide

1

Understand the MCP tool error response format

When a tool fails, the MCP server should return a response with isError: true and a content array containing the error description. This is different from a JSON-RPC error (which indicates a protocol failure). A tool error means the tool ran but could not complete its task — like an API returning 404 or a file not being found. The AI host sees this error and can try a different approach or ask the user for help.

typescript
1// Successful tool response
2{
3 content: [{ type: "text", text: "Result data here" }]
4}
5
6// Error tool response — the correct way to report tool failures
7{
8 content: [{ type: "text", text: "Error: File not found at /path/to/file" }],
9 isError: true
10}
11
12// What happens if you throw instead:
13// The SDK catches it and returns a generic error:
14// { content: [{ type: "text", text: "Internal error" }], isError: true }
15// This is less helpful to the AI

Expected result: You understand the difference between tool errors (isError: true) and protocol errors, and why explicit error responses are better than unhandled throws.

2

Wrap all tool handlers in try/catch

Every tool handler should be wrapped in a try/catch block that catches all errors and returns a descriptive error response. This prevents unhandled exceptions from crashing the server or producing generic error messages. Include the error type and message in the response so the AI can understand what went wrong and potentially try a different approach.

typescript
1server.tool(
2 "fetch-data",
3 "Fetch data from an API",
4 { url: z.string().url() },
5 async ({ url }) => {
6 try {
7 const response = await fetch(url);
8
9 if (!response.ok) {
10 return {
11 content: [{
12 type: "text",
13 text: `API returned HTTP ${response.status}: ${response.statusText}. URL: ${url}`,
14 }],
15 isError: true,
16 };
17 }
18
19 const data = await response.json();
20 return {
21 content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
22 };
23 } catch (error) {
24 const message = error instanceof Error ? error.message : String(error);
25 return {
26 content: [{
27 type: "text",
28 text: `Failed to fetch ${url}: ${message}`,
29 }],
30 isError: true,
31 };
32 }
33 }
34);

Expected result: Tool failures return descriptive error messages instead of crashing the server or showing generic errors.

3

Validate inputs with Zod schemas

The MCP SDK uses Zod for input validation. Define precise schemas for your tool inputs to catch invalid data before your handler runs. Zod automatically rejects malformed inputs with clear error messages. Use specific types (z.string().url(), z.number().min(1).max(100), z.enum([...])) instead of generic z.string() to catch problems early.

typescript
1import { z } from "zod";
2
3// WEAK validation — accepts any string, errors happen inside handler
4server.tool("search", "Search", { query: z.string() }, async ({ query }) => {
5 // If query is empty, the API might return confusing results
6});
7
8// STRONG validation — catches problems before handler runs
9server.tool(
10 "search",
11 "Search for items",
12 {
13 query: z.string()
14 .min(1, "Query cannot be empty")
15 .max(500, "Query too long, max 500 characters"),
16 category: z.enum(["docs", "code", "issues"]).optional(),
17 limit: z.number()
18 .int()
19 .min(1, "Limit must be at least 1")
20 .max(100, "Limit cannot exceed 100")
21 .optional()
22 .default(10),
23 },
24 async ({ query, category, limit }) => {
25 // Inputs are guaranteed to be valid at this point
26 console.error(`Searching: query=${query}, category=${category}, limit=${limit}`);
27 // ... handler logic
28 }
29);

Expected result: Invalid inputs are rejected with clear validation errors before the tool handler code runs.

4

Handle specific error types differently

Different errors warrant different responses. Network errors might suggest retrying. Authentication errors indicate a configuration problem. Not-found errors might mean the AI should try different parameters. Categorize errors so the AI receives actionable guidance, not just a generic failure message.

typescript
1server.tool(
2 "get-repo-info",
3 "Get GitHub repository information",
4 { owner: z.string(), repo: z.string() },
5 async ({ owner, repo }) => {
6 try {
7 const response = await fetch(
8 `https://api.github.com/repos/${owner}/${repo}`,
9 { headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` } }
10 );
11
12 if (response.status === 404) {
13 return {
14 content: [{
15 type: "text",
16 text: `Repository ${owner}/${repo} not found. Check the owner and repo name are correct.`,
17 }],
18 isError: true,
19 };
20 }
21
22 if (response.status === 401) {
23 return {
24 content: [{
25 type: "text",
26 text: "GitHub authentication failed. The GITHUB_TOKEN may be expired or invalid.",
27 }],
28 isError: true,
29 };
30 }
31
32 if (response.status === 403) {
33 return {
34 content: [{
35 type: "text",
36 text: "GitHub rate limit exceeded. Wait a minute and try again.",
37 }],
38 isError: true,
39 };
40 }
41
42 const data = await response.json();
43 return {
44 content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
45 };
46 } catch (error) {
47 return {
48 content: [{
49 type: "text",
50 text: `Network error: ${(error as Error).message}. Check your internet connection.`,
51 }],
52 isError: true,
53 };
54 }
55 }
56);

Expected result: Different error types produce specific, actionable error messages that help the AI adjust its approach.

5

Log errors for debugging while returning clean messages

Log the full error details (stack trace, raw response) to stderr for your debugging needs, but return a clean, user-friendly message in the tool response. The AI sees the clean message, and you can inspect the detailed logs when investigating issues. If you are building production MCP servers and need help implementing comprehensive error handling and monitoring, the RapidDev team can help design robust error strategies.

typescript
1server.tool(
2 "complex-operation",
3 "Perform a complex operation",
4 { input: z.string() },
5 async ({ input }) => {
6 try {
7 const result = await performComplexOperation(input);
8 return { content: [{ type: "text", text: result }] };
9 } catch (error) {
10 // Log full details to stderr (for developer debugging)
11 console.error("Tool 'complex-operation' failed:");
12 console.error(` Input: ${input}`);
13 console.error(` Error: ${(error as Error).stack}`);
14
15 // Return clean message to AI (for user experience)
16 return {
17 content: [{
18 type: "text",
19 text: `The operation failed: ${(error as Error).message}. Try with different input or check the server logs for details.`,
20 }],
21 isError: true,
22 };
23 }
24 }
25);

Expected result: The AI receives a clean, helpful error message while full debugging details are available in the server logs.

Complete working example

src/robust-tools-server.ts
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3import { z } from "zod";
4
5const server = new McpServer({
6 name: "robust-tools-server",
7 version: "1.0.0",
8});
9
10// Helper: create a tool error response
11function toolError(message: string) {
12 return {
13 content: [{ type: "text" as const, text: message }],
14 isError: true,
15 };
16}
17
18// Helper: create a tool success response
19function toolSuccess(text: string) {
20 return {
21 content: [{ type: "text" as const, text }],
22 };
23}
24
25// Tool with comprehensive error handling
26server.tool(
27 "fetch-url",
28 "Fetch content from a URL",
29 {
30 url: z.string().url("Must be a valid URL"),
31 format: z.enum(["json", "text"]).optional().default("json"),
32 },
33 async ({ url, format }) => {
34 try {
35 console.error(`Fetching: ${url} (format: ${format})`);
36 const start = Date.now();
37
38 const controller = new AbortController();
39 const timeout = setTimeout(() => controller.abort(), 30000);
40
41 const response = await fetch(url, { signal: controller.signal });
42 clearTimeout(timeout);
43
44 if (!response.ok) {
45 return toolError(
46 `HTTP ${response.status} from ${url}: ${response.statusText}`
47 );
48 }
49
50 const data = format === "json"
51 ? JSON.stringify(await response.json(), null, 2)
52 : await response.text();
53
54 console.error(`Fetched in ${Date.now() - start}ms`);
55 return toolSuccess(data);
56 } catch (error) {
57 const msg = (error as Error).message;
58 console.error(`Fetch failed: ${msg}`);
59
60 if ((error as Error).name === "AbortError") {
61 return toolError(`Request timed out after 30s: ${url}`);
62 }
63 return toolError(`Failed to fetch ${url}: ${msg}`);
64 }
65 }
66);
67
68const transport = new StdioServerTransport();
69await server.connect(transport);
70console.error("Robust tools server ready.");

Common mistakes when fixing MCP tool execution errors

Why it's a problem: Throwing exceptions from tool handlers instead of returning isError responses

How to avoid: Wrap all handler code in try/catch and return { content: [...], isError: true } instead of throwing. This gives the AI a useful error message.

Why it's a problem: Using generic z.string() when more specific validation exists

How to avoid: Use z.string().url(), z.string().min(1), z.number().int().positive(), z.enum([...]) to catch invalid inputs before they reach your handler logic.

Why it's a problem: Returning error details that expose sensitive information

How to avoid: Log full error details to stderr. Return only a clean, user-appropriate message in the tool response. Never expose stack traces, API keys, or internal paths in tool responses.

Why it's a problem: Not handling specific HTTP status codes differently

How to avoid: Map 401 to auth errors, 404 to not-found, 429 to rate limiting, etc. Each error type should suggest a specific corrective action to the AI.

Best practices

  • Wrap every tool handler in try/catch — no exceptions
  • Return descriptive error messages that help the AI understand what went wrong
  • Use Zod schemas with specific validators for precise input validation
  • Log full error details to stderr for debugging, return clean messages to the AI
  • Create reusable helper functions for common error and success response patterns
  • Handle specific HTTP error codes with targeted error messages
  • Include the input parameters in error messages for context
  • Set timeouts on all external network calls to prevent indefinite hangs

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

My MCP tool keeps returning errors. Here is my tool handler code: [paste code]. Help me add proper error handling with try/catch, input validation with Zod, and descriptive error responses for different failure modes.

MCP Prompt

Add comprehensive error handling to my MCP server's tool handlers. Wrap each tool in try/catch, add Zod input validation, and return specific error messages for common failure modes like network errors, 401/403/404 HTTP responses, and timeouts.

Frequently asked questions

What is the difference between isError: true and a JSON-RPC error?

isError: true means the tool ran but failed at its task (like an API returning 404). A JSON-RPC error (codes like -32700, -32001) means the protocol itself failed (parse error, timeout, method not found). Tool errors are handled gracefully by the AI; protocol errors often terminate the connection.

Can the AI retry a tool that returned isError: true?

Yes, the AI can see the error message and decide to retry with different parameters, try a different tool, or ask the user for help. Descriptive error messages help the AI make better retry decisions.

Should I return the full stack trace in the error message?

No. Stack traces are for developers, not the AI. Log the full stack trace to stderr and return a clean, actionable message in the tool response.

How do I test tool error handling?

Use MCP Inspector to manually invoke tools with invalid inputs, missing parameters, and edge cases. Verify that each scenario returns a helpful isError response instead of crashing the server.

What happens if a tool returns neither isError nor valid content?

The SDK may reject the response as malformed. Always return a content array with at least one item. For errors, include isError: true. For success, omit isError or set it to false.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.