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.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,
};
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",
},
};

View File

@@ -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,
},
});
}

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")) {
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);
}

View File

@@ -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,
},
});
}