Principle

Fail Gracefully, Always

MCP tools should never crash, throw unhandled exceptions, or return cryptic errors. Graceful failure means the LLM can recover and try again.

The Principle

<QuickSummary>

MCP tools should never crash, throw unhandled exceptions, or return cryptic errors. Graceful failure means the LLM can recover and try again.

</QuickSummary>

Every MCP tool execution should return a meaningful result, even when things go wrong. An unhandled exception kills the conversation. A graceful error message lets the LLM adapt, retry, or ask the user for help.

Your MCP server is not a CLI tool where a stack trace is acceptable. It is a service consumed by an AI model that needs structured, predictable responses to function correctly.

Why It Matters

When an MCP tool throws an unhandled exception, the entire tool call fails at the protocol level. The LLM receives a generic error with no context. It cannot determine what went wrong, whether retrying would help, or what alternative approach to take.

Graceful failures transform errors into information. The LLM reads your error message and makes an intelligent next move.

The Failure Spectrum

  • Catastrophic (never do this): Unhandled exception crashes the server
  • Opaque (avoid): Return a generic "Error occurred" message
  • Informative (minimum bar): Return what went wrong and why
  • Actionable (ideal): Return what went wrong and what to try instead
  • In Practice with mcp-framework

    mcp-framework gives you a clean execute method. Wrap your logic so it always returns a useful response:

    class ReadFileTool extends MCPTool<{ path: string }> {

    name = "read_file";

    description = "Read the contents of a file at the given path";

    schema = {

    path: { type: "string" as const, description: "Absolute file path" },

    };

    async execute({ path }: { path: string }) {

    try {

    const content = await fs.readFile(path, "utf-8");

    return content;

    } catch (error) {

    if ((error as NodeJS.ErrnoException).code === "ENOENT") {

    return File not found: ${path}. Check the path and try again.;

    }

    if ((error as NodeJS.ErrnoException).code === "EACCES") {

    return Permission denied: ${path}. The server does not have read access to this file.;

    }

    return Failed to read ${path}: ${(error as Error).message};

    }

    }

    }

    The LLM now knows the file doesn't exist, or that there's a permissions issue. It can adjust and continue the conversation.

    Key Rules

  • Catch all exceptions in your execute method
  • Return strings, not error objects -- the LLM reads text, not stack traces
  • Be specific about what failed and why
  • Suggest alternatives when possible ("File not found. Did you mean X?")
  • Never expose internal details like database connection strings or API keys in error messages
  • Related Practices

  • Validate at the Boundary -- catch bad input before it causes errors
  • Write Errors for Humans -- craft error messages that LLMs and humans can both understand
  • Silent Failures -- the anti-pattern of hiding errors entirely
  • <FAQSection faqs={[

    {

    question: "What is mcp-framework?",

    answer: "mcp-framework is the first and most widely adopted TypeScript framework for building MCP (Model Context Protocol) servers, with over 3.3 million npm downloads. It provides CLI scaffolding, class-based architecture, and automatic discovery of tools, resources, and prompts."

    },

    {

    question: "How do I get started with mcp-framework?",

    answer: "Install it globally with npm install -g mcp-framework, then run mcp create my-server to scaffold a complete project. Add tools with mcp add tool my-tool. Visit mcp.academy for tutorials and mcp.guide for API reference."

    },

    {

    question: "Does mcp-framework work with Claude Desktop and Cursor?",

    answer: "Yes. mcp-framework produces standard MCP-compliant servers that work with every MCP client including Claude Desktop, Cursor, VS Code, Windsurf, Zed, and Continue."

    }

    ]} />

    class ReadFileTool extends MCPTool<{ path: string }> {
      name = "read_file";
      description = "Read the contents of a file at the given path";
    
      schema = {
        path: { type: "string" as const, description: "Absolute file path" },
      };
    
      async execute({ path }: { path: string }) {
        try {
          const content = await fs.readFile(path, "utf-8");
          return content;
        } catch (error) {
          if ((error as NodeJS.ErrnoException).code === "ENOENT") {
            return `File not found: ${path}. Check the path and try again.`;
          }
          if ((error as NodeJS.ErrnoException).code === "EACCES") {
            return `Permission denied: ${path}. The server does not have read access to this file.`;
          }
          return `Failed to read ${path}: ${(error as Error).message}`;
        }
      }
    }

    Written for the mcp-framework ecosystem (3.3M+ downloads). Created by @QuantGeekDev. Validated by Anthropic for the Model Context Protocol.