Files
browser/src/mcp/resources.zig
Karl Seguin 2275416505 Page -> Frame
This is to pave the way for introducing a new "Page" container, which will take
over the page lifecycle currently burdening Session. The ultimate goal of that
is to allow the Session to have multiple pages (mostly for better transitions
between pages), which is hard to do now since the Session has so much state.

This rename was aggressive, e.g. currentPage() -> currentFrame() so that, when
the new Page container is added, you won't see "currentPage()" and wonder:

  "Does 'currentPage' mean the new Page container, or the Frame (which
  used to be called Page)".
2026-04-22 08:42:18 +08:00

113 lines
3.6 KiB
Zig

const std = @import("std");
const lp = @import("lightpanda");
const log = lp.log;
const protocol = @import("protocol.zig");
const Server = @import("Server.zig");
pub const resource_list = [_]protocol.Resource{
.{
.uri = "mcp://page/html",
.name = "Page HTML",
.description = "The serialized HTML DOM of the current page",
.mimeType = "text/html",
},
.{
.uri = "mcp://page/markdown",
.name = "Page Markdown",
.description = "The token-efficient markdown representation of the current page",
.mimeType = "text/markdown",
},
};
pub fn handleList(server: *Server, req: protocol.Request) !void {
const id = req.id orelse return;
try server.sendResult(id, .{ .resources = &resource_list });
}
const ReadParams = struct {
uri: []const u8,
};
const Format = enum { html, markdown };
const ResourceStreamingResult = struct {
contents: []const struct {
uri: []const u8,
mimeType: []const u8,
text: StreamingText,
},
const StreamingText = struct {
frame: *lp.Frame,
format: Format,
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
try jw.beginWriteRaw();
try jw.writer.writeByte('"');
var escaped = protocol.JsonEscapingWriter.init(jw.writer);
switch (self.format) {
.html => lp.dump.root(self.frame.document, .{}, &escaped.writer, self.frame) catch |err| {
log.err(.mcp, "html dump failed", .{ .err = err });
return error.WriteFailed;
},
.markdown => lp.markdown.dump(self.frame.document.asNode(), .{}, &escaped.writer, self.frame) catch |err| {
log.err(.mcp, "markdown dump failed", .{ .err = err });
return error.WriteFailed;
},
}
try jw.writer.writeByte('"');
jw.endWriteRaw();
}
};
};
const ResourceUri = enum {
@"mcp://page/html",
@"mcp://page/markdown",
};
const resource_map = std.StaticStringMap(ResourceUri).initComptime(.{
.{ "mcp://page/html", .@"mcp://page/html" },
.{ "mcp://page/markdown", .@"mcp://page/markdown" },
});
pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
if (req.params == null or req.id == null) {
return server.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, "Missing params");
}
const req_id = req.id.?;
const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch {
return server.sendError(req_id, .InvalidParams, "Invalid params");
};
const uri = resource_map.get(params.uri) orelse {
return server.sendError(req_id, .InvalidRequest, "Resource not found");
};
const frame = server.session.currentFrame() orelse {
return server.sendError(req_id, .FrameNotLoaded, "Page not loaded");
};
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 = .{ .frame = frame, .format = format },
}},
};
server.sendResult(req_id, result) catch {
return server.sendError(req_id, .InternalError, "Failed to serialize resource content");
};
}