@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.
@@ -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
+ }