@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,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInspectionTools = registerInspectionTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const PlaywrightEngine_1 = require("../PlaywrightEngine");
|
|
6
|
+
const snapshot_1 = require("../snapshot");
|
|
7
|
+
const anti_detection_1 = require("../anti-detection");
|
|
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
|
+
const consoleMessages = new Map();
|
|
14
|
+
const networkRequests = new Map();
|
|
15
|
+
// Track which pages already have listeners attached
|
|
16
|
+
const attachedConsolePages = new WeakSet();
|
|
17
|
+
const attachedNetworkPages = new WeakSet();
|
|
18
|
+
function ensureConsoleListener(page, surfaceKey) {
|
|
19
|
+
if (attachedConsolePages.has(page))
|
|
20
|
+
return;
|
|
21
|
+
attachedConsolePages.add(page);
|
|
22
|
+
if (!consoleMessages.has(surfaceKey)) {
|
|
23
|
+
consoleMessages.set(surfaceKey, []);
|
|
24
|
+
}
|
|
25
|
+
page.on('console', (msg) => {
|
|
26
|
+
const entries = consoleMessages.get(surfaceKey);
|
|
27
|
+
if (entries) {
|
|
28
|
+
entries.push({ level: msg.type(), text: msg.text() });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function ensureNetworkListener(page, surfaceKey) {
|
|
33
|
+
if (attachedNetworkPages.has(page))
|
|
34
|
+
return;
|
|
35
|
+
attachedNetworkPages.add(page);
|
|
36
|
+
if (!networkRequests.has(surfaceKey)) {
|
|
37
|
+
networkRequests.set(surfaceKey, []);
|
|
38
|
+
}
|
|
39
|
+
page.on('request', (request) => {
|
|
40
|
+
const entries = networkRequests.get(surfaceKey);
|
|
41
|
+
if (entries) {
|
|
42
|
+
entries.push({
|
|
43
|
+
url: request.url(),
|
|
44
|
+
method: request.method(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
page.on('response', (response) => {
|
|
49
|
+
const entries = networkRequests.get(surfaceKey);
|
|
50
|
+
if (!entries)
|
|
51
|
+
return;
|
|
52
|
+
const url = response.url();
|
|
53
|
+
// Find the matching request entry (last one with same URL and no status yet)
|
|
54
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
55
|
+
if (entries[i].url === url && entries[i].status === undefined) {
|
|
56
|
+
entries[i].status = response.status();
|
|
57
|
+
// Store response headers for later body retrieval
|
|
58
|
+
const headers = response.headers();
|
|
59
|
+
entries[i].response = { headers };
|
|
60
|
+
// Only eagerly capture body for text-based content types
|
|
61
|
+
const contentType = headers['content-type'] ?? '';
|
|
62
|
+
const isTextual = contentType.startsWith('text/') ||
|
|
63
|
+
contentType.includes('application/json') ||
|
|
64
|
+
contentType.includes('application/xml') ||
|
|
65
|
+
contentType.includes('application/xhtml') ||
|
|
66
|
+
contentType.includes('+json') ||
|
|
67
|
+
contentType.includes('+xml');
|
|
68
|
+
if (isTextual) {
|
|
69
|
+
response
|
|
70
|
+
.text()
|
|
71
|
+
.then((body) => {
|
|
72
|
+
if (entries[i].response) {
|
|
73
|
+
entries[i].response.body = body;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
// Body may not be available for all responses
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function surfaceKey(surfaceId) {
|
|
86
|
+
return surfaceId ?? '__default__';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Simple glob-like URL matching.
|
|
90
|
+
* Supports '*' as wildcard for any sequence of characters.
|
|
91
|
+
*/
|
|
92
|
+
function matchesGlob(url, pattern) {
|
|
93
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$', 'i');
|
|
95
|
+
return regex.test(url);
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Registration
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/**
|
|
101
|
+
* Register inspection-related MCP tools on the given server.
|
|
102
|
+
*
|
|
103
|
+
* Tools:
|
|
104
|
+
* - browser_snapshot -- accessibility tree snapshot
|
|
105
|
+
* - browser_screenshot -- page or element screenshot
|
|
106
|
+
* - browser_evaluate -- evaluate JS expression
|
|
107
|
+
* - browser_console -- retrieve console messages
|
|
108
|
+
* - browser_network -- retrieve network requests
|
|
109
|
+
* - browser_response_body -- retrieve response body by URL pattern
|
|
110
|
+
* - browser_highlight -- visually highlight an element
|
|
111
|
+
*/
|
|
112
|
+
function registerInspectionTools(server) {
|
|
113
|
+
const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
// browser_snapshot
|
|
116
|
+
// -----------------------------------------------------------------------
|
|
117
|
+
server.tool('browser_snapshot', 'Take an accessibility tree snapshot of the current page. Returns a text representation of the page structure with interactive elements annotated with ref numbers.', {
|
|
118
|
+
format: zod_1.z
|
|
119
|
+
.enum(['ai', 'aria'])
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('Snapshot format. "ai" annotates interactive elements with ref numbers (default). "aria" returns the full tree.'),
|
|
122
|
+
ref: zod_1.z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('Reserved for future use: ref number to scope the snapshot to a subtree.'),
|
|
126
|
+
surfaceId: optionalSurfaceId,
|
|
127
|
+
}, async ({ format, surfaceId }) => {
|
|
128
|
+
try {
|
|
129
|
+
const page = await engine.getPage(surfaceId);
|
|
130
|
+
if (!page) {
|
|
131
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
132
|
+
}
|
|
133
|
+
const snapshot = await (0, snapshot_1.generateSnapshot)(page, { format: format ?? 'ai' });
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: 'text', text: snapshot }],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: message }],
|
|
142
|
+
isError: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
// browser_screenshot
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
server.tool('browser_screenshot', 'Take a screenshot of the current page or a specific element. Returns the image as base64-encoded PNG.', {
|
|
150
|
+
fullPage: zod_1.z
|
|
151
|
+
.boolean()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('Capture the full scrollable page instead of just the viewport (default false).'),
|
|
154
|
+
ref: zod_1.z
|
|
155
|
+
.string()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('Ref number of an element to screenshot (from browser_snapshot). Omit for full page.'),
|
|
158
|
+
surfaceId: optionalSurfaceId,
|
|
159
|
+
}, async ({ fullPage, ref, surfaceId }) => {
|
|
160
|
+
try {
|
|
161
|
+
const page = await engine.getPage(surfaceId);
|
|
162
|
+
if (!page) {
|
|
163
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
164
|
+
}
|
|
165
|
+
let buffer;
|
|
166
|
+
if (ref) {
|
|
167
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
168
|
+
if (!el) {
|
|
169
|
+
throw new Error(`Could not resolve ref="${ref}" to an element.`);
|
|
170
|
+
}
|
|
171
|
+
buffer = (await el.screenshot());
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
buffer = (await page.screenshot({ fullPage: fullPage ?? false }));
|
|
175
|
+
}
|
|
176
|
+
const base64 = buffer.toString('base64');
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: 'image',
|
|
181
|
+
data: base64,
|
|
182
|
+
mimeType: 'image/png',
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: message }],
|
|
191
|
+
isError: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// browser_evaluate
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
server.tool('browser_evaluate', 'Evaluate a JavaScript expression in the browser page context. Uses userGesture mode for actions requiring user activation.', {
|
|
199
|
+
expression: zod_1.z.string().describe('The JavaScript expression to evaluate.'),
|
|
200
|
+
surfaceId: optionalSurfaceId,
|
|
201
|
+
}, async ({ expression, surfaceId }) => {
|
|
202
|
+
try {
|
|
203
|
+
const page = await engine.getPage(surfaceId);
|
|
204
|
+
if (!page) {
|
|
205
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
206
|
+
}
|
|
207
|
+
const result = await (0, anti_detection_1.evaluateWithGesture)(page, expression);
|
|
208
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: 'text', text: text ?? 'undefined' }],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: 'text', text: message }],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
// browser_console
|
|
223
|
+
// -----------------------------------------------------------------------
|
|
224
|
+
server.tool('browser_console', 'Retrieve console messages collected from the browser page. Messages are accumulated over time; use clear=true to reset.', {
|
|
225
|
+
level: zod_1.z
|
|
226
|
+
.enum(['error', 'warn', 'info', 'all'])
|
|
227
|
+
.optional()
|
|
228
|
+
.describe('Filter by message level. Defaults to "all".'),
|
|
229
|
+
clear: zod_1.z
|
|
230
|
+
.boolean()
|
|
231
|
+
.optional()
|
|
232
|
+
.describe('Clear collected messages after returning them.'),
|
|
233
|
+
surfaceId: optionalSurfaceId,
|
|
234
|
+
}, async ({ level, clear, surfaceId }) => {
|
|
235
|
+
try {
|
|
236
|
+
const page = await engine.getPage(surfaceId);
|
|
237
|
+
if (!page) {
|
|
238
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
239
|
+
}
|
|
240
|
+
const key = surfaceKey(surfaceId);
|
|
241
|
+
ensureConsoleListener(page, key);
|
|
242
|
+
const entries = consoleMessages.get(key) ?? [];
|
|
243
|
+
const filterLevel = level ?? 'all';
|
|
244
|
+
const filtered = filterLevel === 'all'
|
|
245
|
+
? entries
|
|
246
|
+
: entries.filter((e) => {
|
|
247
|
+
if (filterLevel === 'info') {
|
|
248
|
+
return e.level === 'log' || e.level === 'info';
|
|
249
|
+
}
|
|
250
|
+
return e.level === filterLevel;
|
|
251
|
+
});
|
|
252
|
+
const text = filtered.length === 0
|
|
253
|
+
? 'No console messages collected.'
|
|
254
|
+
: filtered.map((e) => `[${e.level}] ${e.text}`).join('\n');
|
|
255
|
+
if (clear) {
|
|
256
|
+
consoleMessages.set(key, []);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: 'text', text }],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: 'text', text: message }],
|
|
266
|
+
isError: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// -----------------------------------------------------------------------
|
|
271
|
+
// browser_network
|
|
272
|
+
// -----------------------------------------------------------------------
|
|
273
|
+
server.tool('browser_network', 'Retrieve network requests collected from the browser page. Requests are accumulated over time. Use a URL glob pattern to filter.', {
|
|
274
|
+
filter: zod_1.z
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe('URL glob pattern to filter requests (e.g. "*api*", "*.json").'),
|
|
278
|
+
surfaceId: optionalSurfaceId,
|
|
279
|
+
}, async ({ filter, surfaceId }) => {
|
|
280
|
+
try {
|
|
281
|
+
const page = await engine.getPage(surfaceId);
|
|
282
|
+
if (!page) {
|
|
283
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
284
|
+
}
|
|
285
|
+
const key = surfaceKey(surfaceId);
|
|
286
|
+
ensureNetworkListener(page, key);
|
|
287
|
+
const entries = networkRequests.get(key) ?? [];
|
|
288
|
+
const filtered = filter
|
|
289
|
+
? entries.filter((e) => matchesGlob(e.url, filter))
|
|
290
|
+
: entries;
|
|
291
|
+
const summary = filtered.map((e) => ({
|
|
292
|
+
url: e.url,
|
|
293
|
+
method: e.method,
|
|
294
|
+
status: e.status ?? '(pending)',
|
|
295
|
+
}));
|
|
296
|
+
const text = summary.length === 0
|
|
297
|
+
? 'No network requests collected.'
|
|
298
|
+
: JSON.stringify(summary, null, 2);
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: 'text', text }],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: 'text', text: message }],
|
|
307
|
+
isError: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
// browser_response_body
|
|
313
|
+
// -----------------------------------------------------------------------
|
|
314
|
+
server.tool('browser_response_body', 'Retrieve the response body for a previously captured network request matching a URL pattern.', {
|
|
315
|
+
urlPattern: zod_1.z
|
|
316
|
+
.string()
|
|
317
|
+
.describe('URL glob pattern to match (e.g. "*api/users*").'),
|
|
318
|
+
surfaceId: optionalSurfaceId,
|
|
319
|
+
}, async ({ urlPattern, surfaceId }) => {
|
|
320
|
+
try {
|
|
321
|
+
const page = await engine.getPage(surfaceId);
|
|
322
|
+
if (!page) {
|
|
323
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
324
|
+
}
|
|
325
|
+
const key = surfaceKey(surfaceId);
|
|
326
|
+
ensureNetworkListener(page, key);
|
|
327
|
+
const entries = networkRequests.get(key) ?? [];
|
|
328
|
+
// Find the last matching entry with a captured body
|
|
329
|
+
let matchedEntry;
|
|
330
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
331
|
+
if (matchesGlob(entries[i].url, urlPattern) && entries[i].response?.body !== undefined) {
|
|
332
|
+
matchedEntry = entries[i];
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!matchedEntry || !matchedEntry.response?.body) {
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: `No response body found for pattern "${urlPattern}". Ensure the request has been made and the response was captured.`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
349
|
+
type: 'text',
|
|
350
|
+
text: matchedEntry.response.body,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: 'text', text: message }],
|
|
359
|
+
isError: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// -----------------------------------------------------------------------
|
|
364
|
+
// browser_highlight
|
|
365
|
+
// -----------------------------------------------------------------------
|
|
366
|
+
server.tool('browser_highlight', 'Visually highlight an element on the page by its ref number. Adds a red outline around the element.', {
|
|
367
|
+
ref: zod_1.z.string().describe('Ref number of the element to highlight (from browser_snapshot).'),
|
|
368
|
+
surfaceId: optionalSurfaceId,
|
|
369
|
+
}, async ({ ref, surfaceId }) => {
|
|
370
|
+
try {
|
|
371
|
+
const page = await engine.getPage(surfaceId);
|
|
372
|
+
if (!page) {
|
|
373
|
+
throw new Error('No browser page available. Call browser_open first.');
|
|
374
|
+
}
|
|
375
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
376
|
+
if (!el) {
|
|
377
|
+
throw new Error(`Could not resolve ref="${ref}" to an element.`);
|
|
378
|
+
}
|
|
379
|
+
await el.evaluate((element) => {
|
|
380
|
+
element.style.outline = '3px solid red';
|
|
381
|
+
element.style.outlineOffset = '2px';
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: 'text', text: 'Element highlighted' }],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: 'text', text: message }],
|
|
391
|
+
isError: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|