mcp: add Model Context Protocol server support

Adds a new `mcp` run mode to start an MCP server over stdio.
Implements tools for navigation and JS evaluation, along with
resources for HTML and Markdown page content.
This commit is contained in:
Adrià Arrufat
2026-02-22 22:32:14 +09:00
parent 18e63df01e
commit a27339b954
10 changed files with 619 additions and 16 deletions

View File

@@ -28,6 +28,7 @@ pub const RunMode = enum {
fetch, fetch,
serve, serve,
version, version,
mcp,
}; };
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096; pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
@@ -59,56 +60,56 @@ pub fn deinit(self: *const Config, allocator: Allocator) void {
pub fn tlsVerifyHost(self: *const Config) bool { pub fn tlsVerifyHost(self: *const Config) bool {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.tls_verify_host, inline .serve, .fetch, .mcp => |opts| opts.common.tls_verify_host,
else => unreachable, else => unreachable,
}; };
} }
pub fn obeyRobots(self: *const Config) bool { pub fn obeyRobots(self: *const Config) bool {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.obey_robots, inline .serve, .fetch, .mcp => |opts| opts.common.obey_robots,
else => unreachable, else => unreachable,
}; };
} }
pub fn httpProxy(self: *const Config) ?[:0]const u8 { pub fn httpProxy(self: *const Config) ?[:0]const u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_proxy, inline .serve, .fetch, .mcp => |opts| opts.common.http_proxy,
else => unreachable, else => unreachable,
}; };
} }
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 { pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.proxy_bearer_token, inline .serve, .fetch, .mcp => |opts| opts.common.proxy_bearer_token,
.help, .version => null, .help, .version => null,
}; };
} }
pub fn httpMaxConcurrent(self: *const Config) u8 { pub fn httpMaxConcurrent(self: *const Config) u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_max_concurrent orelse 10, inline .serve, .fetch, .mcp => |opts| opts.common.http_max_concurrent orelse 10,
else => unreachable, else => unreachable,
}; };
} }
pub fn httpMaxHostOpen(self: *const Config) u8 { pub fn httpMaxHostOpen(self: *const Config) u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_max_host_open orelse 4, inline .serve, .fetch, .mcp => |opts| opts.common.http_max_host_open orelse 4,
else => unreachable, else => unreachable,
}; };
} }
pub fn httpConnectTimeout(self: *const Config) u31 { pub fn httpConnectTimeout(self: *const Config) u31 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_connect_timeout orelse 0, inline .serve, .fetch, .mcp => |opts| opts.common.http_connect_timeout orelse 0,
else => unreachable, else => unreachable,
}; };
} }
pub fn httpTimeout(self: *const Config) u31 { pub fn httpTimeout(self: *const Config) u31 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_timeout orelse 5000, inline .serve, .fetch, .mcp => |opts| opts.common.http_timeout orelse 5000,
else => unreachable, else => unreachable,
}; };
} }
@@ -119,35 +120,35 @@ pub fn httpMaxRedirects(_: *const Config) u8 {
pub fn httpMaxResponseSize(self: *const Config) ?usize { pub fn httpMaxResponseSize(self: *const Config) ?usize {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_max_response_size, inline .serve, .fetch, .mcp => |opts| opts.common.http_max_response_size,
else => unreachable, else => unreachable,
}; };
} }
pub fn logLevel(self: *const Config) ?log.Level { pub fn logLevel(self: *const Config) ?log.Level {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.log_level, inline .serve, .fetch, .mcp => |opts| opts.common.log_level,
else => unreachable, else => unreachable,
}; };
} }
pub fn logFormat(self: *const Config) ?log.Format { pub fn logFormat(self: *const Config) ?log.Format {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.log_format, inline .serve, .fetch, .mcp => |opts| opts.common.log_format,
else => unreachable, else => unreachable,
}; };
} }
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope { pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.log_filter_scopes, inline .serve, .fetch, .mcp => |opts| opts.common.log_filter_scopes,
else => unreachable, else => unreachable,
}; };
} }
pub fn userAgentSuffix(self: *const Config) ?[]const u8 { pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.user_agent_suffix, inline .serve, .fetch, .mcp => |opts| opts.common.user_agent_suffix,
.help, .version => null, .help, .version => null,
}; };
} }
@@ -171,6 +172,7 @@ pub const Mode = union(RunMode) {
fetch: Fetch, fetch: Fetch,
serve: Serve, serve: Serve,
version: void, version: void,
mcp: Mcp,
}; };
pub const Serve = struct { pub const Serve = struct {
@@ -182,6 +184,10 @@ pub const Serve = struct {
common: Common = .{}, common: Common = .{},
}; };
pub const Mcp = struct {
common: Common = .{},
};
pub const DumpFormat = enum { pub const DumpFormat = enum {
html, html,
markdown, markdown,
@@ -322,7 +328,7 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
const usage = const usage =
\\usage: {s} command [options] [URL] \\usage: {s} command [options] [URL]
\\ \\
\\Command can be either 'fetch', 'serve' or 'help' \\Command can be either 'fetch', 'serve', 'mcp' or 'help'
\\ \\
\\fetch command \\fetch command
\\Fetches the specified URL \\Fetches the specified URL
@@ -366,6 +372,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\ Maximum pending connections in the accept queue. \\ Maximum pending connections in the accept queue.
\\ Defaults to 128. \\ Defaults to 128.
\\ \\
++ common_options ++
\\
\\mcp command
\\Starts an MCP (Model Context Protocol) server over stdio
\\Example: {s} mcp
\\
++ common_options ++ ++ common_options ++
\\ \\
\\version command \\version command
@@ -375,7 +387,7 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Displays this message \\Displays this message
\\ \\
; ;
std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name }); std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name });
if (success) { if (success) {
return std.process.cleanExit(); return std.process.cleanExit();
} }
@@ -410,6 +422,8 @@ pub fn parseArgs(allocator: Allocator) !Config {
return init(allocator, exec_name, .{ .help = false }) }, return init(allocator, exec_name, .{ .help = false }) },
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch .fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
return init(allocator, exec_name, .{ .help = false }) }, return init(allocator, exec_name, .{ .help = false }) },
.mcp => .{ .mcp = parseMcpArgs(allocator, &args) catch
return init(allocator, exec_name, .{ .help = false }) },
.version => .{ .version = {} }, .version => .{ .version = {} },
}; };
return init(allocator, exec_name, mode); return init(allocator, exec_name, mode);
@@ -534,6 +548,24 @@ fn parseServeArgs(
return serve; return serve;
} }
fn parseMcpArgs(
allocator: Allocator,
args: *std.process.ArgIterator,
) !Mcp {
var mcp: Mcp = .{};
while (args.next()) |opt| {
if (try parseCommonArg(allocator, opt, args, &mcp.common)) {
continue;
}
log.fatal(.app, "unknown argument", .{ .mode = "mcp", .arg = opt });
return error.UnkownOption;
}
return mcp;
}
fn parseFetchArgs( fn parseFetchArgs(
allocator: Allocator, allocator: Allocator,
args: *std.process.ArgIterator, args: *std.process.ArgIterator,

View File

@@ -435,7 +435,7 @@ pub const Jar = struct {
pub fn removeExpired(self: *Jar, request_time: ?i64) void { pub fn removeExpired(self: *Jar, request_time: ?i64) void {
if (self.cookies.items.len == 0) return; if (self.cookies.items.len == 0) return;
const time = request_time orelse std.time.timestamp(); const time = request_time orelse std.time.timestamp();
var i: usize = self.cookies.items.len ; var i: usize = self.cookies.items.len;
while (i > 0) { while (i > 0) {
i -= 1; i -= 1;
const cookie = &self.cookies.items[i]; const cookie = &self.cookies.items[i];

View File

@@ -29,6 +29,11 @@ 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 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");

View File

@@ -38,6 +38,7 @@ pub const Scope = enum {
not_implemented, not_implemented,
telemetry, telemetry,
unknown_prop, unknown_prop,
mcp,
}; };
const Opts = struct { const Opts = struct {

View File

@@ -130,6 +130,17 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
return err; return err;
}; };
}, },
.mcp => {
log.info(.app, "starting MCP server", .{});
log.opts.format = .logfmt;
var mcp_server = try lp.mcp.Server.init(allocator, app);
defer mcp_server.deinit();
try mcp_server.start();
lp.mcp.router.processRequests(mcp_server);
},
else => unreachable, else => unreachable,
} }
} }

