@wong2kim/wmux 1.1.1 → 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.
@@ -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
+ }