mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
Add waitForSelector MCP tool
Adds a waitForSelector tool to the MCP server that polls for a CSS selector match with a configurable timeout (default 5000ms). Returns the backendNodeId of the matched element for use with click/fill tools. The tool runs the session event loop between selector checks, so dynamically-created elements are found as they appear from JS execution or network responses.
This commit is contained in:
@@ -142,6 +142,20 @@ pub const tool_list = [_]protocol.Tool{
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "waitForSelector",
|
||||
.description = "Wait for an element matching a CSS selector to appear in the page. Returns the backend node ID of the matched element.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "selector": { "type": "string", "description": "The CSS selector to wait for." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 5000." }
|
||||
\\ },
|
||||
\\ "required": ["selector"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||
@@ -243,6 +257,7 @@ const ToolAction = enum {
|
||||
click,
|
||||
fill,
|
||||
scroll,
|
||||
waitForSelector,
|
||||
};
|
||||
|
||||
const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
@@ -257,6 +272,7 @@ const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
.{ "click", .click },
|
||||
.{ "fill", .fill },
|
||||
.{ "scroll", .scroll },
|
||||
.{ "waitForSelector", .waitForSelector },
|
||||
});
|
||||
|
||||
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||
@@ -288,6 +304,7 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
||||
.click => try handleClick(server, arena, req.id.?, call_params.arguments),
|
||||
.fill => try handleFill(server, arena, req.id.?, call_params.arguments),
|
||||
.scroll => try handleScroll(server, arena, req.id.?, call_params.arguments),
|
||||
.waitForSelector => try handleWaitForSelector(server, arena, req.id.?, call_params.arguments),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,6 +549,42 @@ fn handleScroll(server: *Server, arena: std.mem.Allocator, id: std.json.Value, a
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = "Scrolled successfully." }};
|
||||
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 {
|
||||
const WaitParams = struct {
|
||||
selector: [:0]const u8,
|
||||
timeout: ?u32 = null,
|
||||
};
|
||||
const args = try parseArguments(WaitParams, arena, arguments, server, id, "waitForSelector");
|
||||
|
||||
const page = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
const timeout_ms = args.timeout orelse 5000;
|
||||
var timer = try std.time.Timer.start();
|
||||
|
||||
while (true) {
|
||||
const element = Selector.querySelector(page.document.asNode(), args.selector, page) catch {
|
||||
return server.sendError(id, .InvalidParams, "Invalid selector");
|
||||
};
|
||||
|
||||
if (element) |el| {
|
||||
const registered = try server.node_registry.register(el.asNode());
|
||||
const msg = std.fmt.allocPrint(arena, "Element found. backendNodeId: {d}", .{registered.id}) catch "Element found.";
|
||||
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = msg }};
|
||||
return server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
if (elapsed >= timeout_ms) {
|
||||
return server.sendError(id, .InternalError, "Timeout waiting for selector");
|
||||
}
|
||||
|
||||
_ = server.session.wait(.{ .timeout_ms = @min(100, timeout_ms - elapsed) });
|
||||
}
|
||||
}
|
||||
|
||||
fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T {
|
||||
if (arguments == null) {
|
||||
try server.sendError(id, .InvalidParams, "Missing arguments");
|
||||
|
||||
Reference in New Issue
Block a user