142
src/mcp/Server.zig Normal file
View File

@@ -0,0 +1,142 @@
const std = @import("std");
const App = @import("../App.zig");
const protocol = @import("protocol.zig");
const lp = @import("lightpanda");
const HttpClient = @import("../http/Client.zig");
pub const McpServer = struct {
allocator: std.mem.Allocator,
app: *App,
// Browser State
http_client: *HttpClient,
notification: *lp.Notification,
browser: *lp.Browser,
session: *lp.Session,
page: *lp.Page,
// Thread synchronization
io_thread: ?std.Thread = null,
queue_mutex: std.Thread.Mutex = .{},
queue_condition: std.Thread.Condition = .{},
message_queue: std.ArrayListUnmanaged([]const u8) = .empty,
// State
is_running: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
// Stdio
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);
self.allocator = allocator;
self.app = app;
self.message_queue = .empty;
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.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();
return self;
}
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);
// Clean up browser state
self.browser.deinit();
self.allocator.destroy(self.browser);
self.notification.deinit();
self.http_client.deinit();
self.allocator.destroy(self);
}
pub fn start(self: *Self) !void {
self.is_running.store(true, .seq_cst);
self.io_thread = try std.Thread.spawn(.{}, ioWorker, .{self});
}
pub fn stop(self: *Self) void {
self.is_running.store(false, .seq_cst);
self.queue_condition.signal();
}
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| {
std.debug.print("MCP Error: Failed to queue message: {}\n", .{err});
self.allocator.free(msg);
};
self.queue_mutex.unlock();
self.queue_condition.signal();
} else |err| {
if (err == error.EndOfStream) {
self.stop();
break;
}
std.debug.print("MCP IO Error: {}\n", .{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();
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;
}
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();
}
};

