Skip to content

Error Handling

Proper error handling is essential for robust MCP servers and clients.

Error Types

Tool Errors

zig
pub const ToolError = error{
    InvalidArguments,
    ExecutionFailed,
    OutOfMemory,
    Unknown,
};

Resource Errors

zig
pub const ResourceError = error{
    NotFound,
    InvalidUri,
    AccessDenied,
    OutOfMemory,
};

Prompt Errors

zig
pub const PromptError = error{
    InvalidArguments,
    GenerationFailed,
    OutOfMemory,
    Unknown,
};

Handling Errors in Handlers

Returning Errors

zig
fn myToolHandler(
    allocator: std.mem.Allocator,
    args: ?std.json.Value,
) mcp.tools.ToolError!mcp.tools.ToolResult {
    // Missing required argument
    const input = mcp.tools.getStringArg(args, "input") orelse {
        return error.InvalidArguments;
    };

    // Operation failed
    const result = performOperation(input) catch {
        return error.ExecutionFailed;
    };

    // Memory allocation failed
    const output = allocator.alloc(u8, size) catch {
        return error.OutOfMemory;
    };

    return .{ .content = &.{mcp.Content.createText(output)} };
}

Error Results vs Error Returns

There's a difference between returning an error and returning an error result:

Return error - Indicates failure to the MCP protocol:

zig
return error.InvalidArguments;

Return error result - Successful response with error content:

zig
return .{
    .content = &.{mcp.Content.createText("File not found")},
    .isError = true,
};

When to Use Which

ScenarioUse
Missing required argumentreturn error.InvalidArguments
Invalid argument valuereturn error.InvalidArguments
Expected failure (file not found)Error result with isError = true
Unexpected failurereturn error.ExecutionFailed
Out of memoryreturn error.OutOfMemory

JSON-RPC Errors

Standard Error Codes

zig
const ErrorCode = struct {
    pub const PARSE_ERROR = -32700;
    pub const INVALID_REQUEST = -32600;
    pub const METHOD_NOT_FOUND = -32601;
    pub const INVALID_PARAMS = -32602;
    pub const INTERNAL_ERROR = -32603;
};

Creating Error Responses

zig
// Parse error (invalid JSON)
const err = mcp.jsonrpc.createParseError(null);

// Invalid request
const err = mcp.jsonrpc.createInvalidRequest(id, null);

// Method not found
const err = mcp.jsonrpc.createMethodNotFound(id, method);

// Invalid params
const err = mcp.jsonrpc.createInvalidParams(id, "Missing 'name' field");

// Internal error
const err = mcp.jsonrpc.createInternalError(id, null);

Server Error Handling

The server handles errors from handlers:

zig
fn handleToolCall(self: *Server, request: Request) !Message {
    const tool = self.findTool(request.params.name) orelse {
        return .{ .error_response = mcp.jsonrpc.createMethodNotFound(request.id, request.params.name) };
    };

    const result = tool.handler(self.allocator, request.params.arguments) catch |err| {
        return switch (err) {
            error.InvalidArguments => .{ .error_response = mcp.jsonrpc.createInvalidParams(request.id, "Invalid arguments") },
            error.OutOfMemory => .{ .error_response = mcp.jsonrpc.createInternalError(request.id, null) },
            else => .{ .error_response = mcp.jsonrpc.createInternalError(request.id, null) },
        };
    };

    return .{ .response = mcp.jsonrpc.createResponse(request.id, result.toJson()) };
}

Best Practices

Validate Early

zig
fn handler(allocator: Allocator, args: ?json.Value) ToolError!ToolResult {
    // Validate all arguments first
    const a = getArg(args, "a") orelse return error.InvalidArguments;
    const b = getArg(args, "b") orelse return error.InvalidArguments;
    const c = getArg(args, "c") orelse return error.InvalidArguments;

    // Then perform operations
    return doWork(a, b, c);
}

Provide Helpful Messages

zig
fn handler(allocator: Allocator, args: ?json.Value) ToolError!ToolResult {
    const filename = getStringArg(args, "filename") orelse {
        return .{
            .content = &.{Content.createText("Missing 'filename' argument. Please provide the file to process.")},
            .isError = true,
        };
    };

    // ...
}

Use errdefer for Cleanup

zig
fn handler(allocator: Allocator, args: ?json.Value) ToolError!ToolResult {
    const buffer = try allocator.alloc(u8, 1024);
    errdefer allocator.free(buffer);

    const file = try std.fs.openFile(path, .{});
    errdefer file.close();

    // If any of the following fails, cleanup happens automatically
    try processFile(file, buffer);

    return .{ .content = &.{} };
}

Complete Example

zig
const std = @import("std");
const mcp = @import("mcp");

fn processFileHandler(
    allocator: std.mem.Allocator,
    args: ?std.json.Value,
) mcp.tools.ToolError!mcp.tools.ToolResult {
    // 1. Validate arguments
    const path = mcp.tools.getStringArg(args, "path") orelse {
        return .{
            .content = &.{mcp.Content.createText("Error: 'path' argument is required")},
            .isError = true,
        };
    };

    // 2. Check file exists
    std.fs.accessAbsolute(path, .{}) catch {
        return .{
            .content = &.{mcp.Content.createText("Error: File not found at specified path")},
            .isError = true,
        };
    };

    // 3. Read file with proper cleanup
    const contents = std.fs.cwd().readFileAlloc(
        allocator,
        path,
        10 * 1024 * 1024, // 10MB limit
    ) catch |err| {
        const message = switch (err) {
            error.FileTooBig => "Error: File exceeds 10MB limit",
            error.AccessDenied => "Error: Permission denied",
            else => "Error: Failed to read file",
        };
        return .{
            .content = &.{mcp.Content.createText(message)},
            .isError = true,
        };
    };
    defer allocator.free(contents);

    // 4. Process and return
    const result = try std.fmt.allocPrint(
        allocator,
        "Successfully processed {d} bytes from {s}",
        .{ contents.len, path },
    );

    return .{
        .content = &.{mcp.Content.createText(result)},
    };
}

Application-Level Error Reporting

When building MCP servers or clients, it's recommended to handle top-level errors gracefully and provide users with a way to report bugs. mcp.zig provides built-in utilities for this.

Using mcp.reportError

Wrap your main application logic to catch and report unexpected errors:

zig
pub fn main() void {
    if (run()) {
        // Success
    } else |err| {
        // Report error with instructions and link to issue tracker
        mcp.reportError(err);
        std.process.exit(1);
    }
}

This will print the error along with the GitHub Issues URL where users can report bugs if they suspect a library issue.

Accessing Bug Report URL

You can access the official bug report URL directly if you need to construct custom error messages:

zig
const url = mcp.ISSUES_URL;
// https://github.com/muhammad-fiaz/mcp.zig/issues

Update Checking

To ensure your application is running the latest version of mcp.zig, you can enable background update checks. This will check GitHub Releases and log a message if a new version is available.

zig
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    // Check for updates in background
    // This runs on a separate thread; detach to let it run in background
    if (mcp.report.checkForUpdates(allocator)) |t| t.detach();

    // Continue with server initialization...
}

Next Steps