From 48df38cbfed3fa0b6cb29a82ff46076051076f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 3 Mar 2026 15:17:59 +0900 Subject: [PATCH] mcp: improve evaluate error reporting and refactor tool result types --- src/browser/js/TryCatch.zig | 13 +++++ src/mcp/protocol.zig | 14 +++++ src/mcp/tools.zig | 100 +++++++++++++++++++++++++----------- 3 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/browser/js/TryCatch.zig b/src/browser/js/TryCatch.zig index d0f7a7d8..f9909e71 100644 --- a/src/browser/js/TryCatch.zig +++ b/src/browser/js/TryCatch.zig @@ -134,4 +134,17 @@ pub const Caught = struct { try writer.write(prefix ++ ".line", self.line); try writer.write(prefix ++ ".caught", self.caught); } + + pub fn jsonStringify(self: Caught, jw: anytype) !void { + try jw.beginObject(); + try jw.objectField("exception"); + try jw.write(self.exception); + try jw.objectField("stack"); + try jw.write(self.stack); + try jw.objectField("line"); + try jw.write(self.line); + try jw.objectField("caught"); + try jw.write(self.caught); + try jw.endObject(); + } }; diff --git a/src/mcp/protocol.zig b/src/mcp/protocol.zig index 022da7c8..5f5dc7f2 100644 --- a/src/mcp/protocol.zig +++ b/src/mcp/protocol.zig @@ -149,6 +149,20 @@ pub const Resource = struct { mimeType: ?[]const u8 = null, }; +pub fn TextContent(comptime T: type) type { + return struct { + type: []const u8 = "text", + text: T, + }; +} + +pub fn CallToolResult(comptime T: type) type { + return struct { + content: []const TextContent(T), + isError: bool = false, + }; +} + pub const JsonEscapingWriter = struct { inner_writer: *std.Io.Writer, writer: std.Io.Writer, diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 456e531a..3bfb1517 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -204,8 +204,8 @@ fn handleGoto(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arg const args = try parseArguments(GotoParams, arena, arguments, server, id, "goto"); try performGoto(server, args.url, id); - const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Navigated successfully." }}; - try server.sendResult(id, .{ .content = &content }); + const content = [_]protocol.TextContent([]const u8){.{ .text = "Navigated successfully." }}; + try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } fn handleSearch(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -222,8 +222,8 @@ fn handleSearch(server: *Server, arena: std.mem.Allocator, id: std.json.Value, a try performGoto(server, url, id); - const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Search performed successfully." }}; - try server.sendResult(id, .{ .content = &content }); + const content = [_]protocol.TextContent([]const u8){.{ .text = "Search performed successfully." }}; + try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -238,15 +238,10 @@ fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, } else |_| {} } - const result = struct { - content: []const struct { type: []const u8, text: ToolStreamingText }, - }{ - .content = &.{.{ - .type = "text", - .text = .{ .server = server, .action = .markdown }, - }}, - }; - try server.sendResult(id, result); + const content = [_]protocol.TextContent(ToolStreamingText){.{ + .text = .{ .server = server, .action = .markdown }, + }}; + try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }); } fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -261,15 +256,10 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar } else |_| {} } - const result = struct { - content: []const struct { type: []const u8, text: ToolStreamingText }, - }{ - .content = &.{.{ - .type = "text", - .text = .{ .server = server, .action = .links }, - }}, - }; - try server.sendResult(id, result); + const content = [_]protocol.TextContent(ToolStreamingText){.{ + .text = .{ .server = server, .action = .links }, + }}; + try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }); } fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -283,22 +273,30 @@ fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, server.page.js.localScope(&ls); defer ls.deinit(); - 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 server.sendResult(id, .{ .content = &content, .isError = true }); + var try_catch: js.TryCatch = undefined; + try_catch.init(&ls.local); + defer try_catch.deinit(); + + const js_result = ls.local.compileAndRun(args.script, null) catch |err| { + const caught = try_catch.caughtOrError(arena, err); + var aw: std.Io.Writer.Allocating = .init(arena); + try caught.format(&aw.writer); + + const content = [_]protocol.TextContent([]const u8){.{ .text = aw.written() }}; + return server.sendResult(id, protocol.CallToolResult([]const u8){ .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 server.sendResult(id, .{ .content = &content }); + const content = [_]protocol.TextContent([]const u8){.{ .text = str_result }}; + try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } fn handleOver(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { const args = try parseArguments(OverParams, arena, arguments, server, id, "over"); - const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = args.result }}; - try server.sendResult(id, .{ .content = &content }); + const content = [_]protocol.TextContent([]const u8){.{ .text = args.result }}; + try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T { @@ -326,3 +324,47 @@ fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void { } const testing = @import("../testing.zig"); +const router = @import("router.zig"); + +test "MCP - evaluate error reporting" { + defer testing.reset(); + const allocator = testing.allocator; + const app = testing.test_app; + + var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); + defer out_alloc.deinit(); + + var server = try Server.init(allocator, app, &out_alloc.writer); + defer server.deinit(); + + const aa = testing.arena_allocator; + + // Call evaluate with a script that throws an error + const msg = + \\{ + \\ "jsonrpc": "2.0", + \\ "id": 1, + \\ "method": "tools/call", + \\ "params": { + \\ "name": "evaluate", + \\ "arguments": { + \\ "script": "throw new Error('test error')" + \\ } + \\ } + \\} + ; + + try router.handleMessage(server, aa, msg); + + try testing.expectJson( + \\{ + \\ "id": 1, + \\ "result": { + \\ "isError": true, + \\ "content": [ + \\ { "type": "text" } + \\ ] + \\ } + \\} + , out_alloc.writer.buffered()); +}