tools: remove aliases, normalize names to camelCase, simplify dispatch

This commit is contained in:
Adrià Arrufat
2026-04-09 16:19:16 +02:00
parent 286976a7c7
commit 91e61905cf
2 changed files with 41 additions and 87 deletions

View File

@@ -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,

View File

@@ -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')"
\\ }