@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.
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerStateTools = registerStateTools;
4
+ const zod_1 = require("zod");
5
+ const playwright_core_1 = require("playwright-core");
6
+ const PlaywrightEngine_1 = require("../PlaywrightEngine");
7
+ // Optional surfaceId schema reused across tools
8
+ const optionalSurfaceId = zod_1.z
9
+ .string()
10
+ .optional()
11
+ .describe('Target a specific surface by ID. Omit to use the active surface.');
12
+ // ---------------------------------------------------------------------------
13
+ // Registration
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Register state-management MCP tools on the given server.
17
+ *
18
+ * Tools:
19
+ * - browser_cookies -- get, set, or clear cookies
20
+ * - browser_storage -- get, set, or clear localStorage / sessionStorage
21
+ * - browser_emulate -- apply various emulation settings
22
+ * - browser_resize -- change the viewport size
23
+ */
24
+ function registerStateTools(server) {
25
+ const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
26
+ // -----------------------------------------------------------------------
27
+ // browser_cookies
28
+ // -----------------------------------------------------------------------
29
+ server.tool('browser_cookies', 'Manage browser cookies: get, set, or clear cookies for the current browser context.', {
30
+ action: zod_1.z
31
+ .enum(['get', 'set', 'clear'])
32
+ .describe('Action to perform on cookies.'),
33
+ url: zod_1.z
34
+ .string()
35
+ .optional()
36
+ .describe('URL to filter cookies by (for "get" action).'),
37
+ cookies: zod_1.z
38
+ .array(zod_1.z.object({
39
+ name: zod_1.z.string().describe('Cookie name'),
40
+ value: zod_1.z.string().describe('Cookie value'),
41
+ domain: zod_1.z.string().optional().describe('Cookie domain'),
42
+ path: zod_1.z.string().optional().describe('Cookie path'),
43
+ }))
44
+ .optional()
45
+ .describe('Cookies to set (for "set" action).'),
46
+ surfaceId: optionalSurfaceId,
47
+ }, async ({ action, url, cookies, 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
+ const context = page.context();
54
+ switch (action) {
55
+ case 'get': {
56
+ const allCookies = await context.cookies(url ? [url] : []);
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: JSON.stringify(allCookies, null, 2),
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ case 'set': {
67
+ if (!cookies || cookies.length === 0) {
68
+ throw new Error('No cookies provided for "set" action.');
69
+ }
70
+ // Playwright requires url or domain+path for each cookie
71
+ const cookiesToAdd = cookies.map((c) => ({
72
+ name: c.name,
73
+ value: c.value,
74
+ domain: c.domain,
75
+ path: c.path ?? '/',
76
+ url: !c.domain ? (url ?? page.url()) : undefined,
77
+ }));
78
+ await context.addCookies(cookiesToAdd);
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: `Set ${cookies.length} cookie(s).`,
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ case 'clear': {
89
+ await context.clearCookies();
90
+ return {
91
+ content: [{ type: 'text', text: 'Cookies cleared.' }],
92
+ };
93
+ }
94
+ default:
95
+ throw new Error(`Unknown action: ${action}`);
96
+ }
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ return {
101
+ content: [{ type: 'text', text: message }],
102
+ isError: true,
103
+ };
104
+ }
105
+ });
106
+ // -----------------------------------------------------------------------
107
+ // browser_storage
108
+ // -----------------------------------------------------------------------
109
+ server.tool('browser_storage', 'Manage localStorage or sessionStorage: get, set, or clear values.', {
110
+ type: zod_1.z
111
+ .enum(['local', 'session'])
112
+ .describe('Storage type: "local" for localStorage, "session" for sessionStorage.'),
113
+ action: zod_1.z
114
+ .enum(['get', 'set', 'clear'])
115
+ .describe('Action to perform.'),
116
+ key: zod_1.z
117
+ .string()
118
+ .optional()
119
+ .describe('Storage key (for "get" or "set"). Omit for "get" to retrieve all entries.'),
120
+ value: zod_1.z
121
+ .string()
122
+ .optional()
123
+ .describe('Value to set (for "set" action).'),
124
+ surfaceId: optionalSurfaceId,
125
+ }, async ({ type, action, key, value, surfaceId }) => {
126
+ try {
127
+ const page = await engine.getPage(surfaceId);
128
+ if (!page) {
129
+ throw new Error('No browser page available. Call browser_open first.');
130
+ }
131
+ const storageName = type === 'local' ? 'localStorage' : 'sessionStorage';
132
+ switch (action) {
133
+ case 'get': {
134
+ const result = await page.evaluate(([sName, sKey]) => {
135
+ const storage = window[sName];
136
+ if (sKey) {
137
+ return storage.getItem(sKey);
138
+ }
139
+ // Return all entries
140
+ const entries = {};
141
+ for (let i = 0; i < storage.length; i++) {
142
+ const k = storage.key(i);
143
+ if (k !== null) {
144
+ entries[k] = storage.getItem(k) ?? '';
145
+ }
146
+ }
147
+ return entries;
148
+ }, [storageName, key]);
149
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
150
+ return {
151
+ content: [{ type: 'text', text: text ?? 'null' }],
152
+ };
153
+ }
154
+ case 'set': {
155
+ if (!key) {
156
+ throw new Error('Key is required for "set" action.');
157
+ }
158
+ await page.evaluate(([sName, sKey, sValue]) => {
159
+ const storage = window[sName];
160
+ storage.setItem(sKey, sValue);
161
+ }, [storageName, key, value ?? '']);
162
+ return {
163
+ content: [
164
+ {
165
+ type: 'text',
166
+ text: `${storageName}.${key} = "${value ?? ''}"`,
167
+ },
168
+ ],
169
+ };
170
+ }
171
+ case 'clear': {
172
+ await page.evaluate((sName) => {
173
+ const storage = window[sName];
174
+ storage.clear();
175
+ }, storageName);
176
+ return {
177
+ content: [
178
+ { type: 'text', text: `${storageName} cleared.` },
179
+ ],
180
+ };
181
+ }
182
+ default:
183
+ throw new Error(`Unknown action: ${action}`);
184
+ }
185
+ }
186
+ catch (error) {
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ return {
189
+ content: [{ type: 'text', text: message }],
190
+ isError: true,
191
+ };
192
+ }
193
+ });
194
+ // -----------------------------------------------------------------------
195
+ // browser_emulate
196
+ // -----------------------------------------------------------------------
197
+ server.tool('browser_emulate', 'Apply emulation settings to the browser page: offline mode, custom headers, HTTP credentials, geolocation, color scheme, timezone, locale, or device preset.', {
198
+ offline: zod_1.z
199
+ .boolean()
200
+ .optional()
201
+ .describe('Enable or disable offline mode.'),
202
+ headers: zod_1.z
203
+ .record(zod_1.z.string(), zod_1.z.string())
204
+ .optional()
205
+ .describe('Extra HTTP headers to send with every request.'),
206
+ credentials: zod_1.z
207
+ .object({
208
+ username: zod_1.z.string(),
209
+ password: zod_1.z.string(),
210
+ })
211
+ .nullable()
212
+ .optional()
213
+ .describe('HTTP credentials for Basic/Digest auth. Pass null to clear.'),
214
+ geo: zod_1.z
215
+ .object({
216
+ latitude: zod_1.z.number(),
217
+ longitude: zod_1.z.number(),
218
+ accuracy: zod_1.z.number().optional(),
219
+ })
220
+ .nullable()
221
+ .optional()
222
+ .describe('Geolocation override. Pass null to clear.'),
223
+ media: zod_1.z
224
+ .enum(['dark', 'light', 'no-preference'])
225
+ .nullable()
226
+ .optional()
227
+ .describe('Color scheme media emulation. Pass null to reset.'),
228
+ timezone: zod_1.z
229
+ .string()
230
+ .nullable()
231
+ .optional()
232
+ .describe('Timezone override (e.g. "America/New_York"). Pass null to reset.'),
233
+ locale: zod_1.z
234
+ .string()
235
+ .nullable()
236
+ .optional()
237
+ .describe('Locale override (e.g. "en-US"). Pass null to reset.'),
238
+ device: zod_1.z
239
+ .string()
240
+ .nullable()
241
+ .optional()
242
+ .describe('Device preset name from Playwright devices (e.g. "iPhone 13"). Pass null to reset.'),
243
+ surfaceId: optionalSurfaceId,
244
+ }, async ({ offline, headers, credentials, geo, media, timezone, locale, device, surfaceId }) => {
245
+ try {
246
+ const page = await engine.getPage(surfaceId);
247
+ if (!page) {
248
+ throw new Error('No browser page available. Call browser_open first.');
249
+ }
250
+ const context = page.context();
251
+ const applied = [];
252
+ // offline
253
+ if (offline !== undefined) {
254
+ await context.setOffline(offline);
255
+ applied.push(`offline=${offline}`);
256
+ }
257
+ // headers
258
+ if (headers !== undefined) {
259
+ await context.setExtraHTTPHeaders(headers);
260
+ applied.push(`headers=${Object.keys(headers).length} header(s)`);
261
+ }
262
+ // credentials
263
+ if (credentials !== undefined) {
264
+ try {
265
+ await context.setHTTPCredentials(credentials);
266
+ applied.push(credentials ? 'credentials=set' : 'credentials=cleared');
267
+ }
268
+ catch {
269
+ applied.push('credentials=failed (HTTP credentials via context is not supported in CDP mode. Use browser_emulate headers with a Base64-encoded Authorization header instead.)');
270
+ }
271
+ }
272
+ // geo
273
+ if (geo !== undefined) {
274
+ if (geo) {
275
+ await context.setGeolocation(geo);
276
+ await context.grantPermissions(['geolocation']);
277
+ applied.push(`geo=${geo.latitude},${geo.longitude}`);
278
+ }
279
+ else {
280
+ await context.setGeolocation(null);
281
+ applied.push('geo=cleared');
282
+ }
283
+ }
284
+ // media / color scheme
285
+ if (media !== undefined) {
286
+ await page.emulateMedia({
287
+ colorScheme: media,
288
+ });
289
+ applied.push(media ? `colorScheme=${media}` : 'colorScheme=reset');
290
+ }
291
+ // timezone via CDP
292
+ if (timezone !== undefined) {
293
+ const client = await context.newCDPSession(page);
294
+ try {
295
+ if (timezone) {
296
+ await client.send('Emulation.setTimezoneOverride', {
297
+ timezoneId: timezone,
298
+ });
299
+ applied.push(`timezone=${timezone}`);
300
+ }
301
+ else {
302
+ await client.send('Emulation.setTimezoneOverride', {
303
+ timezoneId: '',
304
+ });
305
+ applied.push('timezone=reset');
306
+ }
307
+ }
308
+ finally {
309
+ await client.detach().catch(() => { });
310
+ }
311
+ }
312
+ // locale via CDP
313
+ if (locale !== undefined) {
314
+ const client = await context.newCDPSession(page);
315
+ try {
316
+ if (locale) {
317
+ await client.send('Emulation.setLocaleOverride', {
318
+ locale,
319
+ });
320
+ applied.push(`locale=${locale}`);
321
+ }
322
+ else {
323
+ await client.send('Emulation.setLocaleOverride', {
324
+ locale: '',
325
+ });
326
+ applied.push('locale=reset');
327
+ }
328
+ }
329
+ finally {
330
+ await client.detach().catch(() => { });
331
+ }
332
+ }
333
+ // device preset
334
+ if (device !== undefined) {
335
+ if (device) {
336
+ const deviceDescriptor = playwright_core_1.devices[device];
337
+ if (!deviceDescriptor) {
338
+ throw new Error(`Unknown device "${device}". Use a name from Playwright's device list (e.g. "iPhone 13", "Pixel 5").`);
339
+ }
340
+ await page.setViewportSize(deviceDescriptor.viewport);
341
+ // Apply user agent via extra headers
342
+ await context.setExtraHTTPHeaders({
343
+ ...(headers ?? {}),
344
+ 'User-Agent': deviceDescriptor.userAgent,
345
+ });
346
+ applied.push(`device=${device} (${deviceDescriptor.viewport.width}x${deviceDescriptor.viewport.height})`);
347
+ }
348
+ else {
349
+ applied.push('device=reset (use browser_resize to set viewport)');
350
+ }
351
+ }
352
+ if (applied.length === 0) {
353
+ return {
354
+ content: [
355
+ {
356
+ type: 'text',
357
+ text: 'No emulation settings provided. Pass at least one option.',
358
+ },
359
+ ],
360
+ };
361
+ }
362
+ return {
363
+ content: [
364
+ {
365
+ type: 'text',
366
+ text: `Emulation applied:\n${applied.map((a) => ` - ${a}`).join('\n')}`,
367
+ },
368
+ ],
369
+ };
370
+ }
371
+ catch (error) {
372
+ const message = error instanceof Error ? error.message : String(error);
373
+ return {
374
+ content: [{ type: 'text', text: message }],
375
+ isError: true,
376
+ };
377
+ }
378
+ });
379
+ // -----------------------------------------------------------------------
380
+ // browser_resize
381
+ // -----------------------------------------------------------------------
382
+ server.tool('browser_resize', 'Resize the browser viewport to the specified dimensions.', {
383
+ width: zod_1.z.number().describe('Viewport width in pixels.'),
384
+ height: zod_1.z.number().describe('Viewport height in pixels.'),
385
+ surfaceId: optionalSurfaceId,
386
+ }, async ({ width, height, surfaceId }) => {
387
+ try {
388
+ const page = await engine.getPage(surfaceId);
389
+ if (!page) {
390
+ throw new Error('No browser page available. Call browser_open first.');
391
+ }
392
+ await page.setViewportSize({ width, height });
393
+ return {
394
+ content: [
395
+ {
396
+ type: 'text',
397
+ text: `Viewport resized to ${width}x${height}`,
398
+ },
399
+ ],
400
+ };
401
+ }
402
+ catch (error) {
403
+ const message = error instanceof Error ? error.message : String(error);
404
+ return {
405
+ content: [{ type: 'text', text: message }],
406
+ isError: true,
407
+ };
408
+ }
409
+ });
410
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerUtilityTools = registerUtilityTools;
37
+ const zod_1 = require("zod");
38
+ const fs = __importStar(require("fs"));
39
+ const PlaywrightEngine_1 = require("../PlaywrightEngine");
40
+ // Optional surfaceId schema reused across tools
41
+ const optionalSurfaceId = zod_1.z
42
+ .string()
43
+ .optional()
44
+ .describe('Target a specific surface by ID. Omit to use the active surface.');
45
+ /**
46
+ * Register utility MCP tools on the given server.
47
+ *
48
+ * Tools:
49
+ * - browser_pdf — export the current page as a PDF
50
+ * - browser_trace — start or stop Playwright tracing
51
+ */
52
+ function registerUtilityTools(server) {
53
+ const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
54
+ // -----------------------------------------------------------------------
55
+ // browser_pdf
56
+ // -----------------------------------------------------------------------
57
+ server.tool('browser_pdf', 'Export the current page as a PDF file. Falls back to CDP Page.printToPDF when Playwright pdf() is unavailable (e.g. CDP-connected browsers).', {
58
+ path: zod_1.z
59
+ .string()
60
+ .optional()
61
+ .describe('Output file path for the PDF. Defaults to "output.pdf".'),
62
+ surfaceId: optionalSurfaceId,
63
+ }, async ({ path: outputPath, surfaceId }) => {
64
+ const resolvedPath = outputPath ?? 'output.pdf';
65
+ try {
66
+ const page = await engine.getPage(surfaceId);
67
+ if (!page) {
68
+ throw new Error('No browser page available. Call browser_open first.');
69
+ }
70
+ try {
71
+ // Try Playwright's built-in pdf() first
72
+ await page.pdf({ path: resolvedPath, format: 'A4' });
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: `PDF saved to ${resolvedPath}`,
78
+ },
79
+ ],
80
+ };
81
+ }
82
+ catch {
83
+ // Fallback: use CDP Page.printToPDF directly
84
+ const client = await page.context().newCDPSession(page);
85
+ try {
86
+ const result = await client.send('Page.printToPDF', {
87
+ landscape: false,
88
+ printBackground: true,
89
+ });
90
+ const pdfData = result.data;
91
+ // Write the base64 data to file
92
+ fs.writeFileSync(resolvedPath, Buffer.from(pdfData, 'base64'));
93
+ return {
94
+ content: [
95
+ {
96
+ type: 'text',
97
+ text: `PDF saved to ${resolvedPath} (via CDP)`,
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ finally {
103
+ await client.detach().catch(() => {
104
+ /* best-effort */
105
+ });
106
+ }
107
+ }
108
+ }
109
+ catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ return {
112
+ content: [{ type: 'text', text: message }],
113
+ isError: true,
114
+ };
115
+ }
116
+ });
117
+ // -----------------------------------------------------------------------
118
+ // browser_trace
119
+ // -----------------------------------------------------------------------
120
+ server.tool('browser_trace', 'Start or stop Playwright tracing. Use "start" to begin recording and "stop" to save the trace file.', {
121
+ action: zod_1.z
122
+ .enum(['start', 'stop'])
123
+ .describe('Whether to start or stop tracing.'),
124
+ path: zod_1.z
125
+ .string()
126
+ .optional()
127
+ .describe('Output file path for the trace (used with "stop"). Defaults to "trace.zip".'),
128
+ surfaceId: optionalSurfaceId,
129
+ }, async ({ action, path: outputPath, surfaceId }) => {
130
+ try {
131
+ const page = await engine.getPage(surfaceId);
132
+ if (!page) {
133
+ throw new Error('No browser page available. Call browser_open first.');
134
+ }
135
+ const context = page.context();
136
+ if (action === 'start') {
137
+ await context.tracing.start({ screenshots: true, snapshots: true });
138
+ return {
139
+ content: [
140
+ {
141
+ type: 'text',
142
+ text: 'Tracing started. Call browser_trace with action "stop" to save the trace.',
143
+ },
144
+ ],
145
+ };
146
+ }
147
+ // action === 'stop'
148
+ const resolvedPath = outputPath ?? 'trace.zip';
149
+ await context.tracing.stop({ path: resolvedPath });
150
+ return {
151
+ content: [
152
+ {
153
+ type: 'text',
154
+ text: `Trace saved to ${resolvedPath}`,
155
+ },
156
+ ],
157
+ };
158
+ }
159
+ catch (error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ return {
162
+ content: [{ type: 'text', text: message }],
163
+ isError: true,
164
+ };
165
+ }
166
+ });
167
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerWaitTools = registerWaitTools;
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 wait-related MCP tools on the given server.
13
+ *
14
+ * Tools:
15
+ * - browser_wait — wait for a URL, selector, text, JS predicate, or network idle
16
+ */
17
+ function registerWaitTools(server) {
18
+ const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
19
+ // -----------------------------------------------------------------------
20
+ // browser_wait
21
+ // -----------------------------------------------------------------------
22
+ server.tool('browser_wait', 'Wait for a condition: URL pattern, CSS selector, text content, custom JS predicate, or network idle. Priority: url > selector > text > fn > networkidle.', {
23
+ url: zod_1.z
24
+ .string()
25
+ .optional()
26
+ .describe('URL or glob pattern to wait for (e.g. "**/dashboard**").'),
27
+ selector: zod_1.z
28
+ .string()
29
+ .optional()
30
+ .describe('CSS selector to wait for.'),
31
+ text: zod_1.z
32
+ .string()
33
+ .optional()
34
+ .describe('Text to wait for in document.body.innerText.'),
35
+ fn: zod_1.z
36
+ .string()
37
+ .optional()
38
+ .describe('Custom JavaScript predicate function body to wait for (must return truthy).'),
39
+ timeout: zod_1.z
40
+ .number()
41
+ .optional()
42
+ .describe('Maximum wait time in milliseconds. Defaults to 30000.'),
43
+ surfaceId: optionalSurfaceId,
44
+ }, async ({ url, selector, text, fn, timeout, surfaceId }) => {
45
+ const resolvedTimeout = timeout ?? 30000;
46
+ try {
47
+ const page = await engine.getPage(surfaceId);
48
+ if (!page) {
49
+ throw new Error('No browser page available. Call browser_open first.');
50
+ }
51
+ // Priority: url > selector > text > fn > networkidle
52
+ if (url) {
53
+ await page.waitForURL(url, { timeout: resolvedTimeout });
54
+ return {
55
+ content: [{ type: 'text', text: `Wait completed: URL matched "${url}"` }],
56
+ };
57
+ }
58
+ if (selector) {
59
+ await page.waitForSelector(selector, { timeout: resolvedTimeout });
60
+ return {
61
+ content: [{ type: 'text', text: `Wait completed: selector "${selector}" found` }],
62
+ };
63
+ }
64
+ if (text) {
65
+ await page.waitForFunction((t) => document.body.innerText.includes(t), text, { timeout: resolvedTimeout });
66
+ return {
67
+ content: [{ type: 'text', text: `Wait completed: text "${text}" found` }],
68
+ };
69
+ }
70
+ if (fn) {
71
+ await page.waitForFunction(fn, undefined, { timeout: resolvedTimeout });
72
+ return {
73
+ content: [{ type: 'text', text: `Wait completed: custom predicate satisfied` }],
74
+ };
75
+ }
76
+ // Default: wait for network idle
77
+ await page.waitForLoadState('networkidle', { timeout: resolvedTimeout });
78
+ return {
79
+ content: [{ type: 'text', text: `Wait completed: network idle` }],
80
+ };
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ // Provide clear timeout messaging
85
+ if (message.includes('Timeout') || message.includes('timeout')) {
86
+ const condition = url
87
+ ? `URL "${url}"`
88
+ : selector
89
+ ? `selector "${selector}"`
90
+ : text
91
+ ? `text "${text}"`
92
+ : fn
93
+ ? 'custom predicate'
94
+ : 'network idle';
95
+ return {
96
+ content: [
97
+ {
98
+ type: 'text',
99
+ text: `Timed out after ${resolvedTimeout}ms waiting for ${condition}`,
100
+ },
101
+ ],
102
+ isError: true,
103
+ };
104
+ }
105
+ return {
106
+ content: [{ type: 'text', text: message }],
107
+ isError: true,
108
+ };
109
+ }
110
+ });
111
+ }