97
src/mcp/protocol.zig Normal file
View File

@@ -0,0 +1,97 @@
const std = @import("std");
pub const Request = struct {
jsonrpc: []const u8 = "2.0",
id: std.json.Value,
method: []const u8,
params: ?std.json.Value = null,
};
pub const Response = struct {
jsonrpc: []const u8 = "2.0",
id: std.json.Value,
result: ?std.json.Value = null,
@"error": ?Error = null,
};
pub const Error = struct {
code: i64,
message: []const u8,
data: ?std.json.Value = null,
};
pub const Notification = struct {
jsonrpc: []const u8 = "2.0",
method: []const u8,
params: ?std.json.Value = null,
};
// Core MCP Types mapping to official specification
pub const InitializeRequest = struct {
jsonrpc: []const u8 = "2.0",
id: std.json.Value,
method: []const u8 = "initialize",
params: InitializeParams,
};
pub const InitializeParams = struct {
protocolVersion: []const u8,
capabilities: Capabilities,
clientInfo: Implementation,
};
pub const Capabilities = struct {
experimental: ?std.json.Value = null,
roots: ?RootsCapability = null,
sampling: ?SamplingCapability = null,
};
pub const RootsCapability = struct {
listChanged: ?bool = null,
};
pub const SamplingCapability = struct {};
pub const Implementation = struct {
name: []const u8,
version: []const u8,
};
pub const InitializeResult = struct {
protocolVersion: []const u8,
capabilities: ServerCapabilities,
serverInfo: Implementation,
};
pub const ServerCapabilities = struct {
experimental: ?std.json.Value = null,
logging: ?LoggingCapability = null,
prompts: ?PromptsCapability = null,
resources: ?ResourcesCapability = null,
tools: ?ToolsCapability = null,
};
pub const LoggingCapability = struct {};
pub const PromptsCapability = struct {
listChanged: ?bool = null,
};
pub const ResourcesCapability = struct {
subscribe: ?bool = null,
listChanged: ?bool = null,
};
pub const ToolsCapability = struct {
listChanged: ?bool = null,
};
pub const Tool = struct {
name: []const u8,
description: ?[]const u8 = null,
inputSchema: std.json.Value,
};
pub const Resource = struct {
uri: []const u8,
name: []const u8,
description: ?[]const u8 = null,
mimeType: ?[]const u8 = null,
};

97
src/mcp/resources.zig Normal file
View File

