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 1/3] 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 { From de32e5cf34f67a17c872d5445797295b2803f46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 27 Mar 2026 11:34:06 +0900 Subject: [PATCH 2/3] mcp: handle missing request IDs safely --- src/mcp/resources.zig | 3 ++- src/mcp/router.zig | 3 ++- src/mcp/tools.zig | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index e08aab1b..914f95cd 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -22,7 +22,8 @@ pub const resource_list = [_]protocol.Resource{ }; pub fn handleList(server: *Server, req: protocol.Request) !void { - try server.sendResult(req.id.?, .{ .resources = &resource_list }); + const id = req.id orelse return; + try server.sendResult(id, .{ .resources = &resource_list }); } const ReadParams = struct { diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 38b2e206..3a92aeac 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -80,6 +80,7 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) } fn handleInitialize(server: *Server, req: protocol.Request) !void { + const id = req.id orelse return; const result = protocol.InitializeResult{ .protocolVersion = "2025-11-25", .capabilities = .{ @@ -92,7 +93,7 @@ fn handleInitialize(server: *Server, req: protocol.Request) !void { }, }; - try server.sendResult(req.id.?, result); + try server.sendResult(id, result); } fn handlePing(server: *Server, req: protocol.Request) !void { diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 59c20041..73e7ff9b 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -172,7 +172,8 @@ pub const tool_list = [_]protocol.Tool{ pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { _ = arena; - try server.sendResult(req.id.?, .{ .tools = &tool_list }); + const id = req.id orelse return; + try server.sendResult(id, .{ .tools = &tool_list }); } const GotoParams = struct { From 1d54e6944b36c702b7c4e8d21dd44ffef9d60c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 27 Mar 2026 11:36:18 +0900 Subject: [PATCH 3/3] mcp: send error response when message is too long --- src/mcp/router.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 3a92aeac..6a3385c0 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -16,6 +16,7 @@ pub fn processRequests(server: *Server, reader: *std.io.Reader) !void { const buffered_line = reader.takeDelimiter('\n') catch |err| switch (err) { error.StreamTooLong => { log.err(.mcp, "Message too long", .{}); + try server.sendError(.null, .InvalidRequest, "Message too long"); continue; }, else => return err,