@wong2kim/wmux 1.1.1 → 2.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/README.md +76 -27
- 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 +21 -4
- package/dist/cli/shared/types.js +108 -4
- package/dist/mcp/mcp/index.js +41 -21
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +293 -0
- package/dist/mcp/mcp/playwright/anti-detection.js +63 -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/security.js +29 -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 +459 -0
- package/dist/mcp/mcp/playwright/tools/interaction.js +457 -0
- package/dist/mcp/mcp/playwright/tools/navigation.js +206 -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 +119 -0
- package/dist/mcp/shared/constants.js +3 -0
- package/dist/mcp/shared/rpc.js +21 -4
- package/dist/mcp/shared/types.js +108 -4
- package/package.json +11 -7
|
@@ -0,0 +1,457 @@
|
|
|
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
|
+
const wmux_client_1 = require("../../wmux-client");
|
|
10
|
+
// Optional surfaceId schema reused across tools
|
|
11
|
+
const optionalSurfaceId = zod_1.z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Target a specific surface by ID. Omit to use the active surface.');
|
|
15
|
+
const REF_NOT_FOUND_HINT = 'Element with ref={ref} not found. Run browser_snapshot to get current refs.';
|
|
16
|
+
function refNotFound(ref) {
|
|
17
|
+
return REF_NOT_FOUND_HINT.replace('{ref}', ref);
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// RPC-based interaction helpers (used when Playwright page is unavailable)
|
|
21
|
+
// These resolve elements via data-wmux-ref attributes set by browser_snapshot.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
async function rpcEval(expression, surfaceId) {
|
|
24
|
+
const result = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
25
|
+
expression,
|
|
26
|
+
...(surfaceId && { surfaceId }),
|
|
27
|
+
});
|
|
28
|
+
return result.value;
|
|
29
|
+
}
|
|
30
|
+
async function rpcClick(ref, surfaceId, _double) {
|
|
31
|
+
// Use CDP click: first get element coordinates via JS, then dispatch mouse events
|
|
32
|
+
await (0, wmux_client_1.sendRpc)('browser.click.cdp', {
|
|
33
|
+
selector: `[data-wmux-ref="${ref}"]`,
|
|
34
|
+
...(surfaceId && { surfaceId }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function rpcFill(ref, value, surfaceId) {
|
|
38
|
+
// Click on the element first to focus it
|
|
39
|
+
await rpcClick(ref, surfaceId);
|
|
40
|
+
// Small delay for focus
|
|
41
|
+
await new Promise(r => setTimeout(r, 100));
|
|
42
|
+
// Select all existing text
|
|
43
|
+
await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
44
|
+
expression: `document.execCommand('selectAll')`,
|
|
45
|
+
...(surfaceId && { surfaceId }),
|
|
46
|
+
});
|
|
47
|
+
// Type the new value via CDP Input.insertText (handles CJK, React controlled inputs)
|
|
48
|
+
await (0, wmux_client_1.sendRpc)('browser.type.cdp', {
|
|
49
|
+
text: value,
|
|
50
|
+
...(surfaceId && { surfaceId }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function rpcPressKey(key, surfaceId) {
|
|
54
|
+
await (0, wmux_client_1.sendRpc)('browser.press.cdp', {
|
|
55
|
+
key,
|
|
56
|
+
...(surfaceId && { surfaceId }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register interaction-related MCP tools on the given server.
|
|
61
|
+
*
|
|
62
|
+
* Tools:
|
|
63
|
+
* - browser_click — click or double-click an element
|
|
64
|
+
* - browser_type — type text into an element
|
|
65
|
+
* - browser_fill — fill multiple form fields at once
|
|
66
|
+
* - browser_press_key — press a keyboard key
|
|
67
|
+
* - browser_hover — hover over an element
|
|
68
|
+
* - browser_drag — drag from source to target element
|
|
69
|
+
* - browser_select — select option(s) in a <select>
|
|
70
|
+
* - browser_scroll_into_view — scroll element into viewport
|
|
71
|
+
*/
|
|
72
|
+
function registerInteractionTools(server) {
|
|
73
|
+
const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
|
|
74
|
+
// -----------------------------------------------------------------------
|
|
75
|
+
// browser_click
|
|
76
|
+
// -----------------------------------------------------------------------
|
|
77
|
+
server.tool('browser_click', 'Click an element identified by its ref number from the accessibility snapshot, or by a smartRef from browser_smart_snapshot.', {
|
|
78
|
+
ref: zod_1.z.string().optional().describe('Element ref number from browser_snapshot'),
|
|
79
|
+
smartRef: zod_1.z
|
|
80
|
+
.number()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Element ref number from browser_smart_snapshot (dom-intelligence). If provided, takes priority over ref.'),
|
|
83
|
+
double: zod_1.z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('If true, perform a double-click instead of a single click.'),
|
|
87
|
+
surfaceId: optionalSurfaceId,
|
|
88
|
+
}, async ({ ref, smartRef, double, surfaceId }) => {
|
|
89
|
+
try {
|
|
90
|
+
// Try Playwright first
|
|
91
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
92
|
+
if (page) {
|
|
93
|
+
if (smartRef !== undefined) {
|
|
94
|
+
const selector = (0, dom_intelligence_1.getLocatorByRef)(smartRef);
|
|
95
|
+
if (!selector) {
|
|
96
|
+
throw new Error(`Element with smartRef=${smartRef} not found. Run browser_smart_snapshot to get current refs.`);
|
|
97
|
+
}
|
|
98
|
+
const locator = page.locator(selector);
|
|
99
|
+
if (double)
|
|
100
|
+
await locator.dblclick();
|
|
101
|
+
else
|
|
102
|
+
await locator.click();
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: 'text', text: `Clicked${double ? ' (double)' : ''} element smartRef=${smartRef}` }],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!ref)
|
|
108
|
+
throw new Error('Either ref or smartRef must be provided.');
|
|
109
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
110
|
+
if (!el)
|
|
111
|
+
throw new Error(refNotFound(ref));
|
|
112
|
+
if (double)
|
|
113
|
+
await el.dblclick();
|
|
114
|
+
else
|
|
115
|
+
await el.click();
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: 'text', text: `Clicked${double ? ' (double)' : ''} element ref=${ref}` }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// RPC fallback
|
|
121
|
+
if (!ref && smartRef === undefined)
|
|
122
|
+
throw new Error('Either ref or smartRef must be provided.');
|
|
123
|
+
const resolvedRef = ref ?? String(smartRef);
|
|
124
|
+
await rpcClick(resolvedRef, surfaceId, double);
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: 'text', text: `Clicked${double ? ' (double)' : ''} element ref=${resolvedRef}` }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: message }],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// -----------------------------------------------------------------------
|
|
138
|
+
// browser_type
|
|
139
|
+
// -----------------------------------------------------------------------
|
|
140
|
+
server.tool('browser_type', 'Type text into an element identified by its ref number.', {
|
|
141
|
+
ref: zod_1.z.string().describe('Element ref number from browser_snapshot'),
|
|
142
|
+
text: zod_1.z.string().describe('Text to type into the element'),
|
|
143
|
+
submit: zod_1.z
|
|
144
|
+
.boolean()
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('If true, press Enter after typing.'),
|
|
147
|
+
humanlike: zod_1.z
|
|
148
|
+
.boolean()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('If true, type with randomised human-like delays.'),
|
|
151
|
+
surfaceId: optionalSurfaceId,
|
|
152
|
+
}, async ({ ref, text, submit, humanlike, surfaceId }) => {
|
|
153
|
+
try {
|
|
154
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
155
|
+
if (page) {
|
|
156
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
157
|
+
if (!el)
|
|
158
|
+
throw new Error(refNotFound(ref));
|
|
159
|
+
if (humanlike) {
|
|
160
|
+
await el.click();
|
|
161
|
+
await (0, human_typing_1.typeHumanlike)(page, '', text);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await el.fill(text);
|
|
165
|
+
}
|
|
166
|
+
if (submit)
|
|
167
|
+
await page.keyboard.press('Enter');
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// RPC fallback
|
|
171
|
+
await rpcFill(ref, text, surfaceId);
|
|
172
|
+
if (submit)
|
|
173
|
+
await rpcPressKey('Enter', surfaceId);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: `Typed "${text}" into element ref=${ref}${submit ? ' and submitted' : ''}`,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: 'text', text: message }],
|
|
188
|
+
isError: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// -----------------------------------------------------------------------
|
|
193
|
+
// browser_fill
|
|
194
|
+
// -----------------------------------------------------------------------
|
|
195
|
+
server.tool('browser_fill', 'Fill multiple form fields at once. Each field is identified by a ref number.', {
|
|
196
|
+
fields: zod_1.z
|
|
197
|
+
.array(zod_1.z.object({
|
|
198
|
+
ref: zod_1.z.string().describe('Element ref number'),
|
|
199
|
+
value: zod_1.z.string().describe('Value to fill'),
|
|
200
|
+
}))
|
|
201
|
+
.describe('Array of {ref, value} pairs to fill'),
|
|
202
|
+
surfaceId: optionalSurfaceId,
|
|
203
|
+
}, async ({ fields, surfaceId }) => {
|
|
204
|
+
try {
|
|
205
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
206
|
+
let filled = 0;
|
|
207
|
+
const errors = [];
|
|
208
|
+
for (const field of fields) {
|
|
209
|
+
try {
|
|
210
|
+
if (page) {
|
|
211
|
+
const el = await (0, snapshot_1.resolveRef)(page, field.ref);
|
|
212
|
+
if (!el) {
|
|
213
|
+
errors.push(refNotFound(field.ref));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
await el.fill(field.value);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await rpcFill(field.ref, field.value, surfaceId);
|
|
220
|
+
}
|
|
221
|
+
filled++;
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
let resultText = `Filled ${filled}/${fields.length} field(s).`;
|
|
228
|
+
if (errors.length > 0) {
|
|
229
|
+
resultText += '\nErrors:\n' + errors.join('\n');
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text', text: resultText }],
|
|
233
|
+
...(errors.length > 0 && filled === 0 ? { isError: true } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: 'text', text: message }],
|
|
240
|
+
isError: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
// browser_press_key
|
|
246
|
+
// -----------------------------------------------------------------------
|
|
247
|
+
server.tool('browser_press_key', 'Press a keyboard key (e.g. Enter, Tab, Escape, ArrowDown, Control+a).', {
|
|
248
|
+
key: zod_1.z
|
|
249
|
+
.string()
|
|
250
|
+
.describe('Key to press. Examples: Enter, Tab, Escape, ArrowDown, Control+a, Meta+c'),
|
|
251
|
+
surfaceId: optionalSurfaceId,
|
|
252
|
+
}, async ({ key, surfaceId }) => {
|
|
253
|
+
try {
|
|
254
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
255
|
+
if (page) {
|
|
256
|
+
await page.keyboard.press(key);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
await rpcPressKey(key, surfaceId);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: 'text', text: `Pressed key: ${key}` }],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: 'text', text: message }],
|
|
269
|
+
isError: true,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// -----------------------------------------------------------------------
|
|
274
|
+
// browser_hover
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
server.tool('browser_hover', 'Hover over an element identified by its ref number.', {
|
|
277
|
+
ref: zod_1.z.string().describe('Element ref number from browser_snapshot'),
|
|
278
|
+
surfaceId: optionalSurfaceId,
|
|
279
|
+
}, async ({ ref, surfaceId }) => {
|
|
280
|
+
try {
|
|
281
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
282
|
+
if (page) {
|
|
283
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
284
|
+
if (!el)
|
|
285
|
+
throw new Error(refNotFound(ref));
|
|
286
|
+
await el.hover();
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// RPC fallback: dispatch mouseover event
|
|
290
|
+
const val = await rpcEval(`(() => {
|
|
291
|
+
const el = document.querySelector('[data-wmux-ref="${ref}"]');
|
|
292
|
+
if (!el) return 'not_found';
|
|
293
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
294
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
295
|
+
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
296
|
+
return 'ok';
|
|
297
|
+
})()`, surfaceId);
|
|
298
|
+
if (val === 'not_found')
|
|
299
|
+
throw new Error(refNotFound(ref));
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: 'text', text: `Hovered over element ref=${ref}` }],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: 'text', text: message }],
|
|
309
|
+
isError: true,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// -----------------------------------------------------------------------
|
|
314
|
+
// browser_drag
|
|
315
|
+
// -----------------------------------------------------------------------
|
|
316
|
+
server.tool('browser_drag', 'Drag an element from sourceRef to targetRef.', {
|
|
317
|
+
sourceRef: zod_1.z
|
|
318
|
+
.string()
|
|
319
|
+
.describe('Ref number of the element to drag from'),
|
|
320
|
+
targetRef: zod_1.z.string().describe('Ref number of the element to drop onto'),
|
|
321
|
+
surfaceId: optionalSurfaceId,
|
|
322
|
+
}, async ({ sourceRef, targetRef, surfaceId }) => {
|
|
323
|
+
try {
|
|
324
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
325
|
+
if (page) {
|
|
326
|
+
const sourceEl = await (0, snapshot_1.resolveRef)(page, sourceRef);
|
|
327
|
+
if (!sourceEl)
|
|
328
|
+
throw new Error(refNotFound(sourceRef));
|
|
329
|
+
const targetEl = await (0, snapshot_1.resolveRef)(page, targetRef);
|
|
330
|
+
if (!targetEl)
|
|
331
|
+
throw new Error(refNotFound(targetRef));
|
|
332
|
+
const sourceBox = await sourceEl.boundingBox();
|
|
333
|
+
const targetBox = await targetEl.boundingBox();
|
|
334
|
+
if (!sourceBox || !targetBox) {
|
|
335
|
+
throw new Error('Could not determine bounding box for source or target element.');
|
|
336
|
+
}
|
|
337
|
+
const sourceX = sourceBox.x + sourceBox.width / 2;
|
|
338
|
+
const sourceY = sourceBox.y + sourceBox.height / 2;
|
|
339
|
+
const targetX = targetBox.x + targetBox.width / 2;
|
|
340
|
+
const targetY = targetBox.y + targetBox.height / 2;
|
|
341
|
+
await page.mouse.move(sourceX, sourceY);
|
|
342
|
+
await page.mouse.down();
|
|
343
|
+
await page.mouse.move(targetX, targetY, { steps: 10 });
|
|
344
|
+
await page.mouse.up();
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// RPC fallback: simplified drag via JS events
|
|
348
|
+
const val = await rpcEval(`(() => {
|
|
349
|
+
const src = document.querySelector('[data-wmux-ref="${sourceRef}"]');
|
|
350
|
+
const tgt = document.querySelector('[data-wmux-ref="${targetRef}"]');
|
|
351
|
+
if (!src) return 'source_not_found';
|
|
352
|
+
if (!tgt) return 'target_not_found';
|
|
353
|
+
const dt = new DataTransfer();
|
|
354
|
+
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer: dt }));
|
|
355
|
+
tgt.dispatchEvent(new DragEvent('dragover', { bubbles: true, dataTransfer: dt }));
|
|
356
|
+
tgt.dispatchEvent(new DragEvent('drop', { bubbles: true, dataTransfer: dt }));
|
|
357
|
+
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, dataTransfer: dt }));
|
|
358
|
+
return 'ok';
|
|
359
|
+
})()`, surfaceId);
|
|
360
|
+
if (val === 'source_not_found')
|
|
361
|
+
throw new Error(refNotFound(sourceRef));
|
|
362
|
+
if (val === 'target_not_found')
|
|
363
|
+
throw new Error(refNotFound(targetRef));
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
content: [{ type: 'text', text: `Dragged element ref=${sourceRef} to ref=${targetRef}` }],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
371
|
+
return {
|
|
372
|
+
content: [{ type: 'text', text: message }],
|
|
373
|
+
isError: true,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// -----------------------------------------------------------------------
|
|
378
|
+
// browser_select
|
|
379
|
+
// -----------------------------------------------------------------------
|
|
380
|
+
server.tool('browser_select', 'Select option(s) in a <select> element by value.', {
|
|
381
|
+
ref: zod_1.z.string().describe('Element ref number of the <select>'),
|
|
382
|
+
values: zod_1.z
|
|
383
|
+
.array(zod_1.z.string())
|
|
384
|
+
.describe('Array of option values to select'),
|
|
385
|
+
surfaceId: optionalSurfaceId,
|
|
386
|
+
}, async ({ ref, values, surfaceId }) => {
|
|
387
|
+
try {
|
|
388
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
389
|
+
if (page) {
|
|
390
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
391
|
+
if (!el)
|
|
392
|
+
throw new Error(refNotFound(ref));
|
|
393
|
+
await el.selectOption(values);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const escapedValues = JSON.stringify(values);
|
|
397
|
+
const val = await rpcEval(`(() => {
|
|
398
|
+
const el = document.querySelector('[data-wmux-ref="${ref}"]');
|
|
399
|
+
if (!el || el.tagName !== 'SELECT') return 'not_found';
|
|
400
|
+
const vals = ${escapedValues};
|
|
401
|
+
[...el.options].forEach(o => { o.selected = vals.includes(o.value); });
|
|
402
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
403
|
+
return 'ok';
|
|
404
|
+
})()`, surfaceId);
|
|
405
|
+
if (val === 'not_found')
|
|
406
|
+
throw new Error(refNotFound(ref));
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
content: [{ type: 'text', text: `Selected value(s) [${values.join(', ')}] in element ref=${ref}` }],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: 'text', text: message }],
|
|
416
|
+
isError: true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// -----------------------------------------------------------------------
|
|
421
|
+
// browser_scroll_into_view
|
|
422
|
+
// -----------------------------------------------------------------------
|
|
423
|
+
server.tool('browser_scroll_into_view', 'Scroll an element into the visible viewport.', {
|
|
424
|
+
ref: zod_1.z.string().describe('Element ref number from browser_snapshot'),
|
|
425
|
+
surfaceId: optionalSurfaceId,
|
|
426
|
+
}, async ({ ref, surfaceId }) => {
|
|
427
|
+
try {
|
|
428
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
429
|
+
if (page) {
|
|
430
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
431
|
+
if (!el)
|
|
432
|
+
throw new Error(refNotFound(ref));
|
|
433
|
+
await el.scrollIntoViewIfNeeded();
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const val = await rpcEval(`(() => {
|
|
437
|
+
const el = document.querySelector('[data-wmux-ref="${ref}"]');
|
|
438
|
+
if (!el) return 'not_found';
|
|
439
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
440
|
+
return 'ok';
|
|
441
|
+
})()`, surfaceId);
|
|
442
|
+
if (val === 'not_found')
|
|
443
|
+
throw new Error(refNotFound(ref));
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
content: [{ type: 'text', text: `Scrolled element ref=${ref} into view` }],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
451
|
+
return {
|
|
452
|
+
content: [{ type: 'text', text: message }],
|
|
453
|
+
isError: true,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
const types_1 = require("../../../shared/types");
|
|
7
|
+
const wmux_client_1 = require("../../wmux-client");
|
|
8
|
+
// Optional surfaceId schema reused across tools
|
|
9
|
+
const optionalSurfaceId = zod_1.z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Target a specific surface by ID. Omit to use the active surface.');
|
|
13
|
+
/**
|
|
14
|
+
* Register navigation-related MCP tools on the given server.
|
|
15
|
+
*
|
|
16
|
+
* Tools:
|
|
17
|
+
* - browser_navigate — navigate to a URL
|
|
18
|
+
* - browser_navigate_back — go back in history
|
|
19
|
+
* - browser_tabs — list / new / select / close tabs
|
|
20
|
+
*/
|
|
21
|
+
function registerNavigationTools(server) {
|
|
22
|
+
const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
|
|
23
|
+
// -----------------------------------------------------------------------
|
|
24
|
+
// browser_navigate
|
|
25
|
+
// -----------------------------------------------------------------------
|
|
26
|
+
server.tool('browser_navigate', 'Navigate the browser page to a URL. Returns the final URL after navigation.', {
|
|
27
|
+
url: zod_1.z.string().describe('The URL to navigate to'),
|
|
28
|
+
surfaceId: optionalSurfaceId,
|
|
29
|
+
}, async ({ url, surfaceId }) => {
|
|
30
|
+
try {
|
|
31
|
+
const urlCheck = (0, types_1.validateNavigationUrl)(url);
|
|
32
|
+
if (!urlCheck.valid) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: `URL blocked: ${urlCheck.reason}` }],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Use RPC for fast, reliable navigation (bypasses Playwright CDP discovery)
|
|
39
|
+
await (0, wmux_client_1.sendRpc)('browser.navigate', { url, ...(surfaceId && { surfaceId }) });
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `Navigated to ${url}` }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text', text: message }],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// -----------------------------------------------------------------------
|
|
53
|
+
// browser_navigate_back
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
server.tool('browser_navigate_back', 'Go back in browser history. Returns the current URL after going back.', {
|
|
56
|
+
surfaceId: optionalSurfaceId,
|
|
57
|
+
}, async ({ surfaceId }) => {
|
|
58
|
+
try {
|
|
59
|
+
// Use CDP via RPC for reliability
|
|
60
|
+
await (0, wmux_client_1.sendRpc)('browser.cdp.send', {
|
|
61
|
+
method: 'Page.navigateToHistoryEntry',
|
|
62
|
+
params: {},
|
|
63
|
+
...(surfaceId && { surfaceId }),
|
|
64
|
+
}).catch(() => {
|
|
65
|
+
// Fallback: use history navigation via JS evaluation
|
|
66
|
+
return (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
67
|
+
expression: 'history.back()',
|
|
68
|
+
...(surfaceId && { surfaceId }),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
// Get current URL
|
|
72
|
+
const urlResult = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
73
|
+
expression: 'location.href',
|
|
74
|
+
...(surfaceId && { surfaceId }),
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: 'text', text: `Navigated back to ${urlResult.value}` }],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: message }],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// -----------------------------------------------------------------------
|
|
89
|
+
// browser_tabs
|
|
90
|
+
// -----------------------------------------------------------------------
|
|
91
|
+
server.tool('browser_tabs', 'Manage browser tabs: list all tabs, open a new tab, select a tab, or close a tab.', {
|
|
92
|
+
action: zod_1.z
|
|
93
|
+
.enum(['list', 'new', 'select', 'close'])
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Action to perform. Defaults to "list".'),
|
|
96
|
+
tabId: zod_1.z
|
|
97
|
+
.number()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Tab index (0-based) for "select" or "close" actions.'),
|
|
100
|
+
url: zod_1.z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('URL to open when action is "new".'),
|
|
104
|
+
}, async ({ action, tabId, url }) => {
|
|
105
|
+
try {
|
|
106
|
+
const browser = await engine.getBrowser();
|
|
107
|
+
if (!browser) {
|
|
108
|
+
throw new Error('No browser connected. Call browser_open with a URL first to establish a CDP connection.');
|
|
109
|
+
}
|
|
110
|
+
const resolvedAction = action ?? 'list';
|
|
111
|
+
// Collect all pages across all contexts
|
|
112
|
+
const contexts = browser.contexts();
|
|
113
|
+
const allPages = contexts.flatMap((ctx) => ctx.pages());
|
|
114
|
+
switch (resolvedAction) {
|
|
115
|
+
case 'list': {
|
|
116
|
+
const tabList = allPages.map((p, i) => ({
|
|
117
|
+
tabId: i,
|
|
118
|
+
url: p.url(),
|
|
119
|
+
title: '', // title requires async; filled below
|
|
120
|
+
}));
|
|
121
|
+
// Populate titles
|
|
122
|
+
for (let i = 0; i < allPages.length; i++) {
|
|
123
|
+
try {
|
|
124
|
+
tabList[i].title = await allPages[i].title();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
tabList[i].title = '(unknown)';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: JSON.stringify(tabList, null, 2),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
case 'new': {
|
|
140
|
+
// Use the first context, or fail
|
|
141
|
+
const context = contexts[0];
|
|
142
|
+
if (!context) {
|
|
143
|
+
throw new Error('No browser context available.');
|
|
144
|
+
}
|
|
145
|
+
const newPage = await context.newPage();
|
|
146
|
+
if (url) {
|
|
147
|
+
const urlCheck = (0, types_1.validateNavigationUrl)(url);
|
|
148
|
+
if (!urlCheck.valid) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: 'text', text: `URL blocked: ${urlCheck.reason}` }],
|
|
151
|
+
isError: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
await newPage.goto(url, { waitUntil: 'domcontentloaded' });
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: `Opened new tab (index ${allPages.length}) at ${newPage.url()}`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
case 'select': {
|
|
166
|
+
if (tabId === undefined || tabId < 0 || tabId >= allPages.length) {
|
|
167
|
+
throw new Error(`Invalid tabId=${tabId}. Available tabs: 0-${allPages.length - 1}`);
|
|
168
|
+
}
|
|
169
|
+
await allPages[tabId].bringToFront();
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: `Selected tab ${tabId}: ${allPages[tabId].url()}`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
case 'close': {
|
|
180
|
+
if (tabId === undefined || tabId < 0 || tabId >= allPages.length) {
|
|
181
|
+
throw new Error(`Invalid tabId=${tabId}. Available tabs: 0-${allPages.length - 1}`);
|
|
182
|
+
}
|
|
183
|
+
const closedUrl = allPages[tabId].url();
|
|
184
|
+
await allPages[tabId].close();
|
|
185
|
+
return {
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: 'text',
|
|
189
|
+
text: `Closed tab ${tabId}: ${closedUrl}`,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`Unknown action: ${resolvedAction}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: 'text', text: message }],
|
|
202
|
+
isError: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|