@@ -0,0 +1,97 @@
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 resources = [_]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",
},
};
const result = struct {
resources: []const protocol.Resource,
}{
.resources = &resources,
};
try sendResult(server, req.id, result);
}
const ReadParams = struct {
uri: []const u8,
};
pub fn handleRead(server: *McpServer, arena: std.mem.Allocator, req: protocol.Request) !void {
if (req.params == null) {
return sendError(server, req.id, -32602, "Missing params");
}
const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{}) catch {
return sendError(server, req.id, -32602, "Invalid params");
};
if (std.mem.eql(u8, params.uri, "mcp://page/html")) {
var aw = std.Io.Writer.Allocating.init(arena);
try lp.dump.root(server.page.window._document, .{}, &aw.writer, server.page);
const contents = [_]struct {
uri: []const u8,
mimeType: []const u8,
text: []const u8,
}{.{
.uri = params.uri,
.mimeType = "text/html",
.text = aw.written(),
}};
try sendResult(server, req.id, .{ .contents = &contents });
} else if (std.mem.eql(u8, params.uri, "mcp://page/markdown")) {
var aw = std.Io.Writer.Allocating.init(arena);
try lp.markdown.dump(server.page.window._document.asNode(), .{}, &aw.writer, server.page);
const contents = [_]struct {
uri: []const u8,
mimeType: []const u8,
text: []const u8,
}{.{
.uri = params.uri,
.mimeType = "text/markdown",
.text = aw.written(),
}};
try sendResult(server, req.id, .{ .contents = &contents });
} else {
return sendError(server, req.id, -32602, "Resource not found");
}
}
pub fn sendResult(server: *McpServer, id: std.json.Value, result: anytype) !void {
const GenericResponse = struct {
jsonrpc: []const u8 = "2.0",
id: std.json.Value,
result: @TypeOf(result),
};
try server.sendResponse(GenericResponse{
.id = id,
.result = result,
});
}
pub fn sendError(server: *McpServer, id: std.json.Value, code: i64, message: []const u8) !void {
try server.sendResponse(protocol.Response{
.id = id,
.@"error" = protocol.Error{
.code = code,
.message = message,
},
});
}

82
src/mcp/router.zig Normal file
View File

@@ -0,0 +1,82 @@
const std = @import("std");
const lp = @import("lightpanda");
const McpServer = @import("Server.zig").McpServer;
const protocol = @import("protocol.zig");
const resources = @import("resources.zig");
const tools = @import("tools.zig");
const log = lp.log;
pub fn processRequests(server: *McpServer) void {
while (server.is_running.load(.seq_cst)) {
if (server.getNextMessage()) |msg| {
defer server.allocator.free(msg);
// Critical: Per-request Arena
var arena = std.heap.ArenaAllocator.init(server.allocator);
defer arena.deinit();
handleMessage(server, arena.allocator(), msg) catch |err| {
log.err(.app, "MCP Error processing message", .{ .err = err });
// We should ideally send a parse error response back, but it's hard to extract the ID if parsing failed entirely.
};
}
}
}
fn handleMessage(server: *McpServer, arena: std.mem.Allocator, msg: []const u8) !void {
const parsed = std.json.parseFromSliceLeaky(protocol.Request, arena, msg, .{
.ignore_unknown_fields = true,
}) catch |err| {
log.err(.app, "MCP JSON Parse Error", .{ .err = err, .msg = msg });
return;
};
if (std.mem.eql(u8, parsed.method, "initialize")) {
try handleInitialize(server, parsed);
} else if (std.mem.eql(u8, parsed.method, "resources/list")) {
try resources.handleList(server, parsed);
} else if (std.mem.eql(u8, parsed.method, "resources/read")) {
try resources.handleRead(server, arena, parsed);
} else if (std.mem.eql(u8, parsed.method, "tools/list")) {
try tools.handleList(server, parsed);
} else if (std.mem.eql(u8, parsed.method, "tools/call")) {
try tools.handleCall(server, arena, parsed);
} else {
try server.sendResponse(protocol.Response{
.id = parsed.id,
.@"error" = protocol.Error{
.code = -32601,
.message = "Method not found",
},
});
}
}
fn sendResponseGeneric(server: *McpServer, id: std.json.Value, result: anytype) !void {
const GenericResponse = struct {
jsonrpc: []const u8 = "2.0",
id: std.json.Value,
result: @TypeOf(result),
};
try server.sendResponse(GenericResponse{
.id = id,
.result = result,
});
}
fn handleInitialize(server: *McpServer, req: protocol.Request) !void {
const result = protocol.InitializeResult{
.protocolVersion = "2024-11-05",
.capabilities = .{
.logging = .{},
.resources = .{ .subscribe = false, .listChanged = false },
.tools = .{ .listChanged = false },
},
.serverInfo = .{
.name = "lightpanda-mcp",
.version = "0.1.0",
},
};
try sendResponseGeneric(server, req.id, result);
}

