@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.
- package/README.md +76 -27
- 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 +21 -4
- package/dist/cli/shared/types.js +108 -4
- package/dist/mcp/mcp/index.js +41 -21
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +293 -0
- package/dist/mcp/mcp/playwright/anti-detection.js +63 -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/security.js +29 -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 +459 -0
- package/dist/mcp/mcp/playwright/tools/interaction.js +457 -0
- package/dist/mcp/mcp/playwright/tools/navigation.js +206 -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 +119 -0
- package/dist/mcp/shared/constants.js +3 -0
- package/dist/mcp/shared/rpc.js +21 -4
- package/dist/mcp/shared/types.js +108 -4
- package/package.json +11 -7
|
@@ -0,0 +1,459 @@
|
|
|
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
|
+
const security_1 = require("../security");
|
|
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 consoleMessages = new Map();
|
|
16
|
+
const networkRequests = new Map();
|
|
17
|
+
// Track which pages already have listeners attached
|
|
18
|
+
const attachedConsolePages = new WeakSet();
|
|
19
|
+
const attachedNetworkPages = new WeakSet();
|
|
20
|
+
function ensureConsoleListener(page, surfaceKey) {
|
|
21
|
+
if (attachedConsolePages.has(page))
|
|
22
|
+
return;
|
|
23
|
+
attachedConsolePages.add(page);
|
|
24
|
+
if (!consoleMessages.has(surfaceKey)) {
|
|
25
|
+
consoleMessages.set(surfaceKey, []);
|
|
26
|
+
}
|
|
27
|
+
page.on('console', (msg) => {
|
|
28
|
+
const entries = consoleMessages.get(surfaceKey);
|
|
29
|
+
if (entries) {
|
|
30
|
+
entries.push({ level: msg.type(), text: msg.text() });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function ensureNetworkListener(page, surfaceKey) {
|
|
35
|
+
if (attachedNetworkPages.has(page))
|
|
36
|
+
return;
|
|
37
|
+
attachedNetworkPages.add(page);
|
|
38
|
+
if (!networkRequests.has(surfaceKey)) {
|
|
39
|
+
networkRequests.set(surfaceKey, []);
|
|
40
|
+
}
|
|
41
|
+
page.on('request', (request) => {
|
|
42
|
+
const entries = networkRequests.get(surfaceKey);
|
|
43
|
+
if (entries) {
|
|
44
|
+
entries.push({
|
|
45
|
+
url: request.url(),
|
|
46
|
+
method: request.method(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
page.on('response', (response) => {
|
|
51
|
+
const entries = networkRequests.get(surfaceKey);
|
|
52
|
+
if (!entries)
|
|
53
|
+
return;
|
|
54
|
+
const url = response.url();
|
|
55
|
+
// Find the matching request entry (last one with same URL and no status yet)
|
|
56
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
57
|
+
if (entries[i].url === url && entries[i].status === undefined) {
|
|
58
|
+
entries[i].status = response.status();
|
|
59
|
+
// Store response headers for later body retrieval
|
|
60
|
+
const headers = response.headers();
|
|
61
|
+
entries[i].response = { headers };
|
|
62
|
+
// Only eagerly capture body for text-based content types
|
|
63
|
+
const contentType = headers['content-type'] ?? '';
|
|
64
|
+
const isTextual = contentType.startsWith('text/') ||
|
|
65
|
+
contentType.includes('application/json') ||
|
|
66
|
+
contentType.includes('application/xml') ||
|
|
67
|
+
contentType.includes('application/xhtml') ||
|
|
68
|
+
contentType.includes('+json') ||
|
|
69
|
+
contentType.includes('+xml');
|
|
70
|
+
if (isTextual) {
|
|
71
|
+
response
|
|
72
|
+
.text()
|
|
73
|
+
.then((body) => {
|
|
74
|
+
if (entries[i].response) {
|
|
75
|
+
entries[i].response.body = body;
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.catch(() => {
|
|
79
|
+
// Body may not be available for all responses
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function surfaceKey(surfaceId) {
|
|
88
|
+
return surfaceId ?? '__default__';
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Simple glob-like URL matching.
|
|
92
|
+
* Supports '*' as wildcard for any sequence of characters.
|
|
93
|
+
*/
|
|
94
|
+
function matchesGlob(url, pattern) {
|
|
95
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
96
|
+
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$', 'i');
|
|
97
|
+
return regex.test(url);
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Registration
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Register inspection-related MCP tools on the given server.
|
|
104
|
+
*
|
|
105
|
+
* Tools:
|
|
106
|
+
* - browser_snapshot -- accessibility tree snapshot
|
|
107
|
+
* - browser_screenshot -- page or element screenshot
|
|
108
|
+
* - browser_evaluate -- evaluate JS expression
|
|
109
|
+
* - browser_console -- retrieve console messages
|
|
110
|
+
* - browser_network -- retrieve network requests
|
|
111
|
+
* - browser_response_body -- retrieve response body by URL pattern
|
|
112
|
+
* - browser_highlight -- visually highlight an element
|
|
113
|
+
*/
|
|
114
|
+
function registerInspectionTools(server) {
|
|
115
|
+
const engine = PlaywrightEngine_1.PlaywrightEngine.getInstance();
|
|
116
|
+
// -----------------------------------------------------------------------
|
|
117
|
+
// browser_snapshot
|
|
118
|
+
// -----------------------------------------------------------------------
|
|
119
|
+
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.', {
|
|
120
|
+
format: zod_1.z
|
|
121
|
+
.enum(['ai', 'aria'])
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('Snapshot format. "ai" annotates interactive elements with ref numbers (default). "aria" returns the full tree.'),
|
|
124
|
+
ref: zod_1.z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('Reserved for future use: ref number to scope the snapshot to a subtree.'),
|
|
128
|
+
surfaceId: optionalSurfaceId,
|
|
129
|
+
}, async ({ format, surfaceId }) => {
|
|
130
|
+
try {
|
|
131
|
+
// Try Playwright for full snapshot
|
|
132
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
133
|
+
if (page) {
|
|
134
|
+
const snapshot = await (0, snapshot_1.generateSnapshot)(page, { format: format ?? 'ai' });
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: 'text', text: snapshot }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Fallback: extract page structure via RPC evaluation
|
|
140
|
+
// Tags interactive elements with data-wmux-ref so interaction tools can resolve them
|
|
141
|
+
const result = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
142
|
+
expression: `(() => {
|
|
143
|
+
const sel = 'a[href], button, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="tab"], [contenteditable="true"]';
|
|
144
|
+
const interactives = [...document.querySelectorAll(sel)].slice(0, 100);
|
|
145
|
+
interactives.forEach((el, i) => el.setAttribute('data-wmux-ref', String(i)));
|
|
146
|
+
const title = document.title;
|
|
147
|
+
const url = location.href;
|
|
148
|
+
const lines = ['Page: ' + title, 'URL: ' + url, ''];
|
|
149
|
+
document.querySelectorAll('h1,h2,h3').forEach(h => {
|
|
150
|
+
lines.push(h.tagName + ': ' + (h.textContent || '').trim().substring(0, 80));
|
|
151
|
+
});
|
|
152
|
+
lines.push('', 'Interactive elements (use ref number for click/fill/type):');
|
|
153
|
+
interactives.forEach((el, i) => {
|
|
154
|
+
const tag = el.tagName.toLowerCase();
|
|
155
|
+
const role = el.getAttribute('role') || '';
|
|
156
|
+
const text = (el.textContent || '').trim().substring(0, 60);
|
|
157
|
+
const label = el.getAttribute('aria-label') || '';
|
|
158
|
+
const name = el.getAttribute('name') || '';
|
|
159
|
+
const type = el.getAttribute('type') || '';
|
|
160
|
+
const placeholder = el.getAttribute('placeholder') || '';
|
|
161
|
+
const href = el.getAttribute('href') || '';
|
|
162
|
+
let desc = ' [ref=' + i + '] ' + tag;
|
|
163
|
+
if (type) desc += '[type=' + type + ']';
|
|
164
|
+
if (role) desc += '[role=' + role + ']';
|
|
165
|
+
if (name) desc += ' name="' + name + '"';
|
|
166
|
+
if (label) desc += ' "' + label + '"';
|
|
167
|
+
else if (text) desc += ' "' + text + '"';
|
|
168
|
+
if (placeholder) desc += ' placeholder="' + placeholder + '"';
|
|
169
|
+
if (href) desc += ' -> ' + href.substring(0, 60);
|
|
170
|
+
lines.push(desc);
|
|
171
|
+
});
|
|
172
|
+
return lines.join('\\n');
|
|
173
|
+
})()`,
|
|
174
|
+
...(surfaceId && { surfaceId }),
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: 'text', text: result.value }],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: 'text', text: message }],
|
|
184
|
+
isError: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
// browser_screenshot
|
|
190
|
+
// -----------------------------------------------------------------------
|
|
191
|
+
server.tool('browser_screenshot', 'Take a screenshot of the current page or a specific element. Returns the image as base64-encoded PNG. Requires browser_open to be called first to establish a connection, even if a browser panel is already visible.', {
|
|
192
|
+
fullPage: zod_1.z
|
|
193
|
+
.boolean()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe('Capture the full scrollable page instead of just the viewport (default false).'),
|
|
196
|
+
ref: zod_1.z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe('Ref number of an element to screenshot (from browser_snapshot). Omit for full page.'),
|
|
200
|
+
surfaceId: optionalSurfaceId,
|
|
201
|
+
}, async ({ fullPage, ref, surfaceId }) => {
|
|
202
|
+
try {
|
|
203
|
+
// Try Playwright for element-level screenshots (ref)
|
|
204
|
+
if (ref) {
|
|
205
|
+
const page = await engine.getPage(surfaceId);
|
|
206
|
+
if (page) {
|
|
207
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
208
|
+
if (!el) {
|
|
209
|
+
throw new Error(`Could not resolve ref="${ref}" to an element.`);
|
|
210
|
+
}
|
|
211
|
+
const buffer = (await el.screenshot());
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'image', data: buffer.toString('base64'), mimeType: 'image/png' }],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Use RPC for fast, reliable screenshots (bypasses Playwright CDP discovery)
|
|
218
|
+
const result = await (0, wmux_client_1.sendRpc)('browser.screenshot', {
|
|
219
|
+
...(surfaceId && { surfaceId }),
|
|
220
|
+
...(fullPage && { fullPage }),
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: 'image',
|
|
226
|
+
data: result.data,
|
|
227
|
+
mimeType: 'image/png',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: 'text', text: message }],
|
|
236
|
+
isError: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// browser_evaluate
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
server.tool('browser_evaluate', 'Evaluate a JavaScript expression in the browser page context. Uses userGesture mode for actions requiring user activation.', {
|
|
244
|
+
expression: zod_1.z.string().describe('The JavaScript expression to evaluate.'),
|
|
245
|
+
surfaceId: optionalSurfaceId,
|
|
246
|
+
}, async ({ expression, surfaceId }) => {
|
|
247
|
+
try {
|
|
248
|
+
const warnings = (0, security_1.detectDangerousPatterns)(expression);
|
|
249
|
+
if (warnings.length > 0) {
|
|
250
|
+
console.warn(`[browser_evaluate] Dangerous patterns detected: ${warnings.join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
let result;
|
|
253
|
+
// Try Playwright first for gesture-aware evaluation
|
|
254
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
255
|
+
if (page) {
|
|
256
|
+
result = await (0, anti_detection_1.evaluateWithGesture)(page, expression);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Fallback: RPC evaluation via main process webContents
|
|
260
|
+
const rpcResult = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
261
|
+
expression,
|
|
262
|
+
...(surfaceId && { surfaceId }),
|
|
263
|
+
});
|
|
264
|
+
result = rpcResult.value;
|
|
265
|
+
}
|
|
266
|
+
const text = typeof result === 'string' ? result : (JSON.stringify(result, null, 2) ?? 'undefined');
|
|
267
|
+
if (warnings.length > 0) {
|
|
268
|
+
const warningText = `\u26A0 Security warning: expression contains potentially dangerous patterns: ${warnings.join(', ')}. Exercise caution with untrusted input.\n\n`;
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: warningText + text }],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: 'text', text: text ?? 'undefined' }],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: 'text', text: message }],
|
|
281
|
+
isError: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
// -----------------------------------------------------------------------
|
|
286
|
+
// browser_console
|
|
287
|
+
// -----------------------------------------------------------------------
|
|
288
|
+
server.tool('browser_console', 'Retrieve console messages collected from the browser page. Messages are accumulated over time; use clear=true to reset.', {
|
|
289
|
+
level: zod_1.z
|
|
290
|
+
.enum(['error', 'warn', 'info', 'all'])
|
|
291
|
+
.optional()
|
|
292
|
+
.describe('Filter by message level. Defaults to "all".'),
|
|
293
|
+
clear: zod_1.z
|
|
294
|
+
.boolean()
|
|
295
|
+
.optional()
|
|
296
|
+
.describe('Clear collected messages after returning them.'),
|
|
297
|
+
surfaceId: optionalSurfaceId,
|
|
298
|
+
}, async ({ level, clear, surfaceId }) => {
|
|
299
|
+
try {
|
|
300
|
+
const page = await engine.getPage(surfaceId);
|
|
301
|
+
if (!page) {
|
|
302
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
303
|
+
}
|
|
304
|
+
const key = surfaceKey(surfaceId);
|
|
305
|
+
ensureConsoleListener(page, key);
|
|
306
|
+
const entries = consoleMessages.get(key) ?? [];
|
|
307
|
+
const filterLevel = level ?? 'all';
|
|
308
|
+
const filtered = filterLevel === 'all'
|
|
309
|
+
? entries
|
|
310
|
+
: entries.filter((e) => {
|
|
311
|
+
if (filterLevel === 'info') {
|
|
312
|
+
return e.level === 'log' || e.level === 'info';
|
|
313
|
+
}
|
|
314
|
+
return e.level === filterLevel;
|
|
315
|
+
});
|
|
316
|
+
const text = filtered.length === 0
|
|
317
|
+
? 'No console messages collected.'
|
|
318
|
+
: filtered.map((e) => `[${e.level}] ${e.text}`).join('\n');
|
|
319
|
+
if (clear) {
|
|
320
|
+
consoleMessages.set(key, []);
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: 'text', text }],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: 'text', text: message }],
|
|
330
|
+
isError: true,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// -----------------------------------------------------------------------
|
|
335
|
+
// browser_network
|
|
336
|
+
// -----------------------------------------------------------------------
|
|
337
|
+
server.tool('browser_network', 'Retrieve network requests collected from the browser page. Requests are accumulated over time. Use a URL glob pattern to filter.', {
|
|
338
|
+
filter: zod_1.z
|
|
339
|
+
.string()
|
|
340
|
+
.optional()
|
|
341
|
+
.describe('URL glob pattern to filter requests (e.g. "*api*", "*.json").'),
|
|
342
|
+
surfaceId: optionalSurfaceId,
|
|
343
|
+
}, async ({ filter, surfaceId }) => {
|
|
344
|
+
try {
|
|
345
|
+
const page = await engine.getPage(surfaceId);
|
|
346
|
+
if (!page) {
|
|
347
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
348
|
+
}
|
|
349
|
+
const key = surfaceKey(surfaceId);
|
|
350
|
+
ensureNetworkListener(page, key);
|
|
351
|
+
const entries = networkRequests.get(key) ?? [];
|
|
352
|
+
const filtered = filter
|
|
353
|
+
? entries.filter((e) => matchesGlob(e.url, filter))
|
|
354
|
+
: entries;
|
|
355
|
+
const summary = filtered.map((e) => ({
|
|
356
|
+
url: e.url,
|
|
357
|
+
method: e.method,
|
|
358
|
+
status: e.status ?? '(pending)',
|
|
359
|
+
}));
|
|
360
|
+
const text = summary.length === 0
|
|
361
|
+
? 'No network requests collected.'
|
|
362
|
+
: JSON.stringify(summary, null, 2);
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: 'text', text }],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: 'text', text: message }],
|
|
371
|
+
isError: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
// -----------------------------------------------------------------------
|
|
376
|
+
// browser_response_body
|
|
377
|
+
// -----------------------------------------------------------------------
|
|
378
|
+
server.tool('browser_response_body', 'Retrieve the response body for a previously captured network request matching a URL pattern.', {
|
|
379
|
+
urlPattern: zod_1.z
|
|
380
|
+
.string()
|
|
381
|
+
.describe('URL glob pattern to match (e.g. "*api/users*").'),
|
|
382
|
+
surfaceId: optionalSurfaceId,
|
|
383
|
+
}, async ({ urlPattern, surfaceId }) => {
|
|
384
|
+
try {
|
|
385
|
+
const page = await engine.getPage(surfaceId);
|
|
386
|
+
if (!page) {
|
|
387
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
388
|
+
}
|
|
389
|
+
const key = surfaceKey(surfaceId);
|
|
390
|
+
ensureNetworkListener(page, key);
|
|
391
|
+
const entries = networkRequests.get(key) ?? [];
|
|
392
|
+
// Find the last matching entry with a captured body
|
|
393
|
+
let matchedEntry;
|
|
394
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
395
|
+
if (matchesGlob(entries[i].url, urlPattern) && entries[i].response?.body !== undefined) {
|
|
396
|
+
matchedEntry = entries[i];
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!matchedEntry || !matchedEntry.response?.body) {
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
type: 'text',
|
|
405
|
+
text: `No response body found for pattern "${urlPattern}". Ensure the request has been made and the response was captured.`,
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: 'text',
|
|
414
|
+
text: matchedEntry.response.body,
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: 'text', text: message }],
|
|
423
|
+
isError: true,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
// -----------------------------------------------------------------------
|
|
428
|
+
// browser_highlight
|
|
429
|
+
// -----------------------------------------------------------------------
|
|
430
|
+
server.tool('browser_highlight', 'Visually highlight an element on the page by its ref number. Adds a red outline around the element.', {
|
|
431
|
+
ref: zod_1.z.string().describe('Ref number of the element to highlight (from browser_snapshot).'),
|
|
432
|
+
surfaceId: optionalSurfaceId,
|
|
433
|
+
}, async ({ ref, surfaceId }) => {
|
|
434
|
+
try {
|
|
435
|
+
const page = await engine.getPage(surfaceId);
|
|
436
|
+
if (!page) {
|
|
437
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
438
|
+
}
|
|
439
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
440
|
+
if (!el) {
|
|
441
|
+
throw new Error(`Could not resolve ref="${ref}" to an element.`);
|
|
442
|
+
}
|
|
443
|
+
await el.evaluate((element) => {
|
|
444
|
+
element.style.outline = '3px solid red';
|
|
445
|
+
element.style.outlineOffset = '2px';
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
content: [{ type: 'text', text: 'Element highlighted' }],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: 'text', text: message }],
|
|
455
|
+
isError: true,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|