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.
- package/cli.mjs +102 -3
- package/mcp.mjs +18 -54
- 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
|
-
|
|
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":
|