diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 886e87ba..94973869 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -29,11 +29,7 @@ pub const log = @import("log.zig"); pub const js = @import("browser/js/js.zig"); pub const dump = @import("browser/dump.zig"); pub const markdown = @import("browser/markdown.zig"); -pub const mcp = struct { - pub const Server = @import("mcp/Server.zig").McpServer; - pub const protocol = @import("mcp/protocol.zig"); - pub const router = @import("mcp/router.zig"); -}; +pub const mcp = @import("mcp.zig"); pub const build_config = @import("build_config"); pub const crash_handler = @import("crash_handler.zig"); diff --git a/src/mcp.zig b/src/mcp.zig new file mode 100644 index 00000000..41998f5a --- /dev/null +++ b/src/mcp.zig @@ -0,0 +1,3 @@ +pub const Server = @import("mcp/Server.zig"); +pub const protocol = @import("mcp/protocol.zig"); +pub const router = @import("mcp/router.zig"); diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index 1f984950..08e6b978 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -4,137 +4,134 @@ const lp = @import("lightpanda"); const App = @import("../App.zig"); const HttpClient = @import("../http/Client.zig"); +const Self = @This(); -pub const McpServer = struct { - allocator: std.mem.Allocator, - app: *App, +allocator: std.mem.Allocator, +app: *App, - http_client: *HttpClient, - notification: *lp.Notification, - browser: *lp.Browser, - session: *lp.Session, - page: *lp.Page, +http_client: *HttpClient, +notification: *lp.Notification, +browser: *lp.Browser, +session: *lp.Session, +page: *lp.Page, - io_thread: ?std.Thread = null, - queue_mutex: std.Thread.Mutex = .{}, - queue_condition: std.Thread.Condition = .{}, - message_queue: std.ArrayListUnmanaged([]const u8) = .empty, +io_thread: ?std.Thread = null, +queue_mutex: std.Thread.Mutex = .{}, +queue_condition: std.Thread.Condition = .{}, +message_queue: std.ArrayListUnmanaged([]const u8) = .empty, - is_running: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), +is_running: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), - stdout_mutex: std.Thread.Mutex = .{}, +stdout_mutex: std.Thread.Mutex = .{}, - const Self = @This(); +pub fn init(allocator: std.mem.Allocator, app: *App) !*Self { + const self = try allocator.create(Self); + errdefer allocator.destroy(self); - pub fn init(allocator: std.mem.Allocator, app: *App) !*Self { - const self = try allocator.create(Self); - errdefer allocator.destroy(self); + self.allocator = allocator; + self.app = app; + self.message_queue = .empty; - self.allocator = allocator; - self.app = app; - self.message_queue = .empty; + self.http_client = try app.http.createClient(allocator); + errdefer self.http_client.deinit(); - self.http_client = try app.http.createClient(allocator); - errdefer self.http_client.deinit(); + self.notification = try lp.Notification.init(allocator); + errdefer self.notification.deinit(); - self.notification = try lp.Notification.init(allocator); - errdefer self.notification.deinit(); + self.browser = try allocator.create(lp.Browser); + errdefer allocator.destroy(self.browser); + self.browser.* = try lp.Browser.init(app, .{ .http_client = self.http_client }); + errdefer self.browser.deinit(); - self.browser = try allocator.create(lp.Browser); - errdefer allocator.destroy(self.browser); - self.browser.* = try lp.Browser.init(app, .{ .http_client = self.http_client }); - errdefer self.browser.deinit(); + self.session = try self.browser.newSession(self.notification); + self.page = try self.session.createPage(); - self.session = try self.browser.newSession(self.notification); - self.page = try self.session.createPage(); + return self; +} - return self; +pub fn deinit(self: *Self) void { + self.stop(); + if (self.io_thread) |*thread| { + thread.join(); } - - pub fn deinit(self: *Self) void { - self.stop(); - if (self.io_thread) |*thread| { - thread.join(); - } - for (self.message_queue.items) |msg| { - self.allocator.free(msg); - } - self.message_queue.deinit(self.allocator); - - self.browser.deinit(); - self.allocator.destroy(self.browser); - self.notification.deinit(); - self.http_client.deinit(); - - self.allocator.destroy(self); + for (self.message_queue.items) |msg| { + self.allocator.free(msg); } + self.message_queue.deinit(self.allocator); - pub fn start(self: *Self) !void { - self.is_running.store(true, .seq_cst); - self.io_thread = try std.Thread.spawn(.{}, ioWorker, .{self}); - } + self.browser.deinit(); + self.allocator.destroy(self.browser); + self.notification.deinit(); + self.http_client.deinit(); - pub fn stop(self: *Self) void { - self.is_running.store(false, .seq_cst); - self.queue_mutex.lock(); - self.queue_condition.signal(); - self.queue_mutex.unlock(); - } + self.allocator.destroy(self); +} - fn ioWorker(self: *Self) void { - var stdin_file = std.fs.File.stdin(); - var stdin_buf: [8192]u8 = undefined; - var stdin = stdin_file.reader(&stdin_buf); +pub fn start(self: *Self) !void { + self.is_running.store(true, .seq_cst); + self.io_thread = try std.Thread.spawn(.{}, ioWorker, .{self}); +} - while (self.is_running.load(.seq_cst)) { - const msg_or_err = stdin.interface.adaptToOldInterface().readUntilDelimiterAlloc(self.allocator, '\n', 1024 * 1024 * 10); - if (msg_or_err) |msg| { - if (msg.len == 0) { - self.allocator.free(msg); - continue; - } +pub fn stop(self: *Self) void { + self.is_running.store(false, .seq_cst); + self.queue_mutex.lock(); + self.queue_condition.signal(); + self.queue_mutex.unlock(); +} - self.queue_mutex.lock(); - self.message_queue.append(self.allocator, msg) catch |err| { - lp.log.err(.app, "MCP Queue failed", .{ .err = err }); - self.allocator.free(msg); - }; - self.queue_mutex.unlock(); - self.queue_condition.signal(); - } else |err| { - if (err == error.EndOfStream) { - self.stop(); - break; - } - lp.log.err(.app, "MCP IO Error", .{ .err = err }); - std.Thread.sleep(100 * std.time.ns_per_ms); +fn ioWorker(self: *Self) void { + var stdin_file = std.fs.File.stdin(); + var stdin_buf: [8192]u8 = undefined; + var stdin = stdin_file.reader(&stdin_buf); + + while (self.is_running.load(.seq_cst)) { + const msg_or_err = stdin.interface.adaptToOldInterface().readUntilDelimiterAlloc(self.allocator, '\n', 1024 * 1024 * 10); + if (msg_or_err) |msg| { + if (msg.len == 0) { + self.allocator.free(msg); + continue; } + + self.queue_mutex.lock(); + self.message_queue.append(self.allocator, msg) catch |err| { + lp.log.err(.app, "MCP Queue failed", .{ .err = err }); + self.allocator.free(msg); + }; + self.queue_mutex.unlock(); + self.queue_condition.signal(); + } else |err| { + if (err == error.EndOfStream) { + self.stop(); + break; + } + lp.log.err(.app, "MCP IO Error", .{ .err = err }); + std.Thread.sleep(100 * std.time.ns_per_ms); } } +} - pub fn getNextMessage(self: *Self) ?[]const u8 { - self.queue_mutex.lock(); - defer self.queue_mutex.unlock(); +pub fn getNextMessage(self: *Self) ?[]const u8 { + self.queue_mutex.lock(); + defer self.queue_mutex.unlock(); - while (self.message_queue.items.len == 0 and self.is_running.load(.seq_cst)) { - self.queue_condition.wait(&self.queue_mutex); - } - - if (self.message_queue.items.len > 0) { - return self.message_queue.orderedRemove(0); - } - return null; + while (self.message_queue.items.len == 0 and self.is_running.load(.seq_cst)) { + self.queue_condition.wait(&self.queue_mutex); } - pub fn sendResponse(self: *Self, response: anytype) !void { - self.stdout_mutex.lock(); - defer self.stdout_mutex.unlock(); - - var stdout_file = std.fs.File.stdout(); - var stdout_buf: [8192]u8 = undefined; - var stdout = stdout_file.writer(&stdout_buf); - try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &stdout.interface); - try stdout.interface.writeByte('\n'); - try stdout.interface.flush(); + if (self.message_queue.items.len > 0) { + return self.message_queue.orderedRemove(0); } -}; + return null; +} + +pub fn sendResponse(self: *Self, response: anytype) !void { + self.stdout_mutex.lock(); + defer self.stdout_mutex.unlock(); + + var stdout_file = std.fs.File.stdout(); + var stdout_buf: [8192]u8 = undefined; + var stdout = stdout_file.writer(&stdout_buf); + try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &stdout.interface); + try stdout.interface.writeByte('\n'); + try stdout.interface.flush(); +} diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index e50028b1..cfb77add 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -1,9 +1,11 @@ const std = @import("std"); -const McpServer = @import("Server.zig").McpServer; -const protocol = @import("protocol.zig"); + const lp = @import("lightpanda"); -pub fn handleList(server: *McpServer, req: protocol.Request) !void { +const protocol = @import("protocol.zig"); +const Server = @import("Server.zig"); + +pub fn handleList(server: *Server, req: protocol.Request) !void { const resources = [_]protocol.Resource{ .{ .uri = "mcp://page/html", @@ -32,7 +34,7 @@ const ReadParams = struct { uri: []const u8, }; -pub fn handleRead(server: *McpServer, arena: std.mem.Allocator, req: protocol.Request) !void { +pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { if (req.params == null) { return sendError(server, req.id.?, -32602, "Missing params"); } @@ -78,7 +80,7 @@ pub fn handleRead(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re } } -pub fn sendResult(server: *McpServer, id: std.json.Value, result: anytype) !void { +pub fn sendResult(server: *Server, id: std.json.Value, result: anytype) !void { const GenericResponse = struct { jsonrpc: []const u8 = "2.0", id: std.json.Value, @@ -90,7 +92,7 @@ pub fn sendResult(server: *McpServer, id: std.json.Value, result: anytype) !void }); } -pub fn sendError(server: *McpServer, id: std.json.Value, code: i64, message: []const u8) !void { +pub fn sendError(server: *Server, id: std.json.Value, code: i64, message: []const u8) !void { try server.sendResponse(protocol.Response{ .id = id, .@"error" = protocol.Error{ diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 2e901374..d3d5071a 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -3,12 +3,12 @@ const std = @import("std"); const lp = @import("lightpanda"); const log = lp.log; -const McpServer = @import("Server.zig").McpServer; const protocol = @import("protocol.zig"); const resources = @import("resources.zig"); +const Server = @import("Server.zig"); const tools = @import("tools.zig"); -pub fn processRequests(server: *McpServer) void { +pub fn processRequests(server: *Server) void { while (server.is_running.load(.seq_cst)) { if (server.getNextMessage()) |msg| { defer server.allocator.free(msg); @@ -25,7 +25,7 @@ pub fn processRequests(server: *McpServer) void { } } -fn handleMessage(server: *McpServer, arena: std.mem.Allocator, msg: []const u8) !void { +fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) !void { const parsed = std.json.parseFromSliceLeaky(protocol.Request, arena, msg, .{ .ignore_unknown_fields = true, }) catch |err| { @@ -62,7 +62,7 @@ fn handleMessage(server: *McpServer, arena: std.mem.Allocator, msg: []const u8) } } -fn sendResponseGeneric(server: *McpServer, id: std.json.Value, result: anytype) !void { +fn sendResponseGeneric(server: *Server, id: std.json.Value, result: anytype) !void { const GenericResponse = struct { jsonrpc: []const u8 = "2.0", id: std.json.Value, @@ -74,7 +74,7 @@ fn sendResponseGeneric(server: *McpServer, id: std.json.Value, result: anytype) }); } -fn handleInitialize(server: *McpServer, req: protocol.Request) !void { +fn handleInitialize(server: *Server, req: protocol.Request) !void { const result = protocol.InitializeResult{ .protocolVersion = "2024-11-05", .capabilities = .{ diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 768fc71b..06453ae5 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -7,10 +7,10 @@ const js = lp.js; const Element = @import("../browser/webapi/Element.zig"); const Selector = @import("../browser/webapi/selector/Selector.zig"); const String = @import("../string.zig").String; -const McpServer = @import("Server.zig").McpServer; const protocol = @import("protocol.zig"); +const Server = @import("Server.zig"); -pub fn handleList(server: *McpServer, arena: std.mem.Allocator, req: protocol.Request) !void { +pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { const tools = [_]protocol.Tool{ .{ .name = "goto", @@ -116,7 +116,7 @@ const OverParams = struct { result: []const u8, }; -pub fn handleCall(server: *McpServer, arena: std.mem.Allocator, req: protocol.Request) !void { +pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { if (req.params == null) { return sendError(server, req.id.?, -32602, "Missing params"); } @@ -266,7 +266,7 @@ pub fn handleCall(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re } } -fn performGoto(server: *McpServer, arena: std.mem.Allocator, url: []const u8) !void { +fn performGoto(server: *Server, arena: std.mem.Allocator, url: []const u8) !void { const url_z = try arena.dupeZ(u8, url); _ = server.page.navigate(url_z, .{ .reason = .address_bar, @@ -278,7 +278,7 @@ fn performGoto(server: *McpServer, arena: std.mem.Allocator, url: []const u8) !v _ = server.session.wait(5000); } -pub fn sendResult(server: *McpServer, id: std.json.Value, result: anytype) !void { +pub fn sendResult(server: *Server, id: std.json.Value, result: anytype) !void { const GenericResponse = struct { jsonrpc: []const u8 = "2.0", id: std.json.Value, @@ -290,7 +290,7 @@ pub fn sendResult(server: *McpServer, id: std.json.Value, result: anytype) !void }); } -pub fn sendError(server: *McpServer, id: std.json.Value, code: i64, message: []const u8) !void { +pub fn sendError(server: *Server, id: std.json.Value, code: i64, message: []const u8) !void { try server.sendResponse(protocol.Response{ .id = id, .@"error" = protocol.Error{