mcp: unify error reporting and use named error codes

This commit is contained in:
Adrià Arrufat
2026-03-01 21:29:59 +09:00
parent e6cc3e8c34
commit 8cbc58d257
5 changed files with 55 additions and 91 deletions

View File

@@ -180,3 +180,25 @@ pub fn sendResponse(self: *Self, response: anytype) !void {
try stdout.interface.writeByte('\n'); try stdout.interface.writeByte('\n');
try stdout.interface.flush(); 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,
},
});
}

View File

@@ -20,6 +20,14 @@ pub const Error = struct {
data: ?std.json.Value = null, data: ?std.json.Value = null,
}; };
pub const ErrorCode = enum(i64) {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
};
pub const Notification = struct { pub const Notification = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
method: []const u8, method: []const u8,
@@ -185,7 +193,7 @@ test "protocol error formatting" {
const response = Response{ const response = Response{
.id = .{ .string = "abc" }, .id = .{ .string = "abc" },
.@"error" = .{ .@"error" = .{
.code = -32601, .code = @intFromEnum(ErrorCode.MethodNotFound),
.message = "Method not found", .message = "Method not found",
}, },
}; };

View File

@@ -13,7 +13,7 @@ pub fn handleList(server: *Server, req: protocol.Request) !void {
.resources = server.resources, .resources = server.resources,
}; };
try sendResult(server, req.id.?, result); try server.sendResult(req.id.?, result);
} }
const ReadParams = struct { const ReadParams = struct {
@@ -61,11 +61,11 @@ const ResourceStreamingResult = struct {
pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
if (req.params == null) { 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 { 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")) { 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 }, .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")) { } else if (std.mem.eql(u8, params.uri, "mcp://page/markdown")) {
const result: ResourceStreamingResult = .{ const result: ResourceStreamingResult = .{
.contents = &.{.{ .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 }, .text = .{ .server = server, .uri = params.uri, .format = .markdown },
}}, }},
}; };
try sendResult(server, req.id.?, result); try server.sendResult(req.id.?, result);
} else { } 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,
},
});
}

View File

@@ -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")) { } else if (std.mem.eql(u8, parsed.method, "tools/call")) {
try tools.handleCall(server, arena, parsed); try tools.handleCall(server, arena, parsed);
} else { } else {
try server.sendResponse(protocol.Response{ try server.sendError(parsed.id.?, .MethodNotFound, "Method not found");
.id = parsed.id.?,
.@"error" = protocol.Error{
.code = -32601,
.message = "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 { fn handleInitialize(server: *Server, req: protocol.Request) !void {
const result = protocol.InitializeResult{ const result = protocol.InitializeResult{
.protocolVersion = "2024-11-05", .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);
} }

View File

@@ -17,7 +17,7 @@ pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
.tools = server.tools, .tools = server.tools,
}; };
try sendResult(server, req.id.?, result); try server.sendResult(req.id.?, result);
} }
const GotoParams = struct { const GotoParams = struct {
@@ -79,7 +79,7 @@ const ToolStreamingText = struct {
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
if (req.params == null) { if (req.params == null) {
return sendError(server, req.id.?, -32602, "Missing params"); return server.sendError(req.id.?, .InvalidParams, "Missing params");
} }
const CallParams = struct { 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); var aw: std.Io.Writer.Allocating = .init(arena);
std.json.Stringify.value(req.params.?, .{}, &aw.writer) catch {}; std.json.Stringify.value(req.params.?, .{}, &aw.writer) catch {};
const msg = std.fmt.allocPrint(arena, "Invalid params: {s}", .{aw.written()}) catch "Invalid params"; 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")) { 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")) { } else if (std.mem.eql(u8, call_params.name, "over")) {
try handleOver(server, arena, req.id.?, call_params.arguments); try handleOver(server, arena, req.id.?, call_params.arguments);
} else { } 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); try performGoto(server, args.url, id);
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Navigated successfully." }}; 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 { 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 }; const component: std.Uri.Component = .{ .raw = args.text };
var url_aw = std.Io.Writer.Allocating.init(arena); var url_aw = std.Io.Writer.Allocating.init(arena);
component.formatQuery(&url_aw.writer) catch { 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 { 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); try performGoto(server, url, id);
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Search performed successfully." }}; 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 { 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 }, .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 { 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 }, .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 { 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 js_result = ls.local.compileAndRun(args.script, null) catch {
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Script evaluation failed." }}; 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 str_result = js_result.toStringSliceWithAlloc(arena) catch "undefined";
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = str_result }}; 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 { 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 args = try parseParams(OverParams, arena, arguments, server, id, "over");
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = args.result }}; 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 { 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) { 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; var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "Missing arguments for {s}", .{tool_name}) catch "Missing arguments"; 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 error.InvalidParams;
} }
return std.json.parseFromValueLeaky(T, arena, arguments.?, .{ .ignore_unknown_fields = true }) catch { return std.json.parseFromValueLeaky(T, arena, arguments.?, .{ .ignore_unknown_fields = true }) catch {
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "Invalid arguments for {s}", .{tool_name}) catch "Invalid arguments"; 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; return error.InvalidParams;
}; };
} }
@@ -241,31 +237,9 @@ fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {
.reason = .address_bar, .reason = .address_bar,
.kind = .{ .push = null }, .kind = .{ .push = null },
}) catch { }) catch {
try sendError(server, id, -32603, "Internal error during navigation"); try server.sendError(id, .InternalError, "Internal error during navigation");
return error.NavigationFailed; return error.NavigationFailed;
}; };
_ = server.session.wait(5000); _ = 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,
},
});
}