From 5ec4305a9fd34fe328419239b22732271fc75b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Fri, 27 Feb 2026 22:17:15 +0900 Subject: [PATCH] mcp: add optional url parameter to tools --- src/mcp/Server.zig | 2 +- src/mcp/resources.zig | 2 +- src/mcp/tools.zig | 77 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index f3fa026a..1f984950 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -96,7 +96,7 @@ pub const McpServer = struct { self.queue_mutex.lock(); self.message_queue.append(self.allocator, msg) catch |err| { - lp.log.err(.app, "MCP Error: Failed to queue message", .{ .err = err }); + lp.log.err(.app, "MCP Queue failed", .{ .err = err }); self.allocator.free(msg); }; self.queue_mutex.unlock(); diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index d5ff2d71..e50028b1 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -43,7 +43,7 @@ pub fn handleRead(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re if (std.mem.eql(u8, params.uri, "mcp://page/html")) { var aw = std.Io.Writer.Allocating.init(arena); - lp.dump.root(server.page.document.asNode(), .{}, &aw.writer, server.page) catch { + lp.dump.root(server.page.document, .{}, &aw.writer, server.page) catch { return sendError(server, req.id.?, -32603, "Internal error reading HTML"); }; diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index ec4f6be2..768fc71b 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -40,22 +40,37 @@ pub fn handleList(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re }, .{ .name = "markdown", - .description = "Get the page content in markdown format.", - .inputSchema = std.json.parseFromSliceLeaky(std.json.Value, arena, "{\"type\":\"object\",\"properties\":{}}", .{}) catch unreachable, - }, - .{ - .name = "links", - .description = "Extract all links in the opened page", - .inputSchema = std.json.parseFromSliceLeaky(std.json.Value, arena, "{\"type\":\"object\",\"properties\":{}}", .{}) catch unreachable, - }, - .{ - .name = "evaluate", - .description = "Evaluate JavaScript in the current page context", + .description = "Get the page content in markdown format. If a url is provided, it navigates to that url first.", .inputSchema = std.json.parseFromSliceLeaky(std.json.Value, arena, \\{ \\ "type": "object", \\ "properties": { - \\ "script": { "type": "string" } + \\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching markdown." } + \\ } + \\} + , .{}) catch unreachable, + }, + .{ + .name = "links", + .description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.", + .inputSchema = std.json.parseFromSliceLeaky(std.json.Value, arena, + \\{ + \\ "type": "object", + \\ "properties": { + \\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting links." } + \\ } + \\} + , .{}) catch unreachable, + }, + .{ + .name = "evaluate", + .description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.", + .inputSchema = std.json.parseFromSliceLeaky(std.json.Value, arena, + \\{ + \\ "type": "object", + \\ "properties": { + \\ "script": { "type": "string" }, + \\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." } \\ }, \\ "required": ["script"] \\} @@ -153,6 +168,18 @@ pub fn handleCall(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Search performed successfully." }}; try sendResult(server, req.id.?, .{ .content = &content }); } else if (std.mem.eql(u8, call_params.name, "markdown")) { + const MarkdownParams = struct { + url: ?[]const u8 = null, + }; + if (call_params.arguments) |args_raw| { + if (std.json.parseFromValueLeaky(MarkdownParams, arena, args_raw, .{})) |args| { + if (args.url) |u| { + performGoto(server, arena, u) catch { + return sendError(server, req.id.?, -32603, "Internal error during navigation"); + }; + } + } else |_| {} + } var aw = std.Io.Writer.Allocating.init(arena); lp.markdown.dump(server.page.document.asNode(), .{}, &aw.writer, server.page) catch { return sendError(server, req.id.?, -32603, "Internal error parsing markdown"); @@ -161,6 +188,18 @@ pub fn handleCall(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = aw.written() }}; try sendResult(server, req.id.?, .{ .content = &content }); } else if (std.mem.eql(u8, call_params.name, "links")) { + const LinksParams = struct { + url: ?[]const u8 = null, + }; + if (call_params.arguments) |args_raw| { + if (std.json.parseFromValueLeaky(LinksParams, arena, args_raw, .{})) |args| { + if (args.url) |u| { + performGoto(server, arena, u) catch { + return sendError(server, req.id.?, -32603, "Internal error during navigation"); + }; + } + } else |_| {} + } const list = Selector.querySelectorAll(server.page.document.asNode(), "a[href]", server.page) catch { return sendError(server, req.id.?, -32603, "Internal error querying selector"); }; @@ -183,10 +222,22 @@ pub fn handleCall(server: *McpServer, arena: std.mem.Allocator, req: protocol.Re 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 { + + const EvaluateParamsEx = struct { + script: []const u8, + url: ?[]const u8 = null, + }; + + const args = std.json.parseFromValueLeaky(EvaluateParamsEx, arena, call_params.arguments.?, .{}) catch { return sendError(server, req.id.?, -32602, "Invalid arguments for evaluate"); }; + if (args.url) |url| { + performGoto(server, arena, url) catch { + return sendError(server, req.id.?, -32603, "Internal error during navigation"); + }; + } + var ls: js.Local.Scope = undefined; server.page.js.localScope(&ls); defer ls.deinit();