@accomplish_ai/agent-core 0.2.0 → 0.2.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.
Files changed (78) hide show
  1. package/README.md +141 -0
  2. package/dist/common/constants.d.ts +1 -1
  3. package/dist/common/constants.d.ts.map +1 -1
  4. package/dist/common/constants.js +1 -1
  5. package/dist/common/constants.js.map +1 -1
  6. package/dist/common/types/index.d.ts +14 -10
  7. package/dist/common/types/index.d.ts.map +1 -1
  8. package/dist/common/types/index.js +4 -10
  9. package/dist/common/types/index.js.map +1 -1
  10. package/dist/common/utils/index.d.ts +3 -3
  11. package/dist/common/utils/index.d.ts.map +1 -1
  12. package/dist/common/utils/index.js +3 -3
  13. package/dist/common/utils/index.js.map +1 -1
  14. package/dist/factories/speech.d.ts.map +1 -1
  15. package/dist/factories/speech.js.map +1 -1
  16. package/dist/index.d.ts +1 -6
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8 -20
  19. package/dist/index.js.map +1 -1
  20. package/dist/internal/classes/SecureStorage.d.ts.map +1 -1
  21. package/dist/internal/classes/SecureStorage.js +3 -0
  22. package/dist/internal/classes/SecureStorage.js.map +1 -1
  23. package/dist/internal/classes/TaskManager.d.ts +3 -2
  24. package/dist/internal/classes/TaskManager.d.ts.map +1 -1
  25. package/dist/internal/classes/TaskManager.js +34 -1
  26. package/dist/internal/classes/TaskManager.js.map +1 -1
  27. package/dist/storage/index.d.ts +4 -1
  28. package/dist/storage/index.d.ts.map +1 -1
  29. package/dist/storage/index.js +4 -1
  30. package/dist/storage/index.js.map +1 -1
  31. package/dist/types/log-writer.d.ts +2 -15
  32. package/dist/types/log-writer.d.ts.map +1 -1
  33. package/dist/types/skills-manager.d.ts +13 -0
  34. package/dist/types/skills-manager.d.ts.map +1 -1
  35. package/dist/types/speech.d.ts +3 -2
  36. package/dist/types/speech.d.ts.map +1 -1
  37. package/dist/types/storage.d.ts +70 -0
  38. package/dist/types/storage.d.ts.map +1 -1
  39. package/dist/types/task-manager.d.ts +13 -3
  40. package/dist/types/task-manager.d.ts.map +1 -1
  41. package/dist/types.d.ts +0 -17
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/utils/index.d.ts +16 -13
  44. package/dist/utils/index.d.ts.map +1 -1
  45. package/dist/utils/index.js +13 -13
  46. package/dist/utils/index.js.map +1 -1
  47. package/mcp-tools/dev-browser/server.cjs +144 -0
  48. package/package.json +16 -3
  49. package/mcp-tools/ask-user-question/src/index.ts +0 -183
  50. package/mcp-tools/ask-user-question/tsconfig.json +0 -12
  51. package/mcp-tools/complete-task/src/index.ts +0 -92
  52. package/mcp-tools/dev-browser/src/index.ts +0 -290
  53. package/mcp-tools/dev-browser/src/relay.ts +0 -652
  54. package/mcp-tools/dev-browser/src/types.ts +0 -31
  55. package/mcp-tools/dev-browser/tsconfig.json +0 -36
  56. package/mcp-tools/dev-browser-mcp/src/index.ts +0 -3940
  57. package/mcp-tools/dev-browser-mcp/src/snapshot/compactor.test.ts +0 -86
  58. package/mcp-tools/dev-browser-mcp/src/snapshot/compactor.ts +0 -31
  59. package/mcp-tools/dev-browser-mcp/src/snapshot/differ.test.ts +0 -178
  60. package/mcp-tools/dev-browser-mcp/src/snapshot/differ.ts +0 -167
  61. package/mcp-tools/dev-browser-mcp/src/snapshot/index.ts +0 -19
  62. package/mcp-tools/dev-browser-mcp/src/snapshot/manager.test.ts +0 -247
  63. package/mcp-tools/dev-browser-mcp/src/snapshot/manager.ts +0 -131
  64. package/mcp-tools/dev-browser-mcp/src/snapshot/parser.test.ts +0 -94
  65. package/mcp-tools/dev-browser-mcp/src/snapshot/parser.ts +0 -81
  66. package/mcp-tools/dev-browser-mcp/src/snapshot/priority.test.ts +0 -104
  67. package/mcp-tools/dev-browser-mcp/src/snapshot/priority.ts +0 -84
  68. package/mcp-tools/dev-browser-mcp/src/snapshot/tokens.test.ts +0 -64
  69. package/mcp-tools/dev-browser-mcp/src/snapshot/tokens.ts +0 -36
  70. package/mcp-tools/dev-browser-mcp/src/snapshot/types.ts +0 -89
  71. package/mcp-tools/dev-browser-mcp/tsconfig.json +0 -15
  72. package/mcp-tools/file-permission/src/index.ts +0 -125
  73. package/mcp-tools/file-permission/tsconfig.json +0 -17
  74. package/mcp-tools/report-checkpoint/src/index.ts +0 -127
  75. package/mcp-tools/report-checkpoint/tsconfig.json +0 -12
  76. package/mcp-tools/report-thought/src/index.ts +0 -109
  77. package/mcp-tools/report-thought/tsconfig.json +0 -12
  78. package/mcp-tools/start-task/src/index.ts +0 -86
