openclaw-navigator 4.2.0 → 4.3.0
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 +38 -0
- package/mcp.mjs +507 -0
- package/package.json +6 -3
package/cli.mjs
CHANGED
|
@@ -18,6 +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
|
+
import { dirname, join } from "node:path";
|
|
21
23
|
// readline reserved for future interactive mode
|
|
22
24
|
|
|
23
25
|
// ── Colors (ANSI) ──────────────────────────────────────────────────────────
|
|
@@ -506,6 +508,7 @@ async function main() {
|
|
|
506
508
|
let port = 18790;
|
|
507
509
|
let bindHost = "127.0.0.1";
|
|
508
510
|
let noTunnel = false;
|
|
511
|
+
let withMcp = false;
|
|
509
512
|
|
|
510
513
|
for (let i = 0; i < args.length; i++) {
|
|
511
514
|
if (args[i] === "--port" && args[i + 1]) {
|
|
@@ -517,17 +520,22 @@ async function main() {
|
|
|
517
520
|
if (args[i] === "--no-tunnel") {
|
|
518
521
|
noTunnel = true;
|
|
519
522
|
}
|
|
523
|
+
if (args[i] === "--mcp") {
|
|
524
|
+
withMcp = true;
|
|
525
|
+
}
|
|
520
526
|
if (args[i] === "--help" || args[i] === "-h") {
|
|
521
527
|
console.log(`
|
|
522
528
|
${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
|
|
523
529
|
|
|
524
530
|
${BOLD}Usage:${RESET}
|
|
525
531
|
npx openclaw-navigator Auto-tunnel mode (default, works anywhere)
|
|
532
|
+
npx openclaw-navigator --mcp Bridge + MCP server (for OpenClaw agent)
|
|
526
533
|
npx openclaw-navigator --no-tunnel Local-only mode (SSH/LAN)
|
|
527
534
|
npx openclaw-navigator --port 18790 Custom port
|
|
528
535
|
|
|
529
536
|
${BOLD}Options:${RESET}
|
|
530
537
|
--port <port> Bridge server port (default: 18790)
|
|
538
|
+
--mcp Also start the MCP server (stdio) for OpenClaw agent
|
|
531
539
|
--no-tunnel Skip auto-tunnel, use SSH or LAN instead
|
|
532
540
|
--bind <host> Bind address (default: 127.0.0.1)
|
|
533
541
|
--help Show this help
|
|
@@ -651,9 +659,39 @@ ${BOLD}How it works:${RESET}
|
|
|
651
659
|
console.log(`${BOLD}${GREEN}Bridge is running.${RESET} Waiting for Navigator to connect...`);
|
|
652
660
|
console.log(`${DIM}Press Ctrl+C to stop.${RESET}\n`);
|
|
653
661
|
|
|
662
|
+
// ── Step 6 (optional): Start MCP server ─────────────────────────────
|
|
663
|
+
let mcpProcess = null;
|
|
664
|
+
if (withMcp) {
|
|
665
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
666
|
+
const mcpPath = join(__dirname, "mcp.mjs");
|
|
667
|
+
const mcpEnv = { ...process.env, NAVIGATOR_BRIDGE_URL: `http://localhost:${port}` };
|
|
668
|
+
|
|
669
|
+
mcpProcess = spawn(process.execPath, [mcpPath], {
|
|
670
|
+
stdio: ["pipe", "pipe", "inherit"], // stdin/stdout piped for MCP protocol, stderr visible
|
|
671
|
+
env: mcpEnv,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
mcpProcess.on("error", (err) => {
|
|
675
|
+
warn(`MCP server failed to start: ${err.message}`);
|
|
676
|
+
});
|
|
677
|
+
mcpProcess.on("exit", (code) => {
|
|
678
|
+
if (code !== null && code !== 0) {
|
|
679
|
+
warn(`MCP server exited with code ${code}`);
|
|
680
|
+
}
|
|
681
|
+
mcpProcess = null;
|
|
682
|
+
});
|
|
683
|
+
|
|
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`);
|
|
686
|
+
console.log("");
|
|
687
|
+
}
|
|
688
|
+
|
|
654
689
|
// ── Graceful shutdown ─────────────────────────────────────────────────
|
|
655
690
|
const shutdown = () => {
|
|
656
691
|
console.log(`\n${DIM}Shutting down bridge...${RESET}`);
|
|
692
|
+
if (mcpProcess) {
|
|
693
|
+
mcpProcess.kill();
|
|
694
|
+
}
|
|
657
695
|
if (tunnelProcess) {
|
|
658
696
|
tunnelProcess.kill();
|
|
659
697
|
}
|
package/mcp.mjs
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openclaw-navigator MCP server v4.3.0
|
|
5
|
+
*
|
|
6
|
+
* Exposes the Navigator bridge HTTP API as MCP tools so the OpenClaw agent
|
|
7
|
+
* can control the browser natively via its tool schema.
|
|
8
|
+
*
|
|
9
|
+
* Runs on the OC machine alongside the bridge (cli.mjs).
|
|
10
|
+
* Uses stdio transport — the gateway wires it in via mcporter or MCP config.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node mcp.mjs
|
|
14
|
+
* NAVIGATOR_BRIDGE_URL=http://host:18790 node mcp.mjs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import {
|
|
20
|
+
ListToolsRequestSchema,
|
|
21
|
+
CallToolRequestSchema,
|
|
22
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
|
|
24
|
+
// ── Configuration ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const BRIDGE_URL =
|
|
27
|
+
process.env.NAVIGATOR_BRIDGE_URL ?? "http://localhost:18790";
|
|
28
|
+
const POLL_INTERVAL_MS = 500;
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
30
|
+
const WAIT_READY_TIMEOUT_MS = 20_000;
|
|
31
|
+
|
|
32
|
+
// All logging to stderr — stdout is reserved for MCP JSON-RPC protocol
|
|
33
|
+
const log = (...args) => console.error("[navigator-mcp]", ...args);
|
|
34
|
+
|
|
35
|
+
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
async function bridgeGet(path) {
|
|
38
|
+
const res = await fetch(`${BRIDGE_URL}${path}`);
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`Bridge GET ${path} returned ${res.status}`);
|
|
41
|
+
}
|
|
42
|
+
return res.json();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function bridgePost(path, body) {
|
|
46
|
+
const res = await fetch(`${BRIDGE_URL}${path}`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`Bridge POST ${path} returned ${res.status}`);
|
|
53
|
+
}
|
|
54
|
+
return res.json();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Event polling engine ──────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Poll GET /navigator/events for an event matching `predicate`.
|
|
61
|
+
* Uses a timestamp watermark — only considers events newer than startTime.
|
|
62
|
+
*
|
|
63
|
+
* @param {(event: object) => boolean} predicate
|
|
64
|
+
* @param {number} startTime - timestamp watermark (Date.now() before command)
|
|
65
|
+
* @param {number} [timeout] - max wait in ms
|
|
66
|
+
* @returns {Promise<object|null>} matched event or null on timeout
|
|
67
|
+
*/
|
|
68
|
+
async function waitForEvent(
|
|
69
|
+
predicate,
|
|
70
|
+
startTime,
|
|
71
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
72
|
+
) {
|
|
73
|
+
const deadline = Date.now() + timeout;
|
|
74
|
+
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
try {
|
|
77
|
+
const data = await bridgeGet("/navigator/events?limit=50");
|
|
78
|
+
if (data.ok && Array.isArray(data.events)) {
|
|
79
|
+
for (const event of data.events) {
|
|
80
|
+
if (event.timestamp > startTime && predicate(event)) {
|
|
81
|
+
return event;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log("Poll error:", err.message);
|
|
87
|
+
}
|
|
88
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── sendCommand — post command + optionally poll for result ────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} command - Bridge command name (e.g. "navigate", "click")
|
|
98
|
+
* @param {object} payload - Command payload
|
|
99
|
+
* @param {object} [poll] - If provided, wait for result event
|
|
100
|
+
* @param {string} [poll.eventType] - Match event by type (e.g. "tabs.list")
|
|
101
|
+
* @param {string} [poll.commandName] - Match command.result by data.command
|
|
102
|
+
* @param {number} [poll.timeout] - Custom timeout in ms
|
|
103
|
+
*/
|
|
104
|
+
async function sendCommand(command, payload, poll) {
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
let postResult;
|
|
108
|
+
try {
|
|
109
|
+
postResult = await bridgePost("/navigator/command", { command, payload });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `Bridge unreachable at ${BRIDGE_URL}: ${err.message}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!postResult.ok) {
|
|
118
|
+
return postResult;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fire-and-forget — return immediately
|
|
122
|
+
if (!poll) {
|
|
123
|
+
return { ok: true, commandId: postResult.commandId, command };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build predicate based on poll options
|
|
127
|
+
let predicate;
|
|
128
|
+
if (poll.eventType) {
|
|
129
|
+
predicate = (e) => e.type === poll.eventType;
|
|
130
|
+
} else if (poll.commandName) {
|
|
131
|
+
predicate = (e) =>
|
|
132
|
+
e.type === "command.result" && e.data?.command === poll.commandName;
|
|
133
|
+
} else {
|
|
134
|
+
return { ok: true, commandId: postResult.commandId, command };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const timeout = poll.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
138
|
+
const event = await waitForEvent(predicate, startTime, timeout);
|
|
139
|
+
|
|
140
|
+
if (!event) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: "Command timed out. Navigator may not be connected.",
|
|
144
|
+
commandId: postResult.commandId,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
commandId: postResult.commandId,
|
|
151
|
+
event: {
|
|
152
|
+
type: event.type,
|
|
153
|
+
data: event.data,
|
|
154
|
+
url: event.url,
|
|
155
|
+
title: event.title,
|
|
156
|
+
content: event.content,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Tool definitions (15 tools) ───────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const TOOLS = [
|
|
164
|
+
// ── Direct HTTP ──
|
|
165
|
+
{
|
|
166
|
+
name: "navigator_status",
|
|
167
|
+
description:
|
|
168
|
+
"Get the Navigator bridge status — connection state, active tabs, current URL, uptime, pending commands.",
|
|
169
|
+
inputSchema: { type: "object", properties: {} },
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// ── Fire-and-forget ──
|
|
173
|
+
{
|
|
174
|
+
name: "navigator_navigate",
|
|
175
|
+
description:
|
|
176
|
+
"Navigate the active browser tab to a URL. Returns immediately after queuing the command.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
url: { type: "string", description: "The URL to navigate to" },
|
|
181
|
+
},
|
|
182
|
+
required: ["url"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "navigator_open_tab",
|
|
187
|
+
description:
|
|
188
|
+
"Open a new browser tab, optionally loading a URL. Returns immediately after queuing.",
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
url: {
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "URL to load (opens blank tab if omitted)",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "navigator_close_tab",
|
|
201
|
+
description:
|
|
202
|
+
"Close a browser tab by its index or ID. Returns immediately after queuing.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
tabIndex: { type: "number", description: "Tab index (0-based)" },
|
|
207
|
+
tabId: { type: "string", description: "Tab ID" },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// ── Poll by event type ──
|
|
213
|
+
{
|
|
214
|
+
name: "navigator_list_tabs",
|
|
215
|
+
description:
|
|
216
|
+
"List all open tabs in the Navigator browser with their URLs, titles, and IDs.",
|
|
217
|
+
inputSchema: { type: "object", properties: {} },
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "navigator_snapshot",
|
|
221
|
+
description:
|
|
222
|
+
"Get a snapshot of the browser state — active tab, URL, title, and full tab list.",
|
|
223
|
+
inputSchema: { type: "object", properties: {} },
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "navigator_get_text",
|
|
227
|
+
description:
|
|
228
|
+
"Get the text content of the active page (equivalent to document.body.innerText).",
|
|
229
|
+
inputSchema: { type: "object", properties: {} },
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// ── Poll by command.result ──
|
|
233
|
+
{
|
|
234
|
+
name: "navigator_click",
|
|
235
|
+
description:
|
|
236
|
+
"Click an element on the page by CSS selector. Waits for confirmation from the browser.",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
selector: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "CSS selector of the element to click",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ["selector"],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "navigator_fill",
|
|
250
|
+
description:
|
|
251
|
+
"Fill an input field with text. Waits for confirmation from the browser.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
selector: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "CSS selector of the input element",
|
|
258
|
+
},
|
|
259
|
+
value: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "Text to fill into the field",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
required: ["selector", "value"],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "navigator_submit",
|
|
269
|
+
description:
|
|
270
|
+
"Submit a form. Optionally specify a selector for the form or an element inside it.",
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: {
|
|
274
|
+
selector: {
|
|
275
|
+
type: "string",
|
|
276
|
+
description:
|
|
277
|
+
"CSS selector of the form or an element inside it (submits closest form)",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "navigator_scroll",
|
|
284
|
+
description:
|
|
285
|
+
"Scroll the page in a direction (up/down/top/bottom) or by exact pixel offsets.",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
direction: {
|
|
290
|
+
type: "string",
|
|
291
|
+
enum: ["up", "down", "top", "bottom"],
|
|
292
|
+
description: "Scroll direction shortcut",
|
|
293
|
+
},
|
|
294
|
+
x: {
|
|
295
|
+
type: "number",
|
|
296
|
+
description: "Horizontal pixels to scroll (positive = right)",
|
|
297
|
+
},
|
|
298
|
+
y: {
|
|
299
|
+
type: "number",
|
|
300
|
+
description: "Vertical pixels to scroll (positive = down)",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "navigator_execute_js",
|
|
307
|
+
description:
|
|
308
|
+
"Execute arbitrary JavaScript in the active tab and return the result. Use for anything the other tools don't cover.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
code: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "JavaScript code to execute in the page context",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: ["code"],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "navigator_get_html",
|
|
322
|
+
description: "Get the full HTML source of the active page.",
|
|
323
|
+
inputSchema: { type: "object", properties: {} },
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "navigator_query_element",
|
|
327
|
+
description:
|
|
328
|
+
"Query a DOM element by CSS selector — returns its tag name, text content, and attributes.",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {
|
|
332
|
+
selector: { type: "string", description: "CSS selector" },
|
|
333
|
+
},
|
|
334
|
+
required: ["selector"],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "navigator_wait_ready",
|
|
339
|
+
description:
|
|
340
|
+
'Wait until document.readyState is "complete". Useful after navigation to ensure the page is fully loaded. Times out after 20 seconds.',
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: {
|
|
344
|
+
timeout: {
|
|
345
|
+
type: "number",
|
|
346
|
+
description: "Max wait in milliseconds (default: 10000)",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
// ── Tool handler dispatch ─────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
function jsonResult(data) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function errorResult(message) {
|
|
362
|
+
return {
|
|
363
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
364
|
+
isError: true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function handleTool(name, args) {
|
|
369
|
+
try {
|
|
370
|
+
switch (name) {
|
|
371
|
+
// ── Direct HTTP ──
|
|
372
|
+
case "navigator_status":
|
|
373
|
+
return jsonResult(await bridgeGet("/navigator/status"));
|
|
374
|
+
|
|
375
|
+
// ── Fire-and-forget ──
|
|
376
|
+
case "navigator_navigate":
|
|
377
|
+
return jsonResult(await sendCommand("navigate", { url: args.url }));
|
|
378
|
+
|
|
379
|
+
case "navigator_open_tab":
|
|
380
|
+
return jsonResult(await sendCommand("tabs.open", { url: args.url }));
|
|
381
|
+
|
|
382
|
+
case "navigator_close_tab":
|
|
383
|
+
return jsonResult(
|
|
384
|
+
await sendCommand("tabs.close", {
|
|
385
|
+
tabIndex: args.tabIndex,
|
|
386
|
+
tabId: args.tabId,
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// ── Poll by event type ──
|
|
391
|
+
case "navigator_list_tabs":
|
|
392
|
+
return jsonResult(
|
|
393
|
+
await sendCommand("tabs.list", {}, { eventType: "tabs.list" }),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
case "navigator_snapshot":
|
|
397
|
+
return jsonResult(
|
|
398
|
+
await sendCommand("snapshot", {}, { eventType: "snapshot" }),
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
case "navigator_get_text":
|
|
402
|
+
return jsonResult(
|
|
403
|
+
await sendCommand("page.content", {}, { eventType: "page.content" }),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// ── Poll by command.result ──
|
|
407
|
+
case "navigator_click":
|
|
408
|
+
return jsonResult(
|
|
409
|
+
await sendCommand(
|
|
410
|
+
"click",
|
|
411
|
+
{ selector: args.selector },
|
|
412
|
+
{ commandName: "click" },
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
case "navigator_fill":
|
|
417
|
+
return jsonResult(
|
|
418
|
+
await sendCommand(
|
|
419
|
+
"input.fill",
|
|
420
|
+
{ selector: args.selector, value: args.value },
|
|
421
|
+
{ commandName: "input.fill" },
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
case "navigator_submit":
|
|
426
|
+
return jsonResult(
|
|
427
|
+
await sendCommand(
|
|
428
|
+
"input.submit",
|
|
429
|
+
{ selector: args.selector },
|
|
430
|
+
{ commandName: "input.submit" },
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
case "navigator_scroll":
|
|
435
|
+
return jsonResult(
|
|
436
|
+
await sendCommand(
|
|
437
|
+
"scroll",
|
|
438
|
+
{ direction: args.direction, x: args.x, y: args.y },
|
|
439
|
+
{ commandName: "scroll" },
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
case "navigator_execute_js":
|
|
444
|
+
return jsonResult(
|
|
445
|
+
await sendCommand(
|
|
446
|
+
"execute",
|
|
447
|
+
{ code: args.code },
|
|
448
|
+
{ commandName: "execute" },
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
case "navigator_get_html":
|
|
453
|
+
return jsonResult(
|
|
454
|
+
await sendCommand("page.html", {}, { commandName: "page.html" }),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
case "navigator_query_element":
|
|
458
|
+
return jsonResult(
|
|
459
|
+
await sendCommand(
|
|
460
|
+
"dom.query",
|
|
461
|
+
{ selector: args.selector },
|
|
462
|
+
{ commandName: "dom.query" },
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
case "navigator_wait_ready":
|
|
467
|
+
return jsonResult(
|
|
468
|
+
await sendCommand(
|
|
469
|
+
"wait.ready",
|
|
470
|
+
{ timeout: args.timeout },
|
|
471
|
+
{ commandName: "wait.ready", timeout: WAIT_READY_TIMEOUT_MS },
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
default:
|
|
476
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
log(`Tool ${name} error:`, err.message);
|
|
480
|
+
return errorResult(err.message);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── MCP server wiring ─────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
const server = new Server(
|
|
487
|
+
{ name: "openclaw-navigator", version: "4.3.0" },
|
|
488
|
+
{ capabilities: { tools: {} } },
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
492
|
+
tools: TOOLS,
|
|
493
|
+
}));
|
|
494
|
+
|
|
495
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
496
|
+
const { name, arguments: args = {} } = request.params;
|
|
497
|
+
log(`\u2192 ${name}`, JSON.stringify(args));
|
|
498
|
+
const result = await handleTool(name, args);
|
|
499
|
+
log(`\u2190 ${name}`, result.isError ? "ERROR" : "ok");
|
|
500
|
+
return result;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ── Start stdio transport ─────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
const transport = new StdioServerTransport();
|
|
506
|
+
await server.connect(transport);
|
|
507
|
+
log(`MCP server started \u2014 bridge: ${BRIDGE_URL}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-navigator",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
],
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"bin": {
|
|
16
|
-
"openclaw-navigator": "cli.mjs"
|
|
16
|
+
"openclaw-navigator": "cli.mjs",
|
|
17
|
+
"openclaw-navigator-mcp": "mcp.mjs"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
|
-
"cli.mjs"
|
|
20
|
+
"cli.mjs",
|
|
21
|
+
"mcp.mjs"
|
|
20
22
|
],
|
|
21
23
|
"type": "module",
|
|
22
24
|
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
23
26
|
"cloudflared": "^0.7.1"
|
|
24
27
|
},
|
|
25
28
|
"engines": {
|