browser-debugging-daemon 1.0.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/daemon.js +931 -0
- package/dashboard/app.js +1139 -0
- package/dashboard/index.html +277 -0
- package/dashboard/styles.css +774 -0
- package/index.js +223 -0
- package/mcp_server.js +999 -0
- package/orchestrator/RunTemplateStore.js +30 -0
- package/orchestrator/TaskRunStore.js +33 -0
- package/orchestrator/TaskRunner.js +803 -0
- package/package.json +66 -0
- package/runtime/ArtifactStore.js +202 -0
- package/runtime/BrowserRuntime.js +1706 -0
- package/shared.js +358 -0
- package/subagent/BrowserSubagent.js +689 -0
- package/subagent/OpenAIPlanner.js +382 -0
package/mcp_server.js
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { BrowserRuntime } from "./runtime/BrowserRuntime.js";
|
|
12
|
+
import { BrowserSubagent } from "./subagent/BrowserSubagent.js";
|
|
13
|
+
import { TaskRunner } from "./orchestrator/TaskRunner.js";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const runtime = new BrowserRuntime(__dirname);
|
|
19
|
+
const subagent = new BrowserSubagent(runtime);
|
|
20
|
+
const taskRunner = new TaskRunner(__dirname, { runtime, subagent });
|
|
21
|
+
const SUPPORTED_BROWSER_SOURCES = new Set(["auto", "managed", "attached"]);
|
|
22
|
+
const RUN_TERMINAL_STATUSES = new Set(["completed", "failed", "aborted"]);
|
|
23
|
+
const RUN_HANDOFF_STATUSES = new Set(["waiting_for_instruction", "manual_control_requested", "manual_control"]);
|
|
24
|
+
const RUN_WATCH_READY_STATUSES = new Set([...RUN_TERMINAL_STATUSES, ...RUN_HANDOFF_STATUSES]);
|
|
25
|
+
|
|
26
|
+
function sleep(milliseconds) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeNumberInRange(value, fallback, { min = null, max = null } = {}) {
|
|
31
|
+
const parsed = Number.parseInt(value, 10);
|
|
32
|
+
if (!Number.isFinite(parsed)) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
if (Number.isFinite(min) && parsed < min) {
|
|
36
|
+
return min;
|
|
37
|
+
}
|
|
38
|
+
if (Number.isFinite(max) && parsed > max) {
|
|
39
|
+
return max;
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeBrowserSource(source) {
|
|
45
|
+
const normalized = typeof source === "string" ? source.trim().toLowerCase() : "";
|
|
46
|
+
if (SUPPORTED_BROWSER_SOURCES.has(normalized)) {
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
return "auto";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeOptionalBrowserSource(source) {
|
|
53
|
+
if (typeof source !== "string" || !source.trim()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return normalizeBrowserSource(source);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// -----------------------------------------------------------------
|
|
60
|
+
// 2. 初始化 MCP Server
|
|
61
|
+
// -----------------------------------------------------------------
|
|
62
|
+
const server = new Server(
|
|
63
|
+
{
|
|
64
|
+
name: "browser-automation-mcp",
|
|
65
|
+
version: "1.0.0",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
capabilities: {
|
|
69
|
+
tools: {},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// -----------------------------------------------------------------
|
|
75
|
+
// 3. 注册可用的 Tools (原生函数调用)
|
|
76
|
+
// -----------------------------------------------------------------
|
|
77
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
78
|
+
return {
|
|
79
|
+
tools: [
|
|
80
|
+
{
|
|
81
|
+
name: "browser_goto",
|
|
82
|
+
description: "Navigate the persistent browser to a specified URL.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: { url: { type: "string", description: "The full URL to navigate to" } },
|
|
86
|
+
required: ["url"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "browser_observe",
|
|
91
|
+
description: "Analyze the current page with full observability. Returns: (1) SoM interactive elements with IDs for click/type/hover actions, (2) Accessibility Tree (YAML) for complete page semantics including headings, paragraphs, tables, code blocks, and parent-child structure, (3) page content summary, (4) recent console errors, (5) a full-page screenshot. ALWAYS call this before interacting with the page.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "browser_click",
|
|
99
|
+
description: "Click an element on the page based on the ID returned by `browser_observe`.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: { id: { type: "number", description: "The ID of the Set-of-Mark element" } },
|
|
103
|
+
required: ["id"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "browser_type",
|
|
108
|
+
description: "Type text into an input element based on the ID returned by `browser_observe`. Set submit to true to automatically press Enter after typing.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
id: { type: "number", description: "The ID of the Input element" },
|
|
113
|
+
text: { type: "string", description: "The text to type" },
|
|
114
|
+
submit: { type: "boolean", description: "If true, press Enter after typing to submit the form or send the message." },
|
|
115
|
+
},
|
|
116
|
+
required: ["id", "text"],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "browser_hover",
|
|
121
|
+
description: "Hover over an element on the page based on the ID returned by `browser_observe`.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: { id: { type: "number", description: "The ID of the Set-of-Mark element" } },
|
|
125
|
+
required: ["id"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "browser_keypress",
|
|
130
|
+
description: "Press a keyboard key such as Enter, Escape, or ArrowDown.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: { key: { type: "string", description: "The key name to press" } },
|
|
134
|
+
required: ["key"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "browser_scroll",
|
|
139
|
+
description: "Scroll the page.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: { direction: { type: "string", enum: ["down", "up", "top", "bottom"] } },
|
|
143
|
+
required: ["direction"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "browser_upload",
|
|
148
|
+
description: "Upload files to a file input element on the page based on the ID returned by `browser_observe`. Supports local file paths and/or base64-encoded file content.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
id: { type: "number", description: "The ID of the file input element from Set-of-Mark observation." },
|
|
153
|
+
paths: {
|
|
154
|
+
type: "array",
|
|
155
|
+
items: { type: "string" },
|
|
156
|
+
description: "Absolute file paths on the daemon's local filesystem.",
|
|
157
|
+
},
|
|
158
|
+
files: {
|
|
159
|
+
type: "array",
|
|
160
|
+
items: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
name: { type: "string", description: "Filename with extension." },
|
|
164
|
+
mimeType: { type: "string", description: "MIME type (e.g. image/png)." },
|
|
165
|
+
content: { type: "string", description: "Base64-encoded file content." },
|
|
166
|
+
},
|
|
167
|
+
required: ["name", "content"],
|
|
168
|
+
},
|
|
169
|
+
description: "Base64-encoded file objects.",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
required: ["id"],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "browser_drag",
|
|
177
|
+
description: "Drag a page element onto another element. Both elements are identified by their Set-of-Mark IDs from `browser_observe`.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
fromId: { type: "number", description: "The ID of the element to drag." },
|
|
182
|
+
toId: { type: "number", description: "The ID of the target drop zone element." },
|
|
183
|
+
},
|
|
184
|
+
required: ["fromId", "toId"],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "browser_drag_file",
|
|
189
|
+
description: "Drag local files from the daemon's filesystem onto a page drop zone element. Supports local file paths and/or base64-encoded file content.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
toId: { type: "number", description: "The ID of the drop zone element from Set-of-Mark observation." },
|
|
194
|
+
paths: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: { type: "string" },
|
|
197
|
+
description: "Absolute file paths on the daemon's local filesystem.",
|
|
198
|
+
},
|
|
199
|
+
files: {
|
|
200
|
+
type: "array",
|
|
201
|
+
items: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
name: { type: "string", description: "Filename with extension." },
|
|
205
|
+
mimeType: { type: "string", description: "MIME type (e.g. image/png)." },
|
|
206
|
+
content: { type: "string", description: "Base64-encoded file content." },
|
|
207
|
+
},
|
|
208
|
+
required: ["name", "content"],
|
|
209
|
+
},
|
|
210
|
+
description: "Base64-encoded file objects.",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
required: ["toId"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "browser_debug_state",
|
|
218
|
+
description: "Return recent console, network, error, and artifact state for the active or last browser session.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
limit: { type: "number", description: "How many recent events to include", default: 20 },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "browser_waitFor",
|
|
228
|
+
description: "Wait for a specific condition on the page before proceeding. Supports waiting for text to appear, text to disappear, a CSS selector to become visible, or a fixed time delay. Use this instead of sleep when you need to synchronize with page state changes (e.g., wait for loading to complete, wait for an element to appear).",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
text: { type: "string", description: "Wait until this text appears anywhere on the page." },
|
|
233
|
+
textGone: { type: "string", description: "Wait until this text is no longer present on the page." },
|
|
234
|
+
selector: { type: "string", description: "Wait until a CSS selector matches a visible element." },
|
|
235
|
+
timeout: { type: "number", description: "Maximum wait time in milliseconds (default 30000, max 300000).", default: 30000 },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "browser_evaluate",
|
|
241
|
+
description: "Execute JavaScript code in the browser page context and return the result. The `page` Playwright Page object is available. Use for reading page state, extracting data, triggering events, or any custom logic not covered by other tools. Results are serialized (max 8KB).",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
expression: { type: "string", description: "JavaScript code to execute. The Playwright `page` object is available as `page`. Example: `return await page.title()`" },
|
|
246
|
+
},
|
|
247
|
+
required: ["expression"],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "browser_select_option",
|
|
252
|
+
description: "Select one or more options in a dropdown (<select>) element identified by its SoM ID from `browser_observe`.",
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
id: { type: "number", description: "The SoM element ID of the select element." },
|
|
257
|
+
values: {
|
|
258
|
+
oneOf: [
|
|
259
|
+
{ type: "string" },
|
|
260
|
+
{ type: "array", items: { type: "string" } },
|
|
261
|
+
],
|
|
262
|
+
description: "The option value(s) to select. String for single, array for multiple.",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
required: ["id", "values"],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "browser_handle_dialog",
|
|
270
|
+
description: "Accept or dismiss a browser dialog (alert, confirm, prompt). For prompt dialogs, you can provide text to fill in.",
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: {
|
|
274
|
+
action: { type: "string", enum: ["accept", "dismiss"], description: "Whether to accept or dismiss the dialog.", default: "accept" },
|
|
275
|
+
promptText: { type: "string", description: "Text to enter in a prompt dialog (only used when action is accept)." },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "browser_navigate_back",
|
|
281
|
+
description: "Go back to the previous page in browser history. Equivalent to clicking the browser back button.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "browser_tabs",
|
|
289
|
+
description: "Manage browser tabs: list all open tabs, create a new tab, switch to a tab by index, or close a tab.",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
action: { type: "string", enum: ["list", "new", "close", "select"], description: "Tab action to perform." },
|
|
294
|
+
index: { type: "number", description: "Tab index for 'close' or 'select' actions." },
|
|
295
|
+
url: { type: "string", description: "URL to navigate to when creating a new tab." },
|
|
296
|
+
},
|
|
297
|
+
required: ["action"],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "delegate_browser_task",
|
|
302
|
+
description: "Delegate a multi-step browser task to the autonomous browser subagent. The subagent observes the page, chooses actions, and returns a structured summary with artifacts.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
task_instruction: { type: "string", description: "Detailed browser task to accomplish" },
|
|
307
|
+
max_steps: { type: "number", description: "Maximum planning/action steps before stopping", default: 12 },
|
|
308
|
+
browser_source: {
|
|
309
|
+
type: "string",
|
|
310
|
+
enum: ["auto", "managed", "attached"],
|
|
311
|
+
description: "Browser source strategy. auto prefers attached Chrome and falls back to managed runtime.",
|
|
312
|
+
},
|
|
313
|
+
cdp_endpoint: { type: "string", description: "Optional CDP endpoint for attached mode (default http://127.0.0.1:9222)." },
|
|
314
|
+
auto_stop: { type: "boolean", description: "Whether to stop the runtime after task completion (default true)." },
|
|
315
|
+
},
|
|
316
|
+
required: ["task_instruction"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "browser_task_start",
|
|
321
|
+
description: "Start an Antigravity-style background browser run and return a run ID for polling.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
template_id: { type: "string", description: "Optional run template ID. When provided, template defaults are applied." },
|
|
326
|
+
task_instruction: { type: "string", description: "Detailed browser task to queue" },
|
|
327
|
+
max_steps: { type: "number", description: "Maximum planning/action steps", default: 12 },
|
|
328
|
+
browser_source: {
|
|
329
|
+
type: "string",
|
|
330
|
+
enum: ["auto", "managed", "attached"],
|
|
331
|
+
description: "Browser source strategy. auto prefers attached Chrome and falls back to managed runtime.",
|
|
332
|
+
},
|
|
333
|
+
cdp_endpoint: { type: "string", description: "Optional CDP endpoint for attached mode (default http://127.0.0.1:9222)." },
|
|
334
|
+
handoff_timeout_ms: { type: "number", description: "Optional timeout for waiting human instructions." },
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "browser_task_get",
|
|
340
|
+
description: "Get the current state of a previously started browser run.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: {
|
|
344
|
+
run_id: { type: "string", description: "Run identifier returned by browser_task_start" },
|
|
345
|
+
},
|
|
346
|
+
required: ["run_id"],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "browser_task_list",
|
|
351
|
+
description: "List recent browser task runs.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
limit: { type: "number", description: "How many runs to return", default: 20 },
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "browser_task_reply",
|
|
361
|
+
description: "Reply to a waiting run with new guidance so the subagent can continue.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: "object",
|
|
364
|
+
properties: {
|
|
365
|
+
run_id: { type: "string", description: "Run identifier." },
|
|
366
|
+
instruction: { type: "string", description: "Guidance from the main agent." },
|
|
367
|
+
},
|
|
368
|
+
required: ["run_id", "instruction"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "browser_task_resume",
|
|
373
|
+
description: "Resume a run from manual control or waiting state.",
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
run_id: { type: "string", description: "Run identifier." },
|
|
378
|
+
instruction: { type: "string", description: "Resume instruction (optional)." },
|
|
379
|
+
},
|
|
380
|
+
required: ["run_id"],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "browser_task_manual_control",
|
|
385
|
+
description: "Request manual control for an active run.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
run_id: { type: "string", description: "Run identifier." },
|
|
390
|
+
reason: { type: "string", description: "Why manual control is needed." },
|
|
391
|
+
},
|
|
392
|
+
required: ["run_id"],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "browser_task_abort",
|
|
397
|
+
description: "Abort an active or queued run.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
type: "object",
|
|
400
|
+
properties: {
|
|
401
|
+
run_id: { type: "string", description: "Run identifier." },
|
|
402
|
+
reason: { type: "string", description: "Abort reason." },
|
|
403
|
+
},
|
|
404
|
+
required: ["run_id"],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "browser_task_watch",
|
|
409
|
+
description: "Wait until a run reaches a terminal or handoff-required state, then return the latest run snapshot.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
run_id: { type: "string", description: "Run identifier." },
|
|
414
|
+
timeout_ms: { type: "number", description: "Maximum wait time.", default: 30000 },
|
|
415
|
+
poll_interval_ms: { type: "number", description: "Polling interval.", default: 1500 },
|
|
416
|
+
},
|
|
417
|
+
required: ["run_id"],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "browser_template_save",
|
|
422
|
+
description: "Create or update a reusable run template with start URL, login checks, assertions, and timeout policy.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
type: "object",
|
|
425
|
+
properties: {
|
|
426
|
+
id: { type: "string", description: "Optional existing template ID for updates." },
|
|
427
|
+
name: { type: "string", description: "Template display name." },
|
|
428
|
+
description: { type: "string", description: "Optional template description." },
|
|
429
|
+
task_instruction: { type: "string", description: "Default task instruction for this template." },
|
|
430
|
+
browser_source: { type: "string", enum: ["auto", "managed", "attached"] },
|
|
431
|
+
cdp_endpoint: { type: "string" },
|
|
432
|
+
start_url: { type: "string", description: "Fixed URL to open before task execution." },
|
|
433
|
+
pre_login_checks: {
|
|
434
|
+
type: "array",
|
|
435
|
+
description: "Array of checks with kind/url_includes|title_includes|text_includes and expected.",
|
|
436
|
+
items: { type: "object" },
|
|
437
|
+
},
|
|
438
|
+
assertion_rules: {
|
|
439
|
+
type: "array",
|
|
440
|
+
description: "Array of assertions with kind/url_includes|title_includes|text_includes and expected.",
|
|
441
|
+
items: { type: "object" },
|
|
442
|
+
},
|
|
443
|
+
timeout_policy: {
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: {
|
|
446
|
+
max_steps: { type: "number" },
|
|
447
|
+
handoff_timeout_ms: { type: "number" },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
required: ["name"],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "browser_template_list",
|
|
456
|
+
description: "List reusable run templates.",
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: "object",
|
|
459
|
+
properties: {
|
|
460
|
+
limit: { type: "number", default: 100 },
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "browser_template_run",
|
|
466
|
+
description: "Queue a new run directly from a template ID.",
|
|
467
|
+
inputSchema: {
|
|
468
|
+
type: "object",
|
|
469
|
+
properties: {
|
|
470
|
+
template_id: { type: "string" },
|
|
471
|
+
task_instruction: { type: "string" },
|
|
472
|
+
max_steps: { type: "number" },
|
|
473
|
+
browser_source: { type: "string", enum: ["auto", "managed", "attached"] },
|
|
474
|
+
cdp_endpoint: { type: "string" },
|
|
475
|
+
handoff_timeout_ms: { type: "number" },
|
|
476
|
+
},
|
|
477
|
+
required: ["template_id"],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "browser_template_compare",
|
|
482
|
+
description: "Compare recent runs created from the same template.",
|
|
483
|
+
inputSchema: {
|
|
484
|
+
type: "object",
|
|
485
|
+
properties: {
|
|
486
|
+
template_id: { type: "string" },
|
|
487
|
+
limit: { type: "number", default: 8 },
|
|
488
|
+
},
|
|
489
|
+
required: ["template_id"],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "browser_stop",
|
|
494
|
+
description: "Stop the browser and save the debug trace recording (.zip). Use this when the testing task is complete.",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: "browser_cdp_health",
|
|
502
|
+
description: "Check whether the target Chrome remote debugging endpoint is reachable.",
|
|
503
|
+
inputSchema: {
|
|
504
|
+
type: "object",
|
|
505
|
+
properties: {
|
|
506
|
+
cdp_endpoint: { type: "string", description: "CDP endpoint such as http://127.0.0.1:9222" },
|
|
507
|
+
timeout_ms: { type: "number", description: "Health-check timeout in milliseconds", default: 3000 },
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "browser_text_layout_audit",
|
|
513
|
+
description: "Audit visible UI text for overflow risk using canvas-based line estimation and actual overflow checks.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
limit: { type: "number", description: "Maximum number of candidate elements to inspect", default: 80 },
|
|
518
|
+
selectors: { type: "string", description: "Optional CSS selector list used to pick text-bearing elements." },
|
|
519
|
+
overflow_threshold: { type: "number", description: "Pixel threshold before overflow is considered an issue.", default: 1 },
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "browser_attach_diagnostics",
|
|
525
|
+
description: "Run one-shot diagnostics for attaching to an existing Chrome session and return remediation hints.",
|
|
526
|
+
inputSchema: {
|
|
527
|
+
type: "object",
|
|
528
|
+
properties: {
|
|
529
|
+
cdp_endpoint: { type: "string", description: "CDP endpoint such as http://127.0.0.1:9222" },
|
|
530
|
+
timeout_ms: { type: "number", description: "Diagnostics timeout in milliseconds", default: 3000 },
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// -----------------------------------------------------------------
|
|
539
|
+
// 4. 处理客户端的 Tools 调用
|
|
540
|
+
// -----------------------------------------------------------------
|
|
541
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
542
|
+
try {
|
|
543
|
+
switch (request.params.name) {
|
|
544
|
+
case "browser_goto": {
|
|
545
|
+
const url = request.params.arguments.url;
|
|
546
|
+
const result = await runtime.goto(url);
|
|
547
|
+
|
|
548
|
+
const gotoParts = [
|
|
549
|
+
{ type: "text", text: `Successfully navigated to ${result.url}` },
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
if (result.metadata) {
|
|
553
|
+
gotoParts.push({ type: "text", text: `# Page Metadata\n${JSON.stringify(result.metadata, null, 2)}` });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (result.accessibilityTree) {
|
|
557
|
+
gotoParts.push({ type: "text", text: `# Accessibility Tree (YAML)\n${result.accessibilityTree}` });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (result.pageContent && Object.keys(result.pageContent).length > 0) {
|
|
561
|
+
gotoParts.push({ type: "text", text: `# Page Content\n${JSON.stringify(result.pageContent, null, 2)}` });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
gotoParts.push({ type: "text", text: `# Interactive Elements (SoM)\n${JSON.stringify(result.elements, null, 2)}` });
|
|
565
|
+
|
|
566
|
+
return { content: gotoParts };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
case "browser_observe": {
|
|
570
|
+
const result = await runtime.observe();
|
|
571
|
+
|
|
572
|
+
const parts = [
|
|
573
|
+
{ type: "text", text: `Observation complete. Check ${result.screenshotPath} for reference.` },
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
if (result.accessibilityTree) {
|
|
577
|
+
parts.push({ type: "text", text: `# Accessibility Tree (YAML)\n${result.accessibilityTree}` });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (result.pageContent && Object.keys(result.pageContent).length > 0) {
|
|
581
|
+
parts.push({ type: "text", text: `# Page Content\n${JSON.stringify(result.pageContent, null, 2)}` });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (result.recentErrors && result.recentErrors.length > 0) {
|
|
585
|
+
parts.push({ type: "text", text: `# Recent Console Errors\n${JSON.stringify(result.recentErrors, null, 2)}` });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
parts.push({ type: "text", text: `# Interactive Elements (SoM)\n${JSON.stringify(result.elements, null, 2)}` });
|
|
589
|
+
|
|
590
|
+
return { content: parts };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
case "browser_click": {
|
|
594
|
+
const id = request.params.arguments.id;
|
|
595
|
+
const result = await runtime.click(id);
|
|
596
|
+
return { content: [{ type: "text", text: `Successfully clicked element ${id} [${result.target.tag}]` }] };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
case "browser_type": {
|
|
600
|
+
const { id, text, submit } = request.params.arguments;
|
|
601
|
+
await runtime.type(id, text, { submit: !!submit });
|
|
602
|
+
return { content: [{ type: "text", text: `Successfully typed "${text}" into element ${id}${submit ? " and pressed Enter" : ""}` }] };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
case "browser_hover": {
|
|
606
|
+
const id = request.params.arguments.id;
|
|
607
|
+
const result = await runtime.hover(id);
|
|
608
|
+
return { content: [{ type: "text", text: `Successfully hovered element ${id} [${result.target.tag}]` }] };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
case "browser_keypress": {
|
|
612
|
+
const key = request.params.arguments.key;
|
|
613
|
+
await runtime.keypress(key);
|
|
614
|
+
return { content: [{ type: "text", text: `Successfully pressed ${key}` }] };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
case "browser_scroll": {
|
|
618
|
+
const dir = request.params.arguments.direction;
|
|
619
|
+
await runtime.scroll(dir);
|
|
620
|
+
return { content: [{ type: "text", text: `Scrolled ${dir}` }] };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
case "browser_upload": {
|
|
624
|
+
const uploadArgs = request.params.arguments;
|
|
625
|
+
const uploadResult = await runtime.upload(uploadArgs.id, {
|
|
626
|
+
paths: uploadArgs.paths || [],
|
|
627
|
+
files: uploadArgs.files || [],
|
|
628
|
+
});
|
|
629
|
+
return { content: [{ type: "text", text: `Uploaded ${uploadResult.fileCount} file(s) to element ${uploadArgs.id}` }] };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
case "browser_drag": {
|
|
633
|
+
const dragArgs = request.params.arguments;
|
|
634
|
+
const dragResult = await runtime.drag(dragArgs.fromId, dragArgs.toId);
|
|
635
|
+
return { content: [{ type: "text", text: `Dragged element ${dragArgs.fromId} onto element ${dragArgs.toId}` }] };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
case "browser_drag_file": {
|
|
639
|
+
const dragFileArgs = request.params.arguments;
|
|
640
|
+
const dragFileResult = await runtime.dragFile({
|
|
641
|
+
paths: dragFileArgs.paths || [],
|
|
642
|
+
files: dragFileArgs.files || [],
|
|
643
|
+
toId: dragFileArgs.toId,
|
|
644
|
+
});
|
|
645
|
+
return { content: [{ type: "text", text: `Dragged ${dragFileResult.fileCount} file(s) onto element ${dragFileArgs.toId}` }] };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case "browser_debug_state": {
|
|
649
|
+
const limit = request.params.arguments?.limit ?? 20;
|
|
650
|
+
return {
|
|
651
|
+
content: [{ type: "text", text: JSON.stringify(runtime.getDebugState(limit), null, 2) }]
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
case "browser_waitFor": {
|
|
656
|
+
const waitArgs = request.params.arguments || {};
|
|
657
|
+
const waitResult = await runtime.waitFor({
|
|
658
|
+
text: waitArgs.text,
|
|
659
|
+
textGone: waitArgs.textGone,
|
|
660
|
+
selector: waitArgs.selector,
|
|
661
|
+
timeout: waitArgs.timeout,
|
|
662
|
+
});
|
|
663
|
+
const waitMsg = waitResult.timedOut
|
|
664
|
+
? `Timed out after ${waitResult.waitedMs}ms waiting for ${waitArgs.text || waitArgs.textGone || waitArgs.selector}`
|
|
665
|
+
: `Condition met (${waitResult.matched}) after ${waitResult.waitedMs}ms`;
|
|
666
|
+
return { content: [{ type: "text", text: waitMsg }] };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
case "delegate_browser_task": {
|
|
670
|
+
const taskInstruction = request.params.arguments.task_instruction;
|
|
671
|
+
const maxSteps = request.params.arguments?.max_steps ?? 12;
|
|
672
|
+
const browserSource = normalizeBrowserSource(request.params.arguments?.browser_source);
|
|
673
|
+
const cdpEndpoint = request.params.arguments?.cdp_endpoint;
|
|
674
|
+
const autoStop = request.params.arguments?.auto_stop !== false;
|
|
675
|
+
let result = await subagent.delegateTask(taskInstruction, {
|
|
676
|
+
maxSteps,
|
|
677
|
+
startOptions: {
|
|
678
|
+
source: browserSource,
|
|
679
|
+
cdpEndpoint: cdpEndpoint || undefined,
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (autoStop) {
|
|
684
|
+
const stopResult = await runtime.stop().catch(() => null);
|
|
685
|
+
if (stopResult?.artifacts) {
|
|
686
|
+
result.artifacts = stopResult.artifacts;
|
|
687
|
+
result.debug = {
|
|
688
|
+
...(result.debug || {}),
|
|
689
|
+
artifacts: stopResult.artifacts,
|
|
690
|
+
};
|
|
691
|
+
const refreshedReports = subagent.writeReports(result);
|
|
692
|
+
result.reports = refreshedReports;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
case "browser_task_start": {
|
|
702
|
+
const templateId = request.params.arguments?.template_id;
|
|
703
|
+
const taskInstruction = request.params.arguments?.task_instruction || "";
|
|
704
|
+
const maxSteps = request.params.arguments?.max_steps;
|
|
705
|
+
const browserSource = normalizeOptionalBrowserSource(request.params.arguments?.browser_source);
|
|
706
|
+
const cdpEndpoint = request.params.arguments?.cdp_endpoint;
|
|
707
|
+
const handoffTimeoutMs = request.params.arguments?.handoff_timeout_ms;
|
|
708
|
+
const run = templateId
|
|
709
|
+
? taskRunner.createRunFromTemplate(templateId, {
|
|
710
|
+
taskInstruction,
|
|
711
|
+
maxSteps,
|
|
712
|
+
browserSource: browserSource || undefined,
|
|
713
|
+
cdpEndpoint: cdpEndpoint || null,
|
|
714
|
+
handoffTimeoutMs,
|
|
715
|
+
})
|
|
716
|
+
: taskRunner.createRun(taskInstruction, {
|
|
717
|
+
maxSteps: maxSteps ?? 12,
|
|
718
|
+
browserSource,
|
|
719
|
+
cdpEndpoint: cdpEndpoint || null,
|
|
720
|
+
handoffTimeoutMs,
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }]
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
case "browser_task_get": {
|
|
728
|
+
const runId = request.params.arguments.run_id;
|
|
729
|
+
const run = taskRunner.getRun(runId);
|
|
730
|
+
if (!run) {
|
|
731
|
+
throw new Error(`Run not found: ${runId}`);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }]
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
case "browser_task_list": {
|
|
739
|
+
const limit = request.params.arguments?.limit ?? 20;
|
|
740
|
+
return {
|
|
741
|
+
content: [{ type: "text", text: JSON.stringify(taskRunner.listRuns(limit), null, 2) }]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
case "browser_task_reply": {
|
|
746
|
+
const runId = request.params.arguments?.run_id;
|
|
747
|
+
const instruction = String(request.params.arguments?.instruction || "").trim();
|
|
748
|
+
if (!instruction) {
|
|
749
|
+
throw new Error("instruction is required.");
|
|
750
|
+
}
|
|
751
|
+
const run = await taskRunner.replyToRun(runId, instruction);
|
|
752
|
+
return {
|
|
753
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }]
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
case "browser_task_resume": {
|
|
758
|
+
const runId = request.params.arguments?.run_id;
|
|
759
|
+
const instruction = typeof request.params.arguments?.instruction === "string"
|
|
760
|
+
? request.params.arguments.instruction.trim()
|
|
761
|
+
: "";
|
|
762
|
+
const run = instruction
|
|
763
|
+
? await taskRunner.resumeRun(runId, instruction)
|
|
764
|
+
: await taskRunner.resumeRun(runId);
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
case "browser_task_manual_control": {
|
|
771
|
+
const runId = request.params.arguments?.run_id;
|
|
772
|
+
const reason = String(request.params.arguments?.reason || "Manual control requested by operator.").trim();
|
|
773
|
+
const run = await taskRunner.requestManualControl(runId, reason);
|
|
774
|
+
return {
|
|
775
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }]
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
case "browser_task_abort": {
|
|
780
|
+
const runId = request.params.arguments?.run_id;
|
|
781
|
+
const reason = String(request.params.arguments?.reason || "Run aborted by operator.").trim();
|
|
782
|
+
const run = taskRunner.abortRun(runId, reason);
|
|
783
|
+
return {
|
|
784
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }]
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
case "browser_task_watch": {
|
|
789
|
+
const runId = request.params.arguments?.run_id;
|
|
790
|
+
const timeoutMs = normalizeNumberInRange(
|
|
791
|
+
request.params.arguments?.timeout_ms,
|
|
792
|
+
30000,
|
|
793
|
+
{ min: 0, max: 5 * 60 * 1000 }
|
|
794
|
+
);
|
|
795
|
+
const pollIntervalMs = normalizeNumberInRange(
|
|
796
|
+
request.params.arguments?.poll_interval_ms,
|
|
797
|
+
1500,
|
|
798
|
+
{ min: 200, max: 10000 }
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
let run = taskRunner.getRun(runId);
|
|
802
|
+
if (!run) {
|
|
803
|
+
throw new Error(`Run not found: ${runId}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const startedAt = Date.now();
|
|
807
|
+
const isReady = (candidate) => candidate && RUN_WATCH_READY_STATUSES.has(candidate.status);
|
|
808
|
+
while (!isReady(run) && (Date.now() - startedAt) < timeoutMs) {
|
|
809
|
+
const elapsed = Date.now() - startedAt;
|
|
810
|
+
const remaining = timeoutMs - elapsed;
|
|
811
|
+
const waitMs = Math.max(50, Math.min(pollIntervalMs, remaining));
|
|
812
|
+
await sleep(waitMs);
|
|
813
|
+
run = taskRunner.getRun(runId);
|
|
814
|
+
if (!run) {
|
|
815
|
+
throw new Error(`Run not found: ${runId}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const endedAt = Date.now();
|
|
820
|
+
const timedOut = !isReady(run);
|
|
821
|
+
const watchResult = {
|
|
822
|
+
run,
|
|
823
|
+
watch: {
|
|
824
|
+
runId,
|
|
825
|
+
timedOut,
|
|
826
|
+
waitedMs: endedAt - startedAt,
|
|
827
|
+
readyStatuses: Array.from(RUN_WATCH_READY_STATUSES),
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
content: [{ type: "text", text: JSON.stringify(watchResult, null, 2) }]
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
case "browser_template_save": {
|
|
837
|
+
const args = request.params.arguments || {};
|
|
838
|
+
const template = taskRunner.saveTemplate({
|
|
839
|
+
id: args.id,
|
|
840
|
+
name: args.name,
|
|
841
|
+
description: args.description,
|
|
842
|
+
taskInstruction: args.task_instruction,
|
|
843
|
+
browserSource: normalizeOptionalBrowserSource(args.browser_source) || "auto",
|
|
844
|
+
cdpEndpoint: args.cdp_endpoint || null,
|
|
845
|
+
startUrl: args.start_url,
|
|
846
|
+
preLoginChecks: args.pre_login_checks || [],
|
|
847
|
+
assertionRules: args.assertion_rules || [],
|
|
848
|
+
timeoutPolicy: {
|
|
849
|
+
maxSteps: args.timeout_policy?.max_steps,
|
|
850
|
+
handoffTimeoutMs: args.timeout_policy?.handoff_timeout_ms,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
return {
|
|
854
|
+
content: [{ type: "text", text: JSON.stringify(template, null, 2) }],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
case "browser_template_list": {
|
|
859
|
+
const limit = request.params.arguments?.limit ?? 100;
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: "text", text: JSON.stringify(taskRunner.listTemplates(limit), null, 2) }],
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
case "browser_template_run": {
|
|
866
|
+
const args = request.params.arguments || {};
|
|
867
|
+
const run = taskRunner.createRunFromTemplate(args.template_id, {
|
|
868
|
+
taskInstruction: args.task_instruction || "",
|
|
869
|
+
maxSteps: args.max_steps,
|
|
870
|
+
browserSource: normalizeOptionalBrowserSource(args.browser_source) || undefined,
|
|
871
|
+
cdpEndpoint: args.cdp_endpoint || null,
|
|
872
|
+
handoffTimeoutMs: args.handoff_timeout_ms,
|
|
873
|
+
});
|
|
874
|
+
return {
|
|
875
|
+
content: [{ type: "text", text: JSON.stringify(run, null, 2) }],
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
case "browser_template_compare": {
|
|
880
|
+
const args = request.params.arguments || {};
|
|
881
|
+
const comparison = taskRunner.compareTemplateRuns(args.template_id, {
|
|
882
|
+
limit: args.limit ?? 8,
|
|
883
|
+
});
|
|
884
|
+
if (!comparison.template) {
|
|
885
|
+
throw new Error(`Template not found: ${args.template_id}`);
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
content: [{ type: "text", text: JSON.stringify(comparison, null, 2) }],
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
case "browser_stop": {
|
|
893
|
+
const result = await runtime.stop();
|
|
894
|
+
if (result.alreadyStopped) {
|
|
895
|
+
return { content: [{ type: "text", text: "Browser already stopped." }] };
|
|
896
|
+
}
|
|
897
|
+
return { content: [{ type: "text", text: `Browser stopped. Session saved. Trace: ${result.artifacts?.tracePath}` }] };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
case "browser_cdp_health": {
|
|
901
|
+
const cdpEndpoint = request.params.arguments?.cdp_endpoint;
|
|
902
|
+
const timeoutMs = request.params.arguments?.timeout_ms ?? 3000;
|
|
903
|
+
const health = await runtime.getCdpHealth({
|
|
904
|
+
endpoint: cdpEndpoint,
|
|
905
|
+
timeoutMs,
|
|
906
|
+
});
|
|
907
|
+
return {
|
|
908
|
+
content: [{ type: "text", text: JSON.stringify(health, null, 2) }],
|
|
909
|
+
isError: !health.ok,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
case "browser_text_layout_audit": {
|
|
914
|
+
const audit = await runtime.auditTextLayout({
|
|
915
|
+
limit: request.params.arguments?.limit ?? 80,
|
|
916
|
+
selectors: request.params.arguments?.selectors,
|
|
917
|
+
overflowThreshold: request.params.arguments?.overflow_threshold,
|
|
918
|
+
});
|
|
919
|
+
return {
|
|
920
|
+
content: [{ type: "text", text: JSON.stringify(audit, null, 2) }],
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
case "browser_attach_diagnostics": {
|
|
925
|
+
const diagnostics = await runtime.getCdpDiagnostics({
|
|
926
|
+
endpoint: request.params.arguments?.cdp_endpoint,
|
|
927
|
+
timeoutMs: request.params.arguments?.timeout_ms ?? 3000,
|
|
928
|
+
});
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: "text", text: JSON.stringify(diagnostics, null, 2) }],
|
|
931
|
+
isError: !diagnostics.ok,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
case "browser_evaluate": {
|
|
936
|
+
const expression = request.params.arguments.expression;
|
|
937
|
+
if (!expression) {
|
|
938
|
+
throw new Error("'expression' is required.");
|
|
939
|
+
}
|
|
940
|
+
const result = await runtime.evaluate(expression);
|
|
941
|
+
return {
|
|
942
|
+
content: [{ type: "text", text: result.result }],
|
|
943
|
+
isError: result.isError,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
case "browser_select_option": {
|
|
948
|
+
const { id, values } = request.params.arguments;
|
|
949
|
+
const result = await runtime.selectOption(id, values);
|
|
950
|
+
return { content: [{ type: "text", text: `Selected option(s) ${JSON.stringify(values)} in element ${id}` }] };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
case "browser_handle_dialog": {
|
|
954
|
+
const action = request.params.arguments?.action || "accept";
|
|
955
|
+
const promptText = request.params.arguments?.promptText || "";
|
|
956
|
+
const result = await runtime.handleDialog({ action, promptText });
|
|
957
|
+
if (!result.handled) {
|
|
958
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
959
|
+
}
|
|
960
|
+
return { content: [{ type: "text", text: `Dialog ${result.action}: type=${result.dialogInfo.type}, message="${result.dialogInfo.message}"` }] };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
case "browser_navigate_back": {
|
|
964
|
+
const result = await runtime.goBack();
|
|
965
|
+
return { content: [{ type: "text", text: `Navigated back to ${result.url}` }] };
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
case "browser_tabs": {
|
|
969
|
+
const tabAction = request.params.arguments.action;
|
|
970
|
+
const tabIndex = request.params.arguments.index;
|
|
971
|
+
const tabUrl = request.params.arguments.url;
|
|
972
|
+
const result = await runtime.tabAction(tabAction, { index: tabIndex, url: tabUrl });
|
|
973
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
default:
|
|
977
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
978
|
+
}
|
|
979
|
+
} catch (error) {
|
|
980
|
+
return {
|
|
981
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
982
|
+
isError: true,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// -----------------------------------------------------------------
|
|
988
|
+
// 5. 启动 MCP Server 管道
|
|
989
|
+
// -----------------------------------------------------------------
|
|
990
|
+
async function main() {
|
|
991
|
+
const transport = new StdioServerTransport();
|
|
992
|
+
await server.connect(transport);
|
|
993
|
+
console.error("Browser Automation MCP Server running on stdio");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
main().catch((error) => {
|
|
997
|
+
console.error("Server error:", error);
|
|
998
|
+
process.exit(1);
|
|
999
|
+
});
|