@@ -1,3940 +0,0 @@
1
- #!/usr/bin/env node
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
-
4
- console.error('[dev-browser-mcp] Script starting...');
5
- console.error('[dev-browser-mcp] Node version:', process.version);
6
- console.error('[dev-browser-mcp] CWD:', process.cwd());
7
- console.error('[dev-browser-mcp] ACCOMPLISH_TASK_ID:', process.env.ACCOMPLISH_TASK_ID || '(not set)');
8
-
9
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import {
12
- CallToolRequestSchema,
13
- ListToolsRequestSchema,
14
- type CallToolResult,
15
- } from '@modelcontextprotocol/sdk/types.js';
16
- import { chromium, type Browser, type Page, type ElementHandle } from 'playwright';
17
- import { getSnapshotManager, resetSnapshotManager } from './snapshot/index.js';
18
-
19
- console.error('[dev-browser-mcp] All imports completed successfully');
20
-
21
- const DEV_BROWSER_PORT = parseInt(process.env.DEV_BROWSER_PORT || '9224', 10);
22
- const DEV_BROWSER_URL = `http://localhost:${DEV_BROWSER_PORT}`;
23
- const TASK_ID = process.env.ACCOMPLISH_TASK_ID || 'default';
24
-
25
- function toAIFriendlyError(error: unknown, selector: string): Error {
26
- const message = error instanceof Error ? error.message : String(error);
27
-
28
- if (message.includes('strict mode violation')) {
29
- const countMatch = message.match(/resolved to (\d+) elements/);
30
- const count = countMatch ? countMatch[1] : 'multiple';
31
- return new Error(
32
- `Selector "${selector}" matched ${count} elements. ` +
33
- `Run browser_snapshot() to get updated refs, or use a more specific CSS selector.`
34
- );
35
- }
36
-
37
- if (message.includes('intercepts pointer events') || message.includes('element is not visible')) {
38
- return new Error(
39
- `Element "${selector}" is blocked by another element (likely a modal, overlay, or cookie banner). ` +
40
- `Try: 1) Look for close/dismiss buttons in the snapshot, 2) Press Escape with browser_keyboard, ` +
41
- `3) Click outside the overlay. Then retry your action.`
42
- );
43
- }
44
-
45
- if (message.includes('not visible') && !message.includes('Timeout')) {
46
- return new Error(
47
- `Element "${selector}" exists but is not visible. ` +
48
- `Try: 1) Use browser_scroll to scroll it into view, 2) Check if it's behind an overlay, ` +
49
- `3) Use browser_wait(condition="selector") to wait for it to appear.`
50
- );
51
- }
52
-
53
- if (message.includes('waiting for') && (message.includes('to be visible') || message.includes('Timeout'))) {
54
- return new Error(
55
- `Element "${selector}" not found or not visible within timeout. ` +
56
- `The page may have changed. Run browser_snapshot() to see current page elements.`
57
- );
58
- }
59
-
60
- if (message.includes('Target closed') || message.includes('Session closed') || message.includes('Page closed')) {
61
- return new Error(
62
- `The page or tab was closed unexpectedly. ` +
63
- `Use browser_tabs(action="list") to see open tabs and browser_tabs(action="switch") to switch to the correct one.`
64
- );
65
- }
66
-
67
- if (message.includes('net::ERR_') || message.includes('Navigation failed')) {
68
- return new Error(
69
- `Navigation failed: ${message}. ` +
70
- `Check if the URL is correct and the site is accessible. Try browser_screenshot() to see current state.`
71
- );
72
- }
73
-
74
- return new Error(
75
- `${message}. ` +
76
- `Try taking a new browser_snapshot() to see the current page state before retrying.`
77
- );
78
- }
79
-
80
- let browser: Browser | null = null;
81
- let connectingPromise: Promise<Browser> | null = null;
82
- let cachedServerMode: string | null = null;
83
- let activePageOverride: Page | null = null;
84
- let glowingPage: Page | null = null;
85
- const pagesWithGlowListeners = new WeakSet<Page>();
86
-
87
- async function injectGlowElements(page: Page): Promise<void> {
88
- if (page.isClosed()) return;
89
-
90
- try {
91
- await page.evaluate(() => {
92
- document.getElementById('__dev-browser-active-glow')?.remove();
93
- document.getElementById('__dev-browser-active-glow-style')?.remove();
94
-
95
- const style = document.createElement('style');
96
- style.id = '__dev-browser-active-glow-style';
97
- style.textContent = `
98
- @keyframes devBrowserGlowColor {
99
- 0%, 100% {
100
- border-color: rgba(59, 130, 246, 0.9);
101
- box-shadow:
102
- inset 0 0 30px rgba(59, 130, 246, 0.6),
103
- inset 0 0 60px rgba(59, 130, 246, 0.3),
104
- 0 0 20px rgba(59, 130, 246, 0.4);
105
- }
106
- 25% {
107
- border-color: rgba(168, 85, 247, 0.9);
108
- box-shadow:
109
- inset 0 0 30px rgba(168, 85, 247, 0.6),
110
- inset 0 0 60px rgba(168, 85, 247, 0.3),
111
- 0 0 20px rgba(168, 85, 247, 0.4);
112
- }
113
- 50% {
114
- border-color: rgba(236, 72, 153, 0.9);
115
- box-shadow:
116
- inset 0 0 30px rgba(236, 72, 153, 0.6),
117
- inset 0 0 60px rgba(236, 72, 153, 0.3),
118
- 0 0 20px rgba(236, 72, 153, 0.4);
119
- }
120
- 75% {
121
- border-color: rgba(34, 211, 238, 0.9);
122
- box-shadow:
123
- inset 0 0 30px rgba(34, 211, 238, 0.6),
124
- inset 0 0 60px rgba(34, 211, 238, 0.3),
125
- 0 0 20px rgba(34, 211, 238, 0.4);
126
- }
127
- }
128
- `;
129
- document.head.appendChild(style);
130
-
131
- const overlay = document.createElement('div');
132
- overlay.id = '__dev-browser-active-glow';
133
- overlay.style.cssText = `
134
- position: fixed;
135
- inset: 0;
136
- pointer-events: none;
137
- z-index: 2147483647;
138
- border: 5px solid rgba(59, 130, 246, 0.9);
139
- border-radius: 4px;
140
- box-shadow:
141
- inset 0 0 30px rgba(59, 130, 246, 0.6),
142
- inset 0 0 60px rgba(59, 130, 246, 0.3),
143
- 0 0 20px rgba(59, 130, 246, 0.4);
144
- animation: devBrowserGlowColor 6s ease-in-out infinite;
145
- `;
146
- document.body.appendChild(overlay);
147
- });
148
- } catch (err) {
149
- console.error('[dev-browser-mcp] Error injecting glow elements:', err);
150
- }
151
- }
152
-
153
- async function injectActiveTabGlow(page: Page): Promise<void> {
154
- if (glowingPage && glowingPage !== page && !glowingPage.isClosed()) {
155
- await removeActiveTabGlow(glowingPage);
156
- }
157
-
158
- glowingPage = page;
159
-
160
- await injectGlowElements(page);
161
-
162
- if (!pagesWithGlowListeners.has(page)) {
163
- pagesWithGlowListeners.add(page);
164
-
165
- page.on('load', async () => {
166
- if (glowingPage === page && !page.isClosed()) {
167
- console.error('[dev-browser-mcp] Page navigated, re-injecting glow...');
168
- await injectGlowElements(page);
169
- }
170
- });
171
- }
172
- }
173
-
174
- async function removeActiveTabGlow(page: Page): Promise<void> {
175
- if (page.isClosed()) {
176
- if (glowingPage === page) {
177
- glowingPage = null;
178
- }
179
- return;
180
- }
181
-
182
- try {
183
- await page.evaluate(() => {
184
- document.getElementById('__dev-browser-active-glow')?.remove();
185
- document.getElementById('__dev-browser-active-glow-style')?.remove();
186
- });
187
- } catch {
188
- }
189
-
190
- if (glowingPage === page) {
191
- glowingPage = null;
192
- }
193
- }
194
-
195
- async function fetchWithRetry(
196
- url: string,
197
- options?: RequestInit,
198
- maxRetries = 3,
199
- baseDelayMs = 100
200
- ): Promise<Response> {
201
- let lastError: Error | null = null;
202
- for (let i = 0; i < maxRetries; i++) {
203
- try {
204
- const res = await fetch(url, options);
205
- return res;
206
- } catch (err) {
207
- lastError = err instanceof Error ? err : new Error(String(err));
208
- const isConnectionError = lastError.message.includes('fetch failed') ||
209
- lastError.message.includes('ECONNREFUSED') ||
210
- lastError.message.includes('socket') ||
211
- lastError.message.includes('UND_ERR');
212
- if (!isConnectionError || i >= maxRetries - 1) {
213
- throw lastError;
214
- }
215
- const delay = baseDelayMs * Math.pow(2, i) + Math.random() * 50;
216
- await new Promise((resolve) => setTimeout(resolve, delay));
217
- }
218
- }
219
- throw lastError || new Error('fetchWithRetry failed');
220
- }
221
-
222
- async function ensureConnected(): Promise<Browser> {
223
- if (browser && browser.isConnected()) {
224
- return browser;
225
- }
226
-
227
- if (connectingPromise) {
228
- return connectingPromise;
229
- }
230
-
231
- connectingPromise = (async () => {
232
- try {
233
- const res = await fetchWithRetry(DEV_BROWSER_URL);
234
- if (!res.ok) {
235
- throw new Error(`Server returned ${res.status}: ${await res.text()}`);
236
- }
237
- const info = await res.json() as { wsEndpoint: string; mode?: string };
238
- cachedServerMode = info.mode || 'normal';
239
- browser = await chromium.connectOverCDP(info.wsEndpoint);
240
-
241
- for (const context of browser.contexts()) {
242
- context.on('page', async (page) => {
243
- console.error('[dev-browser-mcp] New page detected, injecting glow immediately...');
244
- setTimeout(async () => {
245
- try {
246
- if (!page.isClosed()) {
247
- await injectActiveTabGlow(page);
248
- console.error('[dev-browser-mcp] Glow injected on new page');
249
- }
250
- } catch (err) {
251
- console.error('[dev-browser-mcp] Failed to inject glow on new page:', err);
252
- }
253
- }, 100);
254
- });
255
-
256
- for (const page of context.pages()) {
257
- if (!page.isClosed() && !glowingPage) {
258
- try {
259
- await injectActiveTabGlow(page);
260
- } catch (err) {
261
- console.error('[dev-browser-mcp] Failed to inject glow on existing page:', err);
262
- }
263
- }
264
- }
265
- }
266
-
267
- return browser;
268
- } finally {
269
- connectingPromise = null;
270
- }
271
- })();
272
-
273
- return connectingPromise;
274
- }
275
-
276
- function getFullPageName(pageName?: string): string {
277
- const name = pageName || 'main';
278
- return `${TASK_ID}-${name}`;
279
- }
280
-
281
- async function findPageByTargetId(b: Browser, targetId: string): Promise<Page | null> {
282
- for (const context of b.contexts()) {
283
- for (const page of context.pages()) {
284
- let cdpSession;
285
- try {
286
- cdpSession = await context.newCDPSession(page);
287
- const { targetInfo } = await cdpSession.send('Target.getTargetInfo');
288
- if (targetInfo.targetId === targetId) {
289
- return page;
290
- }
291
- } catch (err) {
292
- const msg = err instanceof Error ? err.message : String(err);
293
- if (!msg.includes('Target closed') && !msg.includes('Session closed')) {
294
- console.warn(`Unexpected error checking page target: ${msg}`);
295
- }
296
- } finally {
297
- if (cdpSession) {
298
- try {
299
- await cdpSession.detach();
300
- } catch {
301
- }
302
- }
303
- }
304
- }
305
- }
306
- return null;
307
- }
308
-
309
- interface GetPageRequest {
310
- name: string;
311
- viewport?: { width: number; height: number };
312
- }
313
-
314
- interface GetPageResponse {
315
- targetId: string;
316
- url?: string;
317
- }
318
-
319
- async function getPage(pageName?: string): Promise<Page> {
320
- if (activePageOverride) {
321
- if (!activePageOverride.isClosed()) {
322
- return activePageOverride;
323
- }
324
- activePageOverride = null;
325
- }
326
-
327
- const fullName = getFullPageName(pageName);
328
-
329
- const res = await fetchWithRetry(`${DEV_BROWSER_URL}/pages`, {
330
- method: 'POST',
331
- headers: { 'Content-Type': 'application/json' },
332
- body: JSON.stringify({ name: fullName } satisfies GetPageRequest),
333
- });
334
-
335
- if (!res.ok) {
336
- throw new Error(`Failed to get page: ${await res.text()}`);
337
- }
338
-
339
- const pageInfo = await res.json() as GetPageResponse;
340
- const { targetId } = pageInfo;
341
-
342
- const b = await ensureConnected();
343
-
344
- const isExtensionMode = cachedServerMode === 'extension';
345
-
346
- if (isExtensionMode) {
347
- const allPages = b.contexts().flatMap((ctx) => ctx.pages());
348
- if (allPages.length === 0) {
349
- throw new Error('No pages available in browser');
350
- }
351
- if (allPages.length === 1) {
352
- return allPages[0]!;
353
- }
354
- if (pageInfo.url) {
355
- const matchingPage = allPages.find((p) => p.url() === pageInfo.url);
356
- if (matchingPage) {
357
- return matchingPage;
358
- }
359
- }
360
- return allPages[0]!;
361
- }
362
-
363
- const page = await findPageByTargetId(b, targetId);
364
- if (!page) {
365
- throw new Error(`Page "${fullName}" not found in browser contexts`);
366
- }
367
-
368
- return page;
369
- }
370
-
371
- async function waitForPageLoad(page: Page, timeout = 3000): Promise<void> {
372
- try {
373
- await page.waitForLoadState('domcontentloaded', { timeout });
374
- } catch {
375
- }
376
- }
377
-
378
- const SNAPSHOT_SCRIPT = `
379
- (function() {
380
- if (window.__devBrowser_getAISnapshot) return;
381
-
382
- let cacheStyle;
383
- let cachesCounter = 0;
384
-
385
- function beginDOMCaches() {
386
- ++cachesCounter;
387
- cacheStyle = cacheStyle || new Map();
388
- }
389
-
390
- function endDOMCaches() {
391
- if (!--cachesCounter) {
392
- cacheStyle = undefined;
393
- }
394
- }
395
-
396
- function getElementComputedStyle(element, pseudo) {
397
- const cache = cacheStyle;
398
- const cacheKey = pseudo ? undefined : element;
399
- if (cache && cacheKey && cache.has(cacheKey)) return cache.get(cacheKey);
400
- const style = element.ownerDocument && element.ownerDocument.defaultView
401
- ? element.ownerDocument.defaultView.getComputedStyle(element, pseudo)
402
- : undefined;
403
- if (cache && cacheKey) cache.set(cacheKey, style);
404
- return style;
405
- }
406
-
407
- function parentElementOrShadowHost(element) {
408
- if (element.parentElement) return element.parentElement;
409
- if (!element.parentNode) return;
410
- if (element.parentNode.nodeType === 11 && element.parentNode.host)
411
- return element.parentNode.host;
412
- }
413
-
414
- function enclosingShadowRootOrDocument(element) {
415
- let node = element;
416
- while (node.parentNode) node = node.parentNode;
417
- if (node.nodeType === 11 || node.nodeType === 9)
418
- return node;
419
- }
420
-
421
- function closestCrossShadow(element, css, scope) {
422
- while (element) {
423
- const closest = element.closest(css);
424
- if (scope && closest !== scope && closest?.contains(scope)) return;
425
- if (closest) return closest;
426
- element = enclosingShadowHost(element);
427
- }
428
- }
429
-
430
- function enclosingShadowHost(element) {
431
- while (element.parentElement) element = element.parentElement;
432
- return parentElementOrShadowHost(element);
433
- }
434
-
435
- function isElementStyleVisibilityVisible(element, style) {
436
- style = style || getElementComputedStyle(element);
437
- if (!style) return true;
438
- if (style.visibility !== "visible") return false;
439
- const detailsOrSummary = element.closest("details,summary");
440
- if (detailsOrSummary !== element && detailsOrSummary?.nodeName === "DETAILS" && !detailsOrSummary.open)
441
- return false;
442
- return true;
443
- }
444
-
445
- function computeBox(element) {
446
- const style = getElementComputedStyle(element);
447
- if (!style) return { visible: true, inline: false };
448
- const cursor = style.cursor;
449
- if (style.display === "contents") {
450
- for (let child = element.firstChild; child; child = child.nextSibling) {
451
- if (child.nodeType === 1 && isElementVisible(child))
452
- return { visible: true, inline: false, cursor };
453
- if (child.nodeType === 3 && isVisibleTextNode(child))
454
- return { visible: true, inline: true, cursor };
455
- }
456
- return { visible: false, inline: false, cursor };
457
- }
458
- if (!isElementStyleVisibilityVisible(element, style))
459
- return { cursor, visible: false, inline: false };
460
- const rect = element.getBoundingClientRect();
461
- return { rect, cursor, visible: rect.width > 0 && rect.height > 0, inline: style.display === "inline" };
462
- }
463
-
464
- function isElementVisible(element) {
465
- return computeBox(element).visible;
466
- }
467
-
468
- function isVisibleTextNode(node) {
469
- const range = node.ownerDocument.createRange();
470
- range.selectNode(node);
471
- const rect = range.getBoundingClientRect();
472
- return rect.width > 0 && rect.height > 0;
473
- }
474
-
475
- function elementSafeTagName(element) {
476
- const tagName = element.tagName;
477
- if (typeof tagName === "string") return tagName.toUpperCase();
478
- if (element instanceof HTMLFormElement) return "FORM";
479
- return element.tagName.toUpperCase();
480
- }
481
-
482
- function normalizeWhiteSpace(text) {
483
- return text.split("\\u00A0").map(chunk =>
484
- chunk.replace(/\\r\\n/g, "\\n").replace(/[\\u200b\\u00ad]/g, "").replace(/\\s\\s*/g, " ")
485
- ).join("\\u00A0").trim();
486
- }
487
-
488
- function yamlEscapeKeyIfNeeded(str) {
489
- if (!yamlStringNeedsQuotes(str)) return str;
490
- return "'" + str.replace(/'/g, "''") + "'";
491
- }
492
-
493
- function yamlEscapeValueIfNeeded(str) {
494
- if (!yamlStringNeedsQuotes(str)) return str;
495
- return '"' + str.replace(/[\\\\"\x00-\\x1f\\x7f-\\x9f]/g, c => {
496
- switch (c) {
497
- case "\\\\": return "\\\\\\\\";
498
- case '"': return '\\\\"';
499
- case "\\b": return "\\\\b";
500
- case "\\f": return "\\\\f";
501
- case "\\n": return "\\\\n";
502
- case "\\r": return "\\\\r";
503
- case "\\t": return "\\\\t";
504
- default:
505
- const code = c.charCodeAt(0);
506
- return "\\\\x" + code.toString(16).padStart(2, "0");
507
- }
508
- }) + '"';
509
- }
510
-
511
- function yamlStringNeedsQuotes(str) {
512
- if (str.length === 0) return true;
513
- if (/^\\s|\\s$/.test(str)) return true;
514
- if (/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\x9f]/.test(str)) return true;
515
- if (/^-/.test(str)) return true;
516
- if (/[\\n:](\\s|$)/.test(str)) return true;
517
- if (/\\s#/.test(str)) return true;
518
- if (/[\\n\\r]/.test(str)) return true;
519
- if (/^[&*\\],?!>|@"'#%]/.test(str)) return true;
520
- if (/[{}\`]/.test(str)) return true;
521
- if (/^\\[/.test(str)) return true;
522
- if (!isNaN(Number(str)) || ["y","n","yes","no","true","false","on","off","null"].includes(str.toLowerCase())) return true;
523
- return false;
524
- }
525
-
526
- const validRoles = ["alert","alertdialog","application","article","banner","blockquote","button","caption","cell","checkbox","code","columnheader","combobox","complementary","contentinfo","definition","deletion","dialog","directory","document","emphasis","feed","figure","form","generic","grid","gridcell","group","heading","img","insertion","link","list","listbox","listitem","log","main","mark","marquee","math","meter","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","navigation","none","note","option","paragraph","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","strong","subscript","superscript","switch","tab","table","tablist","tabpanel","term","textbox","time","timer","toolbar","tooltip","tree","treegrid","treeitem"];
527
-
528
- let cacheAccessibleName;
529
- let cacheIsHidden;
530
- let cachePointerEvents;
531
- let ariaCachesCounter = 0;
532
-
533
- function beginAriaCaches() {
534
- beginDOMCaches();
535
- ++ariaCachesCounter;
536
- cacheAccessibleName = cacheAccessibleName || new Map();
537
- cacheIsHidden = cacheIsHidden || new Map();
538
- cachePointerEvents = cachePointerEvents || new Map();
539
- }
540
-
541
- function endAriaCaches() {
542
- if (!--ariaCachesCounter) {
543
- cacheAccessibleName = undefined;
544
- cacheIsHidden = undefined;
545
- cachePointerEvents = undefined;
546
- }
547
- endDOMCaches();
548
- }
549
-
550
- function hasExplicitAccessibleName(e) {
551
- return e.hasAttribute("aria-label") || e.hasAttribute("aria-labelledby");
552
- }
553
-
554
- const kAncestorPreventingLandmark = "article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]";
555
-
556
- const kGlobalAriaAttributes = [
557
- ["aria-atomic", undefined],["aria-busy", undefined],["aria-controls", undefined],["aria-current", undefined],
558
- ["aria-describedby", undefined],["aria-details", undefined],["aria-dropeffect", undefined],["aria-flowto", undefined],
559
- ["aria-grabbed", undefined],["aria-hidden", undefined],["aria-keyshortcuts", undefined],
560
- ["aria-label", ["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]],
561
- ["aria-labelledby", ["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]],
562
- ["aria-live", undefined],["aria-owns", undefined],["aria-relevant", undefined],["aria-roledescription", ["generic"]]
563
- ];
564
-
565
- function hasGlobalAriaAttribute(element, forRole) {
566
- return kGlobalAriaAttributes.some(([attr, prohibited]) => !prohibited?.includes(forRole || "") && element.hasAttribute(attr));
567
- }
568
-
569
- function hasTabIndex(element) {
570
- return !Number.isNaN(Number(String(element.getAttribute("tabindex"))));
571
- }
572
-
573
- function isFocusable(element) {
574
- return !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element));
575
- }
576
-
577
- function isNativelyFocusable(element) {
578
- const tagName = elementSafeTagName(element);
579
- if (["BUTTON","DETAILS","SELECT","TEXTAREA"].includes(tagName)) return true;
580
- if (tagName === "A" || tagName === "AREA") return element.hasAttribute("href");
581
- if (tagName === "INPUT") return !element.hidden;
582
- return false;
583
- }
584
-
585
- function isNativelyDisabled(element) {
586
- const isNativeFormControl = ["BUTTON","INPUT","SELECT","TEXTAREA","OPTION","OPTGROUP"].includes(elementSafeTagName(element));
587
- return isNativeFormControl && (element.hasAttribute("disabled") || belongsToDisabledFieldSet(element));
588
- }
589
-
590
- function belongsToDisabledFieldSet(element) {
591
- const fieldSetElement = element?.closest("FIELDSET[DISABLED]");
592
- if (!fieldSetElement) return false;
593
- const legendElement = fieldSetElement.querySelector(":scope > LEGEND");
594
- return !legendElement || !legendElement.contains(element);
595
- }
596
-
597
- const inputTypeToRole = {button:"button",checkbox:"checkbox",image:"button",number:"spinbutton",radio:"radio",range:"slider",reset:"button",submit:"button"};
598
-
599
- function getIdRefs(element, ref) {
600
- if (!ref) return [];
601
- const root = enclosingShadowRootOrDocument(element);
602
- if (!root) return [];
603
- try {
604
- const ids = ref.split(" ").filter(id => !!id);
605
- const result = [];
606
- for (const id of ids) {
607
- const firstElement = root.querySelector("#" + CSS.escape(id));
608
- if (firstElement && !result.includes(firstElement)) result.push(firstElement);
609
- }
610
- return result;
611
- } catch { return []; }
612
- }
613
-
614
- const kImplicitRoleByTagName = {
615
- A: e => e.hasAttribute("href") ? "link" : null,
616
- AREA: e => e.hasAttribute("href") ? "link" : null,
617
- ARTICLE: () => "article", ASIDE: () => "complementary", BLOCKQUOTE: () => "blockquote", BUTTON: () => "button",
618
- CAPTION: () => "caption", CODE: () => "code", DATALIST: () => "listbox", DD: () => "definition",
619
- DEL: () => "deletion", DETAILS: () => "group", DFN: () => "term", DIALOG: () => "dialog", DT: () => "term",
620
- EM: () => "emphasis", FIELDSET: () => "group", FIGURE: () => "figure",
621
- FOOTER: e => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : "contentinfo",
622
- FORM: e => hasExplicitAccessibleName(e) ? "form" : null,
623
- H1: () => "heading", H2: () => "heading", H3: () => "heading", H4: () => "heading", H5: () => "heading", H6: () => "heading",
624
- HEADER: e => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : "banner",
625
- HR: () => "separator", HTML: () => "document",
626
- IMG: e => e.getAttribute("alt") === "" && !e.getAttribute("title") && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? "presentation" : "img",
627
- INPUT: e => {
628
- const type = e.type.toLowerCase();
629
- if (type === "search") return e.hasAttribute("list") ? "combobox" : "searchbox";
630
- if (["email","tel","text","url",""].includes(type)) {
631
- const list = getIdRefs(e, e.getAttribute("list"))[0];
632
- return list && elementSafeTagName(list) === "DATALIST" ? "combobox" : "textbox";
633
- }
634
- if (type === "hidden") return null;
635
- if (type === "file") return "button";
636
- return inputTypeToRole[type] || "textbox";
637
- },
638
- INS: () => "insertion", LI: () => "listitem", MAIN: () => "main", MARK: () => "mark", MATH: () => "math",
639
- MENU: () => "list", METER: () => "meter", NAV: () => "navigation", OL: () => "list", OPTGROUP: () => "group",
640
- OPTION: () => "option", OUTPUT: () => "status", P: () => "paragraph", PROGRESS: () => "progressbar",
641
- SEARCH: () => "search", SECTION: e => hasExplicitAccessibleName(e) ? "region" : null,
642
- SELECT: e => e.hasAttribute("multiple") || e.size > 1 ? "listbox" : "combobox",
643
- STRONG: () => "strong", SUB: () => "subscript", SUP: () => "superscript", SVG: () => "img",
644
- TABLE: () => "table", TBODY: () => "rowgroup",
645
- TD: e => { const table = closestCrossShadow(e, "table"); const role = table ? getExplicitAriaRole(table) : ""; return role === "grid" || role === "treegrid" ? "gridcell" : "cell"; },
646
- TEXTAREA: () => "textbox", TFOOT: () => "rowgroup",
647
- TH: e => { const scope = e.getAttribute("scope"); if (scope === "col" || scope === "colgroup") return "columnheader"; if (scope === "row" || scope === "rowgroup") return "rowheader"; return "columnheader"; },
648
- THEAD: () => "rowgroup", TIME: () => "time", TR: () => "row", UL: () => "list"
649
- };
650
-
651
- function getExplicitAriaRole(element) {
652
- const roles = (element.getAttribute("role") || "").split(" ").map(role => role.trim());
653
- return roles.find(role => validRoles.includes(role)) || null;
654
- }
655
-
656
- function getImplicitAriaRole(element) {
657
- const fn = kImplicitRoleByTagName[elementSafeTagName(element)];
658
- return fn ? fn(element) : null;
659
- }
660
-
661
- function hasPresentationConflictResolution(element, role) {
662
- return hasGlobalAriaAttribute(element, role) || isFocusable(element);
663
- }
664
-
665
- function getAriaRole(element) {
666
- const explicitRole = getExplicitAriaRole(element);
667
- if (!explicitRole) return getImplicitAriaRole(element);
668
- if (explicitRole === "none" || explicitRole === "presentation") {
669
- const implicitRole = getImplicitAriaRole(element);
670
- if (hasPresentationConflictResolution(element, implicitRole)) return implicitRole;
671
- }
672
- return explicitRole;
673
- }
674
-
675
- function getAriaBoolean(attr) {
676
- return attr === null ? undefined : attr.toLowerCase() === "true";
677
- }
678
-
679
- function isElementIgnoredForAria(element) {
680
- return ["STYLE","SCRIPT","NOSCRIPT","TEMPLATE"].includes(elementSafeTagName(element));
681
- }
682
-
683
- function isElementHiddenForAria(element) {
684
- if (isElementIgnoredForAria(element)) return true;
685
- const style = getElementComputedStyle(element);
686
- const isSlot = element.nodeName === "SLOT";
687
- if (style?.display === "contents" && !isSlot) {
688
- for (let child = element.firstChild; child; child = child.nextSibling) {
689
- if (child.nodeType === 1 && !isElementHiddenForAria(child)) return false;
690
- if (child.nodeType === 3 && isVisibleTextNode(child)) return false;
691
- }
692
- return true;
693
- }
694
- const isOptionInsideSelect = element.nodeName === "OPTION" && !!element.closest("select");
695
- if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style)) return true;
696
- return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element);
697
- }
698
-
699
- function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element) {
700
- let hidden = cacheIsHidden?.get(element);
701
- if (hidden === undefined) {
702
- hidden = false;
703
- if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot) hidden = true;
704
- if (!hidden) {
705
- const style = getElementComputedStyle(element);
706
- hidden = !style || style.display === "none" || getAriaBoolean(element.getAttribute("aria-hidden")) === true;
707
- }
708
- if (!hidden) {
709
- const parent = parentElementOrShadowHost(element);
710
- if (parent) hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent);
711
- }
712
- cacheIsHidden?.set(element, hidden);
713
- }
714
- return hidden;
715
- }
716
-
717
- function getAriaLabelledByElements(element) {
718
- const ref = element.getAttribute("aria-labelledby");
719
- if (ref === null) return null;
720
- const refs = getIdRefs(element, ref);
721
- return refs.length ? refs : null;
722
- }
723
-
724
- function getElementAccessibleName(element, includeHidden) {
725
- let accessibleName = cacheAccessibleName?.get(element);
726
- if (accessibleName === undefined) {
727
- accessibleName = "";
728
- const elementProhibitsNaming = ["caption","code","definition","deletion","emphasis","generic","insertion","mark","paragraph","presentation","strong","subscript","suggestion","superscript","term","time"].includes(getAriaRole(element) || "");
729
- if (!elementProhibitsNaming) {
730
- accessibleName = normalizeWhiteSpace(getTextAlternativeInternal(element, { includeHidden, visitedElements: new Set(), embeddedInTargetElement: "self" }));
731
- }
732
- cacheAccessibleName?.set(element, accessibleName);
733
- }
734
- return accessibleName;
735
- }
736
-
737
- function getTextAlternativeInternal(element, options) {
738
- if (options.visitedElements.has(element)) return "";
739
- const childOptions = { ...options, embeddedInTargetElement: options.embeddedInTargetElement === "self" ? "descendant" : options.embeddedInTargetElement };
740
-
741
- if (!options.includeHidden) {
742
- const isEmbeddedInHiddenReferenceTraversal = !!options.embeddedInLabelledBy?.hidden || !!options.embeddedInLabel?.hidden;
743
- if (isElementIgnoredForAria(element) || (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) {
744
- options.visitedElements.add(element);
745
- return "";
746
- }
747
- }
748
-
749
- const labelledBy = getAriaLabelledByElements(element);
750
- if (!options.embeddedInLabelledBy) {
751
- const accessibleName = (labelledBy || []).map(ref => getTextAlternativeInternal(ref, { ...options, embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) }, embeddedInTargetElement: undefined, embeddedInLabel: undefined })).join(" ");
752
- if (accessibleName) return accessibleName;
753
- }
754
-
755
- const role = getAriaRole(element) || "";
756
- const tagName = elementSafeTagName(element);
757
-
758
- const ariaLabel = element.getAttribute("aria-label") || "";
759
- if (ariaLabel.trim()) { options.visitedElements.add(element); return ariaLabel; }
760
-
761
- if (!["presentation","none"].includes(role)) {
762
- if (tagName === "INPUT" && ["button","submit","reset"].includes(element.type)) {
763
- options.visitedElements.add(element);
764
- const value = element.value || "";
765
- if (value.trim()) return value;
766
- if (element.type === "submit") return "Submit";
767
- if (element.type === "reset") return "Reset";
768
- return element.getAttribute("title") || "";
769
- }
770
- if (tagName === "INPUT" && element.type === "image") {
771
- options.visitedElements.add(element);
772
- const alt = element.getAttribute("alt") || "";
773
- if (alt.trim()) return alt;
774
- const title = element.getAttribute("title") || "";
775
- if (title.trim()) return title;
776
- return "Submit";
777
- }
778
- if (tagName === "IMG") {
779
- options.visitedElements.add(element);
780
- const alt = element.getAttribute("alt") || "";
781
- if (alt.trim()) return alt;
782
- return element.getAttribute("title") || "";
783
- }
784
- if (!labelledBy && ["BUTTON","INPUT","TEXTAREA","SELECT"].includes(tagName)) {
785
- const labels = element.labels;
786
- if (labels?.length) {
787
- options.visitedElements.add(element);
788
- return [...labels].map(label => getTextAlternativeInternal(label, { ...options, embeddedInLabel: { element: label, hidden: isElementHiddenForAria(label) }, embeddedInLabelledBy: undefined, embeddedInTargetElement: undefined })).filter(name => !!name).join(" ");
789
- }
790
- }
791
- }
792
-
793
- const allowsNameFromContent = ["button","cell","checkbox","columnheader","gridcell","heading","link","menuitem","menuitemcheckbox","menuitemradio","option","radio","row","rowheader","switch","tab","tooltip","treeitem"].includes(role);
794
- if (allowsNameFromContent || !!options.embeddedInLabelledBy || !!options.embeddedInLabel) {
795
- options.visitedElements.add(element);
796
- const accessibleName = innerAccumulatedElementText(element, childOptions);
797
- const maybeTrimmedAccessibleName = options.embeddedInTargetElement === "self" ? accessibleName.trim() : accessibleName;
798
- if (maybeTrimmedAccessibleName) return accessibleName;
799
- }
800
-
801
- if (!["presentation","none"].includes(role) || tagName === "IFRAME") {
802
- options.visitedElements.add(element);
803
- const title = element.getAttribute("title") || "";
804
- if (title.trim()) return title;
805
- }
806
-
807
- options.visitedElements.add(element);
808
- return "";
809
- }
810
-
811
- function innerAccumulatedElementText(element, options) {
812
- const tokens = [];
813
- const visit = (node, skipSlotted) => {
814
- if (skipSlotted && node.assignedSlot) return;
815
- if (node.nodeType === 1) {
816
- const display = getElementComputedStyle(node)?.display || "inline";
817
- let token = getTextAlternativeInternal(node, options);
818
- if (display !== "inline" || node.nodeName === "BR") token = " " + token + " ";
819
- tokens.push(token);
820
- } else if (node.nodeType === 3) {
821
- tokens.push(node.textContent || "");
822
- }
823
- };
824
- const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : [];
825
- if (assignedNodes.length) {
826
- for (const child of assignedNodes) visit(child, false);
827
- } else {
828
- for (let child = element.firstChild; child; child = child.nextSibling) visit(child, true);
829
- if (element.shadowRoot) {
830
- for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(child, true);
831
- }
832
- }
833
- return tokens.join("");
834
- }
835
-
836
- const kAriaCheckedRoles = ["checkbox","menuitemcheckbox","option","radio","switch","menuitemradio","treeitem"];
837
- function getAriaChecked(element) {
838
- const tagName = elementSafeTagName(element);
839
- if (tagName === "INPUT" && element.indeterminate) return "mixed";
840
- if (tagName === "INPUT" && ["checkbox","radio"].includes(element.type)) return element.checked;
841
- if (kAriaCheckedRoles.includes(getAriaRole(element) || "")) {
842
- const checked = element.getAttribute("aria-checked");
843
- if (checked === "true") return true;
844
- if (checked === "mixed") return "mixed";
845
- return false;
846
- }
847
- return false;
848
- }
849
-
850
- const kAriaDisabledRoles = ["application","button","composite","gridcell","group","input","link","menuitem","scrollbar","separator","tab","checkbox","columnheader","combobox","grid","listbox","menu","menubar","menuitemcheckbox","menuitemradio","option","radio","radiogroup","row","rowheader","searchbox","select","slider","spinbutton","switch","tablist","textbox","toolbar","tree","treegrid","treeitem"];
851
- function getAriaDisabled(element) {
852
- return isNativelyDisabled(element) || hasExplicitAriaDisabled(element);
853
- }
854
- function hasExplicitAriaDisabled(element, isAncestor) {
855
- if (!element) return false;
856
- if (isAncestor || kAriaDisabledRoles.includes(getAriaRole(element) || "")) {
857
- const attribute = (element.getAttribute("aria-disabled") || "").toLowerCase();
858
- if (attribute === "true") return true;
859
- if (attribute === "false") return false;
860
- return hasExplicitAriaDisabled(parentElementOrShadowHost(element), true);
861
- }
862
- return false;
863
- }
864
-
865
- const kAriaExpandedRoles = ["application","button","checkbox","combobox","gridcell","link","listbox","menuitem","row","rowheader","tab","treeitem","columnheader","menuitemcheckbox","menuitemradio","switch"];
866
- function getAriaExpanded(element) {
867
- if (elementSafeTagName(element) === "DETAILS") return element.open;
868
- if (kAriaExpandedRoles.includes(getAriaRole(element) || "")) {
869
- const expanded = element.getAttribute("aria-expanded");
870
- if (expanded === null) return undefined;
871
- if (expanded === "true") return true;
872
- return false;
873
- }
874
- return undefined;
875
- }
876
-
877
- const kAriaLevelRoles = ["heading","listitem","row","treeitem"];
878
- function getAriaLevel(element) {
879
- const native = {H1:1,H2:2,H3:3,H4:4,H5:5,H6:6}[elementSafeTagName(element)];
880
- if (native) return native;
881
- if (kAriaLevelRoles.includes(getAriaRole(element) || "")) {
882
- const attr = element.getAttribute("aria-level");
883
- const value = attr === null ? Number.NaN : Number(attr);
884
- if (Number.isInteger(value) && value >= 1) return value;
885
- }
886
- return 0;
887
- }
888
-
889
- const kAriaPressedRoles = ["button"];
890
- function getAriaPressed(element) {
891
- if (kAriaPressedRoles.includes(getAriaRole(element) || "")) {
892
- const pressed = element.getAttribute("aria-pressed");
893
- if (pressed === "true") return true;
894
- if (pressed === "mixed") return "mixed";
895
- }
896
- return false;
897
- }
898
-
899
- const kAriaSelectedRoles = ["gridcell","option","row","tab","rowheader","columnheader","treeitem"];
900
- function getAriaSelected(element) {
901
- if (elementSafeTagName(element) === "OPTION") return element.selected;
902
- if (kAriaSelectedRoles.includes(getAriaRole(element) || "")) return getAriaBoolean(element.getAttribute("aria-selected")) === true;
903
- return false;
904
- }
905
-
906
- function receivesPointerEvents(element) {
907
- const cache = cachePointerEvents;
908
- let e = element;
909
- let result;
910
- const parents = [];
911
- for (; e; e = parentElementOrShadowHost(e)) {
912
- const cached = cache?.get(e);
913
- if (cached !== undefined) { result = cached; break; }
914
- parents.push(e);
915
- const style = getElementComputedStyle(e);
916
- if (!style) { result = true; break; }
917
- const value = style.pointerEvents;
918
- if (value) { result = value !== "none"; break; }
919
- }
920
- if (result === undefined) result = true;
921
- for (const parent of parents) cache?.set(parent, result);
922
- return result;
923
- }
924
-
925
- function getCSSContent(element, pseudo) {
926
- const style = getElementComputedStyle(element, pseudo);
927
- if (!style) return undefined;
928
- const contentValue = style.content;
929
- if (!contentValue || contentValue === "none" || contentValue === "normal") return undefined;
930
- if (style.display === "none" || style.visibility === "hidden") return undefined;
931
- const match = contentValue.match(/^"(.*)"$/);
932
- if (match) {
933
- const content = match[1].replace(/\\\\"/g, '"');
934
- if (pseudo) {
935
- const display = style.display || "inline";
936
- if (display !== "inline") return " " + content + " ";
937
- }
938
- return content;
939
- }
940
- return undefined;
941
- }
942
-
943
- let lastRef = 0;
944
-
945
- function generateAriaTree(rootElement) {
946
- const options = { visibility: "ariaOrVisible", refs: "interactable", refPrefix: "", includeGenericRole: true, renderActive: true, renderCursorPointer: true };
947
- const visited = new Set();
948
- const snapshot = {
949
- root: { role: "fragment", name: "", children: [], element: rootElement, props: {}, box: computeBox(rootElement), receivesPointerEvents: true },
950
- elements: new Map(),
951
- refs: new Map(),
952
- iframeRefs: []
953
- };
954
-
955
- const visit = (ariaNode, node, parentElementVisible) => {
956
- if (visited.has(node)) return;
957
- visited.add(node);
958
- if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
959
- if (!parentElementVisible) return;
960
- const text = node.nodeValue;
961
- if (ariaNode.role !== "textbox" && text) ariaNode.children.push(node.nodeValue || "");
962
- return;
963
- }
964
- if (node.nodeType !== Node.ELEMENT_NODE) return;
965
- const element = node;
966
- const isElementVisibleForAria = !isElementHiddenForAria(element);
967
- let visible = isElementVisibleForAria;
968
- if (options.visibility === "ariaOrVisible") visible = isElementVisibleForAria || isElementVisible(element);
969
- if (options.visibility === "ariaAndVisible") visible = isElementVisibleForAria && isElementVisible(element);
970
- if (options.visibility === "aria" && !visible) return;
971
- const ariaChildren = [];
972
- if (element.hasAttribute("aria-owns")) {
973
- const ids = element.getAttribute("aria-owns").split(/\\s+/);
974
- for (const id of ids) {
975
- const ownedElement = rootElement.ownerDocument.getElementById(id);
976
- if (ownedElement) ariaChildren.push(ownedElement);
977
- }
978
- }
979
- const childAriaNode = visible ? toAriaNode(element, options) : null;
980
- if (childAriaNode) {
981
- if (childAriaNode.ref) {
982
- snapshot.elements.set(childAriaNode.ref, element);
983
- snapshot.refs.set(element, childAriaNode.ref);
984
- if (childAriaNode.role === "iframe") snapshot.iframeRefs.push(childAriaNode.ref);
985
- }
986
- ariaNode.children.push(childAriaNode);
987
- }
988
- processElement(childAriaNode || ariaNode, element, ariaChildren, visible);
989
- };
990
-
991
- function processElement(ariaNode, element, ariaChildren, parentElementVisible) {
992
- const display = getElementComputedStyle(element)?.display || "inline";
993
- const treatAsBlock = display !== "inline" || element.nodeName === "BR" ? " " : "";
994
- if (treatAsBlock) ariaNode.children.push(treatAsBlock);
995
- ariaNode.children.push(getCSSContent(element, "::before") || "");
996
- const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : [];
997
- if (assignedNodes.length) {
998
- for (const child of assignedNodes) visit(ariaNode, child, parentElementVisible);
999
- } else {
1000
- for (let child = element.firstChild; child; child = child.nextSibling) {
1001
- if (!child.assignedSlot) visit(ariaNode, child, parentElementVisible);
1002
- }
1003
- if (element.shadowRoot) {
1004
- for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(ariaNode, child, parentElementVisible);
1005
- }
1006
- }
1007
- for (const child of ariaChildren) visit(ariaNode, child, parentElementVisible);
1008
- ariaNode.children.push(getCSSContent(element, "::after") || "");
1009
- if (treatAsBlock) ariaNode.children.push(treatAsBlock);
1010
- if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0]) ariaNode.children = [];
1011
- if (ariaNode.role === "link" && element.hasAttribute("href")) ariaNode.props["url"] = element.getAttribute("href");
1012
- if (ariaNode.role === "textbox" && element.hasAttribute("placeholder") && element.getAttribute("placeholder") !== ariaNode.name) ariaNode.props["placeholder"] = element.getAttribute("placeholder");
1013
- }
1014
-
1015
- beginAriaCaches();
1016
- try { visit(snapshot.root, rootElement, true); }
1017
- finally { endAriaCaches(); }
1018
- normalizeStringChildren(snapshot.root);
1019
- normalizeGenericRoles(snapshot.root);
1020
- return snapshot;
1021
- }
1022
-
1023
- function computeAriaRef(ariaNode, options) {
1024
- if (options.refs === "none") return;
1025
- if (options.refs === "interactable" && (!ariaNode.box.visible || !ariaNode.receivesPointerEvents)) return;
1026
- let ariaRef = ariaNode.element._ariaRef;
1027
- if (!ariaRef || ariaRef.role !== ariaNode.role || ariaRef.name !== ariaNode.name) {
1028
- ariaRef = { role: ariaNode.role, name: ariaNode.name, ref: (options.refPrefix || "") + "e" + (++lastRef) };
1029
- ariaNode.element._ariaRef = ariaRef;
1030
- }
1031
- ariaNode.ref = ariaRef.ref;
1032
- }
1033
-
1034
- function toAriaNode(element, options) {
1035
- const active = element.ownerDocument.activeElement === element;
1036
- if (element.nodeName === "IFRAME") {
1037
- const ariaNode = { role: "iframe", name: "", children: [], props: {}, element, box: computeBox(element), receivesPointerEvents: true, active };
1038
- computeAriaRef(ariaNode, options);
1039
- return ariaNode;
1040
- }
1041
- const defaultRole = options.includeGenericRole ? "generic" : null;
1042
- const role = getAriaRole(element) || defaultRole;
1043
- if (!role || role === "presentation" || role === "none") return null;
1044
- const name = normalizeWhiteSpace(getElementAccessibleName(element, false) || "");
1045
- const receivesPointerEventsValue = receivesPointerEvents(element);
1046
- const box = computeBox(element);
1047
- if (role === "generic" && box.inline && element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) return null;
1048
- const result = { role, name, children: [], props: {}, element, box, receivesPointerEvents: receivesPointerEventsValue, active };
1049
- computeAriaRef(result, options);
1050
- if (kAriaCheckedRoles.includes(role)) result.checked = getAriaChecked(element);
1051
- if (kAriaDisabledRoles.includes(role)) result.disabled = getAriaDisabled(element);
1052
- if (kAriaExpandedRoles.includes(role)) result.expanded = getAriaExpanded(element);
1053
- if (kAriaLevelRoles.includes(role)) result.level = getAriaLevel(element);
1054
- if (kAriaPressedRoles.includes(role)) result.pressed = getAriaPressed(element);
1055
- if (kAriaSelectedRoles.includes(role)) result.selected = getAriaSelected(element);
1056
- if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
1057
- if (element.type !== "checkbox" && element.type !== "radio" && element.type !== "file") result.children = [element.value];
1058
- }
1059
- return result;
1060
- }
1061
-
1062
- function normalizeGenericRoles(node) {
1063
- const normalizeChildren = (node) => {
1064
- const result = [];
1065
- for (const child of node.children || []) {
1066
- if (typeof child === "string") { result.push(child); continue; }
1067
- const normalized = normalizeChildren(child);
1068
- result.push(...normalized);
1069
- }
1070
- const removeSelf = node.role === "generic" && !node.name && result.length <= 1 && result.every(c => typeof c !== "string" && !!c.ref);
1071
- if (removeSelf) return result;
1072
- node.children = result;
1073
- return [node];
1074
- };
1075
- normalizeChildren(node);
1076
- }
1077
-
1078
- function normalizeStringChildren(rootA11yNode) {
1079
- const flushChildren = (buffer, normalizedChildren) => {
1080
- if (!buffer.length) return;
1081
- const text = normalizeWhiteSpace(buffer.join(""));
1082
- if (text) normalizedChildren.push(text);
1083
- buffer.length = 0;
1084
- };
1085
- const visit = (ariaNode) => {
1086
- const normalizedChildren = [];
1087
- const buffer = [];
1088
- for (const child of ariaNode.children || []) {
1089
- if (typeof child === "string") { buffer.push(child); }
1090
- else { flushChildren(buffer, normalizedChildren); visit(child); normalizedChildren.push(child); }
1091
- }
1092
- flushChildren(buffer, normalizedChildren);
1093
- ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
1094
- if (ariaNode.children.length === 1 && ariaNode.children[0] === ariaNode.name) ariaNode.children = [];
1095
- };
1096
- visit(rootA11yNode);
1097
- }
1098
-
1099
- function hasPointerCursor(ariaNode) { return ariaNode.box.cursor === "pointer"; }
1100
-
1101
- const INTERACTIVE_ROLES = ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox', 'option', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'searchbox', 'slider', 'spinbutton', 'switch', 'dialog', 'alertdialog', 'menu', 'navigation', 'form'];
1102
-
1103
- const ROLE_PRIORITIES = {
1104
- button: 100, textbox: 95, searchbox: 95,
1105
- checkbox: 90, radio: 90, switch: 90,
1106
- combobox: 85, listbox: 85, slider: 85, spinbutton: 85,
1107
- link: 80, tab: 75,
1108
- menuitem: 70, menuitemcheckbox: 70, menuitemradio: 70, option: 70,
1109
- navigation: 60, menu: 60, tablist: 55,
1110
- form: 50, dialog: 50, alertdialog: 50
1111
- };
1112
- const VIEWPORT_BONUS = 50;
1113
- const DEFAULT_PRIORITY = 50;
1114
-
1115
- function isInViewport(box) {
1116
- if (!box || !box.rect) return false;
1117
- const rect = box.rect;
1118
- if (rect.width === 0 || rect.height === 0) return false;
1119
- const vw = window.innerWidth;
1120
- const vh = window.innerHeight;
1121
- return rect.x < vw && rect.y < vh && rect.x + rect.width > 0 && rect.y + rect.height > 0;
1122
- }
1123
-
1124
- function getElementPriority(role, inViewport) {
1125
- const base = ROLE_PRIORITIES[role] || DEFAULT_PRIORITY;
1126
- return inViewport ? base + VIEWPORT_BONUS : base;
1127
- }
1128
-
1129
- function collectScoredElements(root, opts) {
1130
- const elements = [];
1131
- const interactiveOnly = opts.interactiveOnly !== false;
1132
- const viewportOnlyOpt = opts.viewportOnly === true;
1133
-
1134
- function visit(node) {
1135
- const isInteractive = INTERACTIVE_ROLES.includes(node.role);
1136
- if (interactiveOnly && !isInteractive) {
1137
- if (node.children) node.children.forEach(c => typeof c !== 'string' && visit(c));
1138
- return;
1139
- }
1140
- const inVp = isInViewport(node.box);
1141
- if (viewportOnlyOpt && !inVp) {
1142
- if (node.children) node.children.forEach(c => typeof c !== 'string' && visit(c));
1143
- return;
1144
- }
1145
- elements.push({ node, score: getElementPriority(node.role, inVp), inViewport: inVp });
1146
- if (node.children) node.children.forEach(c => typeof c !== 'string' && visit(c));
1147
- }
1148
- visit(root);
1149
- return elements;
1150
- }
1151
-
1152
- function truncateWithBudget(elements, maxElements, maxTokens) {
1153
- const sorted = elements.slice().sort((a, b) => b.score - a.score);
1154
- const included = [];
1155
- let tokenCount = 0;
1156
- let truncationReason = null;
1157
-
1158
- for (const el of sorted) {
1159
- if (included.length >= maxElements) { truncationReason = 'maxElements'; break; }
1160
- const elementTokens = 15;
1161
- if (maxTokens && tokenCount + elementTokens > maxTokens) { truncationReason = 'maxTokens'; break; }
1162
- included.push(el);
1163
- tokenCount += elementTokens;
1164
- }
1165
-
1166
- return {
1167
- elements: included,
1168
- totalElements: elements.length,
1169
- estimatedTokens: tokenCount,
1170
- truncated: included.length < elements.length,
1171
- truncationReason
1172
- };
1173
- }
1174
-
1175
- function renderAriaTree(ariaSnapshot, snapshotOptions) {
1176
- snapshotOptions = snapshotOptions || {};
1177
- const maxElements = snapshotOptions.maxElements || 300;
1178
- const maxTokens = snapshotOptions.maxTokens || 8000;
1179
- const options = { visibility: "ariaOrVisible", refs: "interactable", refPrefix: "", includeGenericRole: true, renderActive: true, renderCursorPointer: true };
1180
- const lines = [];
1181
- let nodesToRender = ariaSnapshot.root.role === "fragment" ? ariaSnapshot.root.children : [ariaSnapshot.root];
1182
-
1183
- const scoredElements = collectScoredElements(ariaSnapshot.root, snapshotOptions);
1184
-
1185
- const truncateResult = truncateWithBudget(scoredElements, maxElements, maxTokens);
1186
-
1187
- const includedRefs = {};
1188
- for (const el of truncateResult.elements) {
1189
- if (el.node.ref) includedRefs[el.node.ref] = true;
1190
- }
1191
-
1192
- if (truncateResult.truncated) {
1193
- const reason = truncateResult.truncationReason === 'maxTokens' ? 'token budget' : 'element limit';
1194
- lines.push("# Elements: " + truncateResult.elements.length + " of " + truncateResult.totalElements + " (truncated: " + reason + ")");
1195
- lines.push("# Tokens: ~" + truncateResult.estimatedTokens);
1196
- }
1197
-
1198
- const isInteractiveRole = (role) => INTERACTIVE_ROLES.includes(role);
1199
-
1200
- const visitText = (text, indent) => {
1201
- if (snapshotOptions.interactiveOnly) return;
1202
- const escaped = yamlEscapeValueIfNeeded(text);
1203
- if (escaped) lines.push(indent + "- text: " + escaped);
1204
- };
1205
-
1206
- const createKey = (ariaNode, renderCursorPointer) => {
1207
- let key = ariaNode.role;
1208
- if (ariaNode.name && ariaNode.name.length <= 900) {
1209
- const name = ariaNode.name;
1210
- if (name) {
1211
- const stringifiedName = name.startsWith("/") && name.endsWith("/") ? name : JSON.stringify(name);
1212
- key += " " + stringifiedName;
1213
- }
1214
- }
1215
- if (ariaNode.checked === "mixed") key += " [checked=mixed]";
1216
- if (ariaNode.checked === true) key += " [checked]";
1217
- if (ariaNode.disabled) key += " [disabled]";
1218
- if (ariaNode.expanded) key += " [expanded]";
1219
- if (ariaNode.active && options.renderActive) key += " [active]";
1220
- if (ariaNode.level) key += " [level=" + ariaNode.level + "]";
1221
- if (ariaNode.pressed === "mixed") key += " [pressed=mixed]";
1222
- if (ariaNode.pressed === true) key += " [pressed]";
1223
- if (ariaNode.selected === true) key += " [selected]";
1224
- if (ariaNode.ref) {
1225
- key += " [ref=" + ariaNode.ref + "]";
1226
- if (renderCursorPointer && hasPointerCursor(ariaNode)) key += " [cursor=pointer]";
1227
- }
1228
- return key;
1229
- };
1230
-
1231
- const getSingleInlinedTextChild = (ariaNode) => {
1232
- return ariaNode?.children.length === 1 && typeof ariaNode.children[0] === "string" && !Object.keys(ariaNode.props).length ? ariaNode.children[0] : undefined;
1233
- };
1234
-
1235
- const visit = (ariaNode, indent, renderCursorPointer) => {
1236
- const isInteractive = isInteractiveRole(ariaNode.role);
1237
- if (snapshotOptions.interactiveOnly && !isInteractive) {
1238
- const childIndent = indent;
1239
- for (const child of ariaNode.children) {
1240
- if (typeof child === "string") continue;
1241
- else visit(child, childIndent, renderCursorPointer);
1242
- }
1243
- return;
1244
- }
1245
-
1246
- if (ariaNode.ref && !includedRefs[ariaNode.ref]) {
1247
- for (const child of ariaNode.children) {
1248
- if (typeof child === "string") continue;
1249
- else visit(child, indent, renderCursorPointer);
1250
- }
1251
- return;
1252
- }
1253
-
1254
- const escapedKey = indent + "- " + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer));
1255
- const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode);
1256
- if (!ariaNode.children.length && !Object.keys(ariaNode.props).length) {
1257
- lines.push(escapedKey);
1258
- } else if (singleInlinedTextChild !== undefined) {
1259
- lines.push(escapedKey + ": " + yamlEscapeValueIfNeeded(singleInlinedTextChild));
1260
- } else {
1261
- lines.push(escapedKey + ":");
1262
- for (const [name, value] of Object.entries(ariaNode.props)) lines.push(indent + " - /" + name + ": " + yamlEscapeValueIfNeeded(value));
1263
- const childIndent = indent + " ";
1264
- const inCursorPointer = !!ariaNode.ref && renderCursorPointer && hasPointerCursor(ariaNode);
1265
- for (const child of ariaNode.children) {
1266
- if (typeof child === "string") visitText(child, childIndent);
1267
- else visit(child, childIndent, renderCursorPointer && !inCursorPointer);
1268
- }
1269
- }
1270
- };
1271
-
1272
- for (const nodeToRender of nodesToRender) {
1273
- if (typeof nodeToRender === "string") visitText(nodeToRender, "");
1274
- else visit(nodeToRender, "", !!options.renderCursorPointer);
1275
- }
1276
- return lines.join("\\n");
1277
- }
1278
-
1279
- function getAISnapshot(options) {
1280
- options = options || {};
1281
- const snapshot = generateAriaTree(document.body);
1282
- const refsObject = {};
1283
- for (const [ref, element] of snapshot.elements) refsObject[ref] = element;
1284
- window.__devBrowserRefs = refsObject;
1285
- return renderAriaTree(snapshot, options);
1286
- }
1287
-
1288
- function selectSnapshotRef(ref) {
1289
- const refs = window.__devBrowserRefs;
1290
- if (!refs) throw new Error("No snapshot refs found. Call getAISnapshot first.");
1291
- const element = refs[ref];
1292
- if (!element) throw new Error('Ref "' + ref + '" not found. Available refs: ' + Object.keys(refs).join(", "));
1293
- return element;
1294
- }
1295
-
1296
- window.__devBrowser_getAISnapshot = getAISnapshot;
1297
- window.__devBrowser_selectSnapshotRef = selectSnapshotRef;
1298
- })();
1299
- `;
1300
-
1301
- interface SnapshotOptions {
1302
- interactiveOnly?: boolean;
1303
- maxElements?: number;
1304
- viewportOnly?: boolean;
1305
- maxTokens?: number;
1306
- fullSnapshot?: boolean;
1307
- }
1308
-
1309
- const DEFAULT_SNAPSHOT_OPTIONS: SnapshotOptions = {
1310
- interactiveOnly: true,
1311
- maxElements: 300,
1312
- maxTokens: 8000,
1313
- };
1314
-
1315
- async function getSnapshotWithHistory(page: Page, options: SnapshotOptions = {}): Promise<string> {
1316
- const rawSnapshot = await getAISnapshot(page, options);
1317
- const url = page.url();
1318
- const title = await page.title();
1319
-
1320
- const manager = getSnapshotManager();
1321
- const result = manager.processSnapshot(rawSnapshot, url, title, {
1322
- fullSnapshot: options.fullSnapshot ?? false,
1323
- interactiveOnly: options.interactiveOnly ?? true,
1324
- });
1325
-
1326
- let output = '';
1327
- const sessionSummary = manager.getSessionSummary();
1328
- if (sessionSummary.history) {
1329
- output += `# ${sessionSummary.history}\n\n`;
1330
- }
1331
-
1332
- if (result.type === 'diff') {
1333
- output += `# Changes Since Last Snapshot\n${result.content}`;
1334
- } else {
1335
- output += result.content;
1336
- }
1337
-
1338
- return output;
1339
- }
1340
-
1341
- async function getAISnapshot(page: Page, options: SnapshotOptions = {}): Promise<string> {
1342
- const isInjected = await page.evaluate(() => {
1343
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1344
- return !!(globalThis as any).__devBrowser_getAISnapshot;
1345
- });
1346
-
1347
- if (!isInjected) {
1348
- await page.evaluate((script: string) => {
1349
- // eslint-disable-next-line no-eval
1350
- eval(script);
1351
- }, SNAPSHOT_SCRIPT);
1352
- }
1353
-
1354
- const snapshot = await page.evaluate((opts) => {
1355
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1356
- return (globalThis as any).__devBrowser_getAISnapshot(opts);
1357
- }, {
1358
- interactiveOnly: options.interactiveOnly || false,
1359
- maxElements: options.maxElements,
1360
- viewportOnly: options.viewportOnly || false,
1361
- maxTokens: options.maxTokens,
1362
- });
1363
- return snapshot;
1364
- }
1365
-
1366
- async function selectSnapshotRef(page: Page, ref: string): Promise<ElementHandle | null> {
1367
- const elementHandle = await page.evaluateHandle((refId: string) => {
1368
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1369
- const w = globalThis as any;
1370
- const refs = w.__devBrowserRefs;
1371
- if (!refs) {
1372
- throw new Error('No snapshot refs found. Call browser_snapshot first.');
1373
- }
1374
- const element = refs[refId];
1375
- if (!element) {
1376
- throw new Error(
1377
- `Ref "${refId}" not found. Available refs: ${Object.keys(refs).join(', ')}`
1378
- );
1379
- }
1380
- return element;
1381
- }, ref);
1382
-
1383
- const element = elementHandle.asElement();
1384
- if (!element) {
1385
- await elementHandle.dispose();
1386
- return null;
1387
- }
1388
-
1389
- return element;
1390
- }
1391
-
1392
- interface BrowserNavigateInput {
1393
- url: string;
1394
- page_name?: string;
1395
- }
1396
-
1397
- interface BrowserSnapshotInput {
1398
- page_name?: string;
1399
- interactive_only?: boolean;
1400
- full_snapshot?: boolean;
1401
- max_elements?: number;
1402
- viewport_only?: boolean;
1403
- include_history?: boolean;
1404
- max_tokens?: number;
1405
- }
1406
-
1407
- interface BrowserClickInput {
1408
- ref?: string;
1409
- selector?: string;
1410
- x?: number;
1411
- y?: number;
1412
- position?: 'center' | 'center-lower';
1413
- button?: 'left' | 'right' | 'middle';
1414
- click_count?: number;
1415
- page_name?: string;
1416
- }
1417
-
1418
- interface BrowserTypeInput {
1419
- ref?: string;
1420
- selector?: string;
1421
- text: string;
1422
- press_enter?: boolean;
1423
- page_name?: string;
1424
- }
1425
-
1426
- interface BrowserScreenshotInput {
1427
- page_name?: string;
1428
- full_page?: boolean;
1429
- }
1430
-
1431
- interface BrowserEvaluateInput {
1432
- script: string;
1433
- page_name?: string;
1434
- }
1435
-
1436
- interface BrowserPagesInput {
1437
- action: 'list' | 'close';
1438
- page_name?: string;
1439
- }
1440
-
1441
- interface BrowserKeyboardInput {
1442
- text?: string;
1443
- key?: string;
1444
- typing_delay?: number;
1445
- page_name?: string;
1446
- }
1447
-
1448
- interface SequenceAction {
1449
- action: 'click' | 'type' | 'snapshot' | 'screenshot' | 'wait';
1450
- ref?: string;
1451
- selector?: string;
1452
- x?: number;
1453
- y?: number;
1454
- text?: string;
1455
- press_enter?: boolean;
1456
- full_page?: boolean;
1457
- timeout?: number;
1458
- }
1459
-
1460
- interface BrowserSequenceInput {
1461
- actions: SequenceAction[];
1462
- page_name?: string;
1463
- }
1464
-
1465
- interface ScriptAction {
1466
- action:
1467
- | 'goto'
1468
- | 'waitForLoad'
1469
- | 'waitForSelector'
1470
- | 'waitForNavigation'
1471
- | 'findAndFill'
1472
- | 'findAndClick'
1473
- | 'fillByRef'
1474
- | 'clickByRef'
1475
- | 'snapshot'
1476
- | 'screenshot'
1477
- | 'keyboard'
1478
- | 'evaluate';
1479
- url?: string;
1480
- selector?: string;
1481
- ref?: string;
1482
- text?: string;
1483
- key?: string;
1484
- pressEnter?: boolean;
1485
- timeout?: number;
1486
- fullPage?: boolean;
1487
- code?: string;
1488
- skipIfNotFound?: boolean;
1489
- }
1490
-
1491
- interface BrowserScriptInput {
1492
- actions: ScriptAction[];
1493
- page_name?: string;
1494
- }
1495
-
1496
- interface BrowserKeyboardInput {
1497
- action: 'press' | 'type' | 'down' | 'up';
1498
- key?: string;
1499
- text?: string;
1500
- typing_delay?: number;
1501
- page_name?: string;
1502
- }
1503
-
1504
- interface BrowserScrollInput {
1505
- direction?: 'up' | 'down' | 'left' | 'right';
1506
- amount?: number;
1507
- ref?: string;
1508
- selector?: string;
1509
- position?: 'top' | 'bottom';
1510
- page_name?: string;
1511
- }
1512
-
1513
- interface BrowserHoverInput {
1514
- ref?: string;
1515
- selector?: string;
1516
- x?: number;
1517
- y?: number;
1518
- page_name?: string;
1519
- }
1520
-
1521
- interface BrowserSelectInput {
1522
- ref?: string;
1523
- selector?: string;
1524
- value?: string;
1525
- label?: string;
1526
- index?: number;
1527
- page_name?: string;
1528
- }
1529
-
1530
- interface BrowserWaitInput {
1531
- condition: 'selector' | 'hidden' | 'navigation' | 'network_idle' | 'timeout' | 'function';
1532
- selector?: string;
1533
- script?: string;
1534
- timeout?: number;
1535
- page_name?: string;
1536
- }
1537
-
1538
- interface BrowserFileUploadInput {
1539
- ref?: string;
1540
- selector?: string;
1541
- files: string[];
1542
- page_name?: string;
1543
- }
1544
-
1545
- interface BrowserDragInput {
1546
- source_ref?: string;
1547
- source_selector?: string;
1548
- source_x?: number;
1549
- source_y?: number;
1550
- target_ref?: string;
1551
- target_selector?: string;
1552
- target_x?: number;
1553
- target_y?: number;
1554
- page_name?: string;
1555
- }
1556
-
1557
- interface BrowserGetTextInput {
1558
- ref?: string;
1559
- selector?: string;
1560
- page_name?: string;
1561
- }
1562
-
1563
- interface BrowserIsVisibleInput {
1564
- ref?: string;
1565
- selector?: string;
1566
- page_name?: string;
1567
- }
1568
-
1569
- interface BrowserIsEnabledInput {
1570
- ref?: string;
1571
- selector?: string;
1572
- page_name?: string;
1573
- }
1574
-
1575
- interface BrowserIsCheckedInput {
1576
- ref?: string;
1577
- selector?: string;
1578
- page_name?: string;
1579
- }
1580
-
1581
- interface BrowserIframeInput {
1582
- action: 'enter' | 'exit';
1583
- ref?: string;
1584
- selector?: string;
1585
- page_name?: string;
1586
- }
1587
-
1588
- interface BrowserTabsInput {
1589
- action: 'list' | 'switch' | 'close' | 'wait_for_new';
1590
- index?: number;
1591
- timeout?: number;
1592
- page_name?: string;
1593
- }
1594
-
1595
- interface BrowserCanvasTypeInput {
1596
- text: string;
1597
- position?: 'start' | 'current';
1598
- page_name?: string;
1599
- }
1600
-
1601
- interface BrowserHighlightInput {
1602
- enabled: boolean;
1603
- page_name?: string;
1604
- }
1605
-
1606
- const server = new Server(
1607
- { name: 'dev-browser-mcp', version: '1.0.0' },
1608
- { capabilities: { tools: {} } }
1609
- );
1610
-
1611
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
1612
- tools: [
1613
- {
1614
- name: 'browser_navigate',
1615
- description: 'Navigate to a URL. TIP: For multi-step workflows (navigate + fill + click), use browser_script instead - it\'s 5-10x faster.',
1616
- inputSchema: {
1617
- type: 'object',
1618
- properties: {
1619
- url: {
1620
- type: 'string',
1621
- description: 'The URL to navigate to (e.g., "https://google.com" or "google.com")',
1622
- },
1623
- page_name: {
1624
- type: 'string',
1625
- description: 'Optional name for the page (default: "main"). Use different names to manage multiple pages.',
1626
- },
1627
- },
1628
- required: ['url'],
1629
- },
1630
- },
1631
- {
1632
- name: 'browser_snapshot',
1633
- description: 'Get ARIA accessibility tree with element refs like [ref=e5]. NOTE: browser_script auto-returns a snapshot, so you rarely need this separately.',
1634
- inputSchema: {
1635
- type: 'object',
1636
- properties: {
1637
- page_name: {
1638
- type: 'string',
1639
- description: 'Optional name of the page to snapshot (default: "main")',
1640
- },
1641
- interactive_only: {
1642
- type: 'boolean',
1643
- description: 'If true, only show interactive elements (buttons, links, inputs, etc.). Default: true.',
1644
- },
1645
- full_snapshot: {
1646
- type: 'boolean',
1647
- description: 'Force a complete snapshot instead of a diff. Use after major page changes (modal opened, dynamic content loaded) or when element refs seem incorrect. Default: false.',
1648
- },
1649
- max_elements: {
1650
- type: 'number',
1651
- description: 'Maximum elements to include (1-1000). Default: 300',
1652
- },
1653
- viewport_only: {
1654
- type: 'boolean',
1655
- description: 'Only include elements visible in viewport. Default: false',
1656
- },
1657
- include_history: {
1658
- type: 'boolean',
1659
- description: 'Include navigation history in output. Default: true',
1660
- },
1661
- max_tokens: {
1662
- type: 'number',
1663
- description: 'Maximum estimated tokens (1000-50000). Default: 8000',
1664
- },
1665
- },
1666
- },
1667
- },
1668
- {
1669
- name: 'browser_click',
1670
- description: 'Click on the page. TIP: For multi-step workflows, use browser_script with findAndClick instead - it\'s faster.',
1671
- inputSchema: {
1672
- type: 'object',
1673
- properties: {
1674
- position: {
1675
- type: 'string',
1676
- enum: ['center', 'center-lower'],
1677
- description: '"center" clicks viewport center. "center-lower" clicks 2/3 down (preferred for Google Docs).',
1678
- },
1679
- x: {
1680
- type: 'number',
1681
- description: 'X coordinate in pixels from left.',
1682
- },
1683
- y: {
1684
- type: 'number',
1685
- description: 'Y coordinate in pixels from top.',
1686
- },
1687
- ref: {
1688
- type: 'string',
1689
- description: 'Element ref from browser_snapshot (e.g., "e5").',
1690
- },
1691
- selector: {
1692
- type: 'string',
1693
- description: 'CSS selector (e.g., "button.submit").',
1694
- },
1695
- button: {
1696
- type: 'string',
1697
- enum: ['left', 'right', 'middle'],
1698
- description: 'Mouse button to click (default: "left"). Use "right" for context menus.',
1699
- },
1700
- click_count: {
1701
- type: 'number',
1702
- description: 'Number of clicks (default: 1). Use 2 for double-click, 3 for triple-click.',
1703
- },
1704
- page_name: {
1705
- type: 'string',
1706
- description: 'Optional name of the page (default: "main")',
1707
- },
1708
- },
1709
- },
1710
- },
1711
- {
1712
- name: 'browser_type',
1713
- description: 'Type text into an input. TIP: For form filling, use browser_script with findAndFill instead - it\'s faster and finds elements at runtime.',
1714
- inputSchema: {
1715
- type: 'object',
1716
- properties: {
1717
- ref: {
1718
- type: 'string',
1719
- description: 'Element ref from browser_snapshot (e.g., "e5"). Preferred over selector.',
1720
- },
1721
- selector: {
1722
- type: 'string',
1723
- description: 'CSS selector to find the input (e.g., "input[name=search]"). Use ref when available.',
1724
- },
1725
- text: {
1726
- type: 'string',
1727
- description: 'The text to type into the field',
1728
- },
1729
- press_enter: {
1730
- type: 'boolean',
1731
- description: 'Whether to press Enter after typing (default: false)',
1732
- },
1733
- page_name: {
1734
- type: 'string',
1735
- description: 'Optional name of the page (default: "main")',
1736
- },
1737
- },
1738
- required: ['text'],
1739
- },
1740
- },
1741
- {
1742
- name: 'browser_screenshot',
1743
- description: 'Take a screenshot. AVOID using this - browser_script auto-returns a snapshot which is faster and more useful. Only use screenshots to show the user what the page looks like.',
1744
- inputSchema: {
1745
- type: 'object',
1746
- properties: {
1747
- page_name: {
1748
- type: 'string',
1749
- description: 'Optional name of the page to screenshot (default: "main")',
1750
- },
1751
- full_page: {
1752
- type: 'boolean',
1753
- description: 'Whether to capture the full scrollable page (default: false, captures viewport only)',
1754
- },
1755
- },
1756
- },
1757
- },
1758
- {
1759
- name: 'browser_evaluate',
1760
- description: 'Execute custom JavaScript in the page context. Use for advanced operations not covered by other tools.',
1761
- inputSchema: {
1762
- type: 'object',
1763
- properties: {
1764
- script: {
1765
- type: 'string',
1766
- description: 'JavaScript code to execute in the page. Must be plain JS (no TypeScript). Use return to get a value back.',
1767
- },
1768
- page_name: {
1769
- type: 'string',
1770
- description: 'Optional name of the page (default: "main")',
1771
- },
1772
- },
1773
- required: ['script'],
1774
- },
1775
- },
1776
- {
1777
- name: 'browser_pages',
1778
- description: 'List all open pages or close a specific page.',
1779
- inputSchema: {
1780
- type: 'object',
1781
- properties: {
1782
- action: {
1783
- type: 'string',
1784
- enum: ['list', 'close'],
1785
- description: '"list" to get all page names, "close" to close a specific page',
1786
- },
1787
- page_name: {
1788
- type: 'string',
1789
- description: 'Required when action is "close" - the name of the page to close',
1790
- },
1791
- },
1792
- required: ['action'],
1793
- },
1794
- },
1795
- {
1796
- name: 'browser_keyboard',
1797
- description: 'Type text or press keys on the currently focused element. Use this for complex editors like Google Docs that don\'t have simple input elements. First click to focus, then use this to type.',
1798
- inputSchema: {
1799
- type: 'object',
1800
- properties: {
1801
- text: {
1802
- type: 'string',
1803
- description: 'Text to type. Each character is typed with proper key events.',
1804
- },
1805
- key: {
1806
- type: 'string',
1807
- description: 'Special key to press (e.g., "Enter", "Tab", "Escape", "Backspace", "ArrowDown"). Can be combined with modifiers like "Control+a", "Shift+Enter".',
1808
- },
1809
- typing_delay: {
1810
- type: 'number',
1811
- description: 'Delay in ms between keystrokes when typing text (default: 20). Set to 0 for instant typing.',
1812
- },
1813
- page_name: {
1814
- type: 'string',
1815
- description: 'Optional page name (default: "main")',
1816
- },
1817
- },
1818
- },
1819
- },
1820
- {
1821
- name: 'browser_sequence',
1822
- description: 'Execute actions in sequence. NOTE: browser_script is better - it finds elements at runtime and auto-returns snapshot. Use browser_sequence only if you already have refs.',
1823
- inputSchema: {
1824
- type: 'object',
1825
- properties: {
1826
- actions: {
1827
- type: 'array',
1828
- description: 'Array of actions to execute in order',
1829
- items: {
1830
- type: 'object',
1831
- properties: {
1832
- action: {
1833
- type: 'string',
1834
- enum: ['click', 'type', 'snapshot', 'screenshot', 'wait'],
1835
- description: 'The action to perform',
1836
- },
1837
- ref: { type: 'string', description: 'Element ref for click/type' },
1838
- selector: { type: 'string', description: 'CSS selector for click/type' },
1839
- x: { type: 'number', description: 'X coordinate for click' },
1840
- y: { type: 'number', description: 'Y coordinate for click' },
1841
- text: { type: 'string', description: 'Text to type' },
1842
- press_enter: { type: 'boolean', description: 'Press Enter after typing' },
1843
- full_page: { type: 'boolean', description: 'Full page screenshot' },
1844
- timeout: { type: 'number', description: 'Wait timeout in ms (default: 1000)' },
1845
- },
1846
- required: ['action'],
1847
- },
1848
- },
1849
- page_name: {
1850
- type: 'string',
1851
- description: 'Optional page name (default: "main")',
1852
- },
1853
- },
1854
- required: ['actions'],
1855
- },
1856
- },
1857
- {
1858
- name: 'browser_keyboard',
1859
- description: 'Send keyboard input. Use for shortcuts (Cmd+V, Ctrl+C), special keys (Enter, Tab, Escape), or typing into canvas apps like Google Docs where browser_type does not work.',
1860
- inputSchema: {
1861
- type: 'object',
1862
- properties: {
1863
- action: {
1864
- type: 'string',
1865
- enum: ['press', 'type', 'down', 'up'],
1866
- description: '"press" for key combo (Enter, Meta+v), "type" for raw text character by character, "down"/"up" for hold/release',
1867
- },
1868
- key: {
1869
- type: 'string',
1870
- description: 'Key to press: "Enter", "Tab", "Escape", "Meta+v", "Control+c", "Shift+ArrowDown"',
1871
- },
1872
- text: {
1873
- type: 'string',
1874
- description: 'Text to type character by character (for action="type")',
1875
- },
1876
- typing_delay: {
1877
- type: 'number',
1878
- description: 'Delay in ms between keystrokes when typing text (default: 20). Set to 0 for instant typing.',
1879
- },
1880
- page_name: {
1881
- type: 'string',
1882
- description: 'Optional page name (default: "main")',
1883
- },
1884
- },
1885
- required: ['action'],
1886
- },
1887
- },
1888
- {
1889
- name: 'browser_scroll',
1890
- description: 'Scroll the page or scroll an element into view.',
1891
- inputSchema: {
1892
- type: 'object',
1893
- properties: {
1894
- direction: {
1895
- type: 'string',
1896
- enum: ['up', 'down', 'left', 'right'],
1897
- description: 'Scroll direction',
1898
- },
1899
- amount: {
1900
- type: 'number',
1901
- description: 'Pixels to scroll (default: 500)',
1902
- },
1903
- ref: {
1904
- type: 'string',
1905
- description: 'Element ref to scroll into view (from browser_snapshot)',
1906
- },
1907
- selector: {
1908
- type: 'string',
1909
- description: 'CSS selector to scroll into view',
1910
- },
1911
- position: {
1912
- type: 'string',
1913
- enum: ['top', 'bottom'],
1914
- description: 'Scroll to page top or bottom',
1915
- },
1916
- page_name: {
1917
- type: 'string',
1918
- description: 'Optional page name (default: "main")',
1919
- },
1920
- },
1921
- },
1922
- },
1923
- {
1924
- name: 'browser_hover',
1925
- description: 'Hover over an element to trigger hover states, dropdowns, or tooltips.',
1926
- inputSchema: {
1927
- type: 'object',
1928
- properties: {
1929
- ref: {
1930
- type: 'string',
1931
- description: 'Element ref from browser_snapshot',
1932
- },
1933
- selector: {
1934
- type: 'string',
1935
- description: 'CSS selector',
1936
- },
1937
- x: {
1938
- type: 'number',
1939
- description: 'X coordinate to hover at',
1940
- },
1941
- y: {
1942
- type: 'number',
1943
- description: 'Y coordinate to hover at',
1944
- },
1945
- page_name: {
1946
- type: 'string',
1947
- description: 'Optional page name (default: "main")',
1948
- },
1949
- },
1950
- },
1951
- },
1952
- {
1953
- name: 'browser_select',
1954
- description: 'Select an option from a <select> dropdown. Native select elements require this tool - browser_click will not work.',
1955
- inputSchema: {
1956
- type: 'object',
1957
- properties: {
1958
- ref: {
1959
- type: 'string',
1960
- description: 'Element ref from browser_snapshot',
1961
- },
1962
- selector: {
1963
- type: 'string',
1964
- description: 'CSS selector for the select element',
1965
- },
1966
- value: {
1967
- type: 'string',
1968
- description: 'Option value attribute to select',
1969
- },
1970
- label: {
1971
- type: 'string',
1972
- description: 'Option visible text to select',
1973
- },
1974
- index: {
1975
- type: 'number',
1976
- description: 'Option index to select (0-based)',
1977
- },
1978
- page_name: {
1979
- type: 'string',
1980
- description: 'Optional page name (default: "main")',
1981
- },
1982
- },
1983
- },
1984
- },
1985
- {
1986
- name: 'browser_wait',
1987
- description: 'Wait for a condition. TIP: browser_script has built-in waitForLoad, waitForSelector, waitForNavigation - prefer using those.',
1988
- inputSchema: {
1989
- type: 'object',
1990
- properties: {
1991
- condition: {
1992
- type: 'string',
1993
- enum: ['selector', 'hidden', 'navigation', 'network_idle', 'timeout', 'function'],
1994
- description: '"selector" waits for element to appear, "hidden" waits for element to disappear, "navigation" waits for page navigation, "network_idle" waits for network to settle, "timeout" waits fixed time, "function" waits for custom JS condition to return true',
1995
- },
1996
- selector: {
1997
- type: 'string',
1998
- description: 'CSS selector (required for "selector" and "hidden" conditions)',
1999
- },
2000
- script: {
2001
- type: 'string',
2002
- description: 'JavaScript expression that returns true when condition is met (required for "function" condition). Example: "document.querySelector(\'.loaded\') !== null"',
2003
- },
2004
- timeout: {
2005
- type: 'number',
2006
- description: 'Max wait time in ms (default: 30000). For "timeout" condition, this is the wait duration.',
2007
- },
2008
- page_name: {
2009
- type: 'string',
2010
- description: 'Optional page name (default: "main")',
2011
- },
2012
- },
2013
- required: ['condition'],
2014
- },
2015
- },
2016
- {
2017
- name: 'browser_file_upload',
2018
- description: 'Upload files to a file input element.',
2019
- inputSchema: {
2020
- type: 'object',
2021
- properties: {
2022
- ref: {
2023
- type: 'string',
2024
- description: 'Element ref from browser_snapshot',
2025
- },
2026
- selector: {
2027
- type: 'string',
2028
- description: 'CSS selector for input[type=file]',
2029
- },
2030
- files: {
2031
- type: 'array',
2032
- items: { type: 'string' },
2033
- description: 'Array of absolute file paths to upload',
2034
- },
2035
- page_name: {
2036
- type: 'string',
2037
- description: 'Optional page name (default: "main")',
2038
- },
2039
- },
2040
- required: ['files'],
2041
- },
2042
- },
2043
- {
2044
- name: 'browser_drag',
2045
- description: 'Drag and drop from source to target location.',
2046
- inputSchema: {
2047
- type: 'object',
2048
- properties: {
2049
- source_ref: {
2050
- type: 'string',
2051
- description: 'Source element ref from browser_snapshot',
2052
- },
2053
- source_selector: {
2054
- type: 'string',
2055
- description: 'Source CSS selector',
2056
- },
2057
- source_x: {
2058
- type: 'number',
2059
- description: 'Source X coordinate',
2060
- },
2061
- source_y: {
2062
- type: 'number',
2063
- description: 'Source Y coordinate',
2064
- },
2065
- target_ref: {
2066
- type: 'string',
2067
- description: 'Target element ref from browser_snapshot',
2068
- },
2069
- target_selector: {
2070
- type: 'string',
2071
- description: 'Target CSS selector',
2072
- },
2073
- target_x: {
2074
- type: 'number',
2075
- description: 'Target X coordinate',
2076
- },
2077
- target_y: {
2078
- type: 'number',
2079
- description: 'Target Y coordinate',
2080
- },
2081
- page_name: {
2082
- type: 'string',
2083
- description: 'Optional page name (default: "main")',
2084
- },
2085
- },
2086
- },
2087
- },
2088
- {
2089
- name: 'browser_get_text',
2090
- description: 'Get text content or input value from an element. Faster than browser_snapshot when you just need one element\'s text.',
2091
- inputSchema: {
2092
- type: 'object',
2093
- properties: {
2094
- ref: {
2095
- type: 'string',
2096
- description: 'Element ref from browser_snapshot',
2097
- },
2098
- selector: {
2099
- type: 'string',
2100
- description: 'CSS selector',
2101
- },
2102
- page_name: {
2103
- type: 'string',
2104
- description: 'Optional page name (default: "main")',
2105
- },
2106
- },
2107
- },
2108
- },
2109
- {
2110
- name: 'browser_is_visible',
2111
- description: 'Check if an element is visible on the page. Returns true/false. Use this to verify actions succeeded before proceeding.',
2112
- inputSchema: {
2113
- type: 'object',
2114
- properties: {
2115
- ref: {
2116
- type: 'string',
2117
- description: 'Element ref from browser_snapshot',
2118
- },
2119
- selector: {
2120
- type: 'string',
2121
- description: 'CSS selector',
2122
- },
2123
- page_name: {
2124
- type: 'string',
2125
- description: 'Optional page name (default: "main")',
2126
- },
2127
- },
2128
- },
2129
- },
2130
- {
2131
- name: 'browser_is_enabled',
2132
- description: 'Check if an element is enabled (not disabled). Returns true/false. Use to verify buttons/inputs are interactive.',
2133
- inputSchema: {
2134
- type: 'object',
2135
- properties: {
2136
- ref: {
2137
- type: 'string',
2138
- description: 'Element ref from browser_snapshot',
2139
- },
2140
- selector: {
2141
- type: 'string',
2142
- description: 'CSS selector',
2143
- },
2144
- page_name: {
2145
- type: 'string',
2146
- description: 'Optional page name (default: "main")',
2147
- },
2148
- },
2149
- },
2150
- },
2151
- {
2152
- name: 'browser_is_checked',
2153
- description: 'Check if a checkbox or radio button is checked. Returns true/false. Use to verify form state.',
2154
- inputSchema: {
2155
- type: 'object',
2156
- properties: {
2157
- ref: {
2158
- type: 'string',
2159
- description: 'Element ref from browser_snapshot',
2160
- },
2161
- selector: {
2162
- type: 'string',
2163
- description: 'CSS selector',
2164
- },
2165
- page_name: {
2166
- type: 'string',
2167
- description: 'Optional page name (default: "main")',
2168
- },
2169
- },
2170
- },
2171
- },
2172
- {
2173
- name: 'browser_iframe',
2174
- description: 'Enter or exit an iframe to interact with its content.',
2175
- inputSchema: {
2176
- type: 'object',
2177
- properties: {
2178
- action: {
2179
- type: 'string',
2180
- enum: ['enter', 'exit'],
2181
- description: '"enter" to switch into an iframe, "exit" to return to main page',
2182
- },
2183
- ref: {
2184
- type: 'string',
2185
- description: 'Iframe element ref (for action="enter")',
2186
- },
2187
- selector: {
2188
- type: 'string',
2189
- description: 'Iframe CSS selector (for action="enter")',
2190
- },
2191
- page_name: {
2192
- type: 'string',
2193
- description: 'Optional page name (default: "main")',
2194
- },
2195
- },
2196
- required: ['action'],
2197
- },
2198
- },
2199
- {
2200
- name: 'browser_tabs',
2201
- description: 'Manage browser tabs/popups. Handle new windows that open from clicks.',
2202
- inputSchema: {
2203
- type: 'object',
2204
- properties: {
2205
- action: {
2206
- type: 'string',
2207
- enum: ['list', 'switch', 'close', 'wait_for_new'],
2208
- description: '"list" shows all tabs, "switch" to tab by index, "close" closes tab by index, "wait_for_new" waits for a popup',
2209
- },
2210
- index: {
2211
- type: 'number',
2212
- description: 'Tab index (0-based) for switch/close actions',
2213
- },
2214
- timeout: {
2215
- type: 'number',
2216
- description: 'Timeout in ms for wait_for_new (default: 5000)',
2217
- },
2218
- page_name: {
2219
- type: 'string',
2220
- description: 'Optional page name (default: "main")',
2221
- },
2222
- },
2223
- required: ['action'],
2224
- },
2225
- },
2226
- {
2227
- name: 'browser_canvas_type',
2228
- description: 'Type text into canvas apps like Google Docs, Sheets, Figma. Clicks in the document, optionally jumps to start, then types.',
2229
- inputSchema: {
2230
- type: 'object',
2231
- properties: {
2232
- text: {
2233
- type: 'string',
2234
- description: 'The text to type',
2235
- },
2236
- position: {
2237
- type: 'string',
2238
- enum: ['start', 'current'],
2239
- description: '"start" jumps to document beginning first (Cmd/Ctrl+Home), "current" types at current cursor position (default: "start")',
2240
- },
2241
- page_name: {
2242
- type: 'string',
2243
- description: 'Optional page name (default: "main")',
2244
- },
2245
- },
2246
- required: ['text'],
2247
- },
2248
- },
2249
- {
2250
- name: 'browser_script',
2251
- description: `⚡ PREFERRED: Execute complete browser workflows in ONE call. 5-10x faster than individual tools.
2252
-
2253
- ALWAYS use this for multi-step tasks. Actions find elements at RUNTIME using CSS selectors.
2254
- Final page snapshot is AUTO-RETURNED - no need to add snapshot action.
2255
-
2256
- Example - complete login:
2257
- {"actions": [
2258
- {"action": "goto", "url": "example.com/login"},
2259
- {"action": "waitForLoad"},
2260
- {"action": "findAndFill", "selector": "input[type='email']", "text": "user@example.com"},
2261
- {"action": "findAndFill", "selector": "input[type='password']", "text": "secret"},
2262
- {"action": "findAndClick", "selector": "button[type='submit']"},
2263
- {"action": "waitForNavigation"}
2264
- ]}
2265
-
2266
- Actions: goto, waitForLoad, waitForSelector, waitForNavigation, findAndFill, findAndClick, fillByRef, clickByRef, snapshot, screenshot, keyboard, evaluate
2267
- }`,
2268
- inputSchema: {
2269
- type: 'object',
2270
- properties: {
2271
- actions: {
2272
- type: 'array',
2273
- description: 'Array of actions to execute in order',
2274
- items: {
2275
- type: 'object',
2276
- properties: {
2277
- action: {
2278
- type: 'string',
2279
- enum: [
2280
- 'goto',
2281
- 'waitForLoad',
2282
- 'waitForSelector',
2283
- 'waitForNavigation',
2284
- 'findAndFill',
2285
- 'findAndClick',
2286
- 'fillByRef',
2287
- 'clickByRef',
2288
- 'snapshot',
2289
- 'screenshot',
2290
- 'keyboard',
2291
- 'evaluate',
2292
- ],
2293
- description: 'The action to perform',
2294
- },
2295
- url: { type: 'string', description: 'URL for goto action' },
2296
- selector: {
2297
- type: 'string',
2298
- description: 'CSS selector for waitForSelector, findAndFill, findAndClick',
2299
- },
2300
- ref: { type: 'string', description: 'Element ref for fillByRef, clickByRef' },
2301
- text: { type: 'string', description: 'Text to type for fill actions or keyboard type' },
2302
- key: { type: 'string', description: 'Key to press for keyboard action (e.g., "Enter", "Tab")' },
2303
- pressEnter: { type: 'boolean', description: 'Press Enter after filling' },
2304
- timeout: { type: 'number', description: 'Timeout in ms (default: 10000)' },
2305
- fullPage: { type: 'boolean', description: 'Full page screenshot' },
2306
- code: { type: 'string', description: 'JavaScript code for evaluate action' },
2307
- skipIfNotFound: {
2308
- type: 'boolean',
2309
- description: 'Skip action if element not found (default: false - will fail)',
2310
- },
2311
- },
2312
- required: ['action'],
2313
- },
2314
- },
2315
- page_name: {
2316
- type: 'string',
2317
- description: 'Optional page name (default: "main")',
2318
- },
2319
- },
2320
- required: ['actions'],
2321
- },
2322
- },
2323
- {
2324
- name: 'browser_batch_actions',
2325
- description: `Extract data from multiple URLs in ONE call. Visits each URL, runs your JS extraction script, returns compact JSON results.
2326
-
2327
- Use this when you need to collect data from many pages (e.g. scrape listings, compare products, gather info from search results). Instead of clicking into each page individually, provide all URLs upfront and get structured data back.
2328
-
2329
- Example - extract price and address from 10 Zillow listings:
2330
- {"urls": ["https://zillow.com/homedetails/.../1_zpid/", "https://zillow.com/homedetails/.../2_zpid/"], "extractScript": "return { price: document.querySelector('[data-testid=\\"price\\"]')?.textContent, address: document.querySelector('h1')?.textContent }", "waitForSelector": "[data-testid='price']"}
2331
-
2332
- Returns JSON only (no snapshots/screenshots) to minimize token usage. Max 20 URLs per call.`,
2333
- inputSchema: {
2334
- type: 'object',
2335
- properties: {
2336
- urls: {
2337
- type: 'array',
2338
- items: { type: 'string' },
2339
- description: 'Array of URLs to visit and extract data from (1-20 URLs)',
2340
- maxItems: 20,
2341
- minItems: 1,
2342
- },
2343
- extractScript: {
2344
- type: 'string',
2345
- description: 'JavaScript code that extracts data from each page. Must return an object. Runs via page.evaluate(). Example: "return { title: document.title, price: document.querySelector(\'.price\')?.textContent }"',
2346
- },
2347
- waitForSelector: {
2348
- type: 'string',
2349
- description: 'Optional CSS selector to wait for before running extractScript (e.g. "[data-testid=\'price\']"). Ensures page content has loaded.',
2350
- },
2351
- page_name: {
2352
- type: 'string',
2353
- description: 'Optional page name (default: "main")',
2354
- },
2355
- },
2356
- required: ['urls', 'extractScript'],
2357
- },
2358
- },
2359
- {
2360
- name: 'browser_highlight',
2361
- description: 'Toggle the visual highlight glow on the current tab. Use to indicate when automation is active on a tab, and turn off when done.',
2362
- inputSchema: {
2363
- type: 'object',
2364
- properties: {
2365
- enabled: {
2366
- type: 'boolean',
2367
- description: 'true to show the highlight glow, false to hide it',
2368
- },
2369
- page_name: {
2370
- type: 'string',
2371
- description: 'Optional page name (default: "main")',
2372
- },
2373
- },
2374
- required: ['enabled'],
2375
- },
2376
- },
2377
- ],
2378
- }));
2379
-
2380
- server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
2381
- const { name, arguments: args } = request.params;
2382
-
2383
- console.error(`[MCP] Tool called: ${name}`, JSON.stringify(args, null, 2));
2384
-
2385
- try {
2386
- switch (name) {
2387
- case 'browser_navigate': {
2388
- const { url, page_name } = args as BrowserNavigateInput;
2389
-
2390
- let fullUrl = url;
2391
- if (!fullUrl.startsWith('http://') && !fullUrl.startsWith('https://')) {
2392
- fullUrl = 'https://' + fullUrl;
2393
- }
2394
-
2395
- resetSnapshotManager();
2396
-
2397
- const page = await getPage(page_name);
2398
- await page.goto(fullUrl);
2399
- await waitForPageLoad(page);
2400
- await injectActiveTabGlow(page);
2401
-
2402
- const title = await page.title();
2403
- const currentUrl = page.url();
2404
- const viewport = page.viewportSize();
2405
-
2406
- const result = {
2407
- content: [{
2408
- type: 'text' as const,
2409
- text: `Navigation successful.
2410
- URL: ${currentUrl}
2411
- Title: ${title}
2412
- Viewport: ${viewport?.width || 1280}x${viewport?.height || 720}
2413
-
2414
- The page has loaded. Use browser_snapshot() to see the page elements and find interactive refs, or browser_screenshot() to see what the page looks like visually.`,
2415
- }],
2416
- isError: false,
2417
- };
2418
- console.error(`[MCP] browser_navigate result:`, JSON.stringify(result, null, 2));
2419
- return result;
2420
- }
2421
-
2422
- case 'browser_snapshot': {
2423
- const { page_name, interactive_only, full_snapshot, max_elements, viewport_only, include_history, max_tokens } = args as BrowserSnapshotInput;
2424
- const page = await getPage(page_name);
2425
-
2426
- const validatedMaxElements = full_snapshot
2427
- ? Infinity
2428
- : Math.min(Math.max(max_elements ?? 300, 1), 1000);
2429
-
2430
- const validatedMaxTokens = full_snapshot
2431
- ? Infinity
2432
- : Math.min(Math.max(max_tokens ?? 8000, 1000), 50000);
2433
-
2434
- const snapshotOptions: SnapshotOptions = {
2435
- interactiveOnly: interactive_only ?? true,
2436
- maxElements: validatedMaxElements,
2437
- viewportOnly: viewport_only ?? false,
2438
- maxTokens: validatedMaxTokens,
2439
- };
2440
-
2441
- const rawSnapshot = await getAISnapshot(page, snapshotOptions);
2442
- const viewport = page.viewportSize();
2443
- const url = page.url();
2444
- const title = await page.title();
2445
-
2446
- const canvasApps = [
2447
- { pattern: /docs\.google\.com/, name: 'Google Docs' },
2448
- { pattern: /sheets\.google\.com/, name: 'Google Sheets' },
2449
- { pattern: /slides\.google\.com/, name: 'Google Slides' },
2450
- { pattern: /figma\.com/, name: 'Figma' },
2451
- { pattern: /canva\.com/, name: 'Canva' },
2452
- { pattern: /miro\.com/, name: 'Miro' },
2453
- ];
2454
- const detectedApp = canvasApps.find(app => app.pattern.test(url));
2455
-
2456
- const manager = getSnapshotManager();
2457
- const result = manager.processSnapshot(rawSnapshot, url, title, {
2458
- fullSnapshot: full_snapshot,
2459
- interactiveOnly: interactive_only ?? true,
2460
- });
2461
-
2462
- let output = '';
2463
-
2464
- const includeHistory = include_history !== false;
2465
- if (includeHistory) {
2466
- const sessionSummary = manager.getSessionSummary();
2467
- if (sessionSummary.history) {
2468
- output += `# ${sessionSummary.history}\n\n`;
2469
- }
2470
- }
2471
-
2472
- output += `# Page Info\n`;
2473
- output += `URL: ${url}\n`;
2474
- output += `Viewport: ${viewport?.width || 1280}x${viewport?.height || 720} (center: ${Math.round((viewport?.width || 1280) / 2)}, ${Math.round((viewport?.height || 720) / 2)})\n`;
2475
-
2476
- if (result.type === 'diff') {
2477
- output += `Mode: Diff (showing changes since last snapshot)\n`;
2478
- } else if (interactive_only ?? true) {
2479
- output += `Mode: Interactive elements only (buttons, links, inputs)\n`;
2480
- }
2481
-
2482
- if (detectedApp) {
2483
- output += `\n⚠️ CANVAS APP DETECTED: ${detectedApp.name}\n`;
2484
- output += `This app uses canvas rendering. Element refs may not work for the main content area.\n`;
2485
- output += `Use: browser_click(position="center-lower") then browser_keyboard(action="type", text="...")\n`;
2486
- output += `(center-lower avoids UI overlays like Google Docs AI suggestions)\n`;
2487
- }
2488
-
2489
- if (result.type === 'diff') {
2490
- output += `\n# Changes Since Last Snapshot\n${result.content}`;
2491
- } else {
2492
- output += `\n# Accessibility Tree\n${result.content}`;
2493
- }
2494
-
2495
- return {
2496
- content: [{
2497
- type: 'text',
2498
- text: output,
2499
- }],
2500
- };
2501
- }
2502
-
2503
- case 'browser_click': {
2504
- const { ref, selector, x, y, position, button, click_count, page_name } = args as BrowserClickInput;
2505
- const page = await getPage(page_name);
2506
-
2507
- const clickOptions: { button?: 'left' | 'right' | 'middle'; clickCount?: number } = {};
2508
- if (button) clickOptions.button = button;
2509
- if (click_count) clickOptions.clickCount = click_count;
2510
-
2511
- const descParts: string[] = [];
2512
- if (click_count === 2) descParts.push('double-click');
2513
- else if (click_count === 3) descParts.push('triple-click');
2514
- else if (click_count && click_count > 1) descParts.push(`${click_count}x click`);
2515
- if (button === 'right') descParts.push('right-click');
2516
- else if (button === 'middle') descParts.push('middle-click');
2517
- const clickDesc = descParts.length > 0 ? ` (${descParts.join(', ')})` : '';
2518
-
2519
- try {
2520
- if (position === 'center' || position === 'center-lower') {
2521
- const viewport = page.viewportSize();
2522
- const clickX = (viewport?.width || 1280) / 2;
2523
- const clickY = position === 'center-lower'
2524
- ? (viewport?.height || 720) * 2 / 3
2525
- : (viewport?.height || 720) / 2;
2526
- await page.mouse.click(clickX, clickY, clickOptions);
2527
- await waitForPageLoad(page);
2528
- const positionName = position === 'center-lower' ? 'center-lower (2/3 down)' : 'center';
2529
- return { content: [{ type: 'text' as const, text: `Clicked viewport ${positionName} (${Math.round(clickX)}, ${Math.round(clickY)})${clickDesc}` }] };
2530
- }
2531
-
2532
- if (x !== undefined && y !== undefined) {
2533
- await page.mouse.click(x, y, clickOptions);
2534
- await waitForPageLoad(page);
2535
- return { content: [{ type: 'text' as const, text: `Clicked at coordinates (${x}, ${y})${clickDesc}` }] };
2536
- } else if (ref) {
2537
- const element = await selectSnapshotRef(page, ref);
2538
- if (!element) {
2539
- return {
2540
- content: [{ type: 'text', text: `Element [ref=${ref}] not found. Run browser_snapshot() to get updated refs - the page may have changed.` }],
2541
- isError: true,
2542
- };
2543
- }
2544
- await element.click(clickOptions);
2545
- await waitForPageLoad(page);
2546
- return { content: [{ type: 'text' as const, text: `Clicked element [ref=${ref}]${clickDesc}` }] };
2547
- } else if (selector) {
2548
- await page.click(selector, clickOptions);
2549
- await waitForPageLoad(page);
2550
- return { content: [{ type: 'text' as const, text: `Clicked element matching "${selector}"${clickDesc}` }] };
2551
- } else {
2552
- return {
2553
- content: [{ type: 'text', text: 'Error: Provide x/y coordinates, ref, selector, or position' }],
2554
- isError: true,
2555
- };
2556
- }
2557
- } catch (err) {
2558
- const targetDesc = ref ? `[ref=${ref}]` : selector ? `"${selector}"` : `(${x}, ${y})`;
2559
- const friendlyError = toAIFriendlyError(err, targetDesc);
2560
- return {
2561
- content: [{ type: 'text', text: friendlyError.message }],
2562
- isError: true,
2563
- };
2564
- }
2565
- }
2566
-
2567
- case 'browser_type': {
2568
- const { ref, selector, text, press_enter, page_name } = args as BrowserTypeInput;
2569
- const page = await getPage(page_name);
2570
-
2571
- try {
2572
- let element: ElementHandle | null = null;
2573
-
2574
- if (ref) {
2575
- element = await selectSnapshotRef(page, ref);
2576
- if (!element) {
2577
- return {
2578
- content: [{ type: 'text', text: `Element [ref=${ref}] not found. Run browser_snapshot() to get updated refs - the page may have changed.` }],
2579
- isError: true,
2580
- };
2581
- }
2582
- } else if (selector) {
2583
- element = await page.$(selector);
2584
- if (!element) {
2585
- return {
2586
- content: [{ type: 'text', text: `Element "${selector}" not found. Run browser_snapshot() to see current page elements.` }],
2587
- isError: true,
2588
- };
2589
- }
2590
- } else {
2591
- return {
2592
- content: [{ type: 'text', text: 'Error: Either ref or selector is required' }],
2593
- isError: true,
2594
- };
2595
- }
2596
-
2597
- await element.click();
2598
- await element.fill(text);
2599
-
2600
- if (press_enter) {
2601
- await element.press('Enter');
2602
- await waitForPageLoad(page);
2603
- }
2604
-
2605
- const target = ref ? `[ref=${ref}]` : `"${selector}"`;
2606
- const enterNote = press_enter ? ' and pressed Enter' : '';
2607
- return {
2608
- content: [{ type: 'text', text: `Typed "${text}" into ${target}${enterNote}` }],
2609
- };
2610
- } catch (err) {
2611
- const targetDesc = ref ? `[ref=${ref}]` : selector || 'element';
2612
- const friendlyError = toAIFriendlyError(err, targetDesc);
2613
- return {
2614
- content: [{ type: 'text', text: friendlyError.message }],
2615
- isError: true,
2616
- };
2617
- }
2618
- }
2619
-
2620
- case 'browser_screenshot': {
2621
- const { page_name, full_page } = args as BrowserScreenshotInput;
2622
- const page = await getPage(page_name);
2623
-
2624
- const screenshotBuffer = await page.screenshot({
2625
- fullPage: full_page ?? false,
2626
- type: 'jpeg',
2627
- quality: 80,
2628
- });
2629
-
2630
- const base64 = screenshotBuffer.toString('base64');
2631
-
2632
- return {
2633
- content: [{
2634
- type: 'image',
2635
- data: base64,
2636
- mimeType: 'image/jpeg',
2637
- }],
2638
- };
2639
- }
2640
-
2641
- case 'browser_evaluate': {
2642
- const { script, page_name } = args as BrowserEvaluateInput;
2643
- const page = await getPage(page_name);
2644
-
2645
- const wrappedScript = `(async () => { ${script} })()`;
2646
- const result = await page.evaluate(wrappedScript);
2647
-
2648
- return {
2649
- content: [{
2650
- type: 'text',
2651
- text: result !== undefined ? JSON.stringify(result, null, 2) : 'Script executed (no return value)',
2652
- }],
2653
- };
2654
- }
2655
-
2656
- case 'browser_pages': {
2657
- const { action, page_name } = args as BrowserPagesInput;
2658
-
2659
- if (action === 'list') {
2660
- const res = await fetchWithRetry(`${DEV_BROWSER_URL}/pages`);
2661
- const data = await res.json() as { pages: string[] };
2662
-
2663
- const taskPrefix = `${TASK_ID}-`;
2664
- const taskPages = data.pages
2665
- .filter(name => name.startsWith(taskPrefix))
2666
- .map(name => name.substring(taskPrefix.length));
2667
-
2668
- return {
2669
- content: [{
2670
- type: 'text',
2671
- text: taskPages.length > 0
2672
- ? `Open pages: ${taskPages.join(', ')}`
2673
- : 'No pages open',
2674
- }],
2675
- };
2676
- } else if (action === 'close') {
2677
- if (!page_name) {
2678
- return {
2679
- content: [{ type: 'text', text: 'Error: page_name is required for close action' }],
2680
- isError: true,
2681
- };
2682
- }
2683
-
2684
- const fullName = getFullPageName(page_name);
2685
- const res = await fetchWithRetry(`${DEV_BROWSER_URL}/pages/${encodeURIComponent(fullName)}`, {
2686
- method: 'DELETE',
2687
- });
2688
-
2689
- if (!res.ok) {
2690
- return {
2691
- content: [{ type: 'text', text: `Error: Failed to close page: ${await res.text()}` }],
2692
- isError: true,
2693
- };
2694
- }
2695
-
2696
- return {
2697
- content: [{ type: 'text', text: `Closed page "${page_name}"` }],
2698
- };
2699
- }
2700
-
2701
- return {
2702
- content: [{ type: 'text', text: `Error: Unknown action "${action}"` }],
2703
- isError: true,
2704
- };
2705
- }
2706
-
2707
- case 'browser_keyboard': {
2708
- const { text, key, typing_delay, page_name } = args as BrowserKeyboardInput;
2709
- const page = await getPage(page_name);
2710
-
2711
- if (!text && !key) {
2712
- return {
2713
- content: [{ type: 'text', text: 'Error: Either text or key must be provided' }],
2714
- isError: true,
2715
- };
2716
- }
2717
-
2718
- const results: string[] = [];
2719
-
2720
- if (text) {
2721
- await page.keyboard.type(text, { delay: typing_delay ?? 20 });
2722
- results.push(`Typed: "${text}"`);
2723
- }
2724
-
2725
- if (key) {
2726
- await page.keyboard.press(key);
2727
- results.push(`Pressed: ${key}`);
2728
- }
2729
-
2730
- return {
2731
- content: [{ type: 'text', text: results.join(', ') }],
2732
- };
2733
- }
2734
-
2735
- case 'browser_sequence': {
2736
- const { actions, page_name } = args as BrowserSequenceInput;
2737
- const page = await getPage(page_name);
2738
- const results: string[] = [];
2739
-
2740
- for (let i = 0; i < actions.length; i++) {
2741
- const step = actions[i];
2742
- const stepNum = i + 1;
2743
-
2744
- try {
2745
- switch (step.action) {
2746
- case 'click': {
2747
- if (step.x !== undefined && step.y !== undefined) {
2748
- await page.mouse.click(step.x, step.y);
2749
- results.push(`${stepNum}. Clicked at (${step.x}, ${step.y})`);
2750
- } else if (step.ref) {
2751
- const element = await selectSnapshotRef(page, step.ref);
2752
- if (!element) throw new Error(`Ref "${step.ref}" not found`);
2753
- await element.click();
2754
- results.push(`${stepNum}. Clicked [ref=${step.ref}]`);
2755
- } else if (step.selector) {
2756
- await page.click(step.selector);
2757
- results.push(`${stepNum}. Clicked "${step.selector}"`);
2758
- } else {
2759
- throw new Error('Click requires x/y, ref, or selector');
2760
- }
2761
- await waitForPageLoad(page);
2762
- break;
2763
- }
2764
-
2765
- case 'type': {
2766
- let element: ElementHandle | null = null;
2767
- if (step.ref) {
2768
- element = await selectSnapshotRef(page, step.ref);
2769
- if (!element) throw new Error(`Ref "${step.ref}" not found`);
2770
- } else if (step.selector) {
2771
- element = await page.$(step.selector);
2772
- if (!element) throw new Error(`Selector "${step.selector}" not found`);
2773
- } else {
2774
- throw new Error('Type requires ref or selector');
2775
- }
2776
- await element.click();
2777
- await element.fill(step.text || '');
2778
- if (step.press_enter) {
2779
- await element.press('Enter');
2780
- await waitForPageLoad(page);
2781
- }
2782
- const target = step.ref ? `[ref=${step.ref}]` : `"${step.selector}"`;
2783
- results.push(`${stepNum}. Typed "${step.text}" into ${target}${step.press_enter ? ' + Enter' : ''}`);
2784
- break;
2785
- }
2786
-
2787
- case 'snapshot': {
2788
- await getSnapshotWithHistory(page, DEFAULT_SNAPSHOT_OPTIONS);
2789
- results.push(`${stepNum}. Snapshot taken (refs updated)`);
2790
- break;
2791
- }
2792
-
2793
- case 'screenshot': {
2794
- results.push(`${stepNum}. Screenshot taken`);
2795
- break;
2796
- }
2797
-
2798
- case 'wait': {
2799
- const timeout = step.timeout || 1000;
2800
- await new Promise(resolve => setTimeout(resolve, timeout));
2801
- results.push(`${stepNum}. Waited ${timeout}ms`);
2802
- break;
2803
- }
2804
-
2805
- default:
2806
- results.push(`${stepNum}. Unknown action: ${step.action}`);
2807
- }
2808
- } catch (err) {
2809
- const errMsg = err instanceof Error ? err.message : String(err);
2810
- results.push(`${stepNum}. FAILED: ${errMsg}`);
2811
- return {
2812
- content: [{ type: 'text', text: `Sequence stopped at step ${stepNum}:\n${results.join('\n')}` }],
2813
- isError: true,
2814
- };
2815
- }
2816
- }
2817
-
2818
- return {
2819
- content: [{ type: 'text', text: `Sequence completed (${actions.length} actions):\n${results.join('\n')}` }],
2820
- };
2821
- }
2822
-
2823
- case 'browser_script': {
2824
- const { actions, page_name } = args as BrowserScriptInput;
2825
- let page = await getPage(page_name);
2826
- const results: string[] = [];
2827
- let snapshotResult = '';
2828
- let screenshotData: { type: 'image'; mimeType: string; data: string } | null = null;
2829
-
2830
- for (let i = 0; i < actions.length; i++) {
2831
- const step = actions[i];
2832
- const stepNum = i + 1;
2833
-
2834
- try {
2835
- switch (step.action) {
2836
- case 'goto': {
2837
- if (!step.url) throw new Error('goto requires url parameter');
2838
- let fullUrl = step.url;
2839
- if (!fullUrl.startsWith('http://') && !fullUrl.startsWith('https://')) {
2840
- fullUrl = 'https://' + fullUrl;
2841
- }
2842
- await page.goto(fullUrl, { waitUntil: 'domcontentloaded', timeout: step.timeout || 30000 });
2843
- results.push(`${stepNum}. Navigated to ${fullUrl}`);
2844
- break;
2845
- }
2846
-
2847
- case 'waitForLoad': {
2848
- await waitForPageLoad(page, step.timeout || 10000);
2849
- results.push(`${stepNum}. Page loaded`);
2850
- break;
2851
- }
2852
-
2853
- case 'waitForSelector': {
2854
- if (!step.selector) throw new Error('waitForSelector requires selector parameter');
2855
- await page.waitForSelector(step.selector, { timeout: step.timeout || 10000 });
2856
- results.push(`${stepNum}. Found "${step.selector}"`);
2857
- break;
2858
- }
2859
-
2860
- case 'waitForNavigation': {
2861
- await page.waitForNavigation({ timeout: step.timeout || 10000 }).catch(() => {});
2862
- results.push(`${stepNum}. Navigation completed`);
2863
- break;
2864
- }
2865
-
2866
- case 'findAndFill': {
2867
- if (!step.selector) throw new Error('findAndFill requires selector parameter');
2868
- const element = await page.$(step.selector);
2869
- if (element) {
2870
- await element.click();
2871
- await element.fill(step.text || '');
2872
- if (step.pressEnter) {
2873
- await element.press('Enter');
2874
- await waitForPageLoad(page);
2875
- }
2876
- results.push(`${stepNum}. Filled "${step.selector}" with "${step.text || ''}"${step.pressEnter ? ' + Enter' : ''}`);
2877
- } else if (step.skipIfNotFound) {
2878
- results.push(`${stepNum}. Skipped (not found): "${step.selector}"`);
2879
- } else {
2880
- throw new Error(`Element not found: "${step.selector}"`);
2881
- }
2882
- break;
2883
- }
2884
-
2885
- case 'findAndClick': {
2886
- if (!step.selector) throw new Error('findAndClick requires selector parameter');
2887
- const element = await page.$(step.selector);
2888
- if (element) {
2889
- await element.click();
2890
- await waitForPageLoad(page);
2891
- results.push(`${stepNum}. Clicked "${step.selector}"`);
2892
- } else if (step.skipIfNotFound) {
2893
- results.push(`${stepNum}. Skipped (not found): "${step.selector}"`);
2894
- } else {
2895
- throw new Error(`Element not found: "${step.selector}"`);
2896
- }
2897
- break;
2898
- }
2899
-
2900
- case 'fillByRef': {
2901
- if (!step.ref) throw new Error('fillByRef requires ref parameter');
2902
- const element = await selectSnapshotRef(page, step.ref);
2903
- if (element) {
2904
- await element.click();
2905
- await element.fill(step.text || '');
2906
- if (step.pressEnter) {
2907
- await element.press('Enter');
2908
- await waitForPageLoad(page);
2909
- }
2910
- results.push(`${stepNum}. Filled [ref=${step.ref}] with "${step.text || ''}"${step.pressEnter ? ' + Enter' : ''}`);
2911
- } else if (step.skipIfNotFound) {
2912
- results.push(`${stepNum}. Skipped (ref not found): "${step.ref}"`);
2913
- } else {
2914
- throw new Error(`Ref not found: "${step.ref}". Run snapshot first.`);
2915
- }
2916
- break;
2917
- }
2918
-
2919
- case 'clickByRef': {
2920
- if (!step.ref) throw new Error('clickByRef requires ref parameter');
2921
- const element = await selectSnapshotRef(page, step.ref);
2922
- if (element) {
2923
- await element.click();
2924
- await waitForPageLoad(page);
2925
- results.push(`${stepNum}. Clicked [ref=${step.ref}]`);
2926
- } else if (step.skipIfNotFound) {
2927
- results.push(`${stepNum}. Skipped (ref not found): "${step.ref}"`);
2928
- } else {
2929
- throw new Error(`Ref not found: "${step.ref}". Run snapshot first.`);
2930
- }
2931
- break;
2932
- }
2933
-
2934
- case 'snapshot': {
2935
- snapshotResult = await getSnapshotWithHistory(page, DEFAULT_SNAPSHOT_OPTIONS);
2936
- results.push(`${stepNum}. Snapshot taken`);
2937
- break;
2938
- }
2939
-
2940
- case 'screenshot': {
2941
- const buffer = await page.screenshot({
2942
- fullPage: step.fullPage ?? false,
2943
- type: 'jpeg',
2944
- quality: 80,
2945
- });
2946
- screenshotData = {
2947
- type: 'image',
2948
- mimeType: 'image/jpeg',
2949
- data: buffer.toString('base64'),
2950
- };
2951
- results.push(`${stepNum}. Screenshot taken`);
2952
- break;
2953
- }
2954
-
2955
- case 'keyboard': {
2956
- if (step.key) {
2957
- await page.keyboard.press(step.key);
2958
- results.push(`${stepNum}. Pressed key: ${step.key}`);
2959
- } else if (step.text) {
2960
- await page.keyboard.type(step.text);
2961
- results.push(`${stepNum}. Typed: "${step.text}"`);
2962
- } else {
2963
- throw new Error('keyboard requires key or text parameter');
2964
- }
2965
- break;
2966
- }
2967
-
2968
- case 'evaluate': {
2969
- if (!step.code) throw new Error('evaluate requires code parameter');
2970
- const evalResult = await page.evaluate((code: string) => {
2971
- // eslint-disable-next-line no-eval
2972
- return eval(code);
2973
- }, step.code);
2974
- results.push(`${stepNum}. Evaluated: ${JSON.stringify(evalResult)}`);
2975
- break;
2976
- }
2977
-
2978
- default:
2979
- results.push(`${stepNum}. Unknown action: ${(step as any).action}`);
2980
- }
2981
- } catch (err) {
2982
- const errMsg = err instanceof Error ? err.message : String(err);
2983
- results.push(`${stepNum}. FAILED: ${errMsg}`);
2984
-
2985
- try {
2986
- snapshotResult = await getSnapshotWithHistory(page, DEFAULT_SNAPSHOT_OPTIONS);
2987
- results.push(`→ Captured page state at failure`);
2988
- } catch {
2989
- }
2990
-
2991
- const content: CallToolResult['content'] = [
2992
- { type: 'text', text: `Script stopped at step ${stepNum}:\n${results.join('\n')}` },
2993
- ];
2994
- if (snapshotResult) {
2995
- content.push({ type: 'text', text: `\nPage state:\n${snapshotResult}` });
2996
- }
2997
- if (screenshotData) {
2998
- content.push(screenshotData);
2999
- }
3000
- return { content, isError: true };
3001
- }
3002
- }
3003
-
3004
- const lastAction = actions[actions.length - 1];
3005
- if (lastAction?.action !== 'snapshot') {
3006
- try {
3007
- await waitForPageLoad(page, 2000);
3008
- snapshotResult = await getSnapshotWithHistory(page, DEFAULT_SNAPSHOT_OPTIONS);
3009
- results.push(`→ Auto-captured final page state`);
3010
- } catch {
3011
- }
3012
- }
3013
-
3014
- const content: CallToolResult['content'] = [
3015
- { type: 'text', text: `Script completed (${actions.length} actions):\n${results.join('\n')}` },
3016
- ];
3017
- if (snapshotResult) {
3018
- content.push({ type: 'text', text: `\nPage state:\n${snapshotResult}` });
3019
- }
3020
- if (screenshotData) {
3021
- content.push(screenshotData);
3022
- }
3023
- return { content };
3024
- }
3025
-
3026
- case 'browser_scroll': {
3027
- const { direction, amount, ref, selector, position, page_name } = args as BrowserScrollInput;
3028
- const page = await getPage(page_name);
3029
-
3030
- if (ref) {
3031
- const element = await selectSnapshotRef(page, ref);
3032
- if (!element) {
3033
- return {
3034
- content: [{ type: 'text', text: `Error: Could not find element with ref "${ref}"` }],
3035
- isError: true,
3036
- };
3037
- }
3038
- await element.scrollIntoViewIfNeeded();
3039
- resetSnapshotManager();
3040
- return {
3041
- content: [{ type: 'text', text: `Scrolled [ref=${ref}] into view` }],
3042
- };
3043
- }
3044
-
3045
- if (selector) {
3046
- const element = await page.$(selector);
3047
- if (!element) {
3048
- return {
3049
- content: [{ type: 'text', text: `Error: Could not find element matching "${selector}"` }],
3050
- isError: true,
3051
- };
3052
- }
3053
- await element.scrollIntoViewIfNeeded();
3054
- resetSnapshotManager();
3055
- return {
3056
- content: [{ type: 'text', text: `Scrolled "${selector}" into view` }],
3057
- };
3058
- }
3059
-
3060
- if (position) {
3061
- if (position === 'top') {
3062
- await page.evaluate(() => window.scrollTo(0, 0));
3063
- resetSnapshotManager();
3064
- return {
3065
- content: [{ type: 'text', text: 'Scrolled to top of page' }],
3066
- };
3067
- } else if (position === 'bottom') {
3068
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
3069
- resetSnapshotManager();
3070
- return {
3071
- content: [{ type: 'text', text: 'Scrolled to bottom of page' }],
3072
- };
3073
- }
3074
- }
3075
-
3076
- if (direction) {
3077
- const scrollAmount = amount || 500;
3078
- let deltaX = 0;
3079
- let deltaY = 0;
3080
-
3081
- switch (direction) {
3082
- case 'up':
3083
- deltaY = -scrollAmount;
3084
- break;
3085
- case 'down':
3086
- deltaY = scrollAmount;
3087
- break;
3088
- case 'left':
3089
- deltaX = -scrollAmount;
3090
- break;
3091
- case 'right':
3092
- deltaX = scrollAmount;
3093
- break;
3094
- }
3095
-
3096
- await page.mouse.wheel(deltaX, deltaY);
3097
- resetSnapshotManager();
3098
- return {
3099
- content: [{ type: 'text', text: `Scrolled ${direction} by ${scrollAmount}px` }],
3100
- };
3101
- }
3102
-
3103
- return {
3104
- content: [{ type: 'text', text: 'Error: Provide direction, ref, selector, or position' }],
3105
- isError: true,
3106
- };
3107
- }
3108
-
3109
- case 'browser_hover': {
3110
- const { ref, selector, x, y, page_name } = args as BrowserHoverInput;
3111
- const page = await getPage(page_name);
3112
-
3113
- if (x !== undefined && y !== undefined) {
3114
- await page.mouse.move(x, y);
3115
- return {
3116
- content: [{ type: 'text', text: `Hovered at coordinates (${x}, ${y})` }],
3117
- };
3118
- }
3119
-
3120
- if (ref) {
3121
- const element = await selectSnapshotRef(page, ref);
3122
- if (!element) {
3123
- return {
3124
- content: [{ type: 'text', text: `Error: Could not find element with ref "${ref}"` }],
3125
- isError: true,
3126
- };
3127
- }
3128
- await element.hover();
3129
- return {
3130
- content: [{ type: 'text', text: `Hovered over [ref=${ref}]` }],
3131
- };
3132
- }
3133
-
3134
- if (selector) {
3135
- await page.hover(selector);
3136
- return {
3137
- content: [{ type: 'text', text: `Hovered over "${selector}"` }],
3138
- };
3139
- }
3140
-
3141
- return {
3142
- content: [{ type: 'text', text: 'Error: Provide ref, selector, or x/y coordinates' }],
3143
- isError: true,
3144
- };
3145
- }
3146
-
3147
- case 'browser_select': {
3148
- const { ref, selector, value, label, index, page_name } = args as BrowserSelectInput;
3149
- const page = await getPage(page_name);
3150
-
3151
- let selectOption: { value?: string; label?: string; index?: number } | undefined;
3152
- if (value !== undefined) {
3153
- selectOption = { value };
3154
- } else if (label !== undefined) {
3155
- selectOption = { label };
3156
- } else if (index !== undefined) {
3157
- selectOption = { index };
3158
- }
3159
-
3160
- if (!selectOption) {
3161
- return {
3162
- content: [{ type: 'text', text: 'Error: Provide value, label, or index to select' }],
3163
- isError: true,
3164
- };
3165
- }
3166
-
3167
- let selectSelector: string;
3168
- if (ref) {
3169
- const element = await selectSnapshotRef(page, ref);
3170
- if (!element) {
3171
- return {
3172
- content: [{ type: 'text', text: `Error: Could not find element with ref "${ref}"` }],
3173
- isError: true,
3174
- };
3175
- }
3176
- await element.selectOption(selectOption);
3177
- const selectedBy = value ? `value="${value}"` : label ? `label="${label}"` : `index=${index}`;
3178
- return {
3179
- content: [{ type: 'text', text: `Selected option (${selectedBy}) in [ref=${ref}]` }],
3180
- };
3181
- }
3182
-
3183
- if (selector) {
3184
- selectSelector = selector;
3185
- } else {
3186
- return {
3187
- content: [{ type: 'text', text: 'Error: Provide ref or selector for the select element' }],
3188
- isError: true,
3189
- };
3190
- }
3191
-
3192
- await page.selectOption(selectSelector, selectOption);
3193
- const selectedBy = value ? `value="${value}"` : label ? `label="${label}"` : `index=${index}`;
3194
- return {
3195
- content: [{ type: 'text', text: `Selected option (${selectedBy}) in "${selectSelector}"` }],
3196
- };
3197
- }
3198
-
3199
- case 'browser_wait': {
3200
- const { condition, selector, script, timeout, page_name } = args as BrowserWaitInput;
3201
- const page = await getPage(page_name);
3202
- const waitTimeout = timeout || 30000;
3203
-
3204
- switch (condition) {
3205
- case 'selector': {
3206
- if (!selector) {
3207
- return {
3208
- content: [{ type: 'text', text: 'Error: "selector" is required for selector condition' }],
3209
- isError: true,
3210
- };
3211
- }
3212
- await page.waitForSelector(selector, { timeout: waitTimeout });
3213
- return {
3214
- content: [{ type: 'text', text: `Element "${selector}" appeared` }],
3215
- };
3216
- }
3217
- case 'hidden': {
3218
- if (!selector) {
3219
- return {
3220
- content: [{ type: 'text', text: 'Error: "selector" is required for hidden condition' }],
3221
- isError: true,
3222
- };
3223
- }
3224
- await page.waitForSelector(selector, { state: 'hidden', timeout: waitTimeout });
3225
- return {
3226
- content: [{ type: 'text', text: `Element "${selector}" is now hidden` }],
3227
- };
3228
- }
3229
- case 'navigation': {
3230
- await page.waitForNavigation({ timeout: waitTimeout });
3231
- return {
3232
- content: [{ type: 'text', text: `Navigation completed. Now at: ${page.url()}` }],
3233
- };
3234
- }
3235
- case 'network_idle': {
3236
- await page.waitForLoadState('networkidle', { timeout: waitTimeout });
3237
- return {
3238
- content: [{ type: 'text', text: 'Network is idle' }],
3239
- };
3240
- }
3241
- case 'timeout': {
3242
- const waitMs = timeout || 1000;
3243
- await page.waitForTimeout(waitMs);
3244
- return {
3245
- content: [{ type: 'text', text: `Waited ${waitMs}ms` }],
3246
- };
3247
- }
3248
- case 'function': {
3249
- if (!script) {
3250
- return {
3251
- content: [{ type: 'text', text: 'Error: "script" is required for function condition. Provide a JS expression that returns true when ready.' }],
3252
- isError: true,
3253
- };
3254
- }
3255
- try {
3256
- await page.waitForFunction(script, { timeout: waitTimeout });
3257
- return {
3258
- content: [{ type: 'text', text: `Custom condition met: ${script.substring(0, 50)}${script.length > 50 ? '...' : ''}` }],
3259
- };
3260
- } catch (err) {
3261
- const friendlyError = toAIFriendlyError(err, script);
3262
- return {
3263
- content: [{ type: 'text', text: friendlyError.message }],
3264
- isError: true,
3265
- };
3266
- }
3267
- }
3268
- default:
3269
- return {
3270
- content: [{ type: 'text', text: `Error: Unknown wait condition "${condition}"` }],
3271
- isError: true,
3272
- };
3273
- }
3274
- }
3275
-
3276
- case 'browser_file_upload': {
3277
- const { ref, selector, files, page_name } = args as BrowserFileUploadInput;
3278
- const page = await getPage(page_name);
3279
-
3280
- if (!files || files.length === 0) {
3281
- return {
3282
- content: [{ type: 'text', text: 'Error: At least one file path is required' }],
3283
- isError: true,
3284
- };
3285
- }
3286
-
3287
- let element: ElementHandle | null = null;
3288
-
3289
- if (ref) {
3290
- element = await selectSnapshotRef(page, ref);
3291
- if (!element) {
3292
- return {
3293
- content: [{ type: 'text', text: `Error: Could not find element with ref "${ref}"` }],
3294
- isError: true,
3295
- };
3296
- }
3297
- } else if (selector) {
3298
- element = await page.$(selector);
3299
- if (!element) {
3300
- return {
3301
- content: [{ type: 'text', text: `Error: Could not find element matching "${selector}"` }],
3302
- isError: true,
3303
- };
3304
- }
3305
- } else {
3306
- return {
3307
- content: [{ type: 'text', text: 'Error: Provide ref or selector for the file input' }],
3308
- isError: true,
3309
- };
3310
- }
3311
-
3312
- await element.setInputFiles(files);
3313
- const target = ref ? `[ref=${ref}]` : `"${selector}"`;
3314
- const fileCount = files.length;
3315
- return {
3316
- content: [{ type: 'text', text: `Uploaded ${fileCount} file(s) to ${target}` }],
3317
- };
3318
- }
3319
-
3320
- case 'browser_drag': {
3321
- const {
3322
- source_ref, source_selector, source_x, source_y,
3323
- target_ref, target_selector, target_x, target_y,
3324
- page_name
3325
- } = args as BrowserDragInput;
3326
- const page = await getPage(page_name);
3327
-
3328
- let sourcePos: { x: number; y: number } | null = null;
3329
-
3330
- if (source_x !== undefined && source_y !== undefined) {
3331
- sourcePos = { x: source_x, y: source_y };
3332
- } else if (source_ref) {
3333
- const element = await selectSnapshotRef(page, source_ref);
3334
- if (!element) {
3335
- return {
3336
- content: [{ type: 'text', text: `Error: Could not find source element with ref "${source_ref}"` }],
3337
- isError: true,
3338
- };
3339
- }
3340
- const box = await element.boundingBox();
3341
- if (!box) {
3342
- return {
3343
- content: [{ type: 'text', text: `Error: Source element [ref=${source_ref}] has no bounding box` }],
3344
- isError: true,
3345
- };
3346
- }
3347
- sourcePos = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
3348
- } else if (source_selector) {
3349
- const element = await page.$(source_selector);
3350
- if (!element) {
3351
- return {
3352
- content: [{ type: 'text', text: `Error: Could not find source element "${source_selector}"` }],
3353
- isError: true,
3354
- };
3355
- }
3356
- const box = await element.boundingBox();
3357
- if (!box) {
3358
- return {
3359
- content: [{ type: 'text', text: `Error: Source element "${source_selector}" has no bounding box` }],
3360
- isError: true,
3361
- };
3362
- }
3363
- sourcePos = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
3364
- }
3365
-
3366
- if (!sourcePos) {
3367
- return {
3368
- content: [{ type: 'text', text: 'Error: Provide source_ref, source_selector, or source_x/source_y' }],
3369
- isError: true,
3370
- };
3371
- }
3372
-
3373
- let targetPos: { x: number; y: number } | null = null;
3374
-
3375
- if (target_x !== undefined && target_y !== undefined) {
3376
- targetPos = { x: target_x, y: target_y };
3377
- } else if (target_ref) {
3378
- const element = await selectSnapshotRef(page, target_ref);
3379
- if (!element) {
3380
- return {
3381
- content: [{ type: 'text', text: `Error: Could not find target element with ref "${target_ref}"` }],
3382
- isError: true,
3383
- };
3384
- }
3385
- const box = await element.boundingBox();
3386
- if (!box) {
3387
- return {
3388
- content: [{ type: 'text', text: `Error: Target element [ref=${target_ref}] has no bounding box` }],
3389
- isError: true,
3390
- };
3391
- }
3392
- targetPos = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
3393
- } else if (target_selector) {
3394
- const element = await page.$(target_selector);
3395
- if (!element) {
3396
- return {
3397
- content: [{ type: 'text', text: `Error: Could not find target element "${target_selector}"` }],
3398
- isError: true,
3399
- };
3400
- }
3401
- const box = await element.boundingBox();
3402
- if (!box) {
3403
- return {
3404
- content: [{ type: 'text', text: `Error: Target element "${target_selector}" has no bounding box` }],
3405
- isError: true,
3406
- };
3407
- }
3408
- targetPos = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
3409
- }
3410
-
3411
- if (!targetPos) {
3412
- return {
3413
- content: [{ type: 'text', text: 'Error: Provide target_ref, target_selector, or target_x/target_y' }],
3414
- isError: true,
3415
- };
3416
- }
3417
-
3418
- await page.mouse.move(sourcePos.x, sourcePos.y);
3419
- await page.mouse.down();
3420
- await page.mouse.move(targetPos.x, targetPos.y, { steps: 10 });
3421
- await page.mouse.up();
3422
-
3423
- const sourceDesc = source_ref ? `[ref=${source_ref}]` : source_selector ? `"${source_selector}"` : `(${source_x}, ${source_y})`;
3424
- const targetDesc = target_ref ? `[ref=${target_ref}]` : target_selector ? `"${target_selector}"` : `(${target_x}, ${target_y})`;
3425
- return {
3426
- content: [{ type: 'text', text: `Dragged from ${sourceDesc} to ${targetDesc}` }],
3427
- };
3428
- }
3429
-
3430
- case 'browser_get_text': {
3431
- const { ref, selector, page_name } = args as BrowserGetTextInput;
3432
- const page = await getPage(page_name);
3433
-
3434
- let element: ElementHandle | null = null;
3435
- let target: string;
3436
-
3437
- if (ref) {
3438
- element = await selectSnapshotRef(page, ref);
3439
- target = `[ref=${ref}]`;
3440
- if (!element) {
3441
- return {
3442
- content: [{ type: 'text', text: `Error: Could not find element with ref "${ref}"` }],
3443
- isError: true,
3444
- };
3445
- }
3446
- } else if (selector) {
3447
- element = await page.$(selector);
3448
- target = `"${selector}"`;
3449
- if (!element) {
3450
- return {
3451
- content: [{ type: 'text', text: `Error: Could not find element matching "${selector}"` }],
3452
- isError: true,
3453
- };
3454
- }
3455
- } else {
3456
- return {
3457
- content: [{ type: 'text', text: 'Error: Provide ref or selector' }],
3458
- isError: true,
3459
- };
3460
- }
3461
-
3462
- const value = await element.evaluate((el) => {
3463
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
3464
- return { type: 'value', text: el.value };
3465
- }
3466
- if (el instanceof HTMLSelectElement) {
3467
- return { type: 'value', text: el.options[el.selectedIndex]?.text || '' };
3468
- }
3469
- return { type: 'text', text: el.textContent || '' };
3470
- });
3471
-
3472
- return {
3473
- content: [{ type: 'text', text: `${target} ${value.type}: "${value.text}"` }],
3474
- };
3475
- }
3476
-
3477
- case 'browser_is_visible': {
3478
- const { ref, selector, page_name } = args as BrowserIsVisibleInput;
3479
- const page = await getPage(page_name);
3480
-
3481
- try {
3482
- if (ref) {
3483
- const element = await selectSnapshotRef(page, ref);
3484
- if (!element) {
3485
- return {
3486
- content: [{ type: 'text', text: `false (element [ref=${ref}] not found - run browser_snapshot() to get updated refs)` }],
3487
- };
3488
- }
3489
- const isVisible = await element.isVisible();
3490
- return {
3491
- content: [{ type: 'text', text: `${isVisible}` }],
3492
- };
3493
- } else if (selector) {
3494
- const element = await page.$(selector);
3495
- if (!element) {
3496
- return {
3497
- content: [{ type: 'text', text: `false (element "${selector}" not found)` }],
3498
- };
3499
- }
3500
- const isVisible = await element.isVisible();
3501
- return {
3502
- content: [{ type: 'text', text: `${isVisible}` }],
3503
- };
3504
- } else {
3505
- return {
3506
- content: [{ type: 'text', text: 'Error: Provide ref or selector' }],
3507
- isError: true,
3508
- };
3509
- }
3510
- } catch (err) {
3511
- const targetDesc = ref ? `[ref=${ref}]` : selector || 'element';
3512
- const friendlyError = toAIFriendlyError(err, targetDesc);
3513
- return {
3514
- content: [{ type: 'text', text: friendlyError.message }],
3515
- isError: true,
3516
- };
3517
- }
3518
- }
3519
-
3520
- case 'browser_is_enabled': {
3521
- const { ref, selector, page_name } = args as BrowserIsEnabledInput;
3522
- const page = await getPage(page_name);
3523
-
3524
- try {
3525
- if (ref) {
3526
- const element = await selectSnapshotRef(page, ref);
3527
- if (!element) {
3528
- return {
3529
- content: [{ type: 'text', text: `false (element [ref=${ref}] not found - run browser_snapshot() to get updated refs)` }],
3530
- };
3531
- }
3532
- const isEnabled = await element.isEnabled();
3533
- return {
3534
- content: [{ type: 'text', text: `${isEnabled}` }],
3535
- };
3536
- } else if (selector) {
3537
- const element = await page.$(selector);
3538
- if (!element) {
3539
- return {
3540
- content: [{ type: 'text', text: `false (element "${selector}" not found)` }],
3541
- };
3542
- }
3543
- const isEnabled = await element.isEnabled();
3544
- return {
3545
- content: [{ type: 'text', text: `${isEnabled}` }],
3546
- };
3547
- } else {
3548
- return {
3549
- content: [{ type: 'text', text: 'Error: Provide ref or selector' }],
3550
- isError: true,
3551
- };
3552
- }
3553
- } catch (err) {
3554
- const targetDesc = ref ? `[ref=${ref}]` : selector || 'element';
3555
- const friendlyError = toAIFriendlyError(err, targetDesc);
3556
- return {
3557
- content: [{ type: 'text', text: friendlyError.message }],
3558
- isError: true,
3559
- };
3560
- }
3561
- }
3562
-
3563
- case 'browser_is_checked': {
3564
- const { ref, selector, page_name } = args as BrowserIsCheckedInput;
3565
- const page = await getPage(page_name);
3566
-
3567
- try {
3568
- if (ref) {
3569
- const element = await selectSnapshotRef(page, ref);
3570
- if (!element) {
3571
- return {
3572
- content: [{ type: 'text', text: `false (element [ref=${ref}] not found - run browser_snapshot() to get updated refs)` }],
3573
- };
3574
- }
3575
- const isChecked = await element.isChecked();
3576
- return {
3577
- content: [{ type: 'text', text: `${isChecked}` }],
3578
- };
3579
- } else if (selector) {
3580
- const element = await page.$(selector);
3581
- if (!element) {
3582
- return {
3583
- content: [{ type: 'text', text: `false (element "${selector}" not found)` }],
3584
- };
3585
- }
3586
- const isChecked = await element.isChecked();
3587
- return {
3588
- content: [{ type: 'text', text: `${isChecked}` }],
3589
- };
3590
- } else {
3591
- return {
3592
- content: [{ type: 'text', text: 'Error: Provide ref or selector' }],
3593
- isError: true,
3594
- };
3595
- }
3596
- } catch (err) {
3597
- const targetDesc = ref ? `[ref=${ref}]` : selector || 'element';
3598
- const friendlyError = toAIFriendlyError(err, targetDesc);
3599
- return {
3600
- content: [{ type: 'text', text: friendlyError.message }],
3601
- isError: true,
3602
- };
3603
- }
3604
- }
3605
-
3606
- case 'browser_iframe': {
3607
- const { action, ref, selector, page_name } = args as BrowserIframeInput;
3608
- const page = await getPage(page_name);
3609
-
3610
- if (action === 'enter') {
3611
- let frameElement: ElementHandle | null = null;
3612
-
3613
- if (ref) {
3614
- frameElement = await selectSnapshotRef(page, ref);
3615
- if (!frameElement) {
3616
- return {
3617
- content: [{ type: 'text', text: `Error: Could not find iframe with ref "${ref}"` }],
3618
- isError: true,
3619
- };
3620
- }
3621
- } else if (selector) {
3622
- frameElement = await page.$(selector);
3623
- if (!frameElement) {
3624
- return {
3625
- content: [{ type: 'text', text: `Error: Could not find iframe matching "${selector}"` }],
3626
- isError: true,
3627
- };
3628
- }
3629
- } else {
3630
- return {
3631
- content: [{ type: 'text', text: 'Error: Provide ref or selector for the iframe' }],
3632
- isError: true,
3633
- };
3634
- }
3635
-
3636
- const frame = await frameElement.contentFrame();
3637
- if (!frame) {
3638
- return {
3639
- content: [{ type: 'text', text: 'Error: Element is not an iframe or frame is not accessible' }],
3640
- isError: true,
3641
- };
3642
- }
3643
-
3644
- const frameUrl = frame.url();
3645
- return {
3646
- content: [{ type: 'text', text: `Entered iframe. Frame URL: ${frameUrl}\nNote: Use browser_evaluate with frame-aware selectors, or take a snapshot to see iframe content.` }],
3647
- };
3648
- } else if (action === 'exit') {
3649
- return {
3650
- content: [{ type: 'text', text: 'Exited iframe. Now working with main page.' }],
3651
- };
3652
- }
3653
-
3654
- return {
3655
- content: [{ type: 'text', text: `Error: Unknown iframe action "${action}"` }],
3656
- isError: true,
3657
- };
3658
- }
3659
-
3660
- case 'browser_tabs': {
3661
- const { action, index, timeout, page_name } = args as BrowserTabsInput;
3662
- const b = await ensureConnected();
3663
-
3664
- if (action === 'list') {
3665
- const allPages = b.contexts().flatMap((ctx) => ctx.pages());
3666
- const pageList = allPages.map((p, i) => `${i}: ${p.url()}`).join('\n');
3667
- let output = `Open tabs (${allPages.length}):\n${pageList}`;
3668
- if (allPages.length > 1) {
3669
- output += `\n\nMultiple tabs detected! Use browser_tabs(action="switch", index=N) to switch to another tab.`;
3670
- }
3671
- return {
3672
- content: [{ type: 'text', text: output }],
3673
- };
3674
- }
3675
-
3676
- if (action === 'switch') {
3677
- if (index === undefined) {
3678
- return {
3679
- content: [{ type: 'text', text: 'Error: index is required for switch action' }],
3680
- isError: true,
3681
- };
3682
- }
3683
- const allPages = b.contexts().flatMap((ctx) => ctx.pages());
3684
- if (index < 0 || index >= allPages.length) {
3685
- return {
3686
- content: [{ type: 'text', text: `Error: Invalid tab index ${index}. Valid range: 0-${allPages.length - 1}` }],
3687
- isError: true,
3688
- };
3689
- }
3690
- const targetPage = allPages[index]!;
3691
- await targetPage.bringToFront();
3692
- activePageOverride = targetPage;
3693
- await injectActiveTabGlow(targetPage);
3694
- return {
3695
- content: [{ type: 'text', text: `Switched to tab ${index}: ${targetPage.url()}\n\nNow use browser_snapshot() to see the content of this tab.` }],
3696
- };
3697
- }
3698
-
3699
- if (action === 'close') {
3700
- if (index === undefined) {
3701
- return {
3702
- content: [{ type: 'text', text: 'Error: index is required for close action' }],
3703
- isError: true,
3704
- };
3705
- }
3706
- const allPages = b.contexts().flatMap((ctx) => ctx.pages());
3707
- if (index < 0 || index >= allPages.length) {
3708
- return {
3709
- content: [{ type: 'text', text: `Error: Invalid tab index ${index}. Valid range: 0-${allPages.length - 1}` }],
3710
- isError: true,
3711
- };
3712
- }
3713
- const targetPage = allPages[index]!;
3714
- const closedUrl = targetPage.url();
3715
- if (activePageOverride === targetPage) {
3716
- activePageOverride = null;
3717
- }
3718
- await targetPage.close();
3719
- return {
3720
- content: [{ type: 'text', text: `Closed tab ${index}: ${closedUrl}` }],
3721
- };
3722
- }
3723
-
3724
- if (action === 'wait_for_new') {
3725
- const waitTimeout = timeout || 5000;
3726
- const context = b.contexts()[0];
3727
- if (!context) {
3728
- return {
3729
- content: [{ type: 'text', text: 'Error: No browser context available' }],
3730
- isError: true,
3731
- };
3732
- }
3733
-
3734
- try {
3735
- const newPage = await context.waitForEvent('page', { timeout: waitTimeout });
3736
- await newPage.waitForLoadState('domcontentloaded');
3737
- const allPages = context.pages();
3738
- const newIndex = allPages.indexOf(newPage);
3739
- activePageOverride = newPage;
3740
- await injectActiveTabGlow(newPage);
3741
- return {
3742
- content: [{ type: 'text', text: `New tab opened at index ${newIndex}: ${newPage.url()}` }],
3743
- };
3744
- } catch {
3745
- return {
3746
- content: [{ type: 'text', text: `No new tab opened within ${waitTimeout}ms` }],
3747
- isError: true,
3748
- };
3749
- }
3750
- }
3751
-
3752
- return {
3753
- content: [{ type: 'text', text: `Error: Unknown tabs action "${action}"` }],
3754
- isError: true,
3755
- };
3756
- }
3757
-
3758
- case 'browser_canvas_type': {
3759
- const { text, position, page_name } = args as BrowserCanvasTypeInput;
3760
- const page = await getPage(page_name);
3761
- const jumpToStart = position !== 'current';
3762
-
3763
- const viewport = page.viewportSize();
3764
- const clickX = (viewport?.width || 1280) / 2;
3765
- const clickY = (viewport?.height || 720) * 2 / 3;
3766
- await page.mouse.click(clickX, clickY);
3767
-
3768
- await page.waitForTimeout(100);
3769
-
3770
- if (jumpToStart) {
3771
- const isMac = process.platform === 'darwin';
3772
- const modifier = isMac ? 'Meta' : 'Control';
3773
- await page.keyboard.press(`${modifier}+Home`);
3774
- await page.waitForTimeout(50);
3775
- }
3776
-
3777
- await page.keyboard.type(text);
3778
-
3779
- const positionDesc = jumpToStart ? 'at document start' : 'at current position';
3780
- return {
3781
- content: [{ type: 'text', text: `Typed "${text.length > 50 ? text.slice(0, 50) + '...' : text}" ${positionDesc}` }],
3782
- };
3783
- }
3784
-
3785
- case 'browser_highlight': {
3786
- const { enabled, page_name } = args as BrowserHighlightInput;
3787
- const page = await getPage(page_name);
3788
-
3789
- if (enabled) {
3790
- await injectActiveTabGlow(page);
3791
- return {
3792
- content: [{ type: 'text', text: 'Highlight enabled - tab now shows color-cycling glow border' }],
3793
- };
3794
- } else {
3795
- await removeActiveTabGlow(page);
3796
- return {
3797
- content: [{ type: 'text', text: 'Highlight disabled - glow removed from tab' }],
3798
- };
3799
- }
3800
- }
3801
-
3802
- case 'browser_batch_actions': {
3803
- const { urls, extractScript, waitForSelector, page_name } = args as {
3804
- urls: string[];
3805
- extractScript: string;
3806
- waitForSelector?: string;
3807
- page_name?: string;
3808
- };
3809
-
3810
- if (!urls || urls.length === 0) {
3811
- return {
3812
- content: [{ type: 'text', text: 'Error: urls array is required and must not be empty' }],
3813
- isError: true,
3814
- };
3815
- }
3816
- if (urls.length > 20) {
3817
- return {
3818
- content: [{ type: 'text', text: 'Error: Maximum 20 URLs per batch call' }],
3819
- isError: true,
3820
- };
3821
- }
3822
- if (!extractScript) {
3823
- return {
3824
- content: [{ type: 'text', text: 'Error: extractScript is required' }],
3825
- isError: true,
3826
- };
3827
- }
3828
-
3829
- const BATCH_TIMEOUT_MS = 120_000;
3830
- const MAX_RESULT_SIZE_BYTES = 1_048_576;
3831
-
3832
- const page = await getPage(page_name);
3833
- const batchResults: Array<{
3834
- url: string;
3835
- status: 'success' | 'failed';
3836
- data?: Record<string, unknown>;
3837
- error?: string;
3838
- }> = [];
3839
-
3840
- const batchStart = Date.now();
3841
-
3842
- for (const url of urls) {
3843
- if (Date.now() - batchStart > BATCH_TIMEOUT_MS) {
3844
- batchResults.push({ url, status: 'failed', error: 'Batch timeout exceeded (2 min limit)' });
3845
- continue;
3846
- }
3847
-
3848
- let fullUrl = url;
3849
- if (!fullUrl.startsWith('http://') && !fullUrl.startsWith('https://')) {
3850
- fullUrl = 'https://' + fullUrl;
3851
- }
3852
-
3853
- const remainingTime = BATCH_TIMEOUT_MS - (Date.now() - batchStart);
3854
- const effectiveTimeout = Math.min(30000, remainingTime);
3855
-
3856
- try {
3857
- await page.goto(fullUrl, { waitUntil: 'domcontentloaded', timeout: effectiveTimeout });
3858
-
3859
- if (waitForSelector) {
3860
- await page.waitForSelector(waitForSelector, { timeout: Math.min(10000, remainingTime) }).catch(() => {});
3861
- }
3862
-
3863
- const data = await page.evaluate((script: string) => {
3864
- const fn = new Function(script);
3865
- return fn();
3866
- }, extractScript);
3867
-
3868
- const serialized = JSON.stringify(data);
3869
- if (serialized.length > MAX_RESULT_SIZE_BYTES) {
3870
- batchResults.push({
3871
- url: fullUrl,
3872
- status: 'failed',
3873
- error: `Result too large: ${serialized.length} bytes (max ${MAX_RESULT_SIZE_BYTES})`,
3874
- });
3875
- continue;
3876
- }
3877
-
3878
- batchResults.push({ url: fullUrl, status: 'success', data });
3879
- } catch (err) {
3880
- const errMsg = err instanceof Error ? err.message : String(err);
3881
- batchResults.push({ url: fullUrl, status: 'failed', error: errMsg });
3882
- }
3883
- }
3884
-
3885
- resetSnapshotManager();
3886
-
3887
- const succeeded = batchResults.filter(r => r.status === 'success').length;
3888
- const failed = batchResults.filter(r => r.status === 'failed').length;
3889
-
3890
- const output = {
3891
- results: batchResults,
3892
- summary: {
3893
- total: urls.length,
3894
- succeeded,
3895
- failed,
3896
- },
3897
- };
3898
-
3899
- return {
3900
- content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
3901
- };
3902
- }
3903
-
3904
- default:
3905
- return {
3906
- content: [{ type: 'text', text: `Error: Unknown tool: ${name}` }],
3907
- isError: true,
3908
- };
3909
- }
3910
- } catch (error) {
3911
- const errorMessage = error instanceof Error ? error.message : String(error);
3912
- return {
3913
- content: [{ type: 'text', text: `Error: ${errorMessage}` }],
3914
- isError: true,
3915
- };
3916
- }
3917
- });
3918
-
3919
- async function main() {
3920
- console.error('[dev-browser-mcp] main() called, creating transport...');
3921
- const transport = new StdioServerTransport();
3922
- console.error('[dev-browser-mcp] Transport created, connecting server...');
3923
- await server.connect(transport);
3924
- console.error('[dev-browser-mcp] Server connected successfully!');
3925
- console.error('[dev-browser-mcp] MCP Server ready and listening for tool calls');
3926
-
3927
- console.error('[dev-browser-mcp] Connecting to browser for auto-glow setup...');
3928
- try {
3929
- await ensureConnected();
3930
- console.error('[dev-browser-mcp] Browser connected, page listeners active');
3931
- } catch (err) {
3932
- console.error('[dev-browser-mcp] Could not connect to browser yet (will retry on first tool call):', err);
3933
- }
3934
- }
3935
-
3936
- console.error('[dev-browser-mcp] Calling main()...');
3937
- main().catch((error) => {
3938
- console.error('[dev-browser-mcp] Failed to start server:', error);
3939
- process.exit(1);
3940
- });