@wong2kim/wmux 1.1.0 → 1.1.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/README.md +14 -4
- package/dist/cli/cli/commands/browser.js +101 -77
- package/dist/cli/cli/index.js +6 -6
- package/dist/cli/shared/constants.js +3 -0
- package/dist/cli/shared/rpc.js +15 -4
- package/dist/mcp/mcp/index.js +41 -21
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +186 -0
- package/dist/mcp/mcp/playwright/anti-detection.js +58 -0
- package/dist/mcp/mcp/playwright/dom-intelligence.js +171 -0
- package/dist/mcp/mcp/playwright/human-typing.js +48 -0
- package/dist/mcp/mcp/playwright/markdown-extractor.js +520 -0
- package/dist/mcp/mcp/playwright/snapshot.js +261 -0
- package/dist/mcp/mcp/playwright/tools/extraction.js +143 -0
- package/dist/mcp/mcp/playwright/tools/file.js +274 -0
- package/dist/mcp/mcp/playwright/tools/inspection.js +395 -0
- package/dist/mcp/mcp/playwright/tools/interaction.js +387 -0
- package/dist/mcp/mcp/playwright/tools/navigation.js +183 -0
- package/dist/mcp/mcp/playwright/tools/state.js +410 -0
- package/dist/mcp/mcp/playwright/tools/utility.js +167 -0
- package/dist/mcp/mcp/playwright/tools/wait.js +111 -0
- package/dist/mcp/shared/constants.js +3 -0
- package/dist/mcp/shared/rpc.js +15 -4
- package/package.json +7 -4
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInteractionTools = registerInteractionTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const PlaywrightEngine_1 = require("../PlaywrightEngine");
|
|
6
|
+
const snapshot_1 = require("../snapshot");
|
|
7
|
+
const dom_intelligence_1 = require("../dom-intelligence");
|
|
8
|
+
const human_typing_1 = require("../human-typing");
|
|
9
|
+
// Optional surfaceId schema reused across tools
|
|
10
|
+
const optionalSurfaceId = zod_1.z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Target a specific surface by ID. Omit to use the active surface.');
|
|
14
|
+
const REF_NOT_FOUND_HINT = 'Element with ref={ref} not found. Run browser_snapshot to get current refs.';
|
|
15
|
+
function refNotFound(ref) {
|
|
16
|
+
return REF_NOT_FOUND_HINT.replace('{ref}', ref);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register interaction-related MCP tools on the given server.
|
|
20
|
+
*
|
|
21
|
+
* Tools:
|
|
22
|
+
* - browser_click — click or double-click an element
|
|
23
|
+
* - browser_type — type text into an element
|
|
24
|
+
* - browser_fill — fill multiple form fields at once
|
|
25
|
+
* - browser_press_key — press a keyboard key
|
|
26
|
+
* - browser_hover — hover over an element
|
|
27
|
+
* - browser_drag — drag from source to target element
|
|
28
|
+
* - browser_select — select option(s) in a <select>
|
|
29
|
+
* - browser_scroll_into_view — scroll element into viewport
|
|
30
|
+
*/
|
|
31
|
+
function registerInteractionTools(server) {
|
|
32
|
+
const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
|
|
33
|
+
// -----------------------------------------------------------------------
|
|
34
|
+
// browser_click
|
|
35
|
+
// -----------------------------------------------------------------------
|
|
36
|
+
server.tool('browser_click', 'Click an element identified by its ref number from the accessibility snapshot, or by a smartRef from browser_smart_snapshot.', {
|
|
37
|
+
ref: zod_1.z.string().optional().describe('Element ref number from browser_snapshot'),
|
|
38
|
+
smartRef: zod_1.z
|
|
39
|
+
.number()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Element ref number from browser_smart_snapshot (dom-intelligence). If provided, takes priority over ref.'),
|
|
42
|
+
double: zod_1.z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('If true, perform a double-click instead of a single click.'),
|
|
46
|
+
surfaceId: optionalSurfaceId,
|
|
47
|
+
}, async ({ ref, smartRef, double, surfaceId }) => {
|
|
48
|
+
try {
|
|
49
|
+
const page = await engine.getPage(surfaceId);
|
|
50
|
+
if (!page) {
|
|
51
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
52
|
+
}
|
|
53
|
+
if (smartRef !== undefined) {
|
|
54
|
+
// Use dom-intelligence ref resolution
|
|
55
|
+
const selector = (0, dom_intelligence_1.getLocatorByRef)(smartRef);
|
|
56
|
+
if (!selector) {
|
|
57
|
+
throw new Error(`Element with smartRef=${smartRef} not found. Run browser_smart_snapshot to get current refs.`);
|
|
58
|
+
}
|
|
59
|
+
const locator = page.locator(selector);
|
|
60
|
+
if (double) {
|
|
61
|
+
await locator.dblclick();
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await locator.click();
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Clicked${double ? ' (double)' : ''} element smartRef=${smartRef}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!ref) {
|
|
76
|
+
throw new Error('Either ref or smartRef must be provided.');
|
|
77
|
+
}
|
|
78
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
79
|
+
if (!el) {
|
|
80
|
+
throw new Error(refNotFound(ref));
|
|
81
|
+
}
|
|
82
|
+
if (double) {
|
|
83
|
+
await el.dblclick();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await el.click();
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `Clicked${double ? ' (double)' : ''} element ref=${ref}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: message }],
|
|
101
|
+
isError: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// -----------------------------------------------------------------------
|
|
106
|
+
// browser_type
|
|
107
|
+
// -----------------------------------------------------------------------
|
|
108
|
+
server.tool('browser_type', 'Type text into an element identified by its ref number.', {
|
|
109
|
+
ref: zod_1.z.string().describe('Element ref number from browser_snapshot'),
|
|
110
|
+
text: zod_1.z.string().describe('Text to type into the element'),
|
|
111
|
+
submit: zod_1.z
|
|
112
|
+
.boolean()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('If true, press Enter after typing.'),
|
|
115
|
+
humanlike: zod_1.z
|
|
116
|
+
.boolean()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('If true, type with randomised human-like delays.'),
|
|
119
|
+
surfaceId: optionalSurfaceId,
|
|
120
|
+
}, async ({ ref, text, submit, humanlike, surfaceId }) => {
|
|
121
|
+
try {
|
|
122
|
+
const page = await engine.getPage(surfaceId);
|
|
123
|
+
if (!page) {
|
|
124
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
125
|
+
}
|
|
126
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
127
|
+
if (!el) {
|
|
128
|
+
throw new Error(refNotFound(ref));
|
|
129
|
+
}
|
|
130
|
+
if (humanlike) {
|
|
131
|
+
// Focus the element first, then use human-like typing via keyboard
|
|
132
|
+
await el.click();
|
|
133
|
+
await (0, human_typing_1.typeHumanlike)(page, '', text);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await el.fill(text);
|
|
137
|
+
}
|
|
138
|
+
if (submit) {
|
|
139
|
+
await page.keyboard.press('Enter');
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: `Typed "${text}" into element ref=${ref}${submit ? ' and submitted' : ''}`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: message }],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
// browser_fill
|
|
160
|
+
// -----------------------------------------------------------------------
|
|
161
|
+
server.tool('browser_fill', 'Fill multiple form fields at once. Each field is identified by a ref number.', {
|
|
162
|
+
fields: zod_1.z
|
|
163
|
+
.array(zod_1.z.object({
|
|
164
|
+
ref: zod_1.z.string().describe('Element ref number'),
|
|
165
|
+
value: zod_1.z.string().describe('Value to fill'),
|
|
166
|
+
}))
|
|
167
|
+
.describe('Array of {ref, value} pairs to fill'),
|
|
168
|
+
surfaceId: optionalSurfaceId,
|
|
169
|
+
}, async ({ fields, surfaceId }) => {
|
|
170
|
+
try {
|
|
171
|
+
const page = await engine.getPage(surfaceId);
|
|
172
|
+
if (!page) {
|
|
173
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
174
|
+
}
|
|
175
|
+
let filled = 0;
|
|
176
|
+
const errors = [];
|
|
177
|
+
for (const field of fields) {
|
|
178
|
+
const el = await (0, snapshot_1.resolveRef)(page, field.ref);
|
|
179
|
+
if (!el) {
|
|
180
|
+
errors.push(refNotFound(field.ref));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
await el.fill(field.value);
|
|
184
|
+
filled++;
|
|
185
|
+
}
|
|
186
|
+
let resultText = `Filled ${filled}/${fields.length} field(s).`;
|
|
187
|
+
if (errors.length > 0) {
|
|
188
|
+
resultText += '\nErrors:\n' + errors.join('\n');
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: 'text', text: resultText }],
|
|
192
|
+
...(errors.length > 0 && filled === 0 ? { isError: true } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: 'text', text: message }],
|
|
199
|
+
isError: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
// browser_press_key
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
server.tool('browser_press_key', 'Press a keyboard key (e.g. Enter, Tab, Escape, ArrowDown, Control+a).', {
|
|
207
|
+
key: zod_1.z
|
|
208
|
+
.string()
|
|
209
|
+
.describe('Key to press. Examples: Enter, Tab, Escape, ArrowDown, Control+a, Meta+c'),
|
|
210
|
+
surfaceId: optionalSurfaceId,
|
|
211
|
+
}, async ({ key, surfaceId }) => {
|
|
212
|
+
try {
|
|
213
|
+
const page = await engine.getPage(surfaceId);
|
|
214
|
+
if (!page) {
|
|
215
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
216
|
+
}
|
|
217
|
+
await page.keyboard.press(key);
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: 'text', text: `Pressed key: ${key}` }],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: 'text', text: message }],
|
|
226
|
+
isError: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// -----------------------------------------------------------------------
|
|
231
|
+
// browser_hover
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
server.tool('browser_hover', 'Hover over an element identified by its ref number.', {
|
|
234
|
+
ref: zod_1.z.string().describe('Element ref number from browser_snapshot'),
|
|
235
|
+
surfaceId: optionalSurfaceId,
|
|
236
|
+
}, async ({ ref, surfaceId }) => {
|
|
237
|
+
try {
|
|
238
|
+
const page = await engine.getPage(surfaceId);
|
|
239
|
+
if (!page) {
|
|
240
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
241
|
+
}
|
|
242
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
243
|
+
if (!el) {
|
|
244
|
+
throw new Error(refNotFound(ref));
|
|
245
|
+
}
|
|
246
|
+
await el.hover();
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{ type: 'text', text: `Hovered over element ref=${ref}` },
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: 'text', text: message }],
|
|
257
|
+
isError: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
// -----------------------------------------------------------------------
|
|
262
|
+
// browser_drag
|
|
263
|
+
// -----------------------------------------------------------------------
|
|
264
|
+
server.tool('browser_drag', 'Drag an element from sourceRef to targetRef.', {
|
|
265
|
+
sourceRef: zod_1.z
|
|
266
|
+
.string()
|
|
267
|
+
.describe('Ref number of the element to drag from'),
|
|
268
|
+
targetRef: zod_1.z.string().describe('Ref number of the element to drop onto'),
|
|
269
|
+
surfaceId: optionalSurfaceId,
|
|
270
|
+
}, async ({ sourceRef, targetRef, surfaceId }) => {
|
|
271
|
+
try {
|
|
272
|
+
const page = await engine.getPage(surfaceId);
|
|
273
|
+
if (!page) {
|
|
274
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
275
|
+
}
|
|
276
|
+
const sourceEl = await (0, snapshot_1.resolveRef)(page, sourceRef);
|
|
277
|
+
if (!sourceEl) {
|
|
278
|
+
throw new Error(refNotFound(sourceRef));
|
|
279
|
+
}
|
|
280
|
+
const targetEl = await (0, snapshot_1.resolveRef)(page, targetRef);
|
|
281
|
+
if (!targetEl) {
|
|
282
|
+
throw new Error(refNotFound(targetRef));
|
|
283
|
+
}
|
|
284
|
+
// Get bounding boxes for source and target
|
|
285
|
+
const sourceBox = await sourceEl.boundingBox();
|
|
286
|
+
const targetBox = await targetEl.boundingBox();
|
|
287
|
+
if (!sourceBox || !targetBox) {
|
|
288
|
+
throw new Error('Could not determine bounding box for source or target element.');
|
|
289
|
+
}
|
|
290
|
+
// Perform drag from center of source to center of target
|
|
291
|
+
const sourceX = sourceBox.x + sourceBox.width / 2;
|
|
292
|
+
const sourceY = sourceBox.y + sourceBox.height / 2;
|
|
293
|
+
const targetX = targetBox.x + targetBox.width / 2;
|
|
294
|
+
const targetY = targetBox.y + targetBox.height / 2;
|
|
295
|
+
await page.mouse.move(sourceX, sourceY);
|
|
296
|
+
await page.mouse.down();
|
|
297
|
+
await page.mouse.move(targetX, targetY, { steps: 10 });
|
|
298
|
+
await page.mouse.up();
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: 'text',
|
|
303
|
+
text: `Dragged element ref=${sourceRef} to ref=${targetRef}`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: 'text', text: message }],
|
|
312
|
+
isError: true,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
// browser_select
|
|
318
|
+
// -----------------------------------------------------------------------
|
|
319
|
+
server.tool('browser_select', 'Select option(s) in a <select> element by value.', {
|
|
320
|
+
ref: zod_1.z.string().describe('Element ref number of the <select>'),
|
|
321
|
+
values: zod_1.z
|
|
322
|
+
.array(zod_1.z.string())
|
|
323
|
+
.describe('Array of option values to select'),
|
|
324
|
+
surfaceId: optionalSurfaceId,
|
|
325
|
+
}, async ({ ref, values, surfaceId }) => {
|
|
326
|
+
try {
|
|
327
|
+
const page = await engine.getPage(surfaceId);
|
|
328
|
+
if (!page) {
|
|
329
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
330
|
+
}
|
|
331
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
332
|
+
if (!el) {
|
|
333
|
+
throw new Error(refNotFound(ref));
|
|
334
|
+
}
|
|
335
|
+
await el.selectOption(values);
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: 'text',
|
|
340
|
+
text: `Selected value(s) [${values.join(', ')}] in element ref=${ref}`,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: 'text', text: message }],
|
|
349
|
+
isError: true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
// -----------------------------------------------------------------------
|
|
354
|
+
// browser_scroll_into_view
|
|
355
|
+
// -----------------------------------------------------------------------
|
|
356
|
+
server.tool('browser_scroll_into_view', 'Scroll an element into the visible viewport.', {
|
|
357
|
+
ref: zod_1.z.string().describe('Element ref number from browser_snapshot'),
|
|
358
|
+
surfaceId: optionalSurfaceId,
|
|
359
|
+
}, async ({ ref, surfaceId }) => {
|
|
360
|
+
try {
|
|
361
|
+
const page = await engine.getPage(surfaceId);
|
|
362
|
+
if (!page) {
|
|
363
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
364
|
+
}
|
|
365
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
366
|
+
if (!el) {
|
|
367
|
+
throw new Error(refNotFound(ref));
|
|
368
|
+
}
|
|
369
|
+
await el.scrollIntoViewIfNeeded();
|
|
370
|
+
return {
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: 'text',
|
|
374
|
+
text: `Scrolled element ref=${ref} into view`,
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
+
return {
|
|
382
|
+
content: [{ type: 'text', text: message }],
|
|
383
|
+
isError: true,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerNavigationTools = registerNavigationTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const PlaywrightEngine_1 = require("../PlaywrightEngine");
|
|
6
|
+
// Optional surfaceId schema reused across tools
|
|
7
|
+
const optionalSurfaceId = zod_1.z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Target a specific surface by ID. Omit to use the active surface.');
|
|
11
|
+
/**
|
|
12
|
+
* Register navigation-related MCP tools on the given server.
|
|
13
|
+
*
|
|
14
|
+
* Tools:
|
|
15
|
+
* - browser_navigate — navigate to a URL
|
|
16
|
+
* - browser_navigate_back — go back in history
|
|
17
|
+
* - browser_tabs — list / new / select / close tabs
|
|
18
|
+
*/
|
|
19
|
+
function registerNavigationTools(server) {
|
|
20
|
+
const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
|
|
21
|
+
// -----------------------------------------------------------------------
|
|
22
|
+
// browser_navigate
|
|
23
|
+
// -----------------------------------------------------------------------
|
|
24
|
+
server.tool('browser_navigate', 'Navigate the browser page to a URL. Returns the final URL after navigation.', {
|
|
25
|
+
url: zod_1.z.string().describe('The URL to navigate to'),
|
|
26
|
+
surfaceId: optionalSurfaceId,
|
|
27
|
+
}, async ({ url, surfaceId }) => {
|
|
28
|
+
try {
|
|
29
|
+
const page = await engine.getPage(surfaceId);
|
|
30
|
+
if (!page) {
|
|
31
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
32
|
+
}
|
|
33
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
34
|
+
const finalUrl = page.url();
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: `Navigated to ${finalUrl}` }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: message }],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// -----------------------------------------------------------------------
|
|
48
|
+
// browser_navigate_back
|
|
49
|
+
// -----------------------------------------------------------------------
|
|
50
|
+
server.tool('browser_navigate_back', 'Go back in browser history. Returns the current URL after going back.', {
|
|
51
|
+
surfaceId: optionalSurfaceId,
|
|
52
|
+
}, async ({ surfaceId }) => {
|
|
53
|
+
try {
|
|
54
|
+
const page = await engine.getPage(surfaceId);
|
|
55
|
+
if (!page) {
|
|
56
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
57
|
+
}
|
|
58
|
+
await page.goBack();
|
|
59
|
+
const currentUrl = page.url();
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: `Navigated back to ${currentUrl}` }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: message }],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// -----------------------------------------------------------------------
|
|
73
|
+
// browser_tabs
|
|
74
|
+
// -----------------------------------------------------------------------
|
|
75
|
+
server.tool('browser_tabs', 'Manage browser tabs: list all tabs, open a new tab, select a tab, or close a tab.', {
|
|
76
|
+
action: zod_1.z
|
|
77
|
+
.enum(['list', 'new', 'select', 'close'])
|
|
78
|
+
.optional()
|
|
79
|
+
.describe('Action to perform. Defaults to "list".'),
|
|
80
|
+
tabId: zod_1.z
|
|
81
|
+
.number()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Tab index (0-based) for "select" or "close" actions.'),
|
|
84
|
+
url: zod_1.z
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('URL to open when action is "new".'),
|
|
88
|
+
}, async ({ action, tabId, url }) => {
|
|
89
|
+
try {
|
|
90
|
+
const browser = await engine.getBrowser();
|
|
91
|
+
if (!browser) {
|
|
92
|
+
throw new Error('No browser connected. Call browser_open first.');
|
|
93
|
+
}
|
|
94
|
+
const resolvedAction = action ?? 'list';
|
|
95
|
+
// Collect all pages across all contexts
|
|
96
|
+
const contexts = browser.contexts();
|
|
97
|
+
const allPages = contexts.flatMap((ctx) => ctx.pages());
|
|
98
|
+
switch (resolvedAction) {
|
|
99
|
+
case 'list': {
|
|
100
|
+
const tabList = allPages.map((p, i) => ({
|
|
101
|
+
tabId: i,
|
|
102
|
+
url: p.url(),
|
|
103
|
+
title: '', // title requires async; filled below
|
|
104
|
+
}));
|
|
105
|
+
// Populate titles
|
|
106
|
+
for (let i = 0; i < allPages.length; i++) {
|
|
107
|
+
try {
|
|
108
|
+
tabList[i].title = await allPages[i].title();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
tabList[i].title = '(unknown)';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: JSON.stringify(tabList, null, 2),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case 'new': {
|
|
124
|
+
// Use the first context, or fail
|
|
125
|
+
const context = contexts[0];
|
|
126
|
+
if (!context) {
|
|
127
|
+
throw new Error('No browser context available.');
|
|
128
|
+
}
|
|
129
|
+
const newPage = await context.newPage();
|
|
130
|
+
if (url) {
|
|
131
|
+
await newPage.goto(url, { waitUntil: 'domcontentloaded' });
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: `Opened new tab (index ${allPages.length}) at ${newPage.url()}`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case 'select': {
|
|
143
|
+
if (tabId === undefined || tabId < 0 || tabId >= allPages.length) {
|
|
144
|
+
throw new Error(`Invalid tabId=${tabId}. Available tabs: 0-${allPages.length - 1}`);
|
|
145
|
+
}
|
|
146
|
+
await allPages[tabId].bringToFront();
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: 'text',
|
|
151
|
+
text: `Selected tab ${tabId}: ${allPages[tabId].url()}`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
case 'close': {
|
|
157
|
+
if (tabId === undefined || tabId < 0 || tabId >= allPages.length) {
|
|
158
|
+
throw new Error(`Invalid tabId=${tabId}. Available tabs: 0-${allPages.length - 1}`);
|
|
159
|
+
}
|
|
160
|
+
const closedUrl = allPages[tabId].url();
|
|
161
|
+
await allPages[tabId].close();
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: `Closed tab ${tabId}: ${closedUrl}`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
default:
|
|
172
|
+
throw new Error(`Unknown action: ${resolvedAction}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: 'text', text: message }],
|
|
179
|
+
isError: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|