@wong2kim/wmux 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/cli/cli/commands/browser.js +101 -77
- package/dist/cli/cli/index.js +6 -6
- package/dist/cli/shared/constants.js +3 -0
- package/dist/cli/shared/rpc.js +15 -4
- package/dist/mcp/mcp/index.js +41 -21
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +186 -0
- package/dist/mcp/mcp/playwright/anti-detection.js +58 -0
- package/dist/mcp/mcp/playwright/dom-intelligence.js +171 -0
- package/dist/mcp/mcp/playwright/human-typing.js +48 -0
- package/dist/mcp/mcp/playwright/markdown-extractor.js +520 -0
- package/dist/mcp/mcp/playwright/snapshot.js +261 -0
- package/dist/mcp/mcp/playwright/tools/extraction.js +143 -0
- package/dist/mcp/mcp/playwright/tools/file.js +274 -0
- package/dist/mcp/mcp/playwright/tools/inspection.js +395 -0
- package/dist/mcp/mcp/playwright/tools/interaction.js +387 -0
- package/dist/mcp/mcp/playwright/tools/navigation.js +183 -0
- package/dist/mcp/mcp/playwright/tools/state.js +410 -0
- package/dist/mcp/mcp/playwright/tools/utility.js +167 -0
- package/dist/mcp/mcp/playwright/tools/wait.js +111 -0
- package/dist/mcp/shared/constants.js +3 -0
- package/dist/mcp/shared/rpc.js +15 -4
- package/package.json +7 -4
|
@@ -0,0 +1,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
|
+
}
|