chrometools-mcp 1.8.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js.backup DELETED
@@ -1,3674 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from "@modelcontextprotocol/sdk/types.js";
9
- import { z } from "zod";
10
- import puppeteer from "puppeteer";
11
- import Jimp from "jimp";
12
- import pixelmatch from "pixelmatch";
13
- import { writeFileSync, mkdirSync, readFileSync } from 'fs';
14
- import { dirname } from 'path';
15
- import { spawn } from 'child_process';
16
- import http from 'http';
17
- import { fileURLToPath } from 'url';
18
- import path from 'path';
19
-
20
- // Figma token from environment variable (can be set in MCP config)
21
- const FIGMA_TOKEN = process.env.FIGMA_TOKEN || null;
22
-
23
- // Get current directory for loading utils
24
- const __filename = fileURLToPath(import.meta.url);
25
- const __dirname = dirname(__filename);
26
-
27
- // Load element finder utilities
28
- const elementFinderUtils = readFileSync(path.join(__dirname, 'element-finder-utils.js'), 'utf-8');
29
-
30
- // Import hints generator
31
- import {
32
- generateNavigationHints,
33
- generateClickHints,
34
- generateFormSubmitHints,
35
- generatePageHints
36
- } from './utils/hints-generator.js';
37
-
38
- // Import Recorder modules
39
- import { injectRecorder } from './recorder/recorder-script.js';
40
- import { executeScenario } from './recorder/scenario-executor.js';
41
- import {
42
- initializeStorage,
43
- saveScenario,
44
- loadScenario,
45
- listScenarios,
46
- searchScenarios,
47
- deleteScenario
48
- } from './recorder/scenario-storage.js';
49
-
50
- // Import Angular tools
51
- import {
52
- listAngularComponents,
53
- getAngularComponent,
54
- callAngularMethod,
55
- getAngularForm,
56
- submitAngularForm
57
- } from './angular-tools.js';
58
-
59
- // Import Figma tools
60
- import {
61
- parseFigmaUrl,
62
- normalizeFigmaNodeId,
63
- fetchFigmaAPI,
64
- getFigmaFile,
65
- listFigmaPages,
66
- searchFigmaFrames,
67
- getFigmaComponents,
68
- getFigmaStyles,
69
- getFigmaColorPalette,
70
- extractTextFromNode,
71
- collectAllText
72
- } from './figma-tools.js';
73
-
74
- // Detect WSL environment
75
- const isWSL = (() => {
76
- try {
77
- const fs = require('fs');
78
- const proc_version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
79
- return proc_version.includes('microsoft') || proc_version.includes('wsl');
80
- } catch {
81
- return false;
82
- }
83
- })();
84
-
85
- // Detect Windows environment (including WSL)
86
- const isWindows = process.platform === 'win32' || isWSL;
87
-
88
- // Get Chrome executable path based on platform
89
- function getChromePath() {
90
- if (process.platform === 'win32') {
91
- // Native Windows
92
- return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
93
- } else if (isWSL) {
94
- // WSL - use Windows Chrome
95
- return '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe';
96
- } else {
97
- // Linux
98
- return '/usr/bin/google-chrome';
99
- }
100
- }
101
-
102
- // Get temp directory based on platform
103
- function getTempDir() {
104
- if (process.platform === 'win32') {
105
- return process.env.TEMP || 'C:\\Windows\\Temp';
106
- } else if (isWSL) {
107
- return '/mnt/c/Windows/Temp';
108
- } else {
109
- return process.env.TMPDIR || '/tmp';
110
- }
111
- }
112
-
113
- // Global browser instance (persists between requests)
114
- let browserPromise = null;
115
- const openPages = new Map();
116
- let lastPage = null;
117
- let chromeProcess = null;
118
-
119
- // Console logs storage
120
- const consoleLogs = [];
121
-
122
- // Network requests storage
123
- const networkRequests = [];
124
-
125
- // Page analysis cache (method 4)
126
- const pageAnalysisCache = new Map();
127
-
128
- // Track pages with recorder injected
129
- const pagesWithRecorder = new WeakSet();
130
-
131
- // Debug port for Chrome remote debugging
132
- const CHROME_DEBUG_PORT = 9222;
133
-
134
- // Helper function to get WebSocket endpoint from Chrome
135
- async function getChromeWebSocketEndpoint(port = CHROME_DEBUG_PORT, maxRetries = 10) {
136
- for (let i = 0; i < maxRetries; i++) {
137
- try {
138
- const response = await new Promise((resolve, reject) => {
139
- const req = http.get(`http://localhost:${port}/json/version`, (res) => {
140
- let data = '';
141
- res.on('data', chunk => data += chunk);
142
- res.on('end', () => resolve(data));
143
- });
144
- req.on('error', reject);
145
- req.setTimeout(1000);
146
- });
147
-
148
- const info = JSON.parse(response);
149
- if (info.webSocketDebuggerUrl) {
150
- return info.webSocketDebuggerUrl;
151
- }
152
- } catch (err) {
153
- // Chrome might not be ready yet, wait and retry
154
- await new Promise(resolve => setTimeout(resolve, 500));
155
- }
156
- }
157
- throw new Error('Could not get Chrome WebSocket endpoint after multiple retries');
158
- }
159
-
160
- // Initialize browser (singleton)
161
- async function getBrowser() {
162
- // Check if we have a cached browser and if it's still connected
163
- if (browserPromise) {
164
- try {
165
- const cachedBrowser = await browserPromise;
166
- if (cachedBrowser && cachedBrowser.isConnected()) {
167
- return cachedBrowser;
168
- }
169
- // Browser disconnected, reset the promise
170
- console.error("[chrometools-mcp] Browser disconnected, will reconnect...");
171
- browserPromise = null;
172
- } catch (error) {
173
- console.error("[chrometools-mcp] Error checking cached browser:", error.message);
174
- browserPromise = null;
175
- }
176
- }
177
-
178
- if (!browserPromise) {
179
- browserPromise = (async () => {
180
- try {
181
- let browser;
182
- let endpoint;
183
-
184
- // Try to connect to existing Chrome with remote debugging
185
- try {
186
- endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 2);
187
- browser = await puppeteer.connect({
188
- browserWSEndpoint: endpoint,
189
- defaultViewport: null,
190
- });
191
- console.error("[chrometools-mcp] Connected to existing Chrome instance");
192
- console.error("[chrometools-mcp] WebSocket endpoint:", endpoint);
193
-
194
- // Set up disconnect handler to reset browserPromise
195
- browser.on('disconnected', () => {
196
- console.error("[chrometools-mcp] Browser disconnected");
197
- browserPromise = null;
198
- });
199
-
200
- return browser;
201
- } catch (connectError) {
202
- console.error("[chrometools-mcp] No existing Chrome found, launching new instance...");
203
- }
204
-
205
- // Launch new Chrome with remote debugging enabled
206
- const chromePath = getChromePath();
207
- const userDataDir = `${getTempDir()}/chrome-mcp-profile`;
208
-
209
- console.error("[chrometools-mcp] Chrome path:", chromePath);
210
- console.error("[chrometools-mcp] User data dir:", userDataDir);
211
-
212
- chromeProcess = spawn(chromePath, [
213
- `--remote-debugging-port=${CHROME_DEBUG_PORT}`,
214
- '--no-first-run',
215
- '--no-default-browser-check',
216
- `--user-data-dir=${userDataDir}`,
217
- ], {
218
- detached: true,
219
- stdio: 'ignore',
220
- });
221
-
222
- chromeProcess.unref(); // Allow Node to exit even if Chrome is running
223
-
224
- console.error("[chrometools-mcp] Chrome launched with remote debugging on port", CHROME_DEBUG_PORT);
225
-
226
- // Wait for Chrome to start and get the endpoint
227
- endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 20);
228
-
229
- // Connect to the Chrome instance
230
- browser = await puppeteer.connect({
231
- browserWSEndpoint: endpoint,
232
- defaultViewport: null,
233
- });
234
-
235
- console.error("[chrometools-mcp] Connected to Chrome instance");
236
- console.error("[chrometools-mcp] WebSocket endpoint:", endpoint);
237
-
238
- // Set up disconnect handler to reset browserPromise
239
- browser.on('disconnected', () => {
240
- console.error("[chrometools-mcp] Browser disconnected");
241
- browserPromise = null;
242
- });
243
-
244
- return browser;
245
- } catch (error) {
246
- // Check if it's a display-related error in WSL
247
- if (isWSL && (
248
- error.message.includes('DISPLAY') ||
249
- error.message.includes('connect ECONNREFUSED') ||
250
- error.message.includes('cannot open display')
251
- )) {
252
- const helpMessage = `
253
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
254
- ❌ WSL X Server Error Detected
255
-
256
- You are running in WSL environment with headless:false mode.
257
- This requires an X server to display the browser GUI.
258
-
259
- 🔧 Solution:
260
- 1. Start X server on Windows (e.g., VcXsrv, X410)
261
- 2. Set DISPLAY in your MCP config:
262
-
263
- {
264
- "mcpServers": {
265
- "chrometools": {
266
- "env": {
267
- "DISPLAY": "172.25.96.1:0"
268
- }
269
- }
270
- }
271
- }
272
-
273
- 📚 For detailed setup instructions, see:
274
- WSL_SETUP.md in chrometools-mcp package
275
-
276
- 💡 Alternative: Run in headless mode (modify index.js)
277
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
278
- `;
279
- console.error(helpMessage);
280
- throw new Error(`WSL X Server not available. ${error.message}\n\nSee above for setup instructions.`);
281
- }
282
-
283
- // Re-throw other errors as-is
284
- throw error;
285
- }
286
- })();
287
- }
288
- return browserPromise;
289
- }
290
-
291
- // Setup navigation listener for recorder auto-reinjection
292
- // Track pages with network monitoring to prevent duplicate setup
293
- const pagesWithNetworkMonitoring = new WeakSet();
294
-
295
- // Setup network monitoring with auto-reinitialization on navigation
296
- async function setupNetworkMonitoring(page) {
297
- // Prevent duplicate setup on the same page
298
- if (pagesWithNetworkMonitoring.has(page)) {
299
- return;
300
- }
301
- pagesWithNetworkMonitoring.add(page);
302
-
303
- const client = await page.target().createCDPSession();
304
- await client.send('Network.enable');
305
-
306
- client.on('Network.requestWillBeSent', (event) => {
307
- const timestamp = new Date().toISOString();
308
- networkRequests.push({
309
- requestId: event.requestId,
310
- url: event.request.url,
311
- method: event.request.method,
312
- headers: event.request.headers,
313
- postData: event.request.postData,
314
- timestamp,
315
- type: event.type, // Document, Stylesheet, Image, Media, Font, Script, XHR, Fetch, etc.
316
- initiator: event.initiator.type, // parser, script, other
317
- status: 'pending',
318
- documentURL: event.documentURL
319
- });
320
- });
321
-
322
- client.on('Network.responseReceived', (event) => {
323
- const req = networkRequests.find(r => r.requestId === event.requestId);
324
- if (req) {
325
- req.status = event.response.status;
326
- req.statusText = event.response.statusText;
327
- req.responseHeaders = event.response.headers;
328
- req.mimeType = event.response.mimeType;
329
- req.fromCache = event.response.fromDiskCache || event.response.fromServiceWorker;
330
- req.timing = event.response.timing;
331
- }
332
- });
333
-
334
- client.on('Network.loadingFinished', (event) => {
335
- const req = networkRequests.find(r => r.requestId === event.requestId);
336
- if (req && req.status === 'pending') {
337
- req.status = 'completed';
338
- }
339
- if (req) {
340
- req.encodedDataLength = event.encodedDataLength;
341
- req.finishedTimestamp = new Date().toISOString();
342
- }
343
- });
344
-
345
- client.on('Network.loadingFailed', (event) => {
346
- const req = networkRequests.find(r => r.requestId === event.requestId);
347
- if (req) {
348
- req.status = 'failed';
349
- req.errorText = event.errorText;
350
- req.canceled = event.canceled;
351
- req.finishedTimestamp = new Date().toISOString();
352
- }
353
- });
354
-
355
- // Auto-reinitialize on navigation (CDP session is reset on navigation)
356
- let lastUrl = page.url();
357
-
358
- page.on('framenavigated', async (frame) => {
359
- // Only handle main frame navigation
360
- if (frame !== page.mainFrame()) return;
361
-
362
- const currentUrl = frame.url();
363
-
364
- // Skip if URL hasn't changed
365
- if (currentUrl === lastUrl) return;
366
- lastUrl = currentUrl;
367
-
368
- // Remove from tracking set to allow re-setup
369
- pagesWithNetworkMonitoring.delete(page);
370
-
371
- // Small delay to let navigation settle
372
- setTimeout(async () => {
373
- try {
374
- await setupNetworkMonitoring(page);
375
- } catch (error) {
376
- console.error('[chrometools-mcp] Failed to reinitialize network monitoring:', error.message);
377
- }
378
- }, 100);
379
- });
380
- }
381
-
382
- async function setupRecorderAutoReinjection(page) {
383
- let reinjectionTimeout = null;
384
- let lastUrl = null;
385
-
386
- // Handle navigation events (form submits, link clicks, history API)
387
- page.on('framenavigated', async (frame) => {
388
- // Only handle main frame navigation
389
- if (frame !== page.mainFrame()) return;
390
-
391
- // Get current URL
392
- const currentUrl = frame.url();
393
-
394
- // Skip if URL hasn't changed (prevents duplicate injections on same page)
395
- if (currentUrl === lastUrl) {
396
- return;
397
- }
398
- lastUrl = currentUrl;
399
-
400
- // Clear any pending reinjection
401
- if (reinjectionTimeout) {
402
- clearTimeout(reinjectionTimeout);
403
- }
404
-
405
- // Debounce reinjection (wait 100ms for navigation to settle)
406
- reinjectionTimeout = setTimeout(async () => {
407
- // Check if this page had recorder before
408
- if (pagesWithRecorder.has(page)) {
409
- try {
410
- await injectRecorder(page);
411
- } catch (error) {
412
- console.error('[chrometools-mcp] Failed to re-inject recorder:', error.message);
413
- }
414
- }
415
- }, 100);
416
- });
417
-
418
- // Handle page reloads (F5, Ctrl+R) - use 'load' event
419
- page.on('load', async () => {
420
- // Check if this page had recorder before
421
- if (pagesWithRecorder.has(page)) {
422
- try {
423
- await injectRecorder(page);
424
- } catch (error) {
425
- console.error('[chrometools-mcp] Failed to re-inject recorder after reload:', error.message);
426
- }
427
- }
428
- });
429
- }
430
-
431
- // Get or create page for URL
432
- async function getOrCreatePage(url) {
433
- const browser = await getBrowser();
434
-
435
- // Check if page for this URL already exists
436
- if (openPages.has(url)) {
437
- const existingPage = openPages.get(url);
438
- if (!existingPage.isClosed()) {
439
- lastPage = existingPage;
440
- return existingPage;
441
- }
442
- openPages.delete(url);
443
- }
444
-
445
- // Create new page
446
- const page = await browser.newPage();
447
-
448
- // Set up console log capture
449
- const client = await page.target().createCDPSession();
450
- await client.send('Runtime.enable');
451
- await client.send('Log.enable');
452
-
453
- client.on('Runtime.consoleAPICalled', (event) => {
454
- const timestamp = new Date().toISOString();
455
- const args = event.args.map(arg => {
456
- if (arg.value !== undefined) return arg.value;
457
- if (arg.description) return arg.description;
458
- return String(arg);
459
- });
460
-
461
- consoleLogs.push({
462
- type: event.type, // log, warn, error, info, debug
463
- timestamp,
464
- message: args.join(' '),
465
- stackTrace: event.stackTrace
466
- });
467
- });
468
-
469
- client.on('Log.entryAdded', (event) => {
470
- const entry = event.entry;
471
- consoleLogs.push({
472
- type: entry.level, // verbose, info, warning, error
473
- timestamp: new Date(entry.timestamp).toISOString(),
474
- message: entry.text,
475
- source: entry.source,
476
- url: entry.url,
477
- lineNumber: entry.lineNumber
478
- });
479
- });
480
-
481
- // Setup network monitoring with auto-reinitialization on navigation
482
- await setupNetworkMonitoring(page);
483
-
484
- // Setup recorder auto-reinjection on navigation
485
- setupRecorderAutoReinjection(page);
486
-
487
- await page.goto(url, { waitUntil: 'networkidle2' });
488
- openPages.set(url, page);
489
- lastPage = page;
490
-
491
- return page;
492
- }
493
-
494
- // Get last opened page (for tools that don't need URL)
495
- async function getLastOpenPage() {
496
- if (!lastPage || lastPage.isClosed()) {
497
- throw new Error('No page is currently open. Use openBrowser first to open a page.');
498
- }
499
-
500
- // Setup recorder auto-reinjection if not already set up
501
- // Check if page already has navigation listener
502
- const listenerCount = lastPage.listenerCount('framenavigated');
503
- if (listenerCount === 0) {
504
- setupRecorderAutoReinjection(lastPage);
505
- }
506
-
507
- return lastPage;
508
- }
509
-
510
- // Helper function to normalize Figma node ID (convert URL format to API format)
511
- // Figma helper functions moved to figma-tools.js
512
-
513
- // Helper function to process screenshot with compression and scaling
514
- async function processScreenshot(screenshotBuffer, options = {}) {
515
- const {
516
- maxWidth = 1024,
517
- maxHeight = 8000, // API limit is 8000px
518
- quality = 80,
519
- format = 'auto',
520
- maxFileSize = 3 * 1024 * 1024 // 3 MB limit
521
- } = options;
522
-
523
- // Load image with Jimp
524
- const image = await Jimp.read(screenshotBuffer);
525
- const originalWidth = image.bitmap.width;
526
- const originalHeight = image.bitmap.height;
527
- const originalSize = screenshotBuffer.length;
528
-
529
- let processed = false;
530
-
531
- // Apply scaling if needed to fit within maxWidth and maxHeight
532
- if (maxWidth !== null || maxHeight !== null) {
533
- let newWidth = originalWidth;
534
- let newHeight = originalHeight;
535
-
536
- // Calculate scale factors for both dimensions
537
- let scaleWidth = 1.0;
538
- let scaleHeight = 1.0;
539
-
540
- if (maxWidth !== null && originalWidth > maxWidth) {
541
- scaleWidth = maxWidth / originalWidth;
542
- }
543
-
544
- if (maxHeight !== null && originalHeight > maxHeight) {
545
- scaleHeight = maxHeight / originalHeight;
546
- }
547
-
548
- // Use the smaller scale factor to ensure both dimensions fit
549
- const scale = Math.min(scaleWidth, scaleHeight);
550
-
551
- if (scale < 1.0) {
552
- newWidth = Math.round(originalWidth * scale);
553
- newHeight = Math.round(originalHeight * scale);
554
- image.resize(newWidth, newHeight);
555
- processed = true;
556
- }
557
- }
558
-
559
- // Determine output format
560
- let outputFormat = format;
561
- let mimeType = 'image/png';
562
-
563
- if (format === 'auto') {
564
- // Auto-select: use JPEG for large images, PNG for small
565
- const estimatedSize = image.bitmap.width * image.bitmap.height * 4;
566
- outputFormat = estimatedSize > 500000 ? 'jpeg' : 'png'; // ~500KB threshold
567
- }
568
-
569
- // Convert to buffer with appropriate format and quality
570
- let currentQuality = quality;
571
- let resultBuffer;
572
- let compressionAttempts = 0;
573
- const maxCompressionAttempts = 10;
574
-
575
- if (outputFormat === 'jpeg') {
576
- image.quality(currentQuality);
577
- resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
578
- mimeType = 'image/jpeg';
579
- processed = true;
580
-
581
- // If file exceeds maxFileSize, reduce quality iteratively
582
- while (resultBuffer.length > maxFileSize && compressionAttempts < maxCompressionAttempts) {
583
- compressionAttempts++;
584
- // Reduce quality by 10 points each iteration
585
- currentQuality = Math.max(10, currentQuality - 10);
586
- image.quality(currentQuality);
587
- resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
588
-
589
- // If quality is already at minimum and still too large, scale down the image
590
- if (currentQuality === 10 && resultBuffer.length > maxFileSize) {
591
- const scaleFactor = Math.sqrt(maxFileSize / resultBuffer.length * 0.9); // 0.9 for safety margin
592
- const newWidth = Math.round(image.bitmap.width * scaleFactor);
593
- const newHeight = Math.round(image.bitmap.height * scaleFactor);
594
- image.resize(newWidth, newHeight);
595
- image.quality(currentQuality);
596
- resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
597
- processed = true;
598
- }
599
- }
600
- } else {
601
- resultBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
602
- mimeType = 'image/png';
603
-
604
- // If PNG exceeds maxFileSize, convert to JPEG and compress
605
- if (resultBuffer.length > maxFileSize) {
606
- outputFormat = 'jpeg';
607
- mimeType = 'image/jpeg';
608
- currentQuality = quality;
609
- image.quality(currentQuality);
610
- resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
611
- processed = true;
612
-
613
- // Reduce quality iteratively if still too large
614
- while (resultBuffer.length > maxFileSize && compressionAttempts < maxCompressionAttempts) {
615
- compressionAttempts++;
616
- currentQuality = Math.max(10, currentQuality - 10);
617
- image.quality(currentQuality);
618
- resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
619
-
620
- // If quality is already at minimum and still too large, scale down the image
621
- if (currentQuality === 10 && resultBuffer.length > maxFileSize) {
622
- const scaleFactor = Math.sqrt(maxFileSize / resultBuffer.length * 0.9);
623
- const newWidth = Math.round(image.bitmap.width * scaleFactor);
624
- const newHeight = Math.round(image.bitmap.height * scaleFactor);
625
- image.resize(newWidth, newHeight);
626
- image.quality(currentQuality);
627
- resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
628
- processed = true;
629
- }
630
- }
631
- }
632
- }
633
-
634
- // Return original if no processing was needed and format is PNG
635
- if (!processed && outputFormat === 'png' && resultBuffer.length <= maxFileSize) {
636
- return {
637
- buffer: screenshotBuffer,
638
- mimeType: 'image/png',
639
- metadata: {
640
- width: originalWidth,
641
- height: originalHeight,
642
- originalSize,
643
- finalSize: screenshotBuffer.length,
644
- format: 'png',
645
- compressed: false,
646
- scaled: false
647
- }
648
- };
649
- }
650
-
651
- return {
652
- buffer: resultBuffer,
653
- mimeType,
654
- metadata: {
655
- width: image.bitmap.width,
656
- height: image.bitmap.height,
657
- originalWidth,
658
- originalHeight,
659
- originalSize,
660
- finalSize: resultBuffer.length,
661
- format: outputFormat,
662
- compressed: outputFormat === 'jpeg' || compressionAttempts > 0,
663
- scaled: processed,
664
- compressionRatio: Math.round((1 - resultBuffer.length / originalSize) * 100),
665
- quality: outputFormat === 'jpeg' ? currentQuality : undefined,
666
- compressionAttempts: compressionAttempts > 0 ? compressionAttempts : undefined,
667
- autoCompressed: compressionAttempts > 0 || (outputFormat === 'jpeg' && format === 'png')
668
- }
669
- };
670
- }
671
-
672
- // Calculate SSIM (Structural Similarity Index) for image comparison
673
- function calculateSSIM(img1Data, img2Data, width, height) {
674
- if (img1Data.length !== img2Data.length) {
675
- return 0;
676
- }
677
-
678
- const windowSize = 8;
679
- const k1 = 0.01;
680
- const k2 = 0.03;
681
- const c1 = (k1 * 255) ** 2;
682
- const c2 = (k2 * 255) ** 2;
683
-
684
- let ssimSum = 0;
685
- let validWindows = 0;
686
-
687
- for (let y = 0; y <= height - windowSize; y += windowSize) {
688
- for (let x = 0; x <= width - windowSize; x += windowSize) {
689
- let sum1 = 0, sum2 = 0, sum1Sq = 0, sum2Sq = 0, sum12 = 0;
690
-
691
- for (let dy = 0; dy < windowSize; dy++) {
692
- for (let dx = 0; dx < windowSize; dx++) {
693
- const idx = ((y + dy) * width + (x + dx)) * 4;
694
- if (idx + 2 >= img1Data.length) continue;
695
-
696
- const gray1 = (img1Data[idx] * 0.299 + img1Data[idx + 1] * 0.587 + img1Data[idx + 2] * 0.114);
697
- const gray2 = (img2Data[idx] * 0.299 + img2Data[idx + 1] * 0.587 + img2Data[idx + 2] * 0.114);
698
-
699
- sum1 += gray1;
700
- sum2 += gray2;
701
- sum1Sq += gray1 * gray1;
702
- sum2Sq += gray2 * gray2;
703
- sum12 += gray1 * gray2;
704
- }
705
- }
706
-
707
- const n = windowSize * windowSize;
708
- const mean1 = sum1 / n;
709
- const mean2 = sum2 / n;
710
- const variance1 = (sum1Sq / n) - (mean1 * mean1);
711
- const variance2 = (sum2Sq / n) - (mean2 * mean2);
712
- const covariance = (sum12 / n) - (mean1 * mean2);
713
-
714
- const ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
715
- ((mean1 * mean1 + mean2 * mean2 + c1) * (variance1 + variance2 + c2));
716
-
717
- ssimSum += ssim;
718
- validWindows++;
719
- }
720
- }
721
-
722
- return validWindows > 0 ? ssimSum / validWindows : 0;
723
- }
724
-
725
- // Cleanup on exit
726
- process.on("SIGINT", async () => {
727
- if (browserPromise) {
728
- const browser = await browserPromise;
729
- await browser.close();
730
- }
731
- process.exit(0);
732
- });
733
-
734
- // Create MCP server
735
- const server = new Server(
736
- {
737
- name: "chrometools-mcp",
738
- version: "1.0.2",
739
- },
740
- {
741
- capabilities: {
742
- tools: {},
743
- },
744
- }
745
- );
746
-
747
- // CSS property categorization
748
- const CSS_CATEGORIES = {
749
- layout: [
750
- 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
751
- 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
752
- 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
753
- 'position', 'top', 'right', 'bottom', 'left', 'z-index',
754
- 'display', 'float', 'clear', 'overflow', 'overflow-x', 'overflow-y',
755
- 'flex', 'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',
756
- 'justify-content', 'align-items', 'align-content', 'align-self', 'order',
757
- 'grid', 'grid-template', 'grid-template-columns', 'grid-template-rows', 'grid-gap',
758
- 'gap', 'row-gap', 'column-gap',
759
- 'box-sizing', 'visibility', 'clip', 'clip-path'
760
- ],
761
- typography: [
762
- 'font', 'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
763
- 'line-height', 'letter-spacing', 'word-spacing', 'text-align', 'text-decoration',
764
- 'text-transform', 'text-indent', 'text-overflow', 'white-space', 'word-break',
765
- 'word-wrap', 'overflow-wrap', 'hyphens', 'direction', 'unicode-bidi',
766
- 'writing-mode', 'vertical-align'
767
- ],
768
- colors: [
769
- 'color', 'background', 'background-color', 'background-image', 'background-position',
770
- 'background-size', 'background-repeat', 'background-attachment', 'background-clip',
771
- 'background-origin', 'background-blend-mode',
772
- 'border-color', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
773
- 'outline-color', 'text-decoration-color', 'caret-color', 'column-rule-color'
774
- ],
775
- visual: [
776
- 'opacity', 'transform', 'transform-origin', 'transform-style', 'perspective',
777
- 'perspective-origin', 'backface-visibility',
778
- 'transition', 'transition-property', 'transition-duration', 'transition-timing-function', 'transition-delay',
779
- 'animation', 'animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay',
780
- 'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state',
781
- 'filter', 'backdrop-filter', 'mix-blend-mode', 'isolation',
782
- 'box-shadow', 'text-shadow',
783
- 'border', 'border-width', 'border-style', 'border-radius',
784
- 'border-top', 'border-right', 'border-bottom', 'border-left',
785
- 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
786
- 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
787
- 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius',
788
- 'outline', 'outline-width', 'outline-style', 'outline-offset',
789
- 'cursor', 'pointer-events', 'user-select'
790
- ]
791
- };
792
-
793
- // Common default CSS values to filter out
794
- const CSS_DEFAULTS = {
795
- 'display': 'inline',
796
- 'position': 'static',
797
- 'float': 'none',
798
- 'clear': 'none',
799
- 'visibility': 'visible',
800
- 'overflow': 'visible',
801
- 'overflow-x': 'visible',
802
- 'overflow-y': 'visible',
803
- 'z-index': 'auto',
804
- 'opacity': '1',
805
- 'transform': 'none',
806
- 'filter': 'none',
807
- 'backdrop-filter': 'none',
808
- 'box-shadow': 'none',
809
- 'text-shadow': 'none',
810
- 'border-style': 'none',
811
- 'border-width': '0px',
812
- 'outline-style': 'none',
813
- 'outline-width': '0px',
814
- 'margin': '0px',
815
- 'margin-top': '0px',
816
- 'margin-right': '0px',
817
- 'margin-bottom': '0px',
818
- 'margin-left': '0px',
819
- 'padding': '0px',
820
- 'padding-top': '0px',
821
- 'padding-right': '0px',
822
- 'padding-bottom': '0px',
823
- 'padding-left': '0px',
824
- 'background-image': 'none',
825
- 'transition': 'all 0s ease 0s',
826
- 'animation': 'none',
827
- 'pointer-events': 'auto',
828
- 'user-select': 'auto',
829
- 'cursor': 'auto',
830
- 'text-decoration': 'none',
831
- 'text-transform': 'none',
832
- 'font-weight': '400',
833
- 'font-style': 'normal',
834
- 'font-variant': 'normal',
835
- 'letter-spacing': 'normal',
836
- 'word-spacing': 'normal',
837
- 'text-align': 'start',
838
- 'white-space': 'normal',
839
- 'word-break': 'normal',
840
- 'overflow-wrap': 'normal',
841
- 'hyphens': 'manual'
842
- };
843
-
844
- // Filter computed CSS styles based on options
845
- function filterCssStyles(computedStyle, options = {}) {
846
- const { category, properties, includeDefaults = false } = options;
847
-
848
- let filtered = computedStyle;
849
-
850
- // Filter by specific properties (highest priority)
851
- if (properties && properties.length > 0) {
852
- filtered = filtered.filter(prop =>
853
- properties.some(p => prop.name.toLowerCase() === p.toLowerCase())
854
- );
855
- }
856
- // Filter by category
857
- else if (category && category !== 'all') {
858
- const categoryProps = CSS_CATEGORIES[category] || [];
859
- filtered = filtered.filter(prop =>
860
- categoryProps.some(p => prop.name.toLowerCase().startsWith(p.toLowerCase()))
861
- );
862
- }
863
-
864
- // Filter out default values if requested
865
- if (!includeDefaults) {
866
- filtered = filtered.filter(prop => {
867
- const defaultValue = CSS_DEFAULTS[prop.name];
868
- if (!defaultValue) return true;
869
-
870
- // Normalize values for comparison
871
- const normalizedValue = prop.value.replace(/\s+/g, ' ').trim();
872
- const normalizedDefault = defaultValue.replace(/\s+/g, ' ').trim();
873
-
874
- return normalizedValue !== normalizedDefault;
875
- });
876
- }
877
-
878
- return filtered;
879
- }
880
-
881
- // Tool schemas
882
- const PingSchema = z.object({
883
- message: z.string().optional().describe("Optional message to send"),
884
- });
885
-
886
- const OpenBrowserSchema = z.object({
887
- url: z.string().describe("URL to open in the browser"),
888
- });
889
-
890
- const ClickSchema = z.object({
891
- selector: z.string().describe("CSS selector for element to click"),
892
- waitAfter: z.number().optional().describe("Milliseconds to wait after click (default: 1500)"),
893
- screenshot: z.boolean().optional().describe("Capture screenshot after click (default: false for performance)"),
894
- timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
895
- });
896
-
897
- const TypeSchema = z.object({
898
- selector: z.string().describe("CSS selector for input element"),
899
- text: z.string().describe("Text to type"),
900
- delay: z.number().optional().describe("Delay between keystrokes in ms (default: 0)"),
901
- clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
902
- });
903
-
904
- const GetElementSchema = z.object({
905
- selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
906
- });
907
-
908
- const GetComputedCssSchema = z.object({
909
- selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
910
- category: z.enum(['all', 'layout', 'typography', 'colors', 'visual']).optional().describe("Filter by CSS category: 'layout' (sizing, positioning), 'typography' (fonts, text), 'colors' (color schemes), 'visual' (effects, transforms), 'all' (default)"),
911
- properties: z.array(z.string()).optional().describe("Specific CSS properties to return (e.g., ['color', 'font-size']). Overrides category filter."),
912
- includeDefaults: z.boolean().optional().describe("Include properties with default values (default: false)"),
913
- });
914
-
915
- const GetBoxModelSchema = z.object({
916
- selector: z.string().describe("CSS selector for element"),
917
- });
918
-
919
- const ScreenshotSchema = z.object({
920
- selector: z.string().describe("CSS selector for element to screenshot"),
921
- padding: z.number().optional().describe("Padding around element in pixels (default: 0)"),
922
- maxWidth: z.number().nullable().optional().describe("Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)"),
923
- maxHeight: z.number().nullable().optional().describe("Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)"),
924
- quality: z.number().min(1).max(100).optional().describe("JPEG quality 1-100 (default: 80, only applies to JPEG format)"),
925
- format: z.enum(['png', 'jpeg', 'auto']).optional().describe("Image format: 'png', 'jpeg', or 'auto' (default: 'auto' - chooses based on size)"),
926
- });
927
-
928
- const SaveScreenshotSchema = z.object({
929
- selector: z.string().describe("CSS selector for element to screenshot"),
930
- filePath: z.string().describe("Absolute path where to save file"),
931
- padding: z.number().optional().describe("Padding around element in pixels (default: 0)"),
932
- maxWidth: z.number().nullable().optional().describe("Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)"),
933
- maxHeight: z.number().nullable().optional().describe("Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)"),
934
- quality: z.number().min(1).max(100).optional().describe("JPEG quality 1-100 (default: 80, only applies to JPEG format)"),
935
- format: z.enum(['png', 'jpeg', 'auto']).optional().describe("Image format: 'png', 'jpeg', or 'auto' (default: 'auto' - chooses based on size)"),
936
- });
937
-
938
- const ScrollToSchema = z.object({
939
- selector: z.string().describe("CSS selector for element to scroll to"),
940
- behavior: z.enum(['auto', 'smooth']).optional().describe("Scroll behavior (default: auto)"),
941
- });
942
-
943
- const WaitForElementSchema = z.object({
944
- selector: z.string().describe("CSS selector to wait for"),
945
- timeout: z.number().optional().describe("Maximum time to wait in milliseconds (default: 5000)"),
946
- visible: z.boolean().optional().describe("Wait for element to be visible (default: true)"),
947
- });
948
-
949
- const ExecuteScriptSchema = z.object({
950
- script: z.string().describe("JavaScript code to execute in page context"),
951
- waitAfter: z.number().optional().describe("Milliseconds to wait after execution (default: 500)"),
952
- screenshot: z.boolean().optional().describe("Capture screenshot after execution (default: false for performance)"),
953
- timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
954
- });
955
-
956
- // Phase 2 schemas
957
- const GetConsoleLogsSchema = z.object({
958
- types: z.array(z.enum(['log', 'warn', 'error', 'info', 'debug', 'verbose', 'warning']))
959
- .optional()
960
- .describe("Filter by log types (default: all)"),
961
- clear: z.boolean().optional().describe("Clear logs after reading (default: false)"),
962
- });
963
-
964
- // Network tools schemas
965
- const ListNetworkRequestsSchema = z.object({
966
- types: z.array(z.enum(['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Script', 'XHR', 'Fetch', 'WebSocket', 'Other']))
967
- .optional()
968
- .default(['Fetch', 'XHR'])
969
- .describe("Filter by request types (default: Fetch, XHR)"),
970
- status: z.enum(['pending', 'completed', 'failed', 'all'])
971
- .optional()
972
- .describe("Filter by status (default: all)"),
973
- clear: z.boolean().optional().describe("Clear requests after reading (default: false)"),
974
- });
975
-
976
- const GetNetworkRequestSchema = z.object({
977
- requestId: z.string().describe("Request ID to get details for"),
978
- });
979
-
980
- const FilterNetworkRequestsSchema = z.object({
981
- urlPattern: z.string().describe("URL pattern to filter by (regex or partial match)"),
982
- types: z.array(z.enum(['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Script', 'XHR', 'Fetch', 'WebSocket', 'Other']))
983
- .optional()
984
- .default(['Fetch', 'XHR'])
985
- .describe("Filter by request types (default: Fetch, XHR)"),
986
- clear: z.boolean().optional().describe("Clear requests after reading (default: false)"),
987
- });
988
-
989
- const HoverSchema = z.object({
990
- selector: z.string().describe("CSS selector for element to hover"),
991
- });
992
-
993
- const SetStylesSchema = z.object({
994
- selector: z.string().describe("CSS selector for element to modify"),
995
- styles: z.array(z.object({
996
- name: z.string().describe("CSS property name (e.g., 'color')"),
997
- value: z.string().describe("CSS property value (e.g., 'red')")
998
- })).describe("Array of CSS property name-value pairs"),
999
- });
1000
-
1001
- const SetViewportSchema = z.object({
1002
- width: z.number().min(320).max(4000).describe("Viewport width in pixels (320-4000)"),
1003
- height: z.number().min(200).max(3000).describe("Viewport height in pixels (200-3000)"),
1004
- deviceScaleFactor: z.number().min(0.5).max(3).optional().describe("Device pixel ratio (0.5-3, default: 1)"),
1005
- });
1006
-
1007
- const GetViewportSchema = z.object({});
1008
-
1009
- const NavigateToSchema = z.object({
1010
- url: z.string().describe("URL to navigate to"),
1011
- waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2'])
1012
- .optional()
1013
- .describe("Wait until event (default: networkidle2)"),
1014
- });
1015
-
1016
- // Angular tools schemas
1017
- const ListAngularComponentsSchema = z.object({
1018
- includeHidden: z.boolean().optional().describe("Include components in hidden tabs/panels (default: false)"),
1019
- });
1020
-
1021
- const GetAngularComponentSchema = z.object({
1022
- selector: z.string().describe("CSS selector for the Angular component element"),
1023
- includePrivate: z.boolean().optional().describe("Include private methods/properties (default: false)"),
1024
- });
1025
-
1026
- const CallAngularMethodSchema = z.object({
1027
- selector: z.string().describe("CSS selector for the Angular component element"),
1028
- method: z.string().describe("Method name to call"),
1029
- args: z.array(z.any()).optional().describe("Method arguments (default: [])"),
1030
- });
1031
-
1032
- const GetAngularFormSchema = z.object({
1033
- selector: z.string().describe("CSS selector for the component containing the form"),
1034
- formProperty: z.string().optional().describe("Form property name (auto-detects if omitted)"),
1035
- });
1036
-
1037
- const SubmitAngularFormSchema = z.object({
1038
- selector: z.string().describe("CSS selector for the component containing the form"),
1039
- formProperty: z.string().optional().describe("Form property name (auto-detects if omitted)"),
1040
- waitForResponse: z.boolean().optional().describe("Wait for network request after submit (default: true)"),
1041
- });
1042
-
1043
- // Figma tools schemas
1044
- const GetFigmaFrameSchema = z.object({
1045
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1046
- fileKey: z.string().describe("Figma file key (from URL: figma.com/file/FILE_KEY/...)"),
1047
- nodeId: z.string().describe("Figma node ID (frame/component ID)"),
1048
- scale: z.number().min(0.1).max(4).optional().describe("Export scale (0.1-4, default: 2)"),
1049
- format: z.enum(['png', 'jpg', 'svg']).optional().describe("Export format (default: png)")
1050
- });
1051
-
1052
- const CompareFigmaToElementSchema = z.object({
1053
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1054
- fileKey: z.string().describe("Figma file key"),
1055
- nodeId: z.string().describe("Figma frame/component ID"),
1056
- selector: z.string().describe("CSS selector for page element"),
1057
- threshold: z.number().min(0).max(1).optional().describe("Difference threshold (0-1, default: 0.05)"),
1058
- figmaScale: z.number().min(0.1).max(4).optional().describe("Figma export scale (default: 2)")
1059
- });
1060
-
1061
- const GetFigmaSpecsSchema = z.object({
1062
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1063
- fileKey: z.string().describe("Figma file key"),
1064
- nodeId: z.string().describe("Figma frame/component ID")
1065
- });
1066
-
1067
- const ParseFigmaUrlSchema = z.object({
1068
- url: z.string().describe("Full Figma URL or fileKey")
1069
- });
1070
-
1071
- const ListFigmaPagesSchema = z.object({
1072
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1073
- fileKey: z.string().describe("Figma file key or full Figma URL")
1074
- });
1075
-
1076
- const SearchFigmaFramesSchema = z.object({
1077
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1078
- fileKey: z.string().describe("Figma file key or full Figma URL"),
1079
- searchQuery: z.string().describe("Search query")
1080
- });
1081
-
1082
- const GetFigmaComponentsSchema = z.object({
1083
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1084
- fileKey: z.string().describe("Figma file key or full Figma URL")
1085
- });
1086
-
1087
- const GetFigmaStylesSchema = z.object({
1088
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1089
- fileKey: z.string().describe("Figma file key or full Figma URL")
1090
- });
1091
-
1092
- const GetFigmaColorPaletteSchema = z.object({
1093
- figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
1094
- fileKey: z.string().describe("Figma file key or full Figma URL")
1095
- });
1096
-
1097
- // New AI optimization tools schemas
1098
- const SmartFindElementSchema = z.object({
1099
- description: z.string().describe("Natural language description of element to find (e.g., 'login button', 'email field')"),
1100
- maxResults: z.number().min(1).max(20).optional().describe("Maximum number of candidates to return (default: 5)"),
1101
- action: z.object({
1102
- type: z.enum(['click', 'type', 'scrollTo', 'screenshot', 'hover', 'setStyles']).describe("Action to perform on the best match"),
1103
- text: z.string().optional().describe("Text to type (required for 'type' action)"),
1104
- styles: z.array(z.object({
1105
- name: z.string(),
1106
- value: z.string()
1107
- })).optional().describe("Styles to apply (required for 'setStyles' action)"),
1108
- screenshot: z.boolean().optional().describe("Capture screenshot after action (default: false)"),
1109
- waitAfter: z.number().optional().describe("Wait time in ms after action"),
1110
- }).optional().describe("Optional action to perform on the best matching element"),
1111
- });
1112
-
1113
- const AnalyzePageSchema = z.object({
1114
- refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
1115
- });
1116
-
1117
- const GetAllInteractiveElementsSchema = z.object({
1118
- includeHidden: z.boolean().optional().describe("Include hidden elements (default: false)"),
1119
- });
1120
-
1121
- const FindElementsByTextSchema = z.object({
1122
- text: z.string().describe("Text to search for in elements"),
1123
- exact: z.boolean().optional().describe("Exact match only (default: false)"),
1124
- caseSensitive: z.boolean().optional().describe("Case sensitive search (default: false)"),
1125
- action: z.object({
1126
- type: z.enum(['click', 'type', 'scrollTo', 'screenshot', 'hover', 'setStyles']).describe("Action to perform on the first match"),
1127
- text: z.string().optional().describe("Text to type (required for 'type' action)"),
1128
- styles: z.array(z.object({
1129
- name: z.string(),
1130
- value: z.string()
1131
- })).optional().describe("Styles to apply (required for 'setStyles' action)"),
1132
- screenshot: z.boolean().optional().describe("Capture screenshot after action (default: false)"),
1133
- waitAfter: z.number().optional().describe("Wait time in ms after action"),
1134
- }).optional().describe("Optional action to perform on the first matching element"),
1135
- });
1136
-
1137
- // List available tools
1138
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1139
- return {
1140
- tools: [
1141
- {
1142
- name: "ping",
1143
- description: "Simple ping-pong tool for testing. Returns 'pong' with optional message.",
1144
- inputSchema: {
1145
- type: "object",
1146
- properties: {
1147
- message: { type: "string", description: "Optional message to include in response" },
1148
- },
1149
- },
1150
- },
1151
- {
1152
- name: "openBrowser",
1153
- description: "Opens a browser window and navigates to the specified URL. Browser window remains open for further interactions. Use this as the first step before other tools.",
1154
- inputSchema: {
1155
- type: "object",
1156
- properties: {
1157
- url: { type: "string", description: "URL to navigate to (e.g., https://example.com)" },
1158
- },
1159
- required: ["url"],
1160
- },
1161
- },
1162
- {
1163
- name: "click",
1164
- description: "Click on an element to trigger interactions like opening modals, navigating, or submitting forms. Waits for animations. Screenshot is optional for better performance.",
1165
- inputSchema: {
1166
- type: "object",
1167
- properties: {
1168
- selector: { type: "string", description: "CSS selector for element to click" },
1169
- waitAfter: { type: "number", description: "Milliseconds to wait after click (default: 1500)" },
1170
- screenshot: { type: "boolean", description: "Capture screenshot after click (default: false for performance)" },
1171
- timeout: { type: "number", description: "Maximum time to wait for operation in ms (default: 30000)" },
1172
- },
1173
- required: ["selector"],
1174
- },
1175
- },
1176
- {
1177
- name: "type",
1178
- description: "Type text into an input field, textarea, or contenteditable element. Can optionally clear the field first and control typing speed for realistic input simulation.",
1179
- inputSchema: {
1180
- type: "object",
1181
- properties: {
1182
- selector: { type: "string", description: "CSS selector for input element" },
1183
- text: { type: "string", description: "Text to type" },
1184
- delay: { type: "number", description: "Delay between keystrokes in ms (default: 0)" },
1185
- clearFirst: { type: "boolean", description: "Clear field before typing (default: true)" },
1186
- },
1187
- required: ["selector", "text"],
1188
- },
1189
- },
1190
- {
1191
- name: "getElement",
1192
- description: "Get HTML markup of specific element. TIP: Use analyzePage instead to get structured data of ALL page elements at once - much more efficient than inspecting elements one by one.",
1193
- inputSchema: {
1194
- type: "object",
1195
- properties: {
1196
- selector: { type: "string", description: "CSS selector (optional, defaults to body)" },
1197
- },
1198
- },
1199
- },
1200
- {
1201
- name: "getComputedCss",
1202
- description: "Get all computed CSS styles applied to an element. Essential for debugging layout issues, checking responsive design, and verifying CSS properties. Returns complete computed styles.",
1203
- inputSchema: {
1204
- type: "object",
1205
- properties: {
1206
- selector: { type: "string", description: "CSS selector (optional, defaults to body)" },
1207
- category: {
1208
- type: "string",
1209
- enum: ["all", "layout", "typography", "colors", "visual"],
1210
- description: "Filter by CSS category: 'layout' (sizing, positioning), 'typography' (fonts, text), 'colors' (color schemes), 'visual' (effects, transforms), 'all' (default)"
1211
- },
1212
- properties: {
1213
- type: "array",
1214
- items: { type: "string" },
1215
- description: "Specific CSS properties to return (e.g., ['color', 'font-size']). Overrides category filter."
1216
- },
1217
- includeDefaults: {
1218
- type: "boolean",
1219
- description: "Include properties with default values (default: false)"
1220
- },
1221
- },
1222
- },
1223
- },
1224
- {
1225
- name: "getBoxModel",
1226
- description: "Get precise element dimensions, positioning, margins, padding, and borders. Returns complete box model data including content, padding, border, and margin dimensions.",
1227
- inputSchema: {
1228
- type: "object",
1229
- properties: {
1230
- selector: { type: "string", description: "CSS selector for element" },
1231
- },
1232
- required: ["selector"],
1233
- },
1234
- },
1235
- {
1236
- name: "screenshot",
1237
- description: "Capture visual image of element (uses 15-25k tokens). USE FOR: visual comparison with design, documentation, checking layout/colors. DON'T USE FOR debugging form data (use analyzePage to see actual values), after button clicks (use analyzePage with refresh:true to see what changed), validation errors (use analyzePage or getAngularForm). Screenshot shows visual appearance - for debugging DATA use analyzePage.",
1238
- inputSchema: {
1239
- type: "object",
1240
- properties: {
1241
- selector: { type: "string", description: "CSS selector for element to screenshot" },
1242
- padding: { type: "number", description: "Padding around element in pixels (default: 0)" },
1243
- maxWidth: { type: "number", description: "Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)" },
1244
- maxHeight: { type: "number", description: "Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)" },
1245
- quality: { type: "number", minimum: 1, maximum: 100, description: "JPEG quality 1-100 (default: 80, only applies to JPEG format)" },
1246
- format: { type: "string", enum: ["png", "jpeg", "auto"], description: "Image format: 'png', 'jpeg', or 'auto' (default: 'auto' - chooses based on size)" },
1247
- },
1248
- required: ["selector"],
1249
- },
1250
- },
1251
- {
1252
- name: "saveScreenshot",
1253
- description: "Save optimized screenshot directly to filesystem without returning in context. By default, auto-scales to 1024px width and 8000px height (API limit) and uses smart compression. Perfect for baseline screenshots and reducing file sizes. Use maxWidth: null and format: 'png' for original quality.",
1254
- inputSchema: {
1255
- type: "object",
1256
- properties: {
1257
- selector: { type: "string", description: "CSS selector for element to screenshot" },
1258
- filePath: { type: "string", description: "Absolute path where to save file (extension auto-adjusted based on format)" },
1259
- padding: { type: "number", description: "Padding around element in pixels (default: 0)" },
1260
- maxWidth: { type: "number", description: "Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)" },
1261
- maxHeight: { type: "number", description: "Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)" },
1262
- quality: { type: "number", minimum: 1, maximum: 100, description: "JPEG quality 1-100 (default: 80, only applies to JPEG format)" },
1263
- format: { type: "string", enum: ["png", "jpeg", "auto"], description: "Image format: 'png', 'jpeg', or 'auto' (default: 'auto' - chooses based on size)" },
1264
- },
1265
- required: ["selector", "filePath"],
1266
- },
1267
- },
1268
- {
1269
- name: "scrollTo",
1270
- description: "Scroll the page to bring an element into view. Useful for testing lazy loading, sticky elements, and ensuring elements are visible. Supports smooth or instant scrolling.",
1271
- inputSchema: {
1272
- type: "object",
1273
- properties: {
1274
- selector: { type: "string", description: "CSS selector for element to scroll to" },
1275
- behavior: { type: "string", enum: ["auto", "smooth"], description: "Scroll behavior (default: auto)" },
1276
- },
1277
- required: ["selector"],
1278
- },
1279
- },
1280
- {
1281
- name: "waitForElement",
1282
- description: "Wait for an element to appear on the page. Essential for dynamic content, autocomplete dropdowns, lazy-loaded elements. Use this instead of executeScript with setTimeout when waiting for elements to load.",
1283
- inputSchema: {
1284
- type: "object",
1285
- properties: {
1286
- selector: { type: "string", description: "CSS selector to wait for" },
1287
- timeout: { type: "number", description: "Maximum time to wait in milliseconds (default: 5000)" },
1288
- visible: { type: "boolean", description: "Wait for element to be visible (default: true)" },
1289
- },
1290
- required: ["selector"],
1291
- },
1292
- },
1293
- {
1294
- name: "executeScript",
1295
- description: "Execute JavaScript code. USE ONLY when specialized tools insufficient. DO NOT use for: Angular components (use getAngularComponent), Angular forms (use getAngularForm), calling Angular methods (use callAngularMethod), finding elements (use analyzePage or findElementsByText). Before using, check if Angular tools (listAngularComponents, getAngularComponent, getAngularForm, callAngularMethod) can solve your task. Last resort only.",
1296
- inputSchema: {
1297
- type: "object",
1298
- properties: {
1299
- script: { type: "string", description: "JavaScript code to execute" },
1300
- waitAfter: { type: "number", description: "Milliseconds to wait after execution (default: 500)" },
1301
- screenshot: { type: "boolean", description: "Capture screenshot after execution (default: false for performance)" },
1302
- timeout: { type: "number", description: "Maximum time to wait for operation in ms (default: 30000)" },
1303
- },
1304
- required: ["script"],
1305
- },
1306
- },
1307
- {
1308
- name: "getConsoleLogs",
1309
- description: "Retrieve all console.log, console.warn, console.error messages from the browser. Essential for debugging JavaScript errors and tracking application behavior. Logs are captured automatically from page load.",
1310
- inputSchema: {
1311
- type: "object",
1312
- properties: {
1313
- types: { type: "array", items: { type: "string", enum: ["log", "warn", "error", "info", "debug", "verbose", "warning"] }, description: "Filter by log types (default: all)" },
1314
- clear: { type: "boolean", description: "Clear logs after reading (default: false)" },
1315
- },
1316
- },
1317
- },
1318
- {
1319
- name: "listNetworkRequests",
1320
- description: "Get compact list of network requests with method, URL, and status only. Perfect for overview of API calls. Use getNetworkRequest for full details of specific request. Requests captured automatically from page load.",
1321
- inputSchema: {
1322
- type: "object",
1323
- properties: {
1324
- types: { type: "array", items: { type: "string", enum: ["Document", "Stylesheet", "Image", "Media", "Font", "Script", "XHR", "Fetch", "WebSocket", "Other"] }, description: "Filter by request types (default: Fetch, XHR)" },
1325
- status: { type: "string", enum: ["pending", "completed", "failed", "all"], description: "Filter by status (default: all)" },
1326
- clear: { type: "boolean", description: "Clear requests after reading (default: false)" },
1327
- },
1328
- },
1329
- },
1330
- {
1331
- name: "getNetworkRequest",
1332
- description: "Get full details of a specific network request including headers, payload, and response. Use requestId from listNetworkRequests.",
1333
- inputSchema: {
1334
- type: "object",
1335
- properties: {
1336
- requestId: { type: "string", description: "Request ID to get details for" },
1337
- },
1338
- required: ["requestId"],
1339
- },
1340
- },
1341
- {
1342
- name: "filterNetworkRequests",
1343
- description: "Filter network requests by URL pattern and get full details. Returns all matching requests with headers, payloads, and responses.",
1344
- inputSchema: {
1345
- type: "object",
1346
- properties: {
1347
- urlPattern: { type: "string", description: "URL pattern to filter by (regex or partial match)" },
1348
- types: { type: "array", items: { type: "string", enum: ["Document", "Stylesheet", "Image", "Media", "Font", "Script", "XHR", "Fetch", "WebSocket", "Other"] }, description: "Filter by request types (default: Fetch, XHR)" },
1349
- clear: { type: "boolean", description: "Clear requests after reading (default: false)" },
1350
- },
1351
- required: ["urlPattern"],
1352
- },
1353
- },
1354
- {
1355
- name: "hover",
1356
- description: "Simulate mouse hover over an element to test hover effects, tooltips, dropdown menus, and interactive states. Essential for testing CSS :hover pseudo-classes.",
1357
- inputSchema: {
1358
- type: "object",
1359
- properties: {
1360
- selector: { type: "string", description: "CSS selector for element to hover" },
1361
- },
1362
- required: ["selector"],
1363
- },
1364
- },
1365
- {
1366
- name: "setStyles",
1367
- description: "Apply inline CSS styles to an element for live editing and prototyping. Perfect for testing design changes without modifying source code.",
1368
- inputSchema: {
1369
- type: "object",
1370
- properties: {
1371
- selector: { type: "string", description: "CSS selector for element to modify" },
1372
- styles: {
1373
- type: "array",
1374
- items: {
1375
- type: "object",
1376
- properties: {
1377
- name: { type: "string", description: "CSS property name" },
1378
- value: { type: "string", description: "CSS property value" },
1379
- },
1380
- required: ["name", "value"],
1381
- },
1382
- description: "Array of CSS property name-value pairs",
1383
- },
1384
- },
1385
- required: ["selector", "styles"],
1386
- },
1387
- },
1388
- {
1389
- name: "setViewport",
1390
- description: "Change viewport dimensions for responsive design testing. Test how your layout adapts to different screen sizes, mobile devices, tablets, and desktop resolutions.",
1391
- inputSchema: {
1392
- type: "object",
1393
- properties: {
1394
- width: { type: "number", minimum: 320, maximum: 4000, description: "Viewport width in pixels" },
1395
- height: { type: "number", minimum: 200, maximum: 3000, description: "Viewport height in pixels" },
1396
- deviceScaleFactor: { type: "number", minimum: 0.5, maximum: 3, description: "Device pixel ratio (default: 1)" },
1397
- },
1398
- required: ["width", "height"],
1399
- },
1400
- },
1401
- {
1402
- name: "getViewport",
1403
- description: "Get current viewport size and device pixel ratio. Essential for responsive design testing and understanding how content fits on different screen sizes.",
1404
- inputSchema: {
1405
- type: "object",
1406
- properties: {},
1407
- },
1408
- },
1409
- {
1410
- name: "navigateTo",
1411
- description: "Navigate the current page to a new URL. Use this when you need to move to a different page while keeping the same browser instance. Page will be reused if already open.",
1412
- inputSchema: {
1413
- type: "object",
1414
- properties: {
1415
- url: { type: "string", description: "URL to navigate to" },
1416
- waitUntil: { type: "string", enum: ["load", "domcontentloaded", "networkidle0", "networkidle2"], description: "Wait until event (default: networkidle2)" },
1417
- },
1418
- required: ["url"],
1419
- },
1420
- },
1421
- {
1422
- name: "listAngularComponents",
1423
- description: "List all Angular components on the page with their selectors, methods, and properties. Use this FIRST to discover what components are available before calling methods. Much better than blind executeScript attempts.",
1424
- inputSchema: {
1425
- type: "object",
1426
- properties: {
1427
- includeHidden: { type: "boolean", description: "Include components in hidden tabs/panels (default: false)" },
1428
- },
1429
- },
1430
- },
1431
- {
1432
- name: "getAngularComponent",
1433
- description: "PREFERRED tool for inspecting Angular components (instead of executeScript with ng.getComponent). Get all methods, properties, and current state. Automatically extracts public methods and properties with type information. Use this BEFORE executeScript when working with Angular. Example: Instead of executeScript('ng.getComponent(el).someProperty'), use this tool.",
1434
- inputSchema: {
1435
- type: "object",
1436
- properties: {
1437
- selector: { type: "string", description: "CSS selector for the Angular component element" },
1438
- includePrivate: { type: "boolean", description: "Include private methods/properties (default: false)" },
1439
- },
1440
- required: ["selector"],
1441
- },
1442
- },
1443
- {
1444
- name: "callAngularMethod",
1445
- description: "Call a public method on an Angular component. Much more reliable than executeScript. Automatically handles change detection and error reporting.",
1446
- inputSchema: {
1447
- type: "object",
1448
- properties: {
1449
- selector: { type: "string", description: "CSS selector for the Angular component element" },
1450
- method: { type: "string", description: "Method name to call" },
1451
- args: { type: "array", description: "Method arguments (default: [])" },
1452
- },
1453
- required: ["selector", "method"],
1454
- },
1455
- },
1456
- {
1457
- name: "getAngularForm",
1458
- description: "PREFERRED tool for debugging Angular forms (instead of executeScript). Get form data, validation state, errors. Shows BOTH .value and .getRawValue() (includes disabled controls). Works with ReactiveFormsModule and Template-driven. Auto-detects form. Use when: debugging form submission, checking validation, inspecting disabled controls. Example: When form.value seems wrong, this shows both value and rawValue.",
1459
- inputSchema: {
1460
- type: "object",
1461
- properties: {
1462
- selector: { type: "string", description: "CSS selector for the component containing the form" },
1463
- formProperty: { type: "string", description: "Form property name (auto-detects if omitted)" },
1464
- },
1465
- required: ["selector"],
1466
- },
1467
- },
1468
- {
1469
- name: "submitAngularForm",
1470
- description: "Submit Angular form programmatically. Tries multiple strategies automatically: component submit method, form.submit(), native submit event, clicking submit button. Most reliable way to submit Angular forms.",
1471
- inputSchema: {
1472
- type: "object",
1473
- properties: {
1474
- selector: { type: "string", description: "CSS selector for the component containing the form" },
1475
- formProperty: { type: "string", description: "Form property name (auto-detects if omitted)" },
1476
- waitForResponse: { type: "boolean", description: "Wait for network request after submit (default: true)" },
1477
- },
1478
- required: ["selector"],
1479
- },
1480
- },
1481
- {
1482
- name: "getFigmaFrame",
1483
- description: "Export and download a Figma frame as PNG image for comparison. Requires Figma API token and file/node IDs from Figma URLs.",
1484
- inputSchema: {
1485
- type: "object",
1486
- properties: {
1487
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1488
- fileKey: { type: "string", description: "Figma file key (from URL: figma.com/file/FILE_KEY/...)" },
1489
- nodeId: { type: "string", description: "Figma node ID (frame/component ID)" },
1490
- scale: { type: "number", minimum: 0.1, maximum: 4, description: "Export scale (0.1-4, default: 2)" },
1491
- format: { type: "string", enum: ["png", "jpg", "svg"], description: "Export format (default: png)" },
1492
- },
1493
- required: ["fileKey", "nodeId"],
1494
- },
1495
- },
1496
- {
1497
- name: "compareFigmaToElement",
1498
- description: "Compare Figma design directly with browser implementation. The GOLD STANDARD for design-to-code validation. Fetches Figma frame, screenshots element, performs pixel-perfect comparison with difference analysis.",
1499
- inputSchema: {
1500
- type: "object",
1501
- properties: {
1502
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1503
- fileKey: { type: "string", description: "Figma file key" },
1504
- nodeId: { type: "string", description: "Figma frame/component ID" },
1505
- selector: { type: "string", description: "CSS selector for page element" },
1506
- threshold: { type: "number", minimum: 0, maximum: 1, description: "Difference threshold (0-1, default: 0.05)" },
1507
- figmaScale: { type: "number", minimum: 0.1, maximum: 4, description: "Figma export scale (default: 2)" },
1508
- },
1509
- required: ["fileKey", "nodeId", "selector"],
1510
- },
1511
- },
1512
- {
1513
- name: "getFigmaSpecs",
1514
- description: "Extract detailed design specifications from Figma including colors, fonts, dimensions, and spacing. Perfect for design-to-code comparison.",
1515
- inputSchema: {
1516
- type: "object",
1517
- properties: {
1518
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1519
- fileKey: { type: "string", description: "Figma file key" },
1520
- nodeId: { type: "string", description: "Figma frame/component ID" },
1521
- },
1522
- required: ["fileKey", "nodeId"],
1523
- },
1524
- },
1525
- {
1526
- name: "parseFigmaUrl",
1527
- description: "Parse Figma URL to extract fileKey and nodeId. Accepts full Figma URLs (figma.com/file/..., figma.com/design/...) and automatically extracts parameters. Makes it easy to work with Figma links without manual parsing.",
1528
- inputSchema: {
1529
- type: "object",
1530
- properties: {
1531
- url: { type: "string", description: "Full Figma URL (e.g., https://www.figma.com/file/ABC123/Design?node-id=1-2) or just fileKey" },
1532
- },
1533
- required: ["url"],
1534
- },
1535
- },
1536
- {
1537
- name: "listFigmaPages",
1538
- description: "Get file structure: all pages and frames from Figma file. Returns hierarchical list of pages with their frames, IDs, names, and dimensions. Use this FIRST to discover what's in the file before requesting specific nodes.",
1539
- inputSchema: {
1540
- type: "object",
1541
- properties: {
1542
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1543
- fileKey: { type: "string", description: "Figma file key or full Figma URL" },
1544
- },
1545
- required: ["fileKey"],
1546
- },
1547
- },
1548
- {
1549
- name: "searchFigmaFrames",
1550
- description: "Search for frames/components by name in Figma file. Case-insensitive search across all pages. Returns matching nodes with IDs, types, pages, and dimensions.",
1551
- inputSchema: {
1552
- type: "object",
1553
- properties: {
1554
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1555
- fileKey: { type: "string", description: "Figma file key or full Figma URL" },
1556
- searchQuery: { type: "string", description: "Search query (e.g., 'login', 'button', 'header')" },
1557
- },
1558
- required: ["fileKey", "searchQuery"],
1559
- },
1560
- },
1561
- {
1562
- name: "getFigmaComponents",
1563
- description: "Get all components from Figma file (Design System). Returns all COMPONENT and COMPONENT_SET nodes with names, descriptions, and dimensions. Perfect for extracting design system components.",
1564
- inputSchema: {
1565
- type: "object",
1566
- properties: {
1567
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1568
- fileKey: { type: "string", description: "Figma file key or full Figma URL" },
1569
- },
1570
- required: ["fileKey"],
1571
- },
1572
- },
1573
- {
1574
- name: "getFigmaStyles",
1575
- description: "Get all styles from Figma file: color styles, text styles, effect styles, grid styles. Returns design system styles with names and descriptions. Use for extracting shared design tokens.",
1576
- inputSchema: {
1577
- type: "object",
1578
- properties: {
1579
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1580
- fileKey: { type: "string", description: "Figma file key or full Figma URL" },
1581
- },
1582
- required: ["fileKey"],
1583
- },
1584
- },
1585
- {
1586
- name: "getFigmaColorPalette",
1587
- description: "Extract complete color palette from Figma file. Analyzes all fills and strokes, returns unique colors with hex values, rgba, usage count, and examples. Sorted by usage frequency.",
1588
- inputSchema: {
1589
- type: "object",
1590
- properties: {
1591
- figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
1592
- fileKey: { type: "string", description: "Figma file key or full Figma URL" },
1593
- },
1594
- required: ["fileKey"],
1595
- },
1596
- },
1597
- {
1598
- name: "smartFindElement",
1599
- description: "AI-powered element finder using natural language. TIP: Use analyzePage first to get complete page structure with all selectors - it's faster and more reliable. This tool is best for ambiguous searches when exact selector is unknown. Returns multiple candidates ranked by relevance.",
1600
- inputSchema: {
1601
- type: "object",
1602
- properties: {
1603
- description: { type: "string", description: "Natural language description (e.g., 'login button', 'email input', 'submit form')" },
1604
- maxResults: { type: "number", minimum: 1, maximum: 20, description: "Max candidates to return (default: 5)" },
1605
- action: {
1606
- type: "object",
1607
- properties: {
1608
- type: { type: "string", enum: ["click", "type", "scrollTo", "screenshot", "hover", "setStyles"], description: "Action to perform on best match" },
1609
- text: { type: "string", description: "Text to type (for 'type' action)" },
1610
- styles: { type: "array", items: { type: "object", properties: { name: { type: "string" }, value: { type: "string" } } }, description: "Styles to apply (for 'setStyles' action)" },
1611
- screenshot: { type: "boolean", description: "Capture screenshot after action (default: false)" },
1612
- waitAfter: { type: "number", description: "Wait time in ms after action" },
1613
- },
1614
- required: ["type"],
1615
- description: "Optional action to perform on the best matching element",
1616
- },
1617
- },
1618
- required: ["description"],
1619
- },
1620
- },
1621
- {
1622
- name: "analyzePage",
1623
- description: "Get current page state and structure: forms (with values), inputs, buttons, links, selectors. USE AFTER: page load, button clicks, form submissions, AJAX updates, ANY page changes. Use refresh:true to get latest state after interactions. BETTER than screenshot for debugging: shows actual data (form values, errors, validation states) not just visual. 2-5k tokens vs screenshot 15-25k. Cached per URL, use refresh:true after page changes.",
1624
- inputSchema: {
1625
- type: "object",
1626
- properties: {
1627
- refresh: { type: "boolean", description: "Force refresh cached analysis (default: false)" },
1628
- },
1629
- },
1630
- },
1631
- {
1632
- name: "getAllInteractiveElements",
1633
- description: "Get all clickable and interactive elements on the page with their selectors and descriptions. Perfect for understanding what actions are available.",
1634
- inputSchema: {
1635
- type: "object",
1636
- properties: {
1637
- includeHidden: { type: "boolean", description: "Include hidden elements (default: false)" },
1638
- },
1639
- },
1640
- },
1641
- {
1642
- name: "findElementsByText",
1643
- description: "Find all elements containing specific text. Returns elements with their selectors. Can optionally perform actions (click, type, etc.) on the first match immediately.",
1644
- inputSchema: {
1645
- type: "object",
1646
- properties: {
1647
- text: { type: "string", description: "Text to search for" },
1648
- exact: { type: "boolean", description: "Exact match only (default: false)" },
1649
- caseSensitive: { type: "boolean", description: "Case sensitive (default: false)" },
1650
- action: {
1651
- type: "object",
1652
- properties: {
1653
- type: { type: "string", enum: ["click", "type", "scrollTo", "screenshot", "hover", "setStyles"], description: "Action to perform on first match" },
1654
- text: { type: "string", description: "Text to type (for 'type' action)" },
1655
- styles: { type: "array", items: { type: "object", properties: { name: { type: "string" }, value: { type: "string" } } }, description: "Styles to apply (for 'setStyles' action)" },
1656
- screenshot: { type: "boolean", description: "Capture screenshot after action (default: false)" },
1657
- waitAfter: { type: "number", description: "Wait time in ms after action" },
1658
- },
1659
- required: ["type"],
1660
- description: "Optional action to perform on the first matching element",
1661
- },
1662
- },
1663
- required: ["text"],
1664
- },
1665
- },
1666
- {
1667
- name: "enableRecorder",
1668
- description: "Inject recorder UI widget into the current page. Enables visual recording of user interactions with start/stop/save controls.",
1669
- inputSchema: {
1670
- type: "object",
1671
- properties: {},
1672
- },
1673
- },
1674
- {
1675
- name: "executeScenario",
1676
- description: "Execute a recorded scenario by name with optional parameters. Runs all actions in the scenario chain with dependency resolution.",
1677
- inputSchema: {
1678
- type: "object",
1679
- properties: {
1680
- name: { type: "string", description: "Scenario name to execute" },
1681
- parameters: { type: "object", description: "Parameters for scenario execution (e.g., { email: 'user@test.com', password: 'secret' })" },
1682
- executeDependencies: { type: "boolean", description: "Execute dependencies before running scenario (default: true)" },
1683
- },
1684
- required: ["name"],
1685
- },
1686
- },
1687
- {
1688
- name: "listScenarios",
1689
- description: "Get list of all available scenarios with metadata (name, description, tags, dependencies, timestamps).",
1690
- inputSchema: {
1691
- type: "object",
1692
- properties: {},
1693
- },
1694
- },
1695
- {
1696
- name: "searchScenarios",
1697
- description: "Search scenarios by text query or tags. Returns matching scenarios with metadata.",
1698
- inputSchema: {
1699
- type: "object",
1700
- properties: {
1701
- text: { type: "string", description: "Text to search in name/description" },
1702
- tags: { type: "array", items: { type: "string" }, description: "Tags to filter by" },
1703
- },
1704
- },
1705
- },
1706
- {
1707
- name: "getScenarioInfo",
1708
- description: "Get detailed information about a specific scenario including actions, parameters, and dependencies.",
1709
- inputSchema: {
1710
- type: "object",
1711
- properties: {
1712
- name: { type: "string", description: "Scenario name" },
1713
- includeSecrets: { type: "boolean", description: "Include secrets in response (default: false)" },
1714
- },
1715
- required: ["name"],
1716
- },
1717
- },
1718
- {
1719
- name: "deleteScenario",
1720
- description: "Delete a scenario and its associated secrets from storage.",
1721
- inputSchema: {
1722
- type: "object",
1723
- properties: {
1724
- name: { type: "string", description: "Scenario name to delete" },
1725
- },
1726
- required: ["name"],
1727
- },
1728
- },
1729
- ],
1730
- };
1731
- });
1732
-
1733
- // Helper function to execute actions on elements
1734
- async function executeElementAction(page, selector, action) {
1735
- if (!action || !action.type) {
1736
- return null;
1737
- }
1738
-
1739
- const element = await page.$(selector);
1740
- if (!element) {
1741
- throw new Error(`Element not found for action: ${selector}`);
1742
- }
1743
-
1744
- const result = {
1745
- action: action.type,
1746
- selector,
1747
- success: true,
1748
- };
1749
-
1750
- switch (action.type) {
1751
- case 'click':
1752
- await element.click();
1753
- await new Promise(resolve => setTimeout(resolve, action.waitAfter || 1500));
1754
- result.message = `Clicked on ${selector}`;
1755
-
1756
- if (action.screenshot) {
1757
- const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
1758
- result.screenshot = screenshot;
1759
- }
1760
- break;
1761
-
1762
- case 'type':
1763
- if (!action.text) {
1764
- throw new Error('text parameter is required for type action');
1765
- }
1766
- await element.click({ clickCount: 3 });
1767
- await page.keyboard.press('Backspace');
1768
- await element.type(action.text, { delay: 0 });
1769
- await new Promise(resolve => setTimeout(resolve, action.waitAfter || 500));
1770
- result.message = `Typed "${action.text}" into ${selector}`;
1771
-
1772
- if (action.screenshot) {
1773
- const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
1774
- result.screenshot = screenshot;
1775
- }
1776
- break;
1777
-
1778
- case 'scrollTo':
1779
- await element.scrollIntoView({ behavior: 'auto' });
1780
- await new Promise(resolve => setTimeout(resolve, action.waitAfter || 300));
1781
- const position = await page.evaluate(() => ({
1782
- x: window.scrollX,
1783
- y: window.scrollY
1784
- }));
1785
- result.message = `Scrolled to ${selector}`;
1786
- result.position = position;
1787
- break;
1788
-
1789
- case 'screenshot':
1790
- const box = await element.boundingBox();
1791
- if (!box) {
1792
- throw new Error(`Element not visible: ${selector}`);
1793
- }
1794
- const clip = {
1795
- x: Math.max(box.x, 0),
1796
- y: Math.max(box.y, 0),
1797
- width: Math.max(box.width, 1),
1798
- height: Math.max(box.height, 1)
1799
- };
1800
- const screenshot = await page.screenshot({ clip, encoding: 'base64' });
1801
- result.message = `Captured screenshot of ${selector}`;
1802
- result.screenshot = screenshot;
1803
- break;
1804
-
1805
- case 'hover':
1806
- await element.hover();
1807
- await new Promise(resolve => setTimeout(resolve, action.waitAfter || 100));
1808
- result.message = `Hovered over ${selector}`;
1809
-
1810
- if (action.screenshot) {
1811
- const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
1812
- result.screenshot = screenshot;
1813
- }
1814
- break;
1815
-
1816
- case 'setStyles':
1817
- if (!action.styles || !Array.isArray(action.styles)) {
1818
- throw new Error('styles parameter is required for setStyles action');
1819
- }
1820
- const stylesObject = {};
1821
- for (const style of action.styles) {
1822
- stylesObject[style.name] = style.value;
1823
- }
1824
- await page.evaluate((sel, styles) => {
1825
- const el = document.querySelector(sel);
1826
- if (el) {
1827
- Object.entries(styles).forEach(([key, value]) => {
1828
- el.style.setProperty(key, value);
1829
- });
1830
- }
1831
- }, selector, stylesObject);
1832
- await new Promise(resolve => setTimeout(resolve, action.waitAfter || 100));
1833
- result.message = `Applied styles to ${selector}`;
1834
- result.styles = stylesObject;
1835
-
1836
- if (action.screenshot) {
1837
- const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
1838
- result.screenshot = screenshot;
1839
- }
1840
- break;
1841
-
1842
- default:
1843
- throw new Error(`Unknown action type: ${action.type}`);
1844
- }
1845
-
1846
- return result;
1847
- }
1848
-
1849
- // Handle tool calls
1850
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1851
- const { name, arguments: args } = request.params;
1852
-
1853
- try {
1854
- if (name === "ping") {
1855
- const validatedArgs = PingSchema.parse(args);
1856
- const responseMessage = validatedArgs.message
1857
- ? `pong: ${validatedArgs.message}`
1858
- : "pong";
1859
-
1860
- return {
1861
- content: [
1862
- {
1863
- type: "text",
1864
- text: responseMessage,
1865
- },
1866
- ],
1867
- };
1868
- }
1869
-
1870
- if (name === "openBrowser") {
1871
- const validatedArgs = OpenBrowserSchema.parse(args);
1872
- const page = await getOrCreatePage(validatedArgs.url);
1873
- const title = await page.title();
1874
-
1875
- // Generate AI hints
1876
- const hints = await generateNavigationHints(page, validatedArgs.url);
1877
-
1878
- return {
1879
- content: [
1880
- {
1881
- type: "text",
1882
- text: `Browser opened successfully!\nURL: ${validatedArgs.url}\nPage title: ${title}\n\nBrowser remains open for interaction.\n\n** AI HINTS **\nPage type: ${hints.pageType}\nAvailable actions: ${hints.availableActions.join(', ')}\nSuggested next: ${hints.suggestedNext.join('; ')}`,
1883
- },
1884
- ],
1885
- };
1886
- }
1887
-
1888
- if (name === "click") {
1889
- const validatedArgs = ClickSchema.parse(args);
1890
- const page = await getLastOpenPage();
1891
- const timeout = validatedArgs.timeout || 30000;
1892
-
1893
- // Wrap operation in timeout
1894
- const clickOperation = async () => {
1895
- const element = await page.$(validatedArgs.selector);
1896
- if (!element) {
1897
- throw new Error(`Element not found: ${validatedArgs.selector}`);
1898
- }
1899
-
1900
- await element.click();
1901
- await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 1500));
1902
-
1903
- // Generate AI hints after click
1904
- const hints = await generateClickHints(page, validatedArgs.selector);
1905
-
1906
- let hintsText = '\n\n** AI HINTS **';
1907
- if (hints.modalOpened) hintsText += '\nModal opened - interact with it or close';
1908
- if (hints.newElements.length > 0) {
1909
- hintsText += `\nNew elements appeared: ${hints.newElements.map(e => e.type).join(', ')}`;
1910
- }
1911
- if (hints.suggestedNext.length > 0) {
1912
- hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
1913
- }
1914
-
1915
- const content = [
1916
- { type: "text", text: `Clicked: ${validatedArgs.selector}${hintsText}` }
1917
- ];
1918
-
1919
- // Only add screenshot if requested
1920
- if (validatedArgs.screenshot === true) {
1921
- const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
1922
- content.push({ type: "image", data: screenshot, mimeType: "image/png" });
1923
- }
1924
-
1925
- return { content };
1926
- };
1927
-
1928
- // Execute with timeout
1929
- const timeoutPromise = new Promise((_, reject) =>
1930
- setTimeout(() => reject(new Error(`Click operation timed out after ${timeout}ms`)), timeout)
1931
- );
1932
-
1933
- return Promise.race([clickOperation(), timeoutPromise]);
1934
- }
1935
-
1936
- if (name === "type") {
1937
- const validatedArgs = TypeSchema.parse(args);
1938
- const page = await getLastOpenPage();
1939
-
1940
- const element = await page.$(validatedArgs.selector);
1941
- if (!element) {
1942
- throw new Error(`Element not found: ${validatedArgs.selector}`);
1943
- }
1944
-
1945
- const clearFirst = validatedArgs.clearFirst !== undefined ? validatedArgs.clearFirst : true;
1946
- if (clearFirst) {
1947
- await element.click({ clickCount: 3 });
1948
- await page.keyboard.press('Backspace');
1949
- }
1950
-
1951
- await element.type(validatedArgs.text, { delay: validatedArgs.delay || 0 });
1952
-
1953
- return {
1954
- content: [
1955
- { type: "text", text: `Typed "${validatedArgs.text}" into ${validatedArgs.selector}` }
1956
- ],
1957
- };
1958
- }
1959
-
1960
- if (name === "getElement") {
1961
- const validatedArgs = GetElementSchema.parse(args);
1962
- const page = await getLastOpenPage();
1963
-
1964
- const client = await page.target().createCDPSession();
1965
- await client.send('DOM.enable');
1966
-
1967
- const { root } = await client.send('DOM.getDocument');
1968
- const useSelector = (validatedArgs.selector && validatedArgs.selector.trim()) ? validatedArgs.selector : 'body';
1969
-
1970
- const { nodeId } = await client.send('DOM.querySelector', {
1971
- selector: useSelector,
1972
- nodeId: root.nodeId
1973
- });
1974
-
1975
- if (!nodeId) {
1976
- throw new Error(`Element not found: ${validatedArgs.selector}`);
1977
- }
1978
-
1979
- const { outerHTML } = await client.send('DOM.getOuterHTML', { nodeId });
1980
-
1981
- return {
1982
- content: [{ type: "text", text: outerHTML }],
1983
- };
1984
- }
1985
-
1986
- if (name === "getComputedCss") {
1987
- const validatedArgs = GetComputedCssSchema.parse(args);
1988
- const page = await getLastOpenPage();
1989
-
1990
- const client = await page.target().createCDPSession();
1991
- await client.send('DOM.enable');
1992
- await client.send('CSS.enable');
1993
-
1994
- const { root } = await client.send('DOM.getDocument');
1995
- const useSelector = (validatedArgs.selector && validatedArgs.selector.trim()) ? validatedArgs.selector : 'body';
1996
-
1997
- const { nodeId } = await client.send('DOM.querySelector', {
1998
- selector: useSelector,
1999
- nodeId: root.nodeId
2000
- });
2001
-
2002
- if (!nodeId) {
2003
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2004
- }
2005
-
2006
- const { computedStyle } = await client.send('CSS.getComputedStyleForNode', { nodeId });
2007
-
2008
- // Apply filtering based on options
2009
- const filtered = filterCssStyles(computedStyle, {
2010
- category: validatedArgs.category,
2011
- properties: validatedArgs.properties,
2012
- includeDefaults: validatedArgs.includeDefaults
2013
- });
2014
-
2015
- // Add metadata about filtering
2016
- const result = {
2017
- selector: useSelector,
2018
- totalProperties: computedStyle.length,
2019
- filteredProperties: filtered.length,
2020
- filters: {
2021
- category: validatedArgs.category || 'all',
2022
- specificProperties: validatedArgs.properties || null,
2023
- includeDefaults: validatedArgs.includeDefaults || false
2024
- },
2025
- styles: filtered
2026
- };
2027
-
2028
- return {
2029
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2030
- };
2031
- }
2032
-
2033
- if (name === "getBoxModel") {
2034
- const validatedArgs = GetBoxModelSchema.parse(args);
2035
- const page = await getLastOpenPage();
2036
-
2037
- const client = await page.target().createCDPSession();
2038
- await client.send('DOM.enable');
2039
-
2040
- const { root } = await client.send('DOM.getDocument');
2041
- const { nodeId } = await client.send('DOM.querySelector', {
2042
- selector: validatedArgs.selector,
2043
- nodeId: root.nodeId
2044
- });
2045
-
2046
- if (!nodeId) {
2047
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2048
- }
2049
-
2050
- const boxModel = await client.send('DOM.getBoxModel', { nodeId });
2051
- const metrics = await page.evaluate((sel) => {
2052
- const el = document.querySelector(sel);
2053
- if (!el) return null;
2054
- return {
2055
- offsetWidth: el.offsetWidth,
2056
- offsetHeight: el.offsetHeight,
2057
- scrollWidth: el.scrollWidth,
2058
- scrollHeight: el.scrollHeight
2059
- };
2060
- }, validatedArgs.selector);
2061
-
2062
- if (!metrics) {
2063
- throw new Error(`Element not found (render): ${validatedArgs.selector}`);
2064
- }
2065
-
2066
- return {
2067
- content: [{ type: "text", text: JSON.stringify({ boxModel, metrics }, null, 2) }],
2068
- };
2069
- }
2070
-
2071
- if (name === "screenshot") {
2072
- const validatedArgs = ScreenshotSchema.parse(args);
2073
- const page = await getLastOpenPage();
2074
-
2075
- const element = await page.$(validatedArgs.selector);
2076
- if (!element) {
2077
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2078
- }
2079
-
2080
- const box = await element.boundingBox();
2081
- if (!box) {
2082
- throw new Error(`Element is not visible or has no bounding box: ${validatedArgs.selector}`);
2083
- }
2084
-
2085
- const padding = validatedArgs.padding || 0;
2086
- const clip = {
2087
- x: Math.max(box.x - padding, 0),
2088
- y: Math.max(box.y - padding, 0),
2089
- width: Math.max(box.width + padding * 2, 1),
2090
- height: Math.max(box.height + padding * 2, 1)
2091
- };
2092
-
2093
- // Take screenshot as buffer
2094
- const screenshotBuffer = await page.screenshot({ clip, encoding: 'binary' });
2095
-
2096
- // Process with compression and scaling
2097
- const processed = await processScreenshot(screenshotBuffer, {
2098
- maxWidth: validatedArgs.maxWidth ?? 1024,
2099
- maxHeight: validatedArgs.maxHeight ?? 8000,
2100
- quality: validatedArgs.quality ?? 80,
2101
- format: validatedArgs.format ?? 'auto'
2102
- });
2103
-
2104
- // Build info message
2105
- const infoText = `Screenshot captured: ${processed.metadata.width}x${processed.metadata.height} ${processed.metadata.format.toUpperCase()}` +
2106
- (processed.metadata.scaled ? ` (scaled from ${processed.metadata.originalWidth}x${processed.metadata.originalHeight})` : '') +
2107
- (processed.metadata.compressed ? ` (${processed.metadata.compressionRatio}% compression)` : '') +
2108
- `\nSize: ${(processed.metadata.finalSize / 1024).toFixed(1)}KB` +
2109
- (processed.metadata.originalSize !== processed.metadata.finalSize ?
2110
- ` (original: ${(processed.metadata.originalSize / 1024).toFixed(1)}KB)` : '');
2111
-
2112
- return {
2113
- content: [
2114
- {
2115
- type: "text",
2116
- text: infoText
2117
- },
2118
- {
2119
- type: "image",
2120
- data: processed.buffer.toString('base64'),
2121
- mimeType: processed.mimeType
2122
- }
2123
- ],
2124
- };
2125
- }
2126
-
2127
- if (name === "saveScreenshot") {
2128
- const validatedArgs = SaveScreenshotSchema.parse(args);
2129
- const page = await getLastOpenPage();
2130
-
2131
- const element = await page.$(validatedArgs.selector);
2132
- if (!element) {
2133
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2134
- }
2135
-
2136
- const box = await element.boundingBox();
2137
- if (!box) {
2138
- throw new Error(`Element not visible: ${validatedArgs.selector}`);
2139
- }
2140
-
2141
- const padding = validatedArgs.padding || 0;
2142
- const clip = {
2143
- x: Math.max(box.x - padding, 0),
2144
- y: Math.max(box.y - padding, 0),
2145
- width: Math.max(box.width + padding * 2, 1),
2146
- height: Math.max(box.height + padding * 2, 1)
2147
- };
2148
-
2149
- // Get screenshot as buffer (not base64)
2150
- const screenshotBuffer = await page.screenshot({ clip, encoding: 'binary' });
2151
-
2152
- // Process with compression and scaling
2153
- const processed = await processScreenshot(screenshotBuffer, {
2154
- maxWidth: validatedArgs.maxWidth ?? 1024,
2155
- maxHeight: validatedArgs.maxHeight ?? 8000,
2156
- quality: validatedArgs.quality ?? 80,
2157
- format: validatedArgs.format ?? 'auto'
2158
- });
2159
-
2160
- // Ensure directory exists
2161
- const dir = dirname(validatedArgs.filePath);
2162
- mkdirSync(dir, { recursive: true });
2163
-
2164
- // Save to file
2165
- writeFileSync(validatedArgs.filePath, processed.buffer);
2166
-
2167
- const infoText = `Screenshot saved to: ${validatedArgs.filePath}\n` +
2168
- `Dimensions: ${processed.metadata.width}x${processed.metadata.height}\n` +
2169
- `Format: ${processed.metadata.format.toUpperCase()}\n` +
2170
- `Size: ${(processed.metadata.finalSize / 1024).toFixed(1)}KB` +
2171
- (processed.metadata.scaled ? ` (scaled from ${processed.metadata.originalWidth}x${processed.metadata.originalHeight})` : '') +
2172
- (processed.metadata.compressed ? `\nCompression: ${processed.metadata.compressionRatio}% saved` : '');
2173
-
2174
- return {
2175
- content: [
2176
- {
2177
- type: "text",
2178
- text: infoText
2179
- }
2180
- ],
2181
- };
2182
- }
2183
-
2184
- if (name === "scrollTo") {
2185
- const validatedArgs = ScrollToSchema.parse(args);
2186
- const page = await getLastOpenPage();
2187
-
2188
- // Check if element exists
2189
- const elementExists = await page.$(validatedArgs.selector);
2190
- if (!elementExists) {
2191
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2192
- }
2193
-
2194
- // Scroll to element using page.evaluate
2195
- await page.evaluate((selector, behavior) => {
2196
- const element = document.querySelector(selector);
2197
- if (element) {
2198
- element.scrollIntoView({ behavior: behavior || 'auto', block: 'center' });
2199
- }
2200
- }, validatedArgs.selector, validatedArgs.behavior || 'auto');
2201
-
2202
- // Wait for scroll to complete
2203
- await new Promise(resolve => setTimeout(resolve, 500));
2204
-
2205
- const position = await page.evaluate(() => ({
2206
- x: window.scrollX,
2207
- y: window.scrollY
2208
- }));
2209
-
2210
- return {
2211
- content: [
2212
- { type: "text", text: `Scrolled to ${validatedArgs.selector} (position: ${position.x}, ${position.y})` }
2213
- ],
2214
- };
2215
- }
2216
-
2217
- if (name === "waitForElement") {
2218
- const validatedArgs = WaitForElementSchema.parse(args);
2219
- const page = await getLastOpenPage();
2220
- const timeout = validatedArgs.timeout || 5000;
2221
- const waitForVisible = validatedArgs.visible !== false;
2222
-
2223
- try {
2224
- if (waitForVisible) {
2225
- // Wait for element to be visible
2226
- await page.waitForSelector(validatedArgs.selector, { timeout, visible: true });
2227
- } else {
2228
- // Just wait for element to exist in DOM
2229
- await page.waitForSelector(validatedArgs.selector, { timeout });
2230
- }
2231
-
2232
- // Get info about the element
2233
- const elementInfo = await page.evaluate((selector) => {
2234
- const el = document.querySelector(selector);
2235
- if (!el) return null;
2236
-
2237
- const rect = el.getBoundingClientRect();
2238
- return {
2239
- visible: el.offsetParent !== null,
2240
- inViewport: rect.top >= 0 && rect.left >= 0 &&
2241
- rect.bottom <= window.innerHeight &&
2242
- rect.right <= window.innerWidth,
2243
- tagName: el.tagName.toLowerCase(),
2244
- text: el.textContent?.trim().substring(0, 100) || ''
2245
- };
2246
- }, validatedArgs.selector);
2247
-
2248
- return {
2249
- content: [{
2250
- type: "text",
2251
- text: `Element appeared: ${validatedArgs.selector}\n${JSON.stringify(elementInfo, null, 2)}`
2252
- }],
2253
- };
2254
- } catch (error) {
2255
- if (error.name === 'TimeoutError') {
2256
- throw new Error(`Element not found within ${timeout}ms: ${validatedArgs.selector}`);
2257
- }
2258
- throw error;
2259
- }
2260
- }
2261
-
2262
- if (name === "executeScript") {
2263
- const validatedArgs = ExecuteScriptSchema.parse(args);
2264
- const page = await getLastOpenPage();
2265
- const timeout = validatedArgs.timeout || 30000;
2266
-
2267
- // Wrap operation in timeout
2268
- const executeOperation = async () => {
2269
- const result = await page.evaluate((code) => {
2270
- try {
2271
- // eslint-disable-next-line no-eval
2272
- const evalResult = eval(code);
2273
- return { success: true, result: evalResult };
2274
- } catch (error) {
2275
- return { success: false, error: error.message };
2276
- }
2277
- }, validatedArgs.script);
2278
-
2279
- await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 500));
2280
-
2281
- const content = [
2282
- {
2283
- type: "text",
2284
- text: result.success
2285
- ? `Script executed successfully.\nResult: ${JSON.stringify(result.result)}`
2286
- : `Script execution failed: ${result.error}`
2287
- }
2288
- ];
2289
-
2290
- // Only add screenshot if requested
2291
- if (validatedArgs.screenshot === true) {
2292
- const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
2293
- content.push({ type: "image", data: screenshot, mimeType: "image/png" });
2294
- }
2295
-
2296
- return { content };
2297
- };
2298
-
2299
- // Execute with timeout
2300
- const timeoutPromise = new Promise((_, reject) =>
2301
- setTimeout(() => reject(new Error(`Script execution timed out after ${timeout}ms`)), timeout)
2302
- );
2303
-
2304
- return Promise.race([executeOperation(), timeoutPromise]);
2305
- }
2306
-
2307
- if (name === "getConsoleLogs") {
2308
- const validatedArgs = GetConsoleLogsSchema.parse(args);
2309
-
2310
- let logs = consoleLogs;
2311
-
2312
- // Filter by types if specified
2313
- if (validatedArgs.types && validatedArgs.types.length > 0) {
2314
- logs = logs.filter(log => validatedArgs.types.includes(log.type));
2315
- }
2316
-
2317
- const result = {
2318
- count: logs.length,
2319
- logs: logs.map(log => ({
2320
- type: log.type,
2321
- timestamp: log.timestamp,
2322
- message: log.message
2323
- }))
2324
- };
2325
-
2326
- // Clear logs if requested
2327
- if (validatedArgs.clear) {
2328
- consoleLogs.length = 0;
2329
- }
2330
-
2331
- return {
2332
- content: [{
2333
- type: "text",
2334
- text: JSON.stringify(result, null, 2)
2335
- }],
2336
- };
2337
- }
2338
-
2339
- // Tool 1: listNetworkRequests - compact summary
2340
- if (name === "listNetworkRequests") {
2341
- const validatedArgs = ListNetworkRequestsSchema.parse(args);
2342
-
2343
- let requests = networkRequests;
2344
-
2345
- // Filter by types (defaults to Fetch and XHR only)
2346
- if (validatedArgs.types && validatedArgs.types.length > 0) {
2347
- requests = requests.filter(req => validatedArgs.types.includes(req.type));
2348
- }
2349
-
2350
- // Filter by status if specified
2351
- if (validatedArgs.status && validatedArgs.status !== 'all') {
2352
- requests = requests.filter(req => req.status === validatedArgs.status);
2353
- }
2354
-
2355
- const result = {
2356
- count: requests.length,
2357
- requests: requests.map(req => ({
2358
- requestId: req.requestId,
2359
- method: req.method,
2360
- url: req.url,
2361
- status: req.status,
2362
- statusCode: req.status === 'completed' ? req.status : undefined,
2363
- type: req.type,
2364
- }))
2365
- };
2366
-
2367
- // Clear requests if requested
2368
- if (validatedArgs.clear) {
2369
- networkRequests.length = 0;
2370
- }
2371
-
2372
- return {
2373
- content: [{
2374
- type: "text",
2375
- text: JSON.stringify(result, null, 2)
2376
- }],
2377
- };
2378
- }
2379
-
2380
- // Tool 2: getNetworkRequest - full details of single request
2381
- if (name === "getNetworkRequest") {
2382
- const validatedArgs = GetNetworkRequestSchema.parse(args);
2383
-
2384
- const req = networkRequests.find(r => r.requestId === validatedArgs.requestId);
2385
- if (!req) {
2386
- throw new Error(`Request not found: ${validatedArgs.requestId}`);
2387
- }
2388
-
2389
- // Helper to minify JSON strings
2390
- const minifyJson = (str) => {
2391
- if (!str) return str;
2392
- try {
2393
- const parsed = JSON.parse(str);
2394
- return JSON.stringify(parsed); // Minified (no spacing)
2395
- } catch {
2396
- return str; // Return as-is if not valid JSON
2397
- }
2398
- };
2399
-
2400
- const result = {
2401
- requestId: req.requestId,
2402
- url: req.url,
2403
- method: req.method,
2404
- type: req.type,
2405
- status: req.status,
2406
- statusCode: req.status,
2407
- statusText: req.statusText,
2408
- timestamp: req.timestamp,
2409
- finishedTimestamp: req.finishedTimestamp,
2410
- duration: req.finishedTimestamp ? new Date(req.finishedTimestamp) - new Date(req.timestamp) + 'ms' : undefined,
2411
- fromCache: req.fromCache,
2412
- headers: req.headers,
2413
- postData: req.postData ? minifyJson(req.postData) : undefined,
2414
- responseHeaders: req.responseHeaders,
2415
- mimeType: req.mimeType,
2416
- errorText: req.errorText,
2417
- canceled: req.canceled,
2418
- };
2419
-
2420
- return {
2421
- content: [{
2422
- type: "text",
2423
- text: JSON.stringify(result, null, 2)
2424
- }],
2425
- };
2426
- }
2427
-
2428
- // Tool 3: filterNetworkRequests - filter by URL pattern with full details
2429
- if (name === "filterNetworkRequests") {
2430
- const validatedArgs = FilterNetworkRequestsSchema.parse(args);
2431
-
2432
- let requests = networkRequests;
2433
-
2434
- // Filter by types (defaults to Fetch and XHR only)
2435
- if (validatedArgs.types && validatedArgs.types.length > 0) {
2436
- requests = requests.filter(req => validatedArgs.types.includes(req.type));
2437
- }
2438
-
2439
- // Filter by URL pattern
2440
- try {
2441
- const regex = new RegExp(validatedArgs.urlPattern);
2442
- requests = requests.filter(req => regex.test(req.url));
2443
- } catch (error) {
2444
- // Try partial match if regex fails
2445
- requests = requests.filter(req => req.url.includes(validatedArgs.urlPattern));
2446
- }
2447
-
2448
- // Helper to minify JSON strings
2449
- const minifyJson = (str) => {
2450
- if (!str) return str;
2451
- try {
2452
- const parsed = JSON.parse(str);
2453
- return JSON.stringify(parsed); // Minified (no spacing)
2454
- } catch {
2455
- return str; // Return as-is if not valid JSON
2456
- }
2457
- };
2458
-
2459
- const result = {
2460
- count: requests.length,
2461
- pattern: validatedArgs.urlPattern,
2462
- requests: requests.map(req => ({
2463
- requestId: req.requestId,
2464
- url: req.url,
2465
- method: req.method,
2466
- type: req.type,
2467
- status: req.status,
2468
- statusCode: req.status,
2469
- statusText: req.statusText,
2470
- timestamp: req.timestamp,
2471
- duration: req.finishedTimestamp ? new Date(req.finishedTimestamp) - new Date(req.timestamp) + 'ms' : undefined,
2472
- fromCache: req.fromCache,
2473
- headers: req.headers,
2474
- postData: req.postData ? minifyJson(req.postData) : undefined,
2475
- responseHeaders: req.responseHeaders,
2476
- mimeType: req.mimeType,
2477
- errorText: req.errorText,
2478
- }))
2479
- };
2480
-
2481
- // Clear requests if requested
2482
- if (validatedArgs.clear) {
2483
- networkRequests.length = 0;
2484
- }
2485
-
2486
- return {
2487
- content: [{
2488
- type: "text",
2489
- text: JSON.stringify(result, null, 2)
2490
- }],
2491
- };
2492
- }
2493
-
2494
- if (name === "hover") {
2495
- const validatedArgs = HoverSchema.parse(args);
2496
- const page = await getLastOpenPage();
2497
-
2498
- const element = await page.$(validatedArgs.selector);
2499
- if (!element) {
2500
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2501
- }
2502
-
2503
- await element.hover();
2504
- await new Promise(resolve => setTimeout(resolve, 100));
2505
-
2506
- return {
2507
- content: [{
2508
- type: "text",
2509
- text: `Hovered over: ${validatedArgs.selector}`
2510
- }],
2511
- };
2512
- }
2513
-
2514
- if (name === "setStyles") {
2515
- const validatedArgs = SetStylesSchema.parse(args);
2516
- const page = await getLastOpenPage();
2517
-
2518
- const stylesObject = {};
2519
- for (const style of validatedArgs.styles) {
2520
- stylesObject[style.name] = style.value;
2521
- }
2522
-
2523
- const success = await page.evaluate((sel, styles) => {
2524
- const el = document.querySelector(sel);
2525
- if (!el) return false;
2526
- Object.entries(styles).forEach(([key, value]) => {
2527
- el.style.setProperty(key, value);
2528
- });
2529
- return true;
2530
- }, validatedArgs.selector, stylesObject);
2531
-
2532
- if (!success) {
2533
- throw new Error(`Element not found: ${validatedArgs.selector}`);
2534
- }
2535
-
2536
- return {
2537
- content: [{
2538
- type: "text",
2539
- text: `Styles applied to ${validatedArgs.selector}:\n${JSON.stringify(stylesObject, null, 2)}`
2540
- }],
2541
- };
2542
- }
2543
-
2544
- if (name === "setViewport") {
2545
- const validatedArgs = SetViewportSchema.parse(args);
2546
- const page = await getLastOpenPage();
2547
-
2548
- await page.setViewport({
2549
- width: validatedArgs.width,
2550
- height: validatedArgs.height,
2551
- deviceScaleFactor: validatedArgs.deviceScaleFactor || 1
2552
- });
2553
-
2554
- const actual = await page.evaluate(() => ({
2555
- width: window.innerWidth,
2556
- height: window.innerHeight,
2557
- devicePixelRatio: window.devicePixelRatio
2558
- }));
2559
-
2560
- return {
2561
- content: [{
2562
- type: "text",
2563
- text: `Viewport set to ${validatedArgs.width}x${validatedArgs.height}\nActual: ${actual.width}x${actual.height} (DPR: ${actual.devicePixelRatio})`
2564
- }],
2565
- };
2566
- }
2567
-
2568
- if (name === "getViewport") {
2569
- const page = await getLastOpenPage();
2570
-
2571
- const viewport = await page.evaluate(() => ({
2572
- width: window.innerWidth,
2573
- height: window.innerHeight,
2574
- outerWidth: window.outerWidth,
2575
- outerHeight: window.outerHeight,
2576
- devicePixelRatio: window.devicePixelRatio
2577
- }));
2578
-
2579
- return {
2580
- content: [{
2581
- type: "text",
2582
- text: JSON.stringify(viewport, null, 2)
2583
- }],
2584
- };
2585
- }
2586
-
2587
- if (name === "navigateTo") {
2588
- const validatedArgs = NavigateToSchema.parse(args);
2589
- const page = await getLastOpenPage();
2590
-
2591
- // Navigate to the new URL (always navigate, don't use cache)
2592
- await page.goto(validatedArgs.url, { waitUntil: validatedArgs.waitUntil || 'networkidle2' });
2593
-
2594
- const title = await page.title();
2595
-
2596
- // Generate AI hints
2597
- const hints = await generateNavigationHints(page, validatedArgs.url);
2598
-
2599
- return {
2600
- content: [{
2601
- type: "text",
2602
- text: `Navigated to: ${validatedArgs.url}\nPage title: ${title}\n\n** AI HINTS **\nPage type: ${hints.pageType}\nAvailable actions: ${hints.availableActions.join(', ')}\nSuggested next: ${hints.suggestedNext.join('; ')}`
2603
- }],
2604
- };
2605
- }
2606
-
2607
- // Angular tools
2608
- if (name === "listAngularComponents") {
2609
- const validatedArgs = ListAngularComponentsSchema.parse(args);
2610
- const page = await getLastOpenPage();
2611
-
2612
- const components = await listAngularComponents(page, validatedArgs.includeHidden);
2613
-
2614
- return {
2615
- content: [{
2616
- type: "text",
2617
- text: JSON.stringify(components, null, 2)
2618
- }],
2619
- };
2620
- }
2621
-
2622
- if (name === "getAngularComponent") {
2623
- const validatedArgs = GetAngularComponentSchema.parse(args);
2624
- const page = await getLastOpenPage();
2625
-
2626
- const componentInfo = await getAngularComponent(page, validatedArgs.selector, validatedArgs.includePrivate);
2627
-
2628
- return {
2629
- content: [{
2630
- type: "text",
2631
- text: JSON.stringify(componentInfo, null, 2)
2632
- }],
2633
- };
2634
- }
2635
-
2636
- if (name === "callAngularMethod") {
2637
- const validatedArgs = CallAngularMethodSchema.parse(args);
2638
- const page = await getLastOpenPage();
2639
-
2640
- const result = await callAngularMethod(page, validatedArgs.selector, validatedArgs.method, validatedArgs.args);
2641
-
2642
- return {
2643
- content: [{
2644
- type: "text",
2645
- text: `Method '${validatedArgs.method}' called successfully.\n${JSON.stringify(result, null, 2)}`
2646
- }],
2647
- };
2648
- }
2649
-
2650
- if (name === "getAngularForm") {
2651
- const validatedArgs = GetAngularFormSchema.parse(args);
2652
- const page = await getLastOpenPage();
2653
-
2654
- const formData = await getAngularForm(page, validatedArgs.selector, validatedArgs.formProperty);
2655
-
2656
- return {
2657
- content: [{
2658
- type: "text",
2659
- text: JSON.stringify(formData, null, 2)
2660
- }],
2661
- };
2662
- }
2663
-
2664
- if (name === "submitAngularForm") {
2665
- const validatedArgs = SubmitAngularFormSchema.parse(args);
2666
- const page = await getLastOpenPage();
2667
-
2668
- const result = await submitAngularForm(page, validatedArgs.selector, validatedArgs.formProperty, validatedArgs.waitForResponse);
2669
-
2670
- return {
2671
- content: [{
2672
- type: "text",
2673
- text: `Form submitted successfully!\n${JSON.stringify(result, null, 2)}`
2674
- }],
2675
- };
2676
- }
2677
-
2678
- // Figma tools
2679
- if (name === "getFigmaFrame") {
2680
- const validatedArgs = GetFigmaFrameSchema.parse(args);
2681
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
2682
- if (!token) {
2683
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
2684
- }
2685
-
2686
- // Normalize node ID (convert URL format like "123-456" to API format "123:456")
2687
- const nodeId = normalizeFigmaNodeId(validatedArgs.nodeId);
2688
-
2689
- const scale = validatedArgs.scale || 2;
2690
- const format = validatedArgs.format || 'png';
2691
-
2692
- // Get export URL from Figma
2693
- const exportData = await fetchFigmaAPI(
2694
- `images/${validatedArgs.fileKey}?ids=${nodeId}&scale=${scale}&format=${format}`,
2695
- token
2696
- );
2697
-
2698
- if (!exportData.images || !exportData.images[nodeId]) {
2699
- throw new Error(`Failed to export node ${nodeId} from file ${validatedArgs.fileKey}`);
2700
- }
2701
-
2702
- const imageUrl = exportData.images[nodeId];
2703
-
2704
- // Download image
2705
- const imageResponse = await fetch(imageUrl);
2706
- if (!imageResponse.ok) {
2707
- throw new Error(`Failed to download image: ${imageResponse.status}`);
2708
- }
2709
-
2710
- const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
2711
-
2712
- // Get frame info
2713
- const nodesData = await fetchFigmaAPI(`files/${validatedArgs.fileKey}/nodes?ids=${encodeURIComponent(nodeId)}`, token);
2714
- const frameInfo = nodesData.nodes?.[nodeId]?.document;
2715
-
2716
- // Process image to ensure it doesn't exceed 3 MB
2717
- const processedImage = await processScreenshot(imageBuffer, {
2718
- maxWidth: null, // Keep original dimensions
2719
- maxHeight: null,
2720
- quality: 85,
2721
- format: format,
2722
- maxFileSize: 3 * 1024 * 1024 // 3 MB limit
2723
- });
2724
-
2725
- const result = {
2726
- figmaInfo: {
2727
- fileName: nodesData.name || 'Unknown',
2728
- frameId: nodeId,
2729
- frameName: frameInfo?.name || 'Unknown',
2730
- dimensions: frameInfo ? {
2731
- width: frameInfo.absoluteBoundingBox?.width,
2732
- height: frameInfo.absoluteBoundingBox?.height
2733
- } : null,
2734
- exportSettings: {
2735
- scale,
2736
- format: processedImage.metadata.format,
2737
- fileSize: processedImage.metadata.finalSize,
2738
- originalFileSize: imageBuffer.length,
2739
- compressed: processedImage.metadata.autoCompressed,
2740
- quality: processedImage.metadata.quality
2741
- }
2742
- }
2743
- };
2744
-
2745
- return {
2746
- content: [
2747
- { type: 'text', text: JSON.stringify(result, null, 2) },
2748
- {
2749
- type: 'image',
2750
- data: processedImage.buffer.toString('base64'),
2751
- mimeType: processedImage.mimeType
2752
- }
2753
- ]
2754
- };
2755
- }
2756
-
2757
- if (name === "compareFigmaToElement") {
2758
- const validatedArgs = CompareFigmaToElementSchema.parse(args);
2759
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
2760
- if (!token) {
2761
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
2762
- }
2763
-
2764
- const page = await getLastOpenPage();
2765
- const figmaScale = validatedArgs.figmaScale || 2;
2766
- const threshold = validatedArgs.threshold || 0.05;
2767
-
2768
- // Normalize node ID (convert URL format like "123-456" to API format "123:456")
2769
- const nodeId = normalizeFigmaNodeId(validatedArgs.nodeId);
2770
-
2771
- // Get Figma image
2772
- const exportData = await fetchFigmaAPI(
2773
- `images/${validatedArgs.fileKey}?ids=${nodeId}&scale=${figmaScale}&format=png`,
2774
- token
2775
- );
2776
-
2777
- if (!exportData.images || !exportData.images[nodeId]) {
2778
- throw new Error(`Failed to export Figma node ${nodeId}`);
2779
- }
2780
-
2781
- const figmaImageUrl = exportData.images[nodeId];
2782
- const figmaResponse = await fetch(figmaImageUrl);
2783
- const figmaBuffer = Buffer.from(await figmaResponse.arrayBuffer());
2784
-
2785
- // Get page element screenshot
2786
- const element = await page.$(validatedArgs.selector);
2787
- if (!element) {
2788
- throw new Error(`Selector not found: ${validatedArgs.selector}`);
2789
- }
2790
-
2791
- const pageBuffer = await element.screenshot();
2792
-
2793
- // Load images for comparison
2794
- const [figmaImg, pageImg] = await Promise.all([
2795
- Jimp.read(figmaBuffer),
2796
- Jimp.read(pageBuffer)
2797
- ]);
2798
-
2799
- // Resize to same dimensions (use larger dimensions)
2800
- const targetWidth = Math.max(figmaImg.bitmap.width, pageImg.bitmap.width);
2801
- const targetHeight = Math.max(figmaImg.bitmap.height, pageImg.bitmap.height);
2802
-
2803
- figmaImg.resize(targetWidth, targetHeight);
2804
- pageImg.resize(targetWidth, targetHeight);
2805
-
2806
- // Compare images
2807
- const figmaData = new Uint8ClampedArray(figmaImg.bitmap.data);
2808
- const pageData = new Uint8ClampedArray(pageImg.bitmap.data);
2809
- const diffData = new Uint8ClampedArray(targetWidth * targetHeight * 4);
2810
-
2811
- const diffPixels = pixelmatch(figmaData, pageData, diffData, targetWidth, targetHeight, {
2812
- threshold: 0.1,
2813
- includeAA: false
2814
- });
2815
-
2816
- const ssimValue = calculateSSIM(figmaData, pageData, targetWidth, targetHeight);
2817
- const totalPixels = targetWidth * targetHeight;
2818
- const differencePercent = (diffPixels / totalPixels) * 100;
2819
-
2820
- // Analysis
2821
- const analysis = {
2822
- figmaVsPage: {
2823
- identical: diffPixels === 0,
2824
- withinThreshold: differencePercent <= (threshold * 100),
2825
- pixelDifferences: diffPixels,
2826
- differencePercent: Math.round(differencePercent * 100) / 100,
2827
- ssim: Math.round(ssimValue * 10000) / 10000,
2828
- recommendation: differencePercent < 1 ? 'Pixel-perfect match' :
2829
- differencePercent < 3 ? 'Very close to design' :
2830
- differencePercent < 10 ? 'Minor differences detected' :
2831
- 'Significant differences from design'
2832
- },
2833
- dimensions: {
2834
- figma: { width: figmaImg.bitmap.width, height: figmaImg.bitmap.height },
2835
- page: { width: pageImg.bitmap.width, height: pageImg.bitmap.height },
2836
- comparison: { width: targetWidth, height: targetHeight }
2837
- }
2838
- };
2839
-
2840
- // Process images to ensure they don't exceed 3 MB
2841
- const [processedFigma, processedPage] = await Promise.all([
2842
- processScreenshot(figmaBuffer, {
2843
- maxWidth: null,
2844
- maxHeight: null,
2845
- quality: 85,
2846
- format: 'auto',
2847
- maxFileSize: 3 * 1024 * 1024
2848
- }),
2849
- processScreenshot(pageBuffer, {
2850
- maxWidth: null,
2851
- maxHeight: null,
2852
- quality: 85,
2853
- format: 'auto',
2854
- maxFileSize: 3 * 1024 * 1024
2855
- })
2856
- ]);
2857
-
2858
- const content = [
2859
- { type: 'text', text: JSON.stringify(analysis, null, 2) },
2860
- { type: 'image', data: processedFigma.buffer.toString('base64'), mimeType: processedFigma.mimeType },
2861
- { type: 'image', data: processedPage.buffer.toString('base64'), mimeType: processedPage.mimeType }
2862
- ];
2863
-
2864
- // Add difference map if there are differences
2865
- if (diffPixels > 0) {
2866
- const diffImg = new Jimp({ data: Buffer.from(diffData), width: targetWidth, height: targetHeight });
2867
- const diffBuffer = await diffImg.getBufferAsync(Jimp.MIME_PNG);
2868
-
2869
- // Process diff image as well
2870
- const processedDiff = await processScreenshot(diffBuffer, {
2871
- maxWidth: null,
2872
- maxHeight: null,
2873
- quality: 85,
2874
- format: 'auto',
2875
- maxFileSize: 3 * 1024 * 1024
2876
- });
2877
-
2878
- content.push({
2879
- type: 'image',
2880
- data: processedDiff.buffer.toString('base64'),
2881
- mimeType: processedDiff.mimeType
2882
- });
2883
- }
2884
-
2885
- return { content };
2886
- }
2887
-
2888
- if (name === "getFigmaSpecs") {
2889
- const validatedArgs = GetFigmaSpecsSchema.parse(args);
2890
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
2891
- if (!token) {
2892
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
2893
- }
2894
-
2895
- // Normalize node ID (convert URL format like "123-456" to API format "123:456")
2896
- const nodeId = normalizeFigmaNodeId(validatedArgs.nodeId);
2897
-
2898
- // Get specific node via nodes API
2899
- const nodesData = await fetchFigmaAPI(`files/${validatedArgs.fileKey}/nodes?ids=${encodeURIComponent(nodeId)}`, token);
2900
-
2901
- if (!nodesData.nodes || !nodesData.nodes[nodeId]) {
2902
- throw new Error(`Node ${nodeId} not found in Figma file`);
2903
- }
2904
-
2905
- const node = nodesData.nodes[nodeId].document;
2906
-
2907
- // Extract specifications
2908
- const specs = {
2909
- general: {
2910
- name: node.name,
2911
- type: node.type,
2912
- visible: node.visible !== false
2913
- },
2914
- dimensions: node.absoluteBoundingBox ? {
2915
- width: node.absoluteBoundingBox.width,
2916
- height: node.absoluteBoundingBox.height,
2917
- x: node.absoluteBoundingBox.x,
2918
- y: node.absoluteBoundingBox.y
2919
- } : null,
2920
- styling: {},
2921
- children: []
2922
- };
2923
-
2924
- // Analyze styles
2925
- if (node.fills && node.fills.length > 0) {
2926
- specs.styling.fills = node.fills.map(fill => {
2927
- if (fill.type === 'SOLID') {
2928
- const r = Math.round(fill.color.r * 255);
2929
- const g = Math.round(fill.color.g * 255);
2930
- const b = Math.round(fill.color.b * 255);
2931
- const a = fill.opacity || 1;
2932
- return {
2933
- type: fill.type,
2934
- color: `rgba(${r}, ${g}, ${b}, ${a})`,
2935
- hex: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`,
2936
- opacity: a
2937
- };
2938
- }
2939
- return fill;
2940
- });
2941
- }
2942
-
2943
- if (node.strokes && node.strokes.length > 0) {
2944
- specs.styling.strokes = node.strokes.map(stroke => {
2945
- if (stroke.type === 'SOLID') {
2946
- const r = Math.round(stroke.color.r * 255);
2947
- const g = Math.round(stroke.color.g * 255);
2948
- const b = Math.round(stroke.color.b * 255);
2949
- const a = stroke.opacity || 1;
2950
- return {
2951
- type: stroke.type,
2952
- color: `rgba(${r}, ${g}, ${b}, ${a})`,
2953
- hex: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`,
2954
- weight: node.strokeWeight || 1
2955
- };
2956
- }
2957
- return stroke;
2958
- });
2959
- }
2960
-
2961
- // Text content (for TEXT nodes)
2962
- if (node.type === 'TEXT' && node.characters) {
2963
- specs.textContent = {
2964
- text: node.characters,
2965
- characterCount: node.characters.length
2966
- };
2967
- }
2968
-
2969
- // Typography
2970
- if (node.style) {
2971
- specs.styling.typography = {
2972
- fontFamily: node.style.fontFamily,
2973
- fontSize: node.style.fontSize,
2974
- fontWeight: node.style.fontWeight,
2975
- lineHeight: node.style.lineHeightPx || node.style.lineHeightPercent,
2976
- letterSpacing: node.style.letterSpacing,
2977
- textAlign: node.style.textAlignHorizontal,
2978
- textCase: node.style.textCase
2979
- };
2980
- }
2981
-
2982
- // Effects (shadows, blur)
2983
- if (node.effects && node.effects.length > 0) {
2984
- specs.styling.effects = node.effects.map(effect => ({
2985
- type: effect.type,
2986
- visible: effect.visible !== false,
2987
- radius: effect.radius,
2988
- offset: effect.offset,
2989
- color: effect.color ? {
2990
- rgba: `rgba(${Math.round(effect.color.r * 255)}, ${Math.round(effect.color.g * 255)}, ${Math.round(effect.color.b * 255)}, ${effect.color.a || 1})`
2991
- } : null
2992
- }));
2993
- }
2994
-
2995
- // Border radius
2996
- if (node.cornerRadius !== undefined) {
2997
- specs.styling.borderRadius = node.cornerRadius;
2998
- }
2999
- if (node.rectangleCornerRadii) {
3000
- specs.styling.borderRadius = {
3001
- topLeft: node.rectangleCornerRadii[0],
3002
- topRight: node.rectangleCornerRadii[1],
3003
- bottomRight: node.rectangleCornerRadii[2],
3004
- bottomLeft: node.rectangleCornerRadii[3]
3005
- };
3006
- }
3007
-
3008
- // Analyze children with text extraction (using imported function)
3009
- if (node.children && node.children.length > 0) {
3010
- specs.children = node.children.map(child => extractTextFromNode(child));
3011
- }
3012
-
3013
- const allTexts = collectAllText(node);
3014
- if (allTexts.length > 0) {
3015
- specs.allTextContent = allTexts;
3016
- specs.textSummary = {
3017
- totalTextNodes: allTexts.length,
3018
- visibleTextNodes: allTexts.filter(t => t.visible).length,
3019
- combinedText: allTexts.filter(t => t.visible).map(t => t.text).join(' ')
3020
- };
3021
- }
3022
-
3023
- return {
3024
- content: [
3025
- { type: 'text', text: JSON.stringify(specs, null, 2) }
3026
- ]
3027
- };
3028
- }
3029
-
3030
- if (name === "parseFigmaUrl") {
3031
- const validatedArgs = ParseFigmaUrlSchema.parse(args);
3032
- const result = parseFigmaUrl(validatedArgs.url);
3033
-
3034
- return {
3035
- content: [
3036
- { type: 'text', text: JSON.stringify(result, null, 2) }
3037
- ]
3038
- };
3039
- }
3040
-
3041
- if (name === "listFigmaPages") {
3042
- const validatedArgs = ListFigmaPagesSchema.parse(args);
3043
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
3044
- if (!token) {
3045
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
3046
- }
3047
-
3048
- // Parse fileKey from URL if needed
3049
- const parsed = parseFigmaUrl(validatedArgs.fileKey);
3050
- const fileKey = parsed.fileKey;
3051
-
3052
- const result = await listFigmaPages(fileKey, token);
3053
-
3054
- return {
3055
- content: [
3056
- { type: 'text', text: JSON.stringify(result, null, 2) }
3057
- ]
3058
- };
3059
- }
3060
-
3061
- if (name === "searchFigmaFrames") {
3062
- const validatedArgs = SearchFigmaFramesSchema.parse(args);
3063
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
3064
- if (!token) {
3065
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
3066
- }
3067
-
3068
- // Parse fileKey from URL if needed
3069
- const parsed = parseFigmaUrl(validatedArgs.fileKey);
3070
- const fileKey = parsed.fileKey;
3071
-
3072
- const result = await searchFigmaFrames(fileKey, token, validatedArgs.searchQuery);
3073
-
3074
- return {
3075
- content: [
3076
- { type: 'text', text: JSON.stringify(result, null, 2) }
3077
- ]
3078
- };
3079
- }
3080
-
3081
- if (name === "getFigmaComponents") {
3082
- const validatedArgs = GetFigmaComponentsSchema.parse(args);
3083
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
3084
- if (!token) {
3085
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
3086
- }
3087
-
3088
- // Parse fileKey from URL if needed
3089
- const parsed = parseFigmaUrl(validatedArgs.fileKey);
3090
- const fileKey = parsed.fileKey;
3091
-
3092
- const result = await getFigmaComponents(fileKey, token);
3093
-
3094
- return {
3095
- content: [
3096
- { type: 'text', text: JSON.stringify(result, null, 2) }
3097
- ]
3098
- };
3099
- }
3100
-
3101
- if (name === "getFigmaStyles") {
3102
- const validatedArgs = GetFigmaStylesSchema.parse(args);
3103
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
3104
- if (!token) {
3105
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
3106
- }
3107
-
3108
- // Parse fileKey from URL if needed
3109
- const parsed = parseFigmaUrl(validatedArgs.fileKey);
3110
- const fileKey = parsed.fileKey;
3111
-
3112
- const result = await getFigmaStyles(fileKey, token);
3113
-
3114
- return {
3115
- content: [
3116
- { type: 'text', text: JSON.stringify(result, null, 2) }
3117
- ]
3118
- };
3119
- }
3120
-
3121
- if (name === "getFigmaColorPalette") {
3122
- const validatedArgs = GetFigmaColorPaletteSchema.parse(args);
3123
- const token = validatedArgs.figmaToken || FIGMA_TOKEN;
3124
- if (!token) {
3125
- throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
3126
- }
3127
-
3128
- // Parse fileKey from URL if needed
3129
- const parsed = parseFigmaUrl(validatedArgs.fileKey);
3130
- const fileKey = parsed.fileKey;
3131
-
3132
- const result = await getFigmaColorPalette(fileKey, token);
3133
-
3134
- return {
3135
- content: [
3136
- { type: 'text', text: JSON.stringify(result, null, 2) }
3137
- ]
3138
- };
3139
- }
3140
-
3141
- // New AI optimization tools
3142
- if (name === "smartFindElement") {
3143
- const validatedArgs = SmartFindElementSchema.parse(args);
3144
- const page = await getLastOpenPage();
3145
- const maxResults = validatedArgs.maxResults || 5;
3146
-
3147
- // Execute smart search in page context
3148
- const results = await page.evaluate((description, maxResults, utilsCode) => {
3149
- // Inject utilities into page context
3150
- eval(utilsCode);
3151
-
3152
- // Determine element type from description
3153
- const elementType = determineElementType(description);
3154
-
3155
- // Build candidate selectors based on element type
3156
- let candidates = [];
3157
-
3158
- if (elementType.type === 'input' || elementType.type === 'any') {
3159
- candidates.push(...document.querySelectorAll('input'));
3160
- candidates.push(...document.querySelectorAll('textarea'));
3161
- }
3162
-
3163
- if (elementType.type === 'button' || elementType.type === 'any') {
3164
- candidates.push(...document.querySelectorAll('button'));
3165
- candidates.push(...document.querySelectorAll('input[type="submit"]'));
3166
- candidates.push(...document.querySelectorAll('input[type="button"]'));
3167
- candidates.push(...document.querySelectorAll('[role="button"]'));
3168
- }
3169
-
3170
- if (elementType.type === 'link' || elementType.type === 'any') {
3171
- candidates.push(...document.querySelectorAll('a'));
3172
- }
3173
-
3174
- // Analyze each candidate
3175
- const analyzed = candidates.map(el => {
3176
- const context = analyzeButtonContextInPage(el);
3177
-
3178
- // Use appropriate scoring function based on element type
3179
- let score;
3180
- if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
3181
- score = scoreInputField(el, context, description);
3182
- } else {
3183
- score = scoreSubmitButton(el, context, description);
3184
- }
3185
-
3186
- const selector = getUniqueSelectorInPage(el);
3187
-
3188
- return {
3189
- selector,
3190
- text: context.text.substring(0, 100), // Limit text length
3191
- type: el.tagName.toLowerCase(),
3192
- score,
3193
- confidence: Math.min(Math.max(score / 100, 0), 1),
3194
- visible: context.isVisible,
3195
- reason: explainScore(el, context, description, score),
3196
- attributes: {
3197
- id: el.id || null,
3198
- class: el.className || null,
3199
- name: el.name || null,
3200
- type: el.type || null,
3201
- }
3202
- };
3203
- });
3204
-
3205
- // Filter and sort
3206
- return analyzed
3207
- .filter(r => r.score > 5) // Minimum threshold
3208
- .sort((a, b) => b.score - a.score)
3209
- .slice(0, maxResults);
3210
-
3211
- }, validatedArgs.description, maxResults, elementFinderUtils);
3212
-
3213
- const hints = {
3214
- totalCandidates: results.length,
3215
- bestMatch: results[0] || null,
3216
- suggestion: results.length > 0
3217
- ? `Use selector: ${results[0].selector}`
3218
- : 'No good matches found. Try a different description.',
3219
- };
3220
-
3221
- const response = {
3222
- candidates: results,
3223
- hints
3224
- };
3225
-
3226
- // Execute action if provided
3227
- if (validatedArgs.action && results.length > 0) {
3228
- const bestMatch = results[0];
3229
- try {
3230
- const actionResult = await executeElementAction(page, bestMatch.selector, validatedArgs.action);
3231
- response.actionExecuted = actionResult;
3232
-
3233
- // If screenshot was captured, add it to content
3234
- if (actionResult && actionResult.screenshot) {
3235
- return {
3236
- content: [
3237
- { type: 'text', text: JSON.stringify(response, null, 2) },
3238
- { type: 'image', data: actionResult.screenshot, mimeType: 'image/png' }
3239
- ]
3240
- };
3241
- }
3242
- } catch (error) {
3243
- response.actionError = error.message;
3244
- }
3245
- }
3246
-
3247
- return {
3248
- content: [{
3249
- type: 'text',
3250
- text: JSON.stringify(response, null, 2)
3251
- }]
3252
- };
3253
- }
3254
-
3255
- if (name === "analyzePage") {
3256
- const validatedArgs = AnalyzePageSchema.parse(args);
3257
- const page = await getLastOpenPage();
3258
- const pageUrl = page.url();
3259
-
3260
- // Check cache
3261
- if (!validatedArgs.refresh && pageAnalysisCache.has(pageUrl)) {
3262
- const cached = pageAnalysisCache.get(pageUrl);
3263
- return {
3264
- content: [{
3265
- type: 'text',
3266
- text: JSON.stringify({ ...cached, fromCache: true }, null, 2)
3267
- }]
3268
- };
3269
- }
3270
-
3271
- // Perform comprehensive analysis
3272
- const analysis = await page.evaluate((utilsCode) => {
3273
- // Inject utilities
3274
- eval(utilsCode);
3275
-
3276
- const result = {
3277
- url: window.location.href,
3278
- title: document.title,
3279
- forms: [],
3280
- interactiveElements: [],
3281
- inputs: [],
3282
- buttons: [],
3283
- links: [],
3284
- navigation: [],
3285
- };
3286
-
3287
- // Analyze forms
3288
- document.querySelectorAll('form').forEach((form, idx) => {
3289
- const formData = {
3290
- selector: form.id ? `#${form.id}` : `form:nth-of-type(${idx + 1})`,
3291
- action: form.action,
3292
- method: form.method,
3293
- fields: [],
3294
- submitButton: null,
3295
- };
3296
-
3297
- // Find fields
3298
- form.querySelectorAll('input, textarea, select').forEach(field => {
3299
- if (field.type === 'submit' || field.type === 'button') return;
3300
-
3301
- formData.fields.push({
3302
- selector: getUniqueSelectorInPage(field),
3303
- type: field.type || 'text',
3304
- name: field.name,
3305
- id: field.id,
3306
- placeholder: field.placeholder,
3307
- label: (() => {
3308
- const label = field.labels && field.labels[0];
3309
- return label ? label.textContent.trim() : null;
3310
- })(),
3311
- required: field.required,
3312
- });
3313
- });
3314
-
3315
- // Find submit button
3316
- const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
3317
- if (submitBtn) {
3318
- formData.submitButton = {
3319
- selector: getUniqueSelectorInPage(submitBtn),
3320
- text: submitBtn.textContent || submitBtn.value,
3321
- };
3322
- }
3323
-
3324
- result.forms.push(formData);
3325
- });
3326
-
3327
- // All buttons
3328
- document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"]').forEach(btn => {
3329
- if (btn.offsetWidth === 0 && btn.offsetHeight === 0) return; // Skip hidden
3330
-
3331
- result.buttons.push({
3332
- selector: getUniqueSelectorInPage(btn),
3333
- text: (btn.textContent || btn.value || '').trim().substring(0, 50),
3334
- type: btn.type || 'button',
3335
- inForm: btn.closest('form') !== null,
3336
- });
3337
- });
3338
-
3339
- // All inputs
3340
- document.querySelectorAll('input, textarea, select').forEach(input => {
3341
- if (input.type === 'submit' || input.type === 'button' || input.type === 'hidden') return;
3342
- if (input.offsetWidth === 0 && input.offsetHeight === 0) return;
3343
-
3344
- result.inputs.push({
3345
- selector: getUniqueSelectorInPage(input),
3346
- type: input.type || 'text',
3347
- name: input.name,
3348
- placeholder: input.placeholder,
3349
- });
3350
- });
3351
-
3352
- // All links
3353
- document.querySelectorAll('a[href]').forEach(link => {
3354
- if (link.offsetWidth === 0 && link.offsetHeight === 0) return;
3355
-
3356
- const text = link.textContent.trim().substring(0, 50);
3357
- if (!text) return;
3358
-
3359
- result.links.push({
3360
- selector: getUniqueSelectorInPage(link),
3361
- text,
3362
- href: link.href,
3363
- });
3364
- });
3365
-
3366
- // Navigation elements
3367
- document.querySelectorAll('nav a, [role="navigation"] a').forEach(link => {
3368
- result.navigation.push({
3369
- selector: getUniqueSelectorInPage(link),
3370
- text: link.textContent.trim().substring(0, 50),
3371
- href: link.href,
3372
- });
3373
- });
3374
-
3375
- // Interactive elements summary
3376
- document.querySelectorAll('button, a, input, select, textarea, [onclick], [role="button"]').forEach(el => {
3377
- if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
3378
-
3379
- const text = (el.textContent || el.value || el.getAttribute('aria-label') || '').trim();
3380
- if (!text) return;
3381
-
3382
- result.interactiveElements.push({
3383
- selector: getUniqueSelectorInPage(el),
3384
- type: el.tagName.toLowerCase(),
3385
- text: text.substring(0, 50),
3386
- });
3387
- });
3388
-
3389
- return result;
3390
- }, elementFinderUtils);
3391
-
3392
- // Cache the result
3393
- pageAnalysisCache.set(pageUrl, analysis);
3394
-
3395
- // Add hints
3396
- const hints = {
3397
- summary: `Found ${analysis.forms.length} forms, ${analysis.buttons.length} buttons, ${analysis.inputs.length} inputs, ${analysis.links.length} links`,
3398
- suggestion: analysis.forms.length > 0
3399
- ? `Start with form: ${analysis.forms[0].selector}`
3400
- : 'No forms found on this page',
3401
- };
3402
-
3403
- return {
3404
- content: [{
3405
- type: 'text',
3406
- text: JSON.stringify({ ...analysis, hints }, null, 2)
3407
- }]
3408
- };
3409
- }
3410
-
3411
- if (name === "getAllInteractiveElements") {
3412
- const validatedArgs = GetAllInteractiveElementsSchema.parse(args);
3413
- const page = await getLastOpenPage();
3414
-
3415
- const elements = await page.evaluate((includeHidden, utilsCode) => {
3416
- eval(utilsCode);
3417
-
3418
- const results = [];
3419
- const selector = 'button, a[href], input, select, textarea, [onclick], [role="button"], [tabindex]:not([tabindex="-1"])';
3420
-
3421
- document.querySelectorAll(selector).forEach(el => {
3422
- const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
3423
-
3424
- if (!includeHidden && !isVisible) return;
3425
-
3426
- const text = (el.textContent || el.value || el.getAttribute('aria-label') || el.placeholder || '').trim();
3427
-
3428
- results.push({
3429
- selector: getUniqueSelectorInPage(el),
3430
- type: el.tagName.toLowerCase(),
3431
- text: text.substring(0, 100),
3432
- visible: isVisible,
3433
- attributes: {
3434
- id: el.id || null,
3435
- class: el.className || null,
3436
- role: el.getAttribute('role') || null,
3437
- type: el.type || null,
3438
- }
3439
- });
3440
- });
3441
-
3442
- return results;
3443
- }, validatedArgs.includeHidden || false, elementFinderUtils);
3444
-
3445
- return {
3446
- content: [{
3447
- type: 'text',
3448
- text: JSON.stringify({
3449
- count: elements.length,
3450
- elements,
3451
- hints: {
3452
- suggestion: 'Use these selectors directly with click, type, or other tools'
3453
- }
3454
- }, null, 2)
3455
- }]
3456
- };
3457
- }
3458
-
3459
- if (name === "findElementsByText") {
3460
- const validatedArgs = FindElementsByTextSchema.parse(args);
3461
- const page = await getLastOpenPage();
3462
-
3463
- const elements = await page.evaluate((text, exact, caseSensitive, utilsCode) => {
3464
- eval(utilsCode);
3465
-
3466
- const results = [];
3467
- const searchText = caseSensitive ? text : text.toLowerCase();
3468
-
3469
- document.querySelectorAll('*').forEach(el => {
3470
- // Skip script, style, etc
3471
- if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'BR', 'HR'].includes(el.tagName)) return;
3472
-
3473
- // Get element's own text (not children)
3474
- let elementText = '';
3475
- for (const node of el.childNodes) {
3476
- if (node.nodeType === Node.TEXT_NODE) {
3477
- elementText += node.textContent;
3478
- }
3479
- }
3480
-
3481
- elementText = elementText.trim();
3482
- if (!elementText) return;
3483
-
3484
- const compareText = caseSensitive ? elementText : elementText.toLowerCase();
3485
-
3486
- const matches = exact
3487
- ? compareText === searchText
3488
- : compareText.includes(searchText);
3489
-
3490
- if (matches) {
3491
- results.push({
3492
- selector: getUniqueSelectorInPage(el),
3493
- type: el.tagName.toLowerCase(),
3494
- text: elementText.substring(0, 100), // Only first 100 chars for preview
3495
- visible: el.offsetParent !== null, // Add visibility check
3496
- });
3497
- }
3498
- });
3499
-
3500
- return results;
3501
- }, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils);
3502
-
3503
- // Prioritize visible elements and limit results to prevent token overflow
3504
- const visibleElements = elements.filter(el => el.visible);
3505
- const hiddenElements = elements.filter(el => !el.visible);
3506
- const limitedElements = [...visibleElements, ...hiddenElements].slice(0, 20); // Max 20 results
3507
-
3508
- const response = {
3509
- query: validatedArgs.text,
3510
- totalCount: elements.length,
3511
- visibleCount: visibleElements.length,
3512
- count: limitedElements.length,
3513
- elements: limitedElements,
3514
- truncated: elements.length > 20
3515
- };
3516
-
3517
- // Execute action if provided and elements found
3518
- if (validatedArgs.action && elements.length > 0) {
3519
- const firstMatch = elements[0];
3520
- try {
3521
- const actionResult = await executeElementAction(page, firstMatch.selector, validatedArgs.action);
3522
- response.actionExecuted = actionResult;
3523
-
3524
- // If screenshot was captured, add it to content
3525
- if (actionResult && actionResult.screenshot) {
3526
- return {
3527
- content: [
3528
- { type: 'text', text: JSON.stringify(response, null, 2) },
3529
- { type: 'image', data: actionResult.screenshot, mimeType: 'image/png' }
3530
- ]
3531
- };
3532
- }
3533
- } catch (error) {
3534
- response.actionError = error.message;
3535
- }
3536
- }
3537
-
3538
- return {
3539
- content: [{
3540
- type: 'text',
3541
- text: JSON.stringify(response, null, 2)
3542
- }]
3543
- };
3544
- }
3545
-
3546
- if (name === "enableRecorder") {
3547
- const page = await getLastOpenPage();
3548
- const result = await injectRecorder(page);
3549
-
3550
- // Track this page as having recorder enabled
3551
- if (result.success) {
3552
- pagesWithRecorder.add(page);
3553
- }
3554
-
3555
- return {
3556
- content: [{
3557
- type: 'text',
3558
- text: JSON.stringify(result.success ? {
3559
- success: true,
3560
- message: "Recorder UI injected into page. Click 'Start' to begin recording. Recorder will auto-reinject on page navigation/reload."
3561
- } : {
3562
- success: false,
3563
- error: result.error
3564
- }, null, 2)
3565
- }]
3566
- };
3567
- }
3568
-
3569
- if (name === "executeScenario") {
3570
- const page = await getLastOpenPage();
3571
- const options = {};
3572
-
3573
- // Pass executeDependencies option if provided
3574
- if (args.executeDependencies !== undefined) {
3575
- options.executeDependencies = args.executeDependencies;
3576
- }
3577
-
3578
- const result = await executeScenario(args.name, page, args.parameters || {}, options);
3579
-
3580
- return {
3581
- content: [{
3582
- type: 'text',
3583
- text: JSON.stringify(result, null, 2)
3584
- }]
3585
- };
3586
- }
3587
-
3588
- if (name === "listScenarios") {
3589
- const scenarios = await listScenarios();
3590
-
3591
- return {
3592
- content: [{
3593
- type: 'text',
3594
- text: JSON.stringify(scenarios, null, 2)
3595
- }]
3596
- };
3597
- }
3598
-
3599
- if (name === "searchScenarios") {
3600
- const results = await searchScenarios(args);
3601
-
3602
- return {
3603
- content: [{
3604
- type: 'text',
3605
- text: JSON.stringify(results, null, 2)
3606
- }]
3607
- };
3608
- }
3609
-
3610
- if (name === "getScenarioInfo") {
3611
- const scenario = await loadScenario(args.name, args.includeSecrets || false);
3612
-
3613
- return {
3614
- content: [{
3615
- type: 'text',
3616
- text: JSON.stringify(scenario, null, 2)
3617
- }]
3618
- };
3619
- }
3620
-
3621
- if (name === "deleteScenario") {
3622
- const result = await deleteScenario(args.name);
3623
-
3624
- return {
3625
- content: [{
3626
- type: 'text',
3627
- text: JSON.stringify(result, null, 2)
3628
- }]
3629
- };
3630
- }
3631
-
3632
- return {
3633
- content: [
3634
- {
3635
- type: "text",
3636
- text: `Unknown tool: ${name}`,
3637
- },
3638
- ],
3639
- isError: true,
3640
- };
3641
- } catch (error) {
3642
- return {
3643
- content: [
3644
- {
3645
- type: "text",
3646
- text: `Error: ${error.message}`,
3647
- },
3648
- ],
3649
- isError: true,
3650
- };
3651
- }
3652
- });
3653
-
3654
- // Start server
3655
- async function main() {
3656
- console.error("Starting chrometools-mcp server...");
3657
-
3658
- // Show environment info
3659
- if (isWSL) {
3660
- console.error("[chrometools-mcp] WSL environment detected");
3661
- console.error("[chrometools-mcp] GUI mode requires X server (DISPLAY=" + (process.env.DISPLAY || "not set") + ")");
3662
- }
3663
-
3664
- const transport = new StdioServerTransport();
3665
- await server.connect(transport);
3666
-
3667
- console.error("chrometools-mcp server running on stdio");
3668
- console.error("Browser will be initialized on first openBrowser call");
3669
- }
3670
-
3671
- main().catch((error) => {
3672
- console.error("Fatal error:", error);
3673
- process.exit(1);
3674
- });