From 8cbc58d2578934e8da3d5c91c3b907dafb9db22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Sun, 1 Mar 2026 21:29:59 +0900 Subject: [PATCH] mcp: unify error reporting and use named error codes --- src/mcp/Server.zig | 22 ++++++++++++++++ src/mcp/protocol.zig | 10 +++++++- src/mcp/resources.zig | 34 +++++-------------------- src/mcp/router.zig | 22 ++-------------- src/mcp/tools.zig | 58 ++++++++++++------------------------------- 5 files changed, 55 insertions(+), 91 deletions(-) diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index 26c86767..91823ef1 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -180,3 +180,25 @@ pub fn sendResponse(self: *Self, response: anytype) !void { try stdout.interface.writeByte('\n'); try stdout.interface.flush(); } + +pub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void { + const GenericResponse = struct { + jsonrpc: []const u8 = "2.0", + id: std.json.Value, + result: @TypeOf(result), + }; + try self.sendResponse(GenericResponse{ + .id = id, + .result = result, + }); +} + +pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void { + try self.sendResponse(protocol.Response{ + .id = id, + .@"error" = protocol.Error{ + .code = @intFromEnum(code), + .message = message, + }, + }); +} diff --git a/src/mcp/protocol.zig b/src/mcp/protocol.zig index b9385ddb..44a85d33 100644 --- a/src/mcp/protocol.zig +++ b/src/mcp/protocol.zig @@ -20,6 +20,14 @@ pub const Error = struct { data: ?std.json.Value = null, }; +pub const ErrorCode = enum(i64) { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, +}; + pub const Notification = struct { jsonrpc: []const u8 = "2.0", method: []const u8, @@ -185,7 +193,7 @@ test "protocol error formatting" { const response = Response{ .id = .{ .string = "abc" }, .@"error" = .{ - .code = -32601, + .code = @intFromEnum(ErrorCode.MethodNotFound), .message = "Method not found", }, }; diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index f88d4bfe..bbffdd8a 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -13,7 +13,7 @@ pub fn handleList(server: *Server, req: protocol.Request) !void { .resources = server.resources, }; - try sendResult(server, req.id.?, result); + try server.sendResult(req.id.?, result); } const ReadParams = struct { @@ -61,11 +61,11 @@ const ResourceStreamingResult = struct { pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { if (req.params == null) { - return sendError(server, req.id.?, -32602, "Missing params"); + return server.sendError(req.id.?, .InvalidParams, "Missing params"); } const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch { - return sendError(server, req.id.?, -32602, "Invalid params"); + return server.sendError(req.id.?, .InvalidParams, "Invalid params"); }; if (std.mem.eql(u8, params.uri, "mcp://page/html")) { @@ -76,7 +76,7 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque .text = .{ .server = server, .uri = params.uri, .format = .html }, }}, }; - try sendResult(server, req.id.?, result); + try server.sendResult(req.id.?, result); } else if (std.mem.eql(u8, params.uri, "mcp://page/markdown")) { const result: ResourceStreamingResult = .{ .contents = &.{.{ @@ -85,30 +85,8 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque .text = .{ .server = server, .uri = params.uri, .format = .markdown }, }}, }; - try sendResult(server, req.id.?, result); + try server.sendResult(req.id.?, result); } else { - return sendError(server, req.id.?, -32602, "Resource not found"); + return server.sendError(req.id.?, .InvalidRequest, "Resource not found"); } } - -pub fn sendResult(server: *Server, id: std.json.Value, result: anytype) !void { - const GenericResponse = struct { - jsonrpc: []const u8 = "2.0", - id: std.json.Value, - result: @TypeOf(result), - }; - try server.sendResponse(GenericResponse{ - .id = id, - .result = result, - }); -} - -pub fn sendError(server: *Server, id: std.json.Value, code: i64, message: []const u8) !void { - try server.sendResponse(protocol.Response{ - .id = id, - .@"error" = protocol.Error{ - .code = code, - .message = message, - }, - }); -} diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 3c5c4389..a39dc7bd 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -64,28 +64,10 @@ fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) !vo } else if (std.mem.eql(u8, parsed.method, "tools/call")) { try tools.handleCall(server, arena, parsed); } else { - try server.sendResponse(protocol.Response{ - .id = parsed.id.?, - .@"error" = protocol.Error{ - .code = -32601, - .message = "Method not found", - }, - }); + try server.sendError(parsed.id.?, .MethodNotFound, "Method not found"); } } -fn sendResponseGeneric(server: *Server, id: std.json.Value, result: anytype) !void { - const GenericResponse = struct { - jsonrpc: []const u8 = "2.0", - id: std.json.Value, - result: @TypeOf(result), - }; - try server.sendResponse(GenericResponse{ - .id = id, - .result = result, - }); -} - fn handleInitialize(server: *Server, req: protocol.Request) !void { const result = protocol.InitializeResult{ .protocolVersion = "2024-11-05", @@ -100,5 +82,5 @@ fn handleInitialize(server: *Server, req: protocol.Request) !void { }, }; - try sendResponseGeneric(server, req.id.?, result); + try server.sendResult(req.id.?, result); } diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index a6494f04..17e79f86 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -17,7 +17,7 @@ pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Reque .tools = server.tools, }; - try sendResult(server, req.id.?, result); + try server.sendResult(req.id.?, result); } const GotoParams = struct { @@ -79,7 +79,7 @@ const ToolStreamingText = struct { pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { if (req.params == null) { - return sendError(server, req.id.?, -32602, "Missing params"); + return server.sendError(req.id.?, .InvalidParams, "Missing params"); } const CallParams = struct { @@ -91,7 +91,7 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque var aw: std.Io.Writer.Allocating = .init(arena); std.json.Stringify.value(req.params.?, .{}, &aw.writer) catch {}; const msg = std.fmt.allocPrint(arena, "Invalid params: {s}", .{aw.written()}) catch "Invalid params"; - return sendError(server, req.id.?, -32602, msg); + return server.sendError(req.id.?, .InvalidParams, msg); }; if (std.mem.eql(u8, call_params.name, "goto") or std.mem.eql(u8, call_params.name, "navigate")) { @@ -107,7 +107,7 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque } else if (std.mem.eql(u8, call_params.name, "over")) { try handleOver(server, arena, req.id.?, call_params.arguments); } else { - return sendError(server, req.id.?, -32601, "Tool not found"); + return server.sendError(req.id.?, .MethodNotFound, "Tool not found"); } } @@ -116,7 +116,7 @@ fn handleGoto(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arg try performGoto(server, args.url, id); const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Navigated successfully." }}; - try sendResult(server, id, .{ .content = &content }); + try server.sendResult(id, .{ .content = &content }); } fn handleSearch(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -125,16 +125,16 @@ fn handleSearch(server: *Server, arena: std.mem.Allocator, id: std.json.Value, a const component: std.Uri.Component = .{ .raw = args.text }; var url_aw = std.Io.Writer.Allocating.init(arena); component.formatQuery(&url_aw.writer) catch { - return sendError(server, id, -32603, "Internal error formatting query"); + return server.sendError(id, .InternalError, "Internal error formatting query"); }; const url = std.fmt.allocPrintSentinel(arena, "https://duckduckgo.com/?q={s}", .{url_aw.written()}, 0) catch { - return sendError(server, id, -32603, "Internal error formatting URL"); + return server.sendError(id, .InternalError, "Internal error formatting URL"); }; try performGoto(server, url, id); const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Search performed successfully." }}; - try sendResult(server, id, .{ .content = &content }); + try server.sendResult(id, .{ .content = &content }); } fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -155,7 +155,7 @@ fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, .text = .{ .server = server, .action = .markdown }, }}, }; - try sendResult(server, id, result); + try server.sendResult(id, result); } fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -176,7 +176,7 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar .text = .{ .server = server, .action = .links }, }}, }; - try sendResult(server, id, result); + try server.sendResult(id, result); } fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -192,37 +192,33 @@ fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, const js_result = ls.local.compileAndRun(args.script, null) catch { const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Script evaluation failed." }}; - return sendResult(server, id, .{ .content = &content, .isError = true }); + return server.sendResult(id, .{ .content = &content, .isError = true }); }; const str_result = js_result.toStringSliceWithAlloc(arena) catch "undefined"; const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = str_result }}; - try sendResult(server, id, .{ .content = &content }); + try server.sendResult(id, .{ .content = &content }); } fn handleOver(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { const args = try parseParams(OverParams, arena, arguments, server, id, "over"); const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = args.result }}; - try sendResult(server, id, .{ .content = &content }); + try server.sendResult(id, .{ .content = &content }); } fn parseParams(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T { if (arguments == null) { - // We need to print the error message, so we need an allocator. - // But we are in a helper, we should probably just return the error. - // However, sendError sends the response. - // Let's use a fixed buffer for the error message to avoid complex error handling. var buf: [64]u8 = undefined; const msg = std.fmt.bufPrint(&buf, "Missing arguments for {s}", .{tool_name}) catch "Missing arguments"; - try sendError(server, id, -32602, msg); + try server.sendError(id, .InvalidParams, msg); return error.InvalidParams; } return std.json.parseFromValueLeaky(T, arena, arguments.?, .{ .ignore_unknown_fields = true }) catch { var buf: [64]u8 = undefined; const msg = std.fmt.bufPrint(&buf, "Invalid arguments for {s}", .{tool_name}) catch "Invalid arguments"; - try sendError(server, id, -32602, msg); + try server.sendError(id, .InvalidParams, msg); return error.InvalidParams; }; } @@ -241,31 +237,9 @@ fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void { .reason = .address_bar, .kind = .{ .push = null }, }) catch { - try sendError(server, id, -32603, "Internal error during navigation"); + try server.sendError(id, .InternalError, "Internal error during navigation"); return error.NavigationFailed; }; _ = server.session.wait(5000); } - -pub fn sendResult(server: *Server, id: std.json.Value, result: anytype) !void { - const GenericResponse = struct { - jsonrpc: []const u8 = "2.0", - id: std.json.Value, - result: @TypeOf(result), - }; - try server.sendResponse(GenericResponse{ - .id = id, - .result = result, - }); -} - -pub fn sendError(server: *Server, id: std.json.Value, code: i64, message: []const u8) !void { - try server.sendResponse(protocol.Response{ - .id = id, - .@"error" = protocol.Error{ - .code = code, - .message = message, - }, - }); -}