136
src/mcp/tools.zig Normal file
View File

@@ -0,0 +1,136 @@
const std = @import("std");
const McpServer = @import("Server.zig").McpServer;
const protocol = @import("protocol.zig");
const lp = @import("lightpanda");
const log = lp.log;
const js = lp.js;
pub fn handleList(server: *McpServer, req: protocol.Request) !void {
const tools = [_]protocol.Tool{
.{
.name = "navigate",
.description = "Navigate the browser to a specific URL",
.inputSchema = std.json.parseFromSliceLeaky(std.json.Value, server.allocator,
\\{
\\ "type": "object",
\\ "properties": {
\\ "url": { "type": "string" }
\\ },
\\ "required": ["url"]
\\}
, .{}) catch unreachable,
},
.{
.name = "evaluate",
.description = "Evaluate JavaScript in the current page context",
.inputSchema = std.json.parseFromSliceLeaky(std.json.Value, server.allocator,
\\{
\\ "type": "object",
\\ "properties": {
\\ "script": { "type": "string" }
\\ },
\\ "required": ["script"]
\\}
, .{}) catch unreachable,
},
};
const result = struct {
tools: []const protocol.Tool,
}{
.tools = &tools,
};
try sendResult(server, req.id, result);
}
const NavigateParams = struct {
url: []const u8,
};
const EvaluateParams = struct {
script: []const u8,
};
pub fn handleCall(server: *McpServer, arena: std.mem.Allocator, req: protocol.Request) !void {
if (req.params == null) {
return sendError(server, req.id, -32602, "Missing params");
}
const CallParams = struct {
name: []const u8,
arguments: ?std.json.Value = null,
};
const call_params = std.json.parseFromValueLeaky(CallParams, arena, req.params.?, .{}) catch {
return sendError(server, req.id, -32602, "Invalid params");
};
if (std.mem.eql(u8, call_params.name, "navigate")) {
if (call_params.arguments == null) {
return sendError(server, req.id, -32602, "Missing arguments for navigate");
}
const args = std.json.parseFromValueLeaky(NavigateParams, arena, call_params.arguments.?, .{}) catch {
return sendError(server, req.id, -32602, "Invalid arguments for navigate");
};
const url_z = try arena.dupeZ(u8, args.url);
_ = server.page.navigate(url_z, .{
.reason = .address_bar,
.kind = .{ .push = null },
}) catch {
return sendError(server, req.id, -32603, "Failed to navigate");
};
// Wait for page load (simple wait for now)
_ = server.session.wait(5000);
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Navigated successfully." }};
try sendResult(server, req.id, .{ .content = &content });
} else if (std.mem.eql(u8, call_params.name, "evaluate")) {
if (call_params.arguments == null) {
return sendError(server, req.id, -32602, "Missing arguments for evaluate");
}
const args = std.json.parseFromValueLeaky(EvaluateParams, arena, call_params.arguments.?, .{}) catch {
return sendError(server, req.id, -32602, "Invalid arguments for evaluate");
};
var ls: js.Local.Scope = undefined;
server.page.js.localScope(&ls);
defer ls.deinit();
const js_result = ls.local.compileAndRun(args.script, null) catch {
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Script evaluation failed." }};
return sendResult(server, req.id, .{ .content = &content, .isError = true });
};
const str_result = js_result.toStringSliceWithAlloc(arena) catch "undefined";
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = str_result }};
try sendResult(server, req.id, .{ .content = &content });
} else {
return sendError(server, req.id, -32601, "Tool not found");
}
}
pub fn sendResult(server: *McpServer, id: std.json.Value, result: anytype) !void {
const GenericResponse = struct {
jsonrpc: []const u8 = "2.0",
id: std.json.Value,
result: @TypeOf(result),
};
try server.sendResponse(GenericResponse{
.id = id,
.result = result,
});
}
pub fn sendError(server: *McpServer, id: std.json.Value, code: i64, message: []const u8) !void {
try server.sendResponse(protocol.Response{
.id = id,
.@"error" = protocol.Error{
.code = code,
.message = message,
},
});
}