Merge pull request #2011 from lightpanda-io/mcp-fixes

MCP fixes
This commit is contained in:
Karl Seguin
2026-03-27 13:02:59 +08:00
committed by GitHub
3 changed files with 49 additions and 37 deletions

View File

@@ -22,12 +22,14 @@ pub const resource_list = [_]protocol.Resource{
}; };
pub fn handleList(server: *Server, req: protocol.Request) !void { 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 { const ReadParams = struct {
uri: []const u8, uri: []const u8,
}; };
const Format = enum { html, markdown };
const ResourceStreamingResult = struct { const ResourceStreamingResult = struct {
contents: []const struct { contents: []const struct {
@@ -38,7 +40,7 @@ const ResourceStreamingResult = struct {
const StreamingText = struct { const StreamingText = struct {
page: *lp.Page, page: *lp.Page,
format: enum { html, markdown }, format: Format,
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
try jw.beginWriteRaw(); try jw.beginWriteRaw();
@@ -47,9 +49,11 @@ const ResourceStreamingResult = struct {
switch (self.format) { switch (self.format) {
.html => lp.dump.root(self.page.document, .{}, &escaped.writer, self.page) catch |err| { .html => lp.dump.root(self.page.document, .{}, &escaped.writer, self.page) catch |err| {
log.err(.mcp, "html dump failed", .{ .err = 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| { .markdown => lp.markdown.dump(self.page.document.asNode(), .{}, &escaped.writer, self.page) catch |err| {
log.err(.mcp, "markdown dump failed", .{ .err = err }); log.err(.mcp, "markdown dump failed", .{ .err = err });
return error.WriteFailed;
}, },
} }
try jw.writer.writeByte('"'); try jw.writer.writeByte('"');
@@ -86,28 +90,25 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
return server.sendError(req_id, .PageNotLoaded, "Page not loaded"); return server.sendError(req_id, .PageNotLoaded, "Page not loaded");
}; };
switch (uri) { const format: Format = switch (uri) {
.@"mcp://page/html" => { .@"mcp://page/html" => .html,
const result: ResourceStreamingResult = .{ .@"mcp://page/markdown" => .markdown,
.contents = &.{.{ };
.uri = params.uri, const mime_type: []const u8 = switch (uri) {
.mimeType = "text/html", .@"mcp://page/html" => "text/html",
.text = .{ .page = page, .format = .html }, .@"mcp://page/markdown" => "text/markdown",
}}, };
};
try server.sendResult(req_id, result); const result: ResourceStreamingResult = .{
}, .contents = &.{.{
.@"mcp://page/markdown" => { .uri = params.uri,
const result: ResourceStreamingResult = .{ .mimeType = mime_type,
.contents = &.{.{ .text = .{ .page = page, .format = format },
.uri = params.uri, }},
.mimeType = "text/markdown", };
.text = .{ .page = page, .format = .markdown }, server.sendResult(req_id, result) catch {
}}, return server.sendError(req_id, .InternalError, "Failed to serialize resource content");
}; };
try server.sendResult(req_id, result);
},
}
} }
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");

View File

@@ -16,6 +16,7 @@ pub fn processRequests(server: *Server, reader: *std.io.Reader) !void {
const buffered_line = reader.takeDelimiter('\n') catch |err| switch (err) { const buffered_line = reader.takeDelimiter('\n') catch |err| switch (err) {
error.StreamTooLong => { error.StreamTooLong => {
log.err(.mcp, "Message too long", .{}); log.err(.mcp, "Message too long", .{});
try server.sendError(.null, .InvalidRequest, "Message too long");
continue; continue;
}, },
else => return err, else => return err,
@@ -80,6 +81,7 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
} }
fn handleInitialize(server: *Server, req: protocol.Request) !void { fn handleInitialize(server: *Server, req: protocol.Request) !void {
const id = req.id orelse return;
const result = protocol.InitializeResult{ const result = protocol.InitializeResult{
.protocolVersion = "2025-11-25", .protocolVersion = "2025-11-25",
.capabilities = .{ .capabilities = .{
@@ -92,7 +94,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 { fn handlePing(server: *Server, req: protocol.Request) !void {

View File

@@ -172,7 +172,8 @@ pub const tool_list = [_]protocol.Tool{
pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
_ = arena; _ = 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 { const GotoParams = struct {
@@ -205,17 +206,18 @@ const ToolStreamingText = struct {
switch (self.action) { switch (self.action) {
.markdown => lp.markdown.dump(self.page.document.asNode(), .{}, w, self.page) catch |err| { .markdown => lp.markdown.dump(self.page.document.asNode(), .{}, w, self.page) catch |err| {
log.err(.mcp, "markdown dump failed", .{ .err = err }); log.err(.mcp, "markdown dump failed", .{ .err = err });
return error.WriteFailed;
}, },
.links => { .links => {
if (lp.links.collectLinks(self.page.call_arena, self.page.document.asNode(), self.page)) |links| { const links = lp.links.collectLinks(self.page.call_arena, self.page.document.asNode(), self.page) catch |err| {
var first = true;
for (links) |href| {
if (!first) try w.writeByte('\n');
try w.writeAll(href);
first = false;
}
} else |err| {
log.err(.mcp, "query links failed", .{ .err = 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 => { .semantic_tree => {
@@ -241,6 +243,7 @@ const ToolStreamingText = struct {
st.textStringify(w) catch |err| { st.textStringify(w) catch |err| {
log.err(.mcp, "semantic tree dump failed", .{ .err = err }); log.err(.mcp, "semantic tree dump failed", .{ .err = err });
return error.WriteFailed;
}; };
}, },
} }
@@ -331,7 +334,9 @@ fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value,
const content = [_]protocol.TextContent(ToolStreamingText){.{ const content = [_]protocol.TextContent(ToolStreamingText){.{
.text = .{ .page = page, .action = .markdown }, .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 { fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
@@ -341,7 +346,9 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar
const content = [_]protocol.TextContent(ToolStreamingText){.{ const content = [_]protocol.TextContent(ToolStreamingText){.{
.text = .{ .page = page, .action = .links }, .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 { fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
@@ -363,7 +370,9 @@ fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Va
.maxDepth = args.maxDepth, .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 { fn handleInteractiveElements(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {