diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 58c530a7..ba86dc2b 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -42,18 +42,6 @@ fn minify(comptime json: []const u8) []const u8 { }; } -const goto_schema = minify( - \\{ - \\ "type": "object", - \\ "properties": { - \\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." }, - \\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }, - \\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." } - \\ }, - \\ "required": ["url"] - \\} -); - const url_params_schema = minify( \\{ \\ "type": "object", @@ -65,29 +53,21 @@ const url_params_schema = minify( \\} ); -const evaluate_schema = minify( - \\{ - \\ "type": "object", - \\ "properties": { - \\ "script": { "type": "string" }, - \\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." }, - \\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }, - \\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." } - \\ }, - \\ "required": ["script"] - \\} -); - pub const tool_defs = [_]ToolDef{ .{ .name = "goto", .description = "Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.", - .input_schema = goto_schema, - }, - .{ - .name = "navigate", - .description = "Alias for goto. Navigate to a specified URL and load the page in memory.", - .input_schema = goto_schema, + .input_schema = minify( + \\{ + \\ "type": "object", + \\ "properties": { + \\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." }, + \\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }, + \\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." } + \\ }, + \\ "required": ["url"] + \\} + ), }, .{ .name = "markdown", @@ -99,18 +79,24 @@ pub const tool_defs = [_]ToolDef{ .description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.", .input_schema = url_params_schema, }, - .{ - .name = "evaluate", - .description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.", - .input_schema = evaluate_schema, - }, .{ .name = "eval", - .description = "Alias for evaluate. Evaluate JavaScript in the current page context.", - .input_schema = evaluate_schema, + .description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.", + .input_schema = minify( + \\{ + \\ "type": "object", + \\ "properties": { + \\ "script": { "type": "string" }, + \\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." }, + \\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }, + \\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." } + \\ }, + \\ "required": ["script"] + \\} + ), }, .{ - .name = "semantic_tree", + .name = "semanticTree", .description = "Get the page content as a simplified semantic DOM tree for AI reasoning. If a url is provided, it navigates to that url first.", .input_schema = minify( \\{ @@ -321,7 +307,7 @@ pub const ToolError = error{ InternalError, }; -/// Result from evaluate that may represent a JS error (not a tool failure). +/// Result from eval that may represent a JS error (not a tool failure). pub const EvalResult = struct { text: []const u8, is_error: bool = false, @@ -345,16 +331,14 @@ const NodeAndPage = struct { node: *DOMNode, page: *lp.Page }; const Action = enum { goto, - navigate, markdown, links, nodeDetails, interactiveElements, structuredData, detectForms, - evaluate, eval, - semantic_tree, + semanticTree, click, fill, scroll, @@ -370,35 +354,8 @@ const Action = enum { getCookies, }; -const action_map = std.StaticStringMap(Action).initComptime(.{ - .{ "goto", .goto }, - .{ "navigate", .navigate }, - .{ "markdown", .markdown }, - .{ "links", .links }, - .{ "nodeDetails", .nodeDetails }, - .{ "interactiveElements", .interactiveElements }, - .{ "structuredData", .structuredData }, - .{ "detectForms", .detectForms }, - .{ "evaluate", .evaluate }, - .{ "eval", .eval }, - .{ "semantic_tree", .semantic_tree }, - .{ "click", .click }, - .{ "fill", .fill }, - .{ "scroll", .scroll }, - .{ "waitForSelector", .waitForSelector }, - .{ "hover", .hover }, - .{ "press", .press }, - .{ "selectOption", .selectOption }, - .{ "setChecked", .setChecked }, - .{ "findElement", .findElement }, - .{ "getEnv", .getEnv }, - .{ "consoleLogs", .consoleLogs }, - .{ "getUrl", .getUrl }, - .{ "getCookies", .getCookies }, -}); - /// Execute a tool by name. Returns the result text. -/// For `evaluate`/`eval`, use `callEval` to distinguish JS errors from tool errors. +/// For `eval`, use `callEval` to distinguish JS errors from tool errors. pub fn call( session: *lp.Session, registry: *CDPNode.Registry, @@ -406,21 +363,18 @@ pub fn call( tool_name: []const u8, arguments: ?std.json.Value, ) ToolError![]const u8 { - const action = action_map.get(tool_name) orelse return ToolError.InvalidParams; + const action = std.meta.stringToEnum(Action, tool_name) orelse return ToolError.InvalidParams; return switch (action) { - .goto, .navigate => execGoto(session, registry, arena, arguments), + .goto => execGoto(session, registry, arena, arguments), .markdown => execMarkdown(session, registry, arena, arguments), .links => execLinks(session, registry, arena, arguments), .nodeDetails => execNodeDetails(session, registry, arena, arguments), .interactiveElements => execInteractiveElements(session, registry, arena, arguments), .structuredData => execStructuredData(session, registry, arena, arguments), .detectForms => execDetectForms(session, registry, arena, arguments), - .evaluate, .eval => blk: { - const result = execEvaluate(session, registry, arena, arguments); - break :blk result.text; - }, - .semantic_tree => execSemanticTree(session, registry, arena, arguments), + .eval => execEval(session, registry, arena, arguments).text, + .semanticTree => execSemanticTree(session, registry, arena, arguments), .click => execClick(session, registry, arena, arguments), .fill => execFill(session, registry, arena, arguments), .scroll => execScroll(session, registry, arena, arguments), @@ -437,19 +391,19 @@ pub fn call( }; } -/// Like `call`, but for evaluate/eval returns the full EvalResult with is_error flag. +/// Like `call`, but for eval returns the full EvalResult with is_error flag. pub fn callEval( session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.Allocator, arguments: ?std.json.Value, ) EvalResult { - return execEvaluate(session, registry, arena, arguments); + return execEval(session, registry, arena, arguments); } /// Check if a tool name is recognized. pub fn isKnownTool(tool_name: []const u8) bool { - return action_map.get(tool_name) != null; + return std.meta.stringToEnum(Action, tool_name) != null; } // --- Tool implementations --- @@ -574,7 +528,7 @@ fn execDetectForms(session: *lp.Session, registry: *CDPNode.Registry, arena: std return aw.written(); } -fn execEvaluate(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.Allocator, arguments: ?std.json.Value) EvalResult { +fn execEval(session: *lp.Session, registry: *CDPNode.Registry, arena: std.mem.Allocator, arguments: ?std.json.Value) EvalResult { const Params = struct { script: [:0]const u8, url: ?[:0]const u8 = null, diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index bf745a5c..573a7cf8 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -46,8 +46,8 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque return server.sendError(id, .MethodNotFound, "Tool not found"); } - // Special handling for evaluate/eval: JS errors are returned as isError results, not protocol errors - if (std.mem.eql(u8, call_params.name, "evaluate") or std.mem.eql(u8, call_params.name, "eval")) { + // Special handling for eval: JS errors are returned as isError results, not protocol errors + if (std.mem.eql(u8, call_params.name, "eval")) { const result = browser_tools.callEval(server.session, &server.node_registry, arena, call_params.arguments); const content = [_]protocol.TextContent([]const u8){.{ .text = result.text }}; return server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content, .isError = result.is_error }); @@ -70,20 +70,20 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque const router = @import("router.zig"); const testing = @import("../testing.zig"); -test "MCP - evaluate error reporting" { +test "MCP - eval error reporting" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); - // Call evaluate with a script that throws an error + // Call eval with a script that throws an error const msg = \\{ \\ "jsonrpc": "2.0", \\ "id": 1, \\ "method": "tools/call", \\ "params": { - \\ "name": "evaluate", + \\ "name": "eval", \\ "arguments": { \\ "script": "throw new Error('test error')" \\ }