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