mcp: promot Server.zig to file struct

This commit is contained in:
Adrià Arrufat
2026-02-28 21:02:49 +09:00
parent 5ec4305a9f
commit aae9a505e0
6 changed files with 126 additions and 128 deletions

View File

@@ -29,11 +29,7 @@ pub const log = @import("log.zig");
pub const js = @import("browser/js/js.zig"); pub const js = @import("browser/js/js.zig");
pub const dump = @import("browser/dump.zig"); pub const dump = @import("browser/dump.zig");
pub const markdown = @import("browser/markdown.zig"); pub const markdown = @import("browser/markdown.zig");
pub const mcp = struct { pub const mcp = @import("mcp.zig");
pub const Server = @import("mcp/Server.zig").McpServer;
pub const protocol = @import("mcp/protocol.zig");
pub const router = @import("mcp/router.zig");
};
pub const build_config = @import("build_config"); pub const build_config = @import("build_config");
pub const crash_handler = @import("crash_handler.zig"); pub const crash_handler = @import("crash_handler.zig");

3
src/mcp.zig Normal file
View File

@@ -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");

View File

@@ -4,137 +4,134 @@ const lp = @import("lightpanda");
const App = @import("../App.zig"); const App = @import("../App.zig");
const HttpClient = @import("../http/Client.zig"); const HttpClient = @import("../http/Client.zig");
const Self = @This();
pub const McpServer = struct { allocator: std.mem.Allocator,
allocator: std.mem.Allocator, app: *App,
app: *App,
http_client: *HttpClient, http_client: *HttpClient,
notification: *lp.Notification, notification: *lp.Notification,
browser: *lp.Browser, browser: *lp.Browser,
session: *lp.Session, session: *lp.Session,
page: *lp.Page, page: *lp.Page,
io_thread: ?std.Thread = null, io_thread: ?std.Thread = null,
queue_mutex: std.Thread.Mutex = .{}, queue_mutex: std.Thread.Mutex = .{},
queue_condition: std.Thread.Condition = .{}, queue_condition: std.Thread.Condition = .{},
message_queue: std.ArrayListUnmanaged([]const u8) = .empty, 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 { self.allocator = allocator;
const self = try allocator.create(Self); self.app = app;
errdefer allocator.destroy(self); self.message_queue = .empty;
self.allocator = allocator; self.http_client = try app.http.createClient(allocator);
self.app = app; errdefer self.http_client.deinit();
self.message_queue = .empty;
self.http_client = try app.http.createClient(allocator); self.notification = try lp.Notification.init(allocator);
errdefer self.http_client.deinit(); errdefer self.notification.deinit();
self.notification = try lp.Notification.init(allocator); self.browser = try allocator.create(lp.Browser);
errdefer self.notification.deinit(); 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); self.session = try self.browser.newSession(self.notification);
errdefer allocator.destroy(self.browser); self.page = try self.session.createPage();
self.browser.* = try lp.Browser.init(app, .{ .http_client = self.http_client });
errdefer self.browser.deinit();
self.session = try self.browser.newSession(self.notification); return self;
self.page = try self.session.createPage(); }
return self; pub fn deinit(self: *Self) void {
self.stop();
if (self.io_thread) |*thread| {
thread.join();
} }
for (self.message_queue.items) |msg| {
pub fn deinit(self: *Self) void { self.allocator.free(msg);
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);
} }
self.message_queue.deinit(self.allocator);
pub fn start(self: *Self) !void { self.browser.deinit();
self.is_running.store(true, .seq_cst); self.allocator.destroy(self.browser);
self.io_thread = try std.Thread.spawn(.{}, ioWorker, .{self}); self.notification.deinit();
} self.http_client.deinit();
pub fn stop(self: *Self) void { self.allocator.destroy(self);
self.is_running.store(false, .seq_cst); }
self.queue_mutex.lock();
self.queue_condition.signal();
self.queue_mutex.unlock();
}
fn ioWorker(self: *Self) void { pub fn start(self: *Self) !void {
var stdin_file = std.fs.File.stdin(); self.is_running.store(true, .seq_cst);
var stdin_buf: [8192]u8 = undefined; self.io_thread = try std.Thread.spawn(.{}, ioWorker, .{self});
var stdin = stdin_file.reader(&stdin_buf); }
while (self.is_running.load(.seq_cst)) { pub fn stop(self: *Self) void {
const msg_or_err = stdin.interface.adaptToOldInterface().readUntilDelimiterAlloc(self.allocator, '\n', 1024 * 1024 * 10); self.is_running.store(false, .seq_cst);
if (msg_or_err) |msg| { self.queue_mutex.lock();
if (msg.len == 0) { self.queue_condition.signal();
self.allocator.free(msg); self.queue_mutex.unlock();
continue; }
}
self.queue_mutex.lock(); fn ioWorker(self: *Self) void {
self.message_queue.append(self.allocator, msg) catch |err| { var stdin_file = std.fs.File.stdin();
lp.log.err(.app, "MCP Queue failed", .{ .err = err }); var stdin_buf: [8192]u8 = undefined;
self.allocator.free(msg); var stdin = stdin_file.reader(&stdin_buf);
};
self.queue_mutex.unlock(); while (self.is_running.load(.seq_cst)) {
self.queue_condition.signal(); const msg_or_err = stdin.interface.adaptToOldInterface().readUntilDelimiterAlloc(self.allocator, '\n', 1024 * 1024 * 10);
} else |err| { if (msg_or_err) |msg| {
if (err == error.EndOfStream) { if (msg.len == 0) {
self.stop(); self.allocator.free(msg);
break; continue;
}
lp.log.err(.app, "MCP IO Error", .{ .err = err });
std.Thread.sleep(100 * std.time.ns_per_ms);
} }
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 { pub fn getNextMessage(self: *Self) ?[]const u8 {
self.queue_mutex.lock(); self.queue_mutex.lock();
defer self.queue_mutex.unlock(); defer self.queue_mutex.unlock();
while (self.message_queue.items.len == 0 and self.is_running.load(.seq_cst)) { while (self.message_queue.items.len == 0 and self.is_running.load(.seq_cst)) {
self.queue_condition.wait(&self.queue_mutex); self.queue_condition.wait(&self.queue_mutex);
}
if (self.message_queue.items.len > 0) {
return self.message_queue.orderedRemove(0);
}
return null;
} }
pub fn sendResponse(self: *Self, response: anytype) !void { if (self.message_queue.items.len > 0) {
self.stdout_mutex.lock(); return self.message_queue.orderedRemove(0);
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();
} }
}; 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();
}

View File

@@ -1,9 +1,11 @@
const std = @import("std"); const std = @import("std");
const McpServer = @import("Server.zig").McpServer;
const protocol = @import("protocol.zig");
const lp = @import("lightpanda"); 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{ const resources = [_]protocol.Resource{
.{ .{
.uri = "mcp://page/html", .uri = "mcp://page/html",
@@ -32,7 +34,7 @@ const ReadParams = struct {
uri: []const u8, 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) { if (req.params == null) {
return sendError(server, req.id.?, -32602, "Missing params"); 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 { const GenericResponse = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: std.json.Value, 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{ try server.sendResponse(protocol.Response{
.id = id, .id = id,
.@"error" = protocol.Error{ .@"error" = protocol.Error{

View File

@@ -3,12 +3,12 @@ const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = lp.log; const log = lp.log;
const McpServer = @import("Server.zig").McpServer;
const protocol = @import("protocol.zig"); const protocol = @import("protocol.zig");
const resources = @import("resources.zig"); const resources = @import("resources.zig");
const Server = @import("Server.zig");
const tools = @import("tools.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)) { while (server.is_running.load(.seq_cst)) {
if (server.getNextMessage()) |msg| { if (server.getNextMessage()) |msg| {
defer server.allocator.free(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, .{ const parsed = std.json.parseFromSliceLeaky(protocol.Request, arena, msg, .{
.ignore_unknown_fields = true, .ignore_unknown_fields = true,
}) catch |err| { }) 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 { const GenericResponse = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: std.json.Value, 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{ const result = protocol.InitializeResult{
.protocolVersion = "2024-11-05", .protocolVersion = "2024-11-05",
.capabilities = .{ .capabilities = .{

View File

@@ -7,10 +7,10 @@ const js = lp.js;
const Element = @import("../browser/webapi/Element.zig"); const Element = @import("../browser/webapi/Element.zig");
const Selector = @import("../browser/webapi/selector/Selector.zig"); const Selector = @import("../browser/webapi/selector/Selector.zig");
const String = @import("../string.zig").String; const String = @import("../string.zig").String;
const McpServer = @import("Server.zig").McpServer;
const protocol = @import("protocol.zig"); 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{ const tools = [_]protocol.Tool{
.{ .{
.name = "goto", .name = "goto",
@@ -116,7 +116,7 @@ const OverParams = struct {
result: []const u8, 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) { if (req.params == null) {
return sendError(server, req.id.?, -32602, "Missing params"); 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); const url_z = try arena.dupeZ(u8, url);
_ = server.page.navigate(url_z, .{ _ = server.page.navigate(url_z, .{
.reason = .address_bar, .reason = .address_bar,
@@ -278,7 +278,7 @@ fn performGoto(server: *McpServer, arena: std.mem.Allocator, url: []const u8) !v
_ = server.session.wait(5000); _ = 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 { const GenericResponse = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: std.json.Value, 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{ try server.sendResponse(protocol.Response{
.id = id, .id = id,
.@"error" = protocol.Error{ .@"error" = protocol.Error{