From bd861f5ac0a61d4686e6294d4ce1dc3f6a36bcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Sun, 24 May 2026 11:13:56 +0200 Subject: [PATCH] mcp: rename tools to camelCase --- docs/agent-tutorial.md | 12 ++++----- docs/agent.md | 38 ++++++++++++++++----------- src/mcp/Server.zig | 4 +-- src/mcp/tools.zig | 58 +++++++++++++++++++++--------------------- src/script.zig | 2 +- src/script/command.zig | 2 +- 6 files changed, 62 insertions(+), 54 deletions(-) diff --git a/docs/agent-tutorial.md b/docs/agent-tutorial.md index a09c8cc2b..97b1be6ff 100644 --- a/docs/agent-tutorial.md +++ b/docs/agent-tutorial.md @@ -406,16 +406,16 @@ Register the server with your MCP client: From the external agent, call: -1. `record_start { "path": "hn.lp" }` — begins appending state-mutating +1. `recordStart { "path": "hn.lp" }` — begins appending state-mutating tool calls to `hn.lp`. The path must be relative and free of `..`. 2. The same browser tools you'd call anyway: `goto`, `fill`, `click`, `waitForSelector`. Each one that succeeds is appended verbatim; query-only tools (`tree`, `markdown`, `findElement`, `consoleLogs`) are never recorded. -3. `record_comment { "text": "logged in" }` — drop a breadcrumb above +3. `recordComment { "text": "logged in" }` — drop a breadcrumb above the next recorded line. Useful for marking the boundary between LLM-driven phases. -4. `record_stop {}` — closes the recording and returns +4. `recordStop {}` — closes the recording and returns `{path, line_count}`. The output file is byte-equivalent to what `-i hn.lp` produced in @@ -431,13 +431,13 @@ MCP doesn't carry a `--self-heal` flag — self-heal is a two-tool roundtrip the calling agent orchestrates: 1. Read the script. For each non-blank, non-comment line, call - `script_step { "line": "" }`. Comments and blanks are no-ops + `scriptStep { "line": "" }`. Comments and blanks are no-ops on the Lightpanda side. 2. On `isError: true`, the structured error message tells you what failed. Hand the current page state and the failing line to your own LLM; have it return a replacement PandaScript line (or several). -3. Call `script_heal { "path": "...", "replacements": +3. Call `scriptHeal { "path": "...", "replacements": [{ "original_line": "...", "replacement_lines": ["..."] }] }`. Each `original_line` must match verbatim. Lightpanda writes `.bak` first, then atomically rewrites the file with the @@ -445,7 +445,7 @@ roundtrip the calling agent orchestrates: replacement — same format as section 6. 4. Continue from the next line. -`script_step` deliberately does *not* auto-record: the script is +`scriptStep` deliberately does *not* auto-record: the script is already the source of truth during replay, so double-recording would diverge the file from itself. `/login`, `/acceptCookies`, and any line that isn't a slash command are rejected — those need an LLM, which is diff --git a/docs/agent.md b/docs/agent.md index c71c64be9..fdc81a3fd 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -107,7 +107,7 @@ Both require an LLM. `--no-llm` rejects them. In the REPL (and only the REPL), a line that isn't a slash command and doesn't start with `#` is sent to the LLM as a natural-language prompt. In -`.lp` scripts and through MCP `script_step`, the same input is a parse +`.lp` scripts and through MCP `scriptStep`, the same input is a parse error. To leave the REPL, use the `/quit` meta command. ### Example script @@ -224,6 +224,14 @@ browser. No `--provider` or API key is required on the Lightpanda side. } ``` +Tool names are camelCase and case-sensitive — there are no aliases. +Earlier names (`navigate`, `evaluate`, `semantic_tree`, `semanticTree`, +`record_start`, `record_stop`, `record_comment`, `script_step`, +`script_heal`) have been removed; existing MCP clients and saved +prompts must call the canonical tags (`goto`, `eval`, `tree`, +`recordStart`, `recordStop`, `recordComment`, `scriptStep`, +`scriptHeal`). + For sub-task delegation in the other direction — calling Lightpanda's own LLM-driven agent in a one-shot fashion — use `--task` on stdin instead. @@ -233,11 +241,11 @@ instead. `lightpanda mcp` exposes three recording tools so an external agent can capture a session as a `.lp` script for later deterministic replay: -| Tool | Args | Effect | -|------------------|-----------------------|-------------------------------------------------------------------------------------------------| -| `record_start` | `{ path: string }` | Begin appending state-mutating tool calls to `path` (relative, no `..`). Errors if already on. | -| `record_stop` | `{}` | Close the recording and return `{path, line_count}`. Errors if no recording is active. | -| `record_comment` | `{ text: string }` | Write `# ` to the active recording — useful as a breadcrumb above LLM-driven steps. | +| Tool | Args | Effect | +|-----------------|-----------------------|------------------------------------------------------------------------------------------------| +| `recordStart` | `{ path: string }` | Begin appending state-mutating tool calls to `path` (relative, no `..`). Errors if already on. | +| `recordStop` | `{}` | Close the recording and return `{path, line_count}`. Errors if no recording is active. | +| `recordComment` | `{ text: string }` | Write `# ` to the active recording — useful as a breadcrumb above LLM-driven steps. | While recording is active, every `goto` / `click` / `fill` / `scroll` / `hover` / `selectOption` / `setChecked` / `waitForSelector` / `eval` @@ -252,19 +260,19 @@ Self-heal is a two-tool roundtrip: lightpanda runs steps and reports structured failures, the calling agent synthesizes a replacement, and lightpanda atomically rewrites the script. -| Tool | Args | Effect | -|---------------|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| `script_step` | `{ line: string }` | Parse one PandaScript line and run it on the current session. Comments and blank lines are no-ops. Returns `isError: true` with a structured message on failure. | -| `script_heal` | `{ path: string, replacements: [{original_line, replacement_lines}] }` | Atomically rewrite the script in place. A `.bak` of the original is written first; each `original_line` must match verbatim. The first replacement gets a `# [Auto-healed] Original: …` header. | +| Tool | Args | Effect | +|--------------|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `scriptStep` | `{ line: string }` | Parse one PandaScript line and run it on the current session. Comments and blank lines are no-ops. Returns `isError: true` with a structured message on failure. | +| `scriptHeal` | `{ path: string, replacements: [{original_line, replacement_lines}] }` | Atomically rewrite the script in place. A `.bak` of the original is written first; each `original_line` must match verbatim. The first replacement gets a `# [Auto-healed] Original: …` header. | Typical loop on the caller side: read the script, walk lines, call -`script_step` per line, on failure ask the caller's LLM for a -replacement, call `script_heal` with the patch, then continue. Lines -executed via `script_step` are intentionally NOT auto-recorded — replay +`scriptStep` per line, on failure ask the caller's LLM for a +replacement, call `scriptHeal` with the patch, then continue. Lines +executed via `scriptStep` are intentionally NOT auto-recorded — replay shouldn't double-record. `/login`, `/acceptCookies`, and anything that isn't a slash command are -rejected by `script_step`: those require an LLM and belong to the calling +rejected by `scriptStep`: those require an LLM and belong to the calling agent. ## Browser tools @@ -306,7 +314,7 @@ for the LLM. — including natural-language context that accompanies a `/login` — lands in that file. Delete it or move out of sensitive directories if you don't want it retained. -- `record_start` and `script_heal` reject empty, absolute, and `..` +- `recordStart` and `scriptHeal` reject empty, absolute, and `..` paths, but do **not** follow-up on symlinks. On a shared filesystem, a pre-existing symlink at the recording target would be written through to whatever it points at. Prefer a fresh directory you own diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index ef120baf7..5cfb0322e 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -26,8 +26,8 @@ verifier: Verifier, transport: Transport, -/// Optional PandaScript recorder. Activated by the `record_start` tool; -/// cleared by `record_stop`. State-mutating browser tool calls are +/// Optional PandaScript recorder. Activated by the `recordStart` tool; +/// cleared by `recordStop`. State-mutating browser tool calls are /// serialized into the active recorder via `Command.fromToolCall`. recorder: ?Recorder = null, diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 59fc8fcdd..53c12190e 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -89,28 +89,28 @@ const script_heal_schema = browser_tools.minify( const extra_tools = [_]McpTool{ .{ - .name = "record_start", + .name = "recordStart", .description = "Start recording state-mutating browser tool calls into a PandaScript file. Subsequent calls to `goto`, `click`, `fill`, `scroll`, `hover`, `selectOption`, `setChecked`, `waitForSelector`, `eval`, and `extract` get appended as PandaScript lines. Query-only tools (tree, markdown, links, findElement, …) are not recorded.", .inputSchema = record_start_schema, }, .{ - .name = "record_stop", + .name = "recordStop", .description = "Stop the active recording and return the path and number of lines written. Errors if no recording is active.", .inputSchema = record_stop_schema, }, .{ - .name = "record_comment", + .name = "recordComment", .description = "Append a `# ` comment line to the active recording. Useful as a breadcrumb above LLM-driven steps.", .inputSchema = record_comment_schema, }, .{ - .name = "script_step", + .name = "scriptStep", .description = "Parse and execute one PandaScript line on the current browser session. Returns success or a structured failure descriptor (failed line, page URL, error reason) so the calling agent can synthesize a heal step. Comments and blank lines are accepted as no-ops.", .inputSchema = script_step_schema, }, .{ - .name = "script_heal", - .description = "Atomically rewrite a .lp script with in-place line replacements. A `.bak` of the original is written first. Designed for the script_step → fail → script_heal roundtrip where the calling agent owns the LLM that synthesizes replacements.", + .name = "scriptHeal", + .description = "Atomically rewrite a .lp script with in-place line replacements. A `.bak` of the original is written first. Designed for the scriptStep → fail → scriptHeal roundtrip where the calling agent owns the LLM that synthesizes replacements.", .inputSchema = script_heal_schema, }, }; @@ -119,11 +119,11 @@ const all_tools = browser_tool_list ++ extra_tools; /// Tools that bypass the browser-tool dispatch and have their own handlers. const ExtraTool = enum { - record_start, - record_stop, - record_comment, - script_step, - script_heal, + recordStart, + recordStop, + recordComment, + scriptStep, + scriptHeal, }; pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { @@ -142,11 +142,11 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque if (std.meta.stringToEnum(ExtraTool, call_params.name)) |tool| { return switch (tool) { - .record_start => handleRecordStart(server, arena, id, call_params.arguments), - .record_stop => handleRecordStop(server, arena, id), - .record_comment => handleRecordComment(server, arena, id, call_params.arguments), - .script_step => handleScriptStep(server, arena, id, call_params.arguments), - .script_heal => handleScriptHeal(server, arena, id, call_params.arguments), + .recordStart => handleRecordStart(server, arena, id, call_params.arguments), + .recordStop => handleRecordStop(server, arena, id), + .recordComment => handleRecordComment(server, arena, id, call_params.arguments), + .scriptStep => handleScriptStep(server, arena, id, call_params.arguments), + .scriptHeal => handleScriptHeal(server, arena, id, call_params.arguments), }; } @@ -196,7 +196,7 @@ fn recordIfActive(server: *Server, tool: BrowserTool, arguments: ?std.json.Value fn handleRecordStart(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { if (server.recorder != null) { - return sendErrorContent(server, id, "a recording is already active; call record_stop first"); + return sendErrorContent(server, id, "a recording is already active; call recordStop first"); } const Args = struct { path: []const u8 }; const args = browser_tools.parseArgs(Args, arena, arguments) catch { @@ -546,59 +546,59 @@ test "MCP - indexLines: CRLF line endings still match plain LLM keys" { try std.testing.expectEqualStrings("/click selector='old'\r\n", entry.span); } -test "MCP - record_start rejects unsafe path" { +test "MCP - recordStart rejects unsafe path" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); const msg = - \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"record_start","arguments":{"path":"../escape.lp"}}} + \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"recordStart","arguments":{"path":"../escape.lp"}}} ; try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "must be relative") != null); } -test "MCP - record_stop without active recording errors" { +test "MCP - recordStop without active recording errors" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); const msg = - \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"record_stop","arguments":{}}} + \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"recordStop","arguments":{}}} ; try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "no recording is active") != null); } -test "MCP - script_step rejects /login (LLM-required)" { +test "MCP - scriptStep rejects /login (LLM-required)" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); const msg = - \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"script_step","arguments":{"line":"/login"}}} + \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"scriptStep","arguments":{"line":"/login"}}} ; try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "require an LLM") != null); } -test "MCP - script_step rejects bare prose" { +test "MCP - scriptStep rejects bare prose" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); const msg = - \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"script_step","arguments":{"line":"please summarize this page"}}} + \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"scriptStep","arguments":{"line":"please summarize this page"}}} ; try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "could not parse step") != null); } -test "MCP - script_step runs /fill and verifier passes" { +test "MCP - scriptStep runs /fill and verifier passes" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); @@ -607,21 +607,21 @@ test "MCP - script_step runs /fill and verifier passes" { // /fill on the input that exists on the test page; verifier checks // the field's `value` property after execution. const msg = - \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"script_step","arguments":{"line":"/fill selector='#inp' value='hello world'"}}} + \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"scriptStep","arguments":{"line":"/fill selector='#inp' value='hello world'"}}} ; try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "\"isError\":true") == null); try testing.expect(std.mem.indexOf(u8, out.written(), "verification failed") == null); } -test "MCP - script_step accepts comment line" { +test "MCP - scriptStep accepts comment line" { defer testing.reset(); var out: std.io.Writer.Allocating = .init(testing.arena_allocator); const server = try testLoadPage("about:blank", &out.writer); defer server.deinit(); const msg = - \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"script_step","arguments":{"line":"# fetch the homepage"}}} + \\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"scriptStep","arguments":{"line":"# fetch the homepage"}}} ; try router.handleMessage(server, testing.arena_allocator, msg); try testing.expect(std.mem.indexOf(u8, out.written(), "\"isError\":true") == null); diff --git a/src/script.zig b/src/script.zig index 8dc16b852..cb04ed006 100644 --- a/src/script.zig +++ b/src/script.zig @@ -192,7 +192,7 @@ pub fn writeAtomic( } /// Replacement body: either parsed Commands (agent self-heal) or pre-rendered -/// lines (MCP `script_heal`, where the LLM driver supplies raw PandaScript). +/// lines (MCP `scriptHeal`, where the LLM driver supplies raw PandaScript). pub const HealBody = union(enum) { cmds: []const Command, lines: []const []const u8, diff --git a/src/script/command.zig b/src/script/command.zig index 370e7775e..b25a5c85e 100644 --- a/src/script/command.zig +++ b/src/script/command.zig @@ -99,7 +99,7 @@ pub const Command = union(enum) { } // Iterate the schema (not the ObjectMap) so the line order is - // stable across providers — MCP script_heal looks lines up + // stable across providers — MCP scriptHeal looks lines up // verbatim. for (s.fields) |f| { const v = args.get(f.name) orelse continue;