From c8d8ca5e94880562dd6812e0b6dae37671264a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 27 Mar 2026 11:28:47 +0900 Subject: [PATCH] mcp: improve error handling in resources and tools - Handle failures during HTML, Markdown, and link serialization. - Return MCP internal errors when result serialization fails. - Refactor resource reading logic for better clarity and consistency. --- src/mcp/resources.zig | 46 +++++++++++++++++++++---------------------- src/mcp/tools.zig | 30 +++++++++++++++++----------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index dd6af972..e08aab1b 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -28,6 +28,7 @@ pub fn handleList(server: *Server, req: protocol.Request) !void { const ReadParams = struct { uri: []const u8, }; +const Format = enum { html, markdown }; const ResourceStreamingResult = struct { contents: []const struct { @@ -38,7 +39,7 @@ const ResourceStreamingResult = struct { const StreamingText = struct { page: *lp.Page, - format: enum { html, markdown }, + format: Format, pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { try jw.beginWriteRaw(); @@ -47,9 +48,11 @@ const ResourceStreamingResult = struct { switch (self.format) { .html => lp.dump.root(self.page.document, .{}, &escaped.writer, self.page) catch |err| { log.err(.mcp, "html dump failed", .{ .err = err }); + return error.WriteFailed; }, .markdown => lp.markdown.dump(self.page.document.asNode(), .{}, &escaped.writer, self.page) catch |err| { log.err(.mcp, "markdown dump failed", .{ .err = err }); + return error.WriteFailed; }, } try jw.writer.writeByte('"'); @@ -86,28 +89,25 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque return server.sendError(req_id, .PageNotLoaded, "Page not loaded"); }; - switch (uri) { - .@"mcp://page/html" => { - const result: ResourceStreamingResult = .{ - .contents = &.{.{ - .uri = params.uri, - .mimeType = "text/html", - .text = .{ .page = page, .format = .html }, - }}, - }; - try server.sendResult(req_id, result); - }, - .@"mcp://page/markdown" => { - const result: ResourceStreamingResult = .{ - .contents = &.{.{ - .uri = params.uri, - .mimeType = "text/markdown", - .text = .{ .page = page, .format = .markdown }, - }}, - }; - try server.sendResult(req_id, result); - }, - } + const format: Format = switch (uri) { + .@"mcp://page/html" => .html, + .@"mcp://page/markdown" => .markdown, + }; + const mime_type: []const u8 = switch (uri) { + .@"mcp://page/html" => "text/html", + .@"mcp://page/markdown" => "text/markdown", + }; + + const result: ResourceStreamingResult = .{ + .contents = &.{.{ + .uri = params.uri, + .mimeType = mime_type, + .text = .{ .page = page, .format = format }, + }}, + }; + server.sendResult(req_id, result) catch { + return server.sendError(req_id, .InternalError, "Failed to serialize resource content"); + }; } const testing = @import("../testing.zig"); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index f4a82570..59c20041 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -205,17 +205,18 @@ const ToolStreamingText = struct { switch (self.action) { .markdown => lp.markdown.dump(self.page.document.asNode(), .{}, w, self.page) catch |err| { log.err(.mcp, "markdown dump failed", .{ .err = err }); + return error.WriteFailed; }, .links => { - if (lp.links.collectLinks(self.page.call_arena, self.page.document.asNode(), self.page)) |links| { - var first = true; - for (links) |href| { - if (!first) try w.writeByte('\n'); - try w.writeAll(href); - first = false; - } - } else |err| { + const links = lp.links.collectLinks(self.page.call_arena, self.page.document.asNode(), self.page) catch |err| { log.err(.mcp, "query links failed", .{ .err = err }); + return error.WriteFailed; + }; + var first = true; + for (links) |href| { + if (!first) try w.writeByte('\n'); + try w.writeAll(href); + first = false; } }, .semantic_tree => { @@ -241,6 +242,7 @@ const ToolStreamingText = struct { st.textStringify(w) catch |err| { log.err(.mcp, "semantic tree dump failed", .{ .err = err }); + return error.WriteFailed; }; }, } @@ -331,7 +333,9 @@ fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, const content = [_]protocol.TextContent(ToolStreamingText){.{ .text = .{ .page = page, .action = .markdown }, }}; - try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }); + server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }) catch { + return server.sendError(id, .InternalError, "Failed to serialize markdown content"); + }; } fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -341,7 +345,9 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar const content = [_]protocol.TextContent(ToolStreamingText){.{ .text = .{ .page = page, .action = .links }, }}; - try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }); + server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }) catch { + return server.sendError(id, .InternalError, "Failed to serialize links content"); + }; } fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { @@ -363,7 +369,9 @@ fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Va .maxDepth = args.maxDepth, }, }}; - try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }); + server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content }) catch { + return server.sendError(id, .InternalError, "Failed to serialize semantic tree content"); + }; } fn handleInteractiveElements(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {