From 87a06907761373012375c0d8937032f42b82e5bb Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:32:32 -0700 Subject: [PATCH 1/2] mcp: return page state from click/fill/scroll tools After click, fill, and scroll actions, return the current page URL and title instead of static success messages. This gives AI agents immediate feedback about the page state after an action, matching the pattern already used by waitForSelector. Co-Authored-By: Claude Opus 4.6 --- src/mcp/tools.zig | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 1e286209..17d01747 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -103,7 +103,7 @@ pub const tool_list = [_]protocol.Tool{ }, .{ .name = "click", - .description = "Click on an interactive element.", + .description = "Click on an interactive element. Returns the current page URL and title after the click.", .inputSchema = protocol.minify( \\{ \\ "type": "object", @@ -116,7 +116,7 @@ pub const tool_list = [_]protocol.Tool{ }, .{ .name = "fill", - .description = "Fill text into an input element.", + .description = "Fill text into an input element. Returns the filled value and current page URL and title.", .inputSchema = protocol.minify( \\{ \\ "type": "object", @@ -130,7 +130,7 @@ pub const tool_list = [_]protocol.Tool{ }, .{ .name = "scroll", - .description = "Scroll the page or a specific element.", + .description = "Scroll the page or a specific element. Returns the scroll position and current page URL and title.", .inputSchema = protocol.minify( \\{ \\ "type": "object", @@ -489,7 +489,13 @@ fn handleClick(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar return server.sendError(id, .InternalError, "Failed to click element"); }; - const content = [_]protocol.TextContent([]const u8){.{ .text = "Clicked successfully." }}; + const page_title = page.getTitle() catch null; + const result_text = try std.fmt.allocPrint(arena, "Clicked element (backendNodeId: {d}). Page url: {s}, title: {s}", .{ + args.backendNodeId, + page.url, + page_title orelse "(none)", + }); + const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }}; try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } @@ -515,7 +521,14 @@ fn handleFill(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arg return server.sendError(id, .InternalError, "Failed to fill element"); }; - const content = [_]protocol.TextContent([]const u8){.{ .text = "Filled successfully." }}; + const page_title = page.getTitle() catch null; + const result_text = try std.fmt.allocPrint(arena, "Filled element (backendNodeId: {d}) with \"{s}\". Page url: {s}, title: {s}", .{ + args.backendNodeId, + args.text, + page.url, + page_title orelse "(none)", + }); + const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }}; try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } @@ -546,7 +559,14 @@ fn handleScroll(server: *Server, arena: std.mem.Allocator, id: std.json.Value, a return server.sendError(id, .InternalError, "Failed to scroll"); }; - const content = [_]protocol.TextContent([]const u8){.{ .text = "Scrolled successfully." }}; + const page_title = page.getTitle() catch null; + const result_text = try std.fmt.allocPrint(arena, "Scrolled to x: {d}, y: {d}. Page url: {s}, title: {s}", .{ + args.x orelse 0, + args.y orelse 0, + page.url, + page_title orelse "(none)", + }); + const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }}; try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content }); } fn handleWaitForSelector(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { From 9e7f0b4776ad3b3ac79acade0c0ac6a744901b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Sun, 22 Mar 2026 20:39:20 +0900 Subject: [PATCH 2/2] test: verify feedback message content in click/fill/scroll MCP tools --- src/mcp/tools.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 17d01747..e6669325 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -705,6 +705,9 @@ test "MCP - Actions: click, fill, scroll" { const btn_id_str = std.fmt.bufPrint(&btn_id_buf, "{d}", .{btn_id}) catch unreachable; const click_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"click\",\"arguments\":{\"backendNodeId\":", btn_id_str, "}}}" }); try router.handleMessage(server, aa, click_msg); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Clicked element") != null); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Page url: http://localhost:9582/src/browser/tests/mcp_actions.html") != null); + out_alloc.clearRetainingCapacity(); // Test Fill Input const inp = page.document.getElementById("inp", page).?.asNode(); @@ -713,6 +716,9 @@ test "MCP - Actions: click, fill, scroll" { const inp_id_str = std.fmt.bufPrint(&inp_id_buf, "{d}", .{inp_id}) catch unreachable; const fill_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", inp_id_str, ",\"text\":\"hello\"}}}" }); try router.handleMessage(server, aa, fill_msg); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Filled element") != null); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "with \\\"hello\\\"") != null); + out_alloc.clearRetainingCapacity(); // Test Fill Select const sel = page.document.getElementById("sel", page).?.asNode(); @@ -721,6 +727,9 @@ test "MCP - Actions: click, fill, scroll" { const sel_id_str = std.fmt.bufPrint(&sel_id_buf, "{d}", .{sel_id}) catch unreachable; const fill_sel_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", sel_id_str, ",\"text\":\"opt2\"}}}" }); try router.handleMessage(server, aa, fill_sel_msg); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Filled element") != null); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "with \\\"opt2\\\"") != null); + out_alloc.clearRetainingCapacity(); // Test Scroll const scrollbox = page.document.getElementById("scrollbox", page).?.asNode(); @@ -729,6 +738,8 @@ test "MCP - Actions: click, fill, scroll" { const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, "{d}", .{scrollbox_id}) catch unreachable; const scroll_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"scroll\",\"arguments\":{\"backendNodeId\":", scroll_id_str, ",\"y\":50}}}" }); try router.handleMessage(server, aa, scroll_msg); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "Scrolled to x: 0, y: 50") != null); + out_alloc.clearRetainingCapacity(); // Evaluate assertions var ls: js.Local.Scope = undefined;