openclaw-navigator 4.3.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.mjs +102 -3
  2. package/mcp.mjs +18 -54
  3. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -18,8 +18,8 @@ import { randomUUID } from "node:crypto";
18
18
  import { existsSync } from "node:fs";
19
19
  import { createServer } from "node:http";
20
20
  import { networkInterfaces, hostname, userInfo } from "node:os";
21
- import { fileURLToPath } from "node:url";
22
21
  import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
23
  // readline reserved for future interactive mode
24
24
 
25
25
  // ── Colors (ANSI) ──────────────────────────────────────────────────────────
@@ -659,7 +659,7 @@ ${BOLD}How it works:${RESET}
659
659
  console.log(`${BOLD}${GREEN}Bridge is running.${RESET} Waiting for Navigator to connect...`);
660
660
  console.log(`${DIM}Press Ctrl+C to stop.${RESET}\n`);
661
661
 
662
- // ── Step 6 (optional): Start MCP server ─────────────────────────────
662
+ // ── Step 6 (optional): Start MCP server + register with mcporter ────
663
663
  let mcpProcess = null;
664
664
  if (withMcp) {
665
665
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -682,7 +682,106 @@ ${BOLD}How it works:${RESET}
682
682
  });
683
683
 
684
684
  ok(`MCP server started (PID ${mcpProcess.pid}) — bridge: http://localhost:${port}`);
685
- info(` OpenClaw can connect via stdio to this process's MCP server`);
685
+
686
+ // ── Auto-register MCP server with mcporter (if available) ─────────
687
+ // This makes the 15 navigator tools discoverable by the OC agent.
688
+ // Uses --scope home so it persists in ~/.mcporter/mcporter.json.
689
+ try {
690
+ const reg = spawn(
691
+ "mcporter",
692
+ [
693
+ "config",
694
+ "add",
695
+ "navigator",
696
+ "--command",
697
+ process.execPath,
698
+ "--arg",
699
+ mcpPath,
700
+ "--env",
701
+ `NAVIGATOR_BRIDGE_URL=http://localhost:${port}`,
702
+ "--description",
703
+ "Navigator browser control (15 tools: navigate, click, fill, read, etc.)",
704
+ "--scope",
705
+ "home",
706
+ ],
707
+ { stdio: ["ignore", "pipe", "pipe"] },
708
+ );
709
+
710
+ let regOut = "";
711
+ reg.stdout?.on("data", (d) => {
712
+ regOut += d.toString();
713
+ });
714
+ reg.stderr?.on("data", (d) => {
715
+ regOut += d.toString();
716
+ });
717
+
718
+ reg.on("close", (code) => {
719
+ if (code === 0) {
720
+ ok("Registered MCP server with mcporter (15 browser tools available to OC agent)");
721
+ } else if (regOut.includes("already exists")) {
722
+ info(" mcporter: navigator server already registered");
723
+ } else {
724
+ // mcporter not installed or failed — that's fine, not required
725
+ info(" mcporter not found — OC agent can still use the MCP server directly");
726
+ }
727
+ });
728
+
729
+ reg.on("error", () => {
730
+ info(" mcporter not found — OC agent can still use the MCP server directly");
731
+ });
732
+ } catch {
733
+ // mcporter not installed — skip silently
734
+ }
735
+
736
+ // ── Auto-register with OpenClaw agent config (~/.openclaw/openclaw.json) ──
737
+ // The OC agent reads MCP servers from this file, not mcporter.
738
+ // This ensures the 15 Navigator tools show up in the agent's tool schema.
739
+ try {
740
+ const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
741
+ const { homedir } = await import("node:os");
742
+ const ocDir = join(homedir(), ".openclaw");
743
+ const ocConfigPath = join(ocDir, "openclaw.json");
744
+
745
+ let ocConfig = {};
746
+ try {
747
+ ocConfig = JSON.parse(readFileSync(ocConfigPath, "utf8"));
748
+ } catch {
749
+ // File doesn't exist or invalid — start fresh
750
+ }
751
+
752
+ // Ensure the nested structure exists
753
+ if (!ocConfig.agents) ocConfig.agents = {};
754
+ if (!ocConfig.agents.defaults) ocConfig.agents.defaults = {};
755
+ if (!ocConfig.agents.defaults.mcp) ocConfig.agents.defaults.mcp = {};
756
+ if (!ocConfig.agents.defaults.mcp.servers) ocConfig.agents.defaults.mcp.servers = {};
757
+
758
+ // Check if navigator is already configured correctly
759
+ const existing = ocConfig.agents.defaults.mcp.servers.navigator;
760
+ const desired = {
761
+ command: process.execPath,
762
+ args: [mcpPath],
763
+ env: { NAVIGATOR_BRIDGE_URL: `http://localhost:${port}` },
764
+ };
765
+
766
+ const needsUpdate =
767
+ !existing ||
768
+ existing.command !== desired.command ||
769
+ JSON.stringify(existing.args) !== JSON.stringify(desired.args);
770
+
771
+ if (needsUpdate) {
772
+ ocConfig.agents.defaults.mcp.servers.navigator = desired;
773
+ mkdirSync(ocDir, { recursive: true });
774
+ writeFileSync(ocConfigPath, JSON.stringify(ocConfig, null, 2) + "\n", "utf8");
775
+ ok("Registered Navigator MCP in ~/.openclaw/openclaw.json (15 tools → OC agent)");
776
+ info(" Restart the OC gateway to pick up the new tools");
777
+ } else {
778
+ info(" Navigator already registered in ~/.openclaw/openclaw.json");
779
+ }
780
+ } catch (err) {
781
+ warn(`Could not auto-register with OpenClaw config: ${err.message}`);
782
+ info(" Manually add to ~/.openclaw/openclaw.json → agents.defaults.mcp.servers.navigator");
783
+ }
784
+
686
785
  console.log("");
687
786
  }
688
787
 
package/mcp.mjs CHANGED
@@ -16,15 +16,11 @@
16
16
 
17
17
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
- import {
20
- ListToolsRequestSchema,
21
- CallToolRequestSchema,
22
- } from "@modelcontextprotocol/sdk/types.js";
19
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
23
20
 
24
21
  // ── Configuration ─────────────────────────────────────────────────────────
25
22
 
26
- const BRIDGE_URL =
27
- process.env.NAVIGATOR_BRIDGE_URL ?? "http://localhost:18790";
23
+ const BRIDGE_URL = process.env.NAVIGATOR_BRIDGE_URL ?? "http://localhost:18790";
28
24
  const POLL_INTERVAL_MS = 500;
29
25
  const DEFAULT_TIMEOUT_MS = 15_000;
30
26
  const WAIT_READY_TIMEOUT_MS = 20_000;
@@ -65,11 +61,7 @@ async function bridgePost(path, body) {
65
61
  * @param {number} [timeout] - max wait in ms
66
62
  * @returns {Promise<object|null>} matched event or null on timeout
67
63
  */
68
- async function waitForEvent(
69
- predicate,
70
- startTime,
71
- timeout = DEFAULT_TIMEOUT_MS,
72
- ) {
64
+ async function waitForEvent(predicate, startTime, timeout = DEFAULT_TIMEOUT_MS) {
73
65
  const deadline = Date.now() + timeout;
74
66
 
75
67
  while (Date.now() < deadline) {
@@ -128,8 +120,7 @@ async function sendCommand(command, payload, poll) {
128
120
  if (poll.eventType) {
129
121
  predicate = (e) => e.type === poll.eventType;
130
122
  } else if (poll.commandName) {
131
- predicate = (e) =>
132
- e.type === "command.result" && e.data?.command === poll.commandName;
123
+ predicate = (e) => e.type === "command.result" && e.data?.command === poll.commandName;
133
124
  } else {
134
125
  return { ok: true, commandId: postResult.commandId, command };
135
126
  }
@@ -198,8 +189,7 @@ const TOOLS = [
198
189
  },
199
190
  {
200
191
  name: "navigator_close_tab",
201
- description:
202
- "Close a browser tab by its index or ID. Returns immediately after queuing.",
192
+ description: "Close a browser tab by its index or ID. Returns immediately after queuing.",
203
193
  inputSchema: {
204
194
  type: "object",
205
195
  properties: {
@@ -212,20 +202,17 @@ const TOOLS = [
212
202
  // ── Poll by event type ──
213
203
  {
214
204
  name: "navigator_list_tabs",
215
- description:
216
- "List all open tabs in the Navigator browser with their URLs, titles, and IDs.",
205
+ description: "List all open tabs in the Navigator browser with their URLs, titles, and IDs.",
217
206
  inputSchema: { type: "object", properties: {} },
218
207
  },
219
208
  {
220
209
  name: "navigator_snapshot",
221
- description:
222
- "Get a snapshot of the browser state — active tab, URL, title, and full tab list.",
210
+ description: "Get a snapshot of the browser state — active tab, URL, title, and full tab list.",
223
211
  inputSchema: { type: "object", properties: {} },
224
212
  },
225
213
  {
226
214
  name: "navigator_get_text",
227
- description:
228
- "Get the text content of the active page (equivalent to document.body.innerText).",
215
+ description: "Get the text content of the active page (equivalent to document.body.innerText).",
229
216
  inputSchema: { type: "object", properties: {} },
230
217
  },
231
218
 
@@ -247,8 +234,7 @@ const TOOLS = [
247
234
  },
248
235
  {
249
236
  name: "navigator_fill",
250
- description:
251
- "Fill an input field with text. Waits for confirmation from the browser.",
237
+ description: "Fill an input field with text. Waits for confirmation from the browser.",
252
238
  inputSchema: {
253
239
  type: "object",
254
240
  properties: {
@@ -273,16 +259,14 @@ const TOOLS = [
273
259
  properties: {
274
260
  selector: {
275
261
  type: "string",
276
- description:
277
- "CSS selector of the form or an element inside it (submits closest form)",
262
+ description: "CSS selector of the form or an element inside it (submits closest form)",
278
263
  },
279
264
  },
280
265
  },
281
266
  },
282
267
  {
283
268
  name: "navigator_scroll",
284
- description:
285
- "Scroll the page in a direction (up/down/top/bottom) or by exact pixel offsets.",
269
+ description: "Scroll the page in a direction (up/down/top/bottom) or by exact pixel offsets.",
286
270
  inputSchema: {
287
271
  type: "object",
288
272
  properties: {
@@ -389,28 +373,18 @@ async function handleTool(name, args) {
389
373
 
390
374
  // ── Poll by event type ──
391
375
  case "navigator_list_tabs":
392
- return jsonResult(
393
- await sendCommand("tabs.list", {}, { eventType: "tabs.list" }),
394
- );
376
+ return jsonResult(await sendCommand("tabs.list", {}, { eventType: "tabs.list" }));
395
377
 
396
378
  case "navigator_snapshot":
397
- return jsonResult(
398
- await sendCommand("snapshot", {}, { eventType: "snapshot" }),
399
- );
379
+ return jsonResult(await sendCommand("snapshot", {}, { eventType: "snapshot" }));
400
380
 
401
381
  case "navigator_get_text":
402
- return jsonResult(
403
- await sendCommand("page.content", {}, { eventType: "page.content" }),
404
- );
382
+ return jsonResult(await sendCommand("page.content", {}, { eventType: "page.content" }));
405
383
 
406
384
  // ── Poll by command.result ──
407
385
  case "navigator_click":
408
386
  return jsonResult(
409
- await sendCommand(
410
- "click",
411
- { selector: args.selector },
412
- { commandName: "click" },
413
- ),
387
+ await sendCommand("click", { selector: args.selector }, { commandName: "click" }),
414
388
  );
415
389
 
416
390
  case "navigator_fill":
@@ -442,25 +416,15 @@ async function handleTool(name, args) {
442
416
 
443
417
  case "navigator_execute_js":
444
418
  return jsonResult(
445
- await sendCommand(
446
- "execute",
447
- { code: args.code },
448
- { commandName: "execute" },
449
- ),
419
+ await sendCommand("execute", { code: args.code }, { commandName: "execute" }),
450
420
  );
451
421
 
452
422
  case "navigator_get_html":
453
- return jsonResult(
454
- await sendCommand("page.html", {}, { commandName: "page.html" }),
455
- );
423
+ return jsonResult(await sendCommand("page.html", {}, { commandName: "page.html" }));
456
424
 
457
425
  case "navigator_query_element":
458
426
  return jsonResult(
459
- await sendCommand(
460
- "dom.query",
461
- { selector: args.selector },
462
- { commandName: "dom.query" },
463
- ),
427
+ await sendCommand("dom.query", { selector: args.selector }, { commandName: "dom.query" }),
464
428
  );
465
429
 
466
430
  case "navigator_wait_ready":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",