chrometools-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +329 -0
  3. package/WSL_SETUP.md +270 -0
  4. package/index.js +1286 -0
  5. package/package.json +55 -0
package/index.js ADDED
@@ -0,0 +1,1286 @@
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
+
14
+ // Figma token from environment variable (can be set in MCP config)
15
+ const FIGMA_TOKEN = process.env.FIGMA_TOKEN || null;
16
+
17
+ // Global browser instance (persists between requests)
18
+ let browserPromise = null;
19
+ const openPages = new Map();
20
+ let lastPage = null;
21
+
22
+ // Console logs storage
23
+ const consoleLogs = [];
24
+
25
+ // Initialize browser (singleton)
26
+ async function getBrowser() {
27
+ if (!browserPromise) {
28
+ browserPromise = puppeteer.launch({
29
+ headless: false,
30
+ defaultViewport: null,
31
+ args: [
32
+ '--no-sandbox',
33
+ '--disable-setuid-sandbox',
34
+ '--disable-dev-shm-usage',
35
+ ],
36
+ });
37
+ console.error("[chrometools-mcp] Browser initialized (GUI mode)");
38
+ }
39
+ return browserPromise;
40
+ }
41
+
42
+ // Get or create page for URL
43
+ async function getOrCreatePage(url) {
44
+ const browser = await getBrowser();
45
+
46
+ // Check if page for this URL already exists
47
+ if (openPages.has(url)) {
48
+ const existingPage = openPages.get(url);
49
+ if (!existingPage.isClosed()) {
50
+ lastPage = existingPage;
51
+ return existingPage;
52
+ }
53
+ openPages.delete(url);
54
+ }
55
+
56
+ // Create new page
57
+ const page = await browser.newPage();
58
+
59
+ // Set up console log capture
60
+ const client = await page.target().createCDPSession();
61
+ await client.send('Runtime.enable');
62
+ await client.send('Log.enable');
63
+
64
+ client.on('Runtime.consoleAPICalled', (event) => {
65
+ const timestamp = new Date().toISOString();
66
+ const args = event.args.map(arg => {
67
+ if (arg.value !== undefined) return arg.value;
68
+ if (arg.description) return arg.description;
69
+ return String(arg);
70
+ });
71
+
72
+ consoleLogs.push({
73
+ type: event.type, // log, warn, error, info, debug
74
+ timestamp,
75
+ message: args.join(' '),
76
+ stackTrace: event.stackTrace
77
+ });
78
+ });
79
+
80
+ client.on('Log.entryAdded', (event) => {
81
+ const entry = event.entry;
82
+ consoleLogs.push({
83
+ type: entry.level, // verbose, info, warning, error
84
+ timestamp: new Date(entry.timestamp).toISOString(),
85
+ message: entry.text,
86
+ source: entry.source,
87
+ url: entry.url,
88
+ lineNumber: entry.lineNumber
89
+ });
90
+ });
91
+
92
+ await page.goto(url, { waitUntil: 'networkidle2' });
93
+ openPages.set(url, page);
94
+ lastPage = page;
95
+
96
+ return page;
97
+ }
98
+
99
+ // Get last opened page (for tools that don't need URL)
100
+ async function getLastOpenPage() {
101
+ if (!lastPage || lastPage.isClosed()) {
102
+ throw new Error('No page is currently open. Use openBrowser first to open a page.');
103
+ }
104
+ return lastPage;
105
+ }
106
+
107
+ // Figma API helper function
108
+ async function fetchFigmaAPI(endpoint, figmaToken) {
109
+ if (!figmaToken) {
110
+ throw new Error('Figma token is required. Get it from https://www.figma.com/developers/api#access-tokens');
111
+ }
112
+
113
+ const response = await fetch(`https://api.figma.com/v1/${endpoint}`, {
114
+ headers: {
115
+ 'X-Figma-Token': figmaToken
116
+ }
117
+ });
118
+
119
+ if (!response.ok) {
120
+ const error = await response.text();
121
+ throw new Error(`Figma API error: ${response.status} - ${error}`);
122
+ }
123
+
124
+ return response.json();
125
+ }
126
+
127
+ // Calculate SSIM (Structural Similarity Index) for image comparison
128
+ function calculateSSIM(img1Data, img2Data, width, height) {
129
+ if (img1Data.length !== img2Data.length) {
130
+ return 0;
131
+ }
132
+
133
+ const windowSize = 8;
134
+ const k1 = 0.01;
135
+ const k2 = 0.03;
136
+ const c1 = (k1 * 255) ** 2;
137
+ const c2 = (k2 * 255) ** 2;
138
+
139
+ let ssimSum = 0;
140
+ let validWindows = 0;
141
+
142
+ for (let y = 0; y <= height - windowSize; y += windowSize) {
143
+ for (let x = 0; x <= width - windowSize; x += windowSize) {
144
+ let sum1 = 0, sum2 = 0, sum1Sq = 0, sum2Sq = 0, sum12 = 0;
145
+
146
+ for (let dy = 0; dy < windowSize; dy++) {
147
+ for (let dx = 0; dx < windowSize; dx++) {
148
+ const idx = ((y + dy) * width + (x + dx)) * 4;
149
+ if (idx + 2 >= img1Data.length) continue;
150
+
151
+ const gray1 = (img1Data[idx] * 0.299 + img1Data[idx + 1] * 0.587 + img1Data[idx + 2] * 0.114);
152
+ const gray2 = (img2Data[idx] * 0.299 + img2Data[idx + 1] * 0.587 + img2Data[idx + 2] * 0.114);
153
+
154
+ sum1 += gray1;
155
+ sum2 += gray2;
156
+ sum1Sq += gray1 * gray1;
157
+ sum2Sq += gray2 * gray2;
158
+ sum12 += gray1 * gray2;
159
+ }
160
+ }
161
+
162
+ const n = windowSize * windowSize;
163
+ const mean1 = sum1 / n;
164
+ const mean2 = sum2 / n;
165
+ const variance1 = (sum1Sq / n) - (mean1 * mean1);
166
+ const variance2 = (sum2Sq / n) - (mean2 * mean2);
167
+ const covariance = (sum12 / n) - (mean1 * mean2);
168
+
169
+ const ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
170
+ ((mean1 * mean1 + mean2 * mean2 + c1) * (variance1 + variance2 + c2));
171
+
172
+ ssimSum += ssim;
173
+ validWindows++;
174
+ }
175
+ }
176
+
177
+ return validWindows > 0 ? ssimSum / validWindows : 0;
178
+ }
179
+
180
+ // Cleanup on exit
181
+ process.on("SIGINT", async () => {
182
+ if (browserPromise) {
183
+ const browser = await browserPromise;
184
+ await browser.close();
185
+ }
186
+ process.exit(0);
187
+ });
188
+
189
+ // Create MCP server
190
+ const server = new Server(
191
+ {
192
+ name: "chrometools-mcp",
193
+ version: "1.0.0",
194
+ },
195
+ {
196
+ capabilities: {
197
+ tools: {},
198
+ },
199
+ }
200
+ );
201
+
202
+ // Tool schemas
203
+ const PingSchema = z.object({
204
+ message: z.string().optional().describe("Optional message to send"),
205
+ });
206
+
207
+ const OpenBrowserSchema = z.object({
208
+ url: z.string().describe("URL to open in the browser"),
209
+ });
210
+
211
+ const ClickSchema = z.object({
212
+ selector: z.string().describe("CSS selector for element to click"),
213
+ waitAfter: z.number().optional().describe("Milliseconds to wait after click (default: 1500)"),
214
+ });
215
+
216
+ const TypeSchema = z.object({
217
+ selector: z.string().describe("CSS selector for input element"),
218
+ text: z.string().describe("Text to type"),
219
+ delay: z.number().optional().describe("Delay between keystrokes in ms (default: 0)"),
220
+ clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
221
+ });
222
+
223
+ const GetElementSchema = z.object({
224
+ selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
225
+ });
226
+
227
+ const GetComputedCssSchema = z.object({
228
+ selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
229
+ });
230
+
231
+ const GetBoxModelSchema = z.object({
232
+ selector: z.string().describe("CSS selector for element"),
233
+ });
234
+
235
+ const ScreenshotSchema = z.object({
236
+ selector: z.string().describe("CSS selector for element to screenshot"),
237
+ padding: z.number().optional().describe("Padding around element in pixels (default: 0)"),
238
+ });
239
+
240
+ const ScrollToSchema = z.object({
241
+ selector: z.string().describe("CSS selector for element to scroll to"),
242
+ behavior: z.enum(['auto', 'smooth']).optional().describe("Scroll behavior (default: auto)"),
243
+ });
244
+
245
+ const ExecuteScriptSchema = z.object({
246
+ script: z.string().describe("JavaScript code to execute in page context"),
247
+ waitAfter: z.number().optional().describe("Milliseconds to wait after execution (default: 500)"),
248
+ });
249
+
250
+ // Phase 2 schemas
251
+ const GetConsoleLogsSchema = z.object({
252
+ types: z.array(z.enum(['log', 'warn', 'error', 'info', 'debug', 'verbose', 'warning']))
253
+ .optional()
254
+ .describe("Filter by log types (default: all)"),
255
+ clear: z.boolean().optional().describe("Clear logs after reading (default: false)"),
256
+ });
257
+
258
+ const HoverSchema = z.object({
259
+ selector: z.string().describe("CSS selector for element to hover"),
260
+ });
261
+
262
+ const SetStylesSchema = z.object({
263
+ selector: z.string().describe("CSS selector for element to modify"),
264
+ styles: z.array(z.object({
265
+ name: z.string().describe("CSS property name (e.g., 'color')"),
266
+ value: z.string().describe("CSS property value (e.g., 'red')")
267
+ })).describe("Array of CSS property name-value pairs"),
268
+ });
269
+
270
+ const SetViewportSchema = z.object({
271
+ width: z.number().min(320).max(4000).describe("Viewport width in pixels (320-4000)"),
272
+ height: z.number().min(200).max(3000).describe("Viewport height in pixels (200-3000)"),
273
+ deviceScaleFactor: z.number().min(0.5).max(3).optional().describe("Device pixel ratio (0.5-3, default: 1)"),
274
+ });
275
+
276
+ const GetViewportSchema = z.object({});
277
+
278
+ const NavigateToSchema = z.object({
279
+ url: z.string().describe("URL to navigate to"),
280
+ waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2'])
281
+ .optional()
282
+ .describe("Wait until event (default: networkidle2)"),
283
+ });
284
+
285
+ // Figma tools schemas
286
+ const GetFigmaFrameSchema = z.object({
287
+ figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
288
+ fileKey: z.string().describe("Figma file key (from URL: figma.com/file/FILE_KEY/...)"),
289
+ nodeId: z.string().describe("Figma node ID (frame/component ID)"),
290
+ scale: z.number().min(0.1).max(4).optional().describe("Export scale (0.1-4, default: 2)"),
291
+ format: z.enum(['png', 'jpg', 'svg']).optional().describe("Export format (default: png)")
292
+ });
293
+
294
+ const CompareFigmaToElementSchema = z.object({
295
+ figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
296
+ fileKey: z.string().describe("Figma file key"),
297
+ nodeId: z.string().describe("Figma frame/component ID"),
298
+ selector: z.string().describe("CSS selector for page element"),
299
+ threshold: z.number().min(0).max(1).optional().describe("Difference threshold (0-1, default: 0.05)"),
300
+ figmaScale: z.number().min(0.1).max(4).optional().describe("Figma export scale (default: 2)")
301
+ });
302
+
303
+ const GetFigmaSpecsSchema = z.object({
304
+ figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
305
+ fileKey: z.string().describe("Figma file key"),
306
+ nodeId: z.string().describe("Figma frame/component ID")
307
+ });
308
+
309
+ // List available tools
310
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
311
+ return {
312
+ tools: [
313
+ {
314
+ name: "ping",
315
+ description: "Simple ping-pong tool for testing. Returns 'pong' with optional message.",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ message: { type: "string", description: "Optional message to include in response" },
320
+ },
321
+ },
322
+ },
323
+ {
324
+ name: "openBrowser",
325
+ 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.",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ url: { type: "string", description: "URL to navigate to (e.g., https://example.com)" },
330
+ },
331
+ required: ["url"],
332
+ },
333
+ },
334
+ {
335
+ name: "click",
336
+ description: "Click on an element to trigger interactions like opening modals, navigating, or submitting forms. Waits for animations and returns a screenshot showing the result.",
337
+ inputSchema: {
338
+ type: "object",
339
+ properties: {
340
+ selector: { type: "string", description: "CSS selector for element to click" },
341
+ waitAfter: { type: "number", description: "Milliseconds to wait after click (default: 1500)" },
342
+ },
343
+ required: ["selector"],
344
+ },
345
+ },
346
+ {
347
+ name: "type",
348
+ 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.",
349
+ inputSchema: {
350
+ type: "object",
351
+ properties: {
352
+ selector: { type: "string", description: "CSS selector for input element" },
353
+ text: { type: "string", description: "Text to type" },
354
+ delay: { type: "number", description: "Delay between keystrokes in ms (default: 0)" },
355
+ clearFirst: { type: "boolean", description: "Clear field before typing (default: true)" },
356
+ },
357
+ required: ["selector", "text"],
358
+ },
359
+ },
360
+ {
361
+ name: "getElement",
362
+ description: "Get the HTML markup of an element for inspection and debugging. If no selector is provided, returns the entire <body> element. Useful for understanding component structure.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ selector: { type: "string", description: "CSS selector (optional, defaults to body)" },
367
+ },
368
+ },
369
+ },
370
+ {
371
+ name: "getComputedCss",
372
+ 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.",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ selector: { type: "string", description: "CSS selector (optional, defaults to body)" },
377
+ },
378
+ },
379
+ },
380
+ {
381
+ name: "getBoxModel",
382
+ description: "Get precise element dimensions, positioning, margins, padding, and borders. Returns complete box model data including content, padding, border, and margin dimensions.",
383
+ inputSchema: {
384
+ type: "object",
385
+ properties: {
386
+ selector: { type: "string", description: "CSS selector for element" },
387
+ },
388
+ required: ["selector"],
389
+ },
390
+ },
391
+ {
392
+ name: "screenshot",
393
+ description: "Capture a PNG screenshot of a specific element. Perfect for visual documentation, design reviews, and debugging. Supports optional padding to include surrounding context.",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ selector: { type: "string", description: "CSS selector for element to screenshot" },
398
+ padding: { type: "number", description: "Padding around element in pixels (default: 0)" },
399
+ },
400
+ required: ["selector"],
401
+ },
402
+ },
403
+ {
404
+ name: "scrollTo",
405
+ 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.",
406
+ inputSchema: {
407
+ type: "object",
408
+ properties: {
409
+ selector: { type: "string", description: "CSS selector for element to scroll to" },
410
+ behavior: { type: "string", enum: ["auto", "smooth"], description: "Scroll behavior (default: auto)" },
411
+ },
412
+ required: ["selector"],
413
+ },
414
+ },
415
+ {
416
+ name: "executeScript",
417
+ description: "Execute arbitrary JavaScript code in the page context. Perfect for complex interactions, setting values, triggering events, or any custom page manipulation. Returns execution result and a screenshot.",
418
+ inputSchema: {
419
+ type: "object",
420
+ properties: {
421
+ script: { type: "string", description: "JavaScript code to execute" },
422
+ waitAfter: { type: "number", description: "Milliseconds to wait after execution (default: 500)" },
423
+ },
424
+ required: ["script"],
425
+ },
426
+ },
427
+ {
428
+ name: "getConsoleLogs",
429
+ 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.",
430
+ inputSchema: {
431
+ type: "object",
432
+ properties: {
433
+ types: { type: "array", items: { type: "string", enum: ["log", "warn", "error", "info", "debug", "verbose", "warning"] }, description: "Filter by log types (default: all)" },
434
+ clear: { type: "boolean", description: "Clear logs after reading (default: false)" },
435
+ },
436
+ },
437
+ },
438
+ {
439
+ name: "hover",
440
+ description: "Simulate mouse hover over an element to test hover effects, tooltips, dropdown menus, and interactive states. Essential for testing CSS :hover pseudo-classes.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ selector: { type: "string", description: "CSS selector for element to hover" },
445
+ },
446
+ required: ["selector"],
447
+ },
448
+ },
449
+ {
450
+ name: "setStyles",
451
+ description: "Apply inline CSS styles to an element for live editing and prototyping. Perfect for testing design changes without modifying source code.",
452
+ inputSchema: {
453
+ type: "object",
454
+ properties: {
455
+ selector: { type: "string", description: "CSS selector for element to modify" },
456
+ styles: {
457
+ type: "array",
458
+ items: {
459
+ type: "object",
460
+ properties: {
461
+ name: { type: "string", description: "CSS property name" },
462
+ value: { type: "string", description: "CSS property value" },
463
+ },
464
+ required: ["name", "value"],
465
+ },
466
+ description: "Array of CSS property name-value pairs",
467
+ },
468
+ },
469
+ required: ["selector", "styles"],
470
+ },
471
+ },
472
+ {
473
+ name: "setViewport",
474
+ description: "Change viewport dimensions for responsive design testing. Test how your layout adapts to different screen sizes, mobile devices, tablets, and desktop resolutions.",
475
+ inputSchema: {
476
+ type: "object",
477
+ properties: {
478
+ width: { type: "number", minimum: 320, maximum: 4000, description: "Viewport width in pixels" },
479
+ height: { type: "number", minimum: 200, maximum: 3000, description: "Viewport height in pixels" },
480
+ deviceScaleFactor: { type: "number", minimum: 0.5, maximum: 3, description: "Device pixel ratio (default: 1)" },
481
+ },
482
+ required: ["width", "height"],
483
+ },
484
+ },
485
+ {
486
+ name: "getViewport",
487
+ description: "Get current viewport size and device pixel ratio. Essential for responsive design testing and understanding how content fits on different screen sizes.",
488
+ inputSchema: {
489
+ type: "object",
490
+ properties: {},
491
+ },
492
+ },
493
+ {
494
+ name: "navigateTo",
495
+ 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.",
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: {
499
+ url: { type: "string", description: "URL to navigate to" },
500
+ waitUntil: { type: "string", enum: ["load", "domcontentloaded", "networkidle0", "networkidle2"], description: "Wait until event (default: networkidle2)" },
501
+ },
502
+ required: ["url"],
503
+ },
504
+ },
505
+ {
506
+ name: "getFigmaFrame",
507
+ description: "Export and download a Figma frame as PNG image for comparison. Requires Figma API token and file/node IDs from Figma URLs.",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: {
511
+ figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
512
+ fileKey: { type: "string", description: "Figma file key (from URL: figma.com/file/FILE_KEY/...)" },
513
+ nodeId: { type: "string", description: "Figma node ID (frame/component ID)" },
514
+ scale: { type: "number", minimum: 0.1, maximum: 4, description: "Export scale (0.1-4, default: 2)" },
515
+ format: { type: "string", enum: ["png", "jpg", "svg"], description: "Export format (default: png)" },
516
+ },
517
+ required: ["fileKey", "nodeId"],
518
+ },
519
+ },
520
+ {
521
+ name: "compareFigmaToElement",
522
+ 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.",
523
+ inputSchema: {
524
+ type: "object",
525
+ properties: {
526
+ figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
527
+ fileKey: { type: "string", description: "Figma file key" },
528
+ nodeId: { type: "string", description: "Figma frame/component ID" },
529
+ selector: { type: "string", description: "CSS selector for page element" },
530
+ threshold: { type: "number", minimum: 0, maximum: 1, description: "Difference threshold (0-1, default: 0.05)" },
531
+ figmaScale: { type: "number", minimum: 0.1, maximum: 4, description: "Figma export scale (default: 2)" },
532
+ },
533
+ required: ["fileKey", "nodeId", "selector"],
534
+ },
535
+ },
536
+ {
537
+ name: "getFigmaSpecs",
538
+ description: "Extract detailed design specifications from Figma including colors, fonts, dimensions, and spacing. Perfect for design-to-code comparison.",
539
+ inputSchema: {
540
+ type: "object",
541
+ properties: {
542
+ figmaToken: { type: "string", description: "Figma API token (optional if FIGMA_TOKEN env var is set)" },
543
+ fileKey: { type: "string", description: "Figma file key" },
544
+ nodeId: { type: "string", description: "Figma frame/component ID" },
545
+ },
546
+ required: ["fileKey", "nodeId"],
547
+ },
548
+ },
549
+ ],
550
+ };
551
+ });
552
+
553
+ // Handle tool calls
554
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
555
+ const { name, arguments: args } = request.params;
556
+
557
+ try {
558
+ if (name === "ping") {
559
+ const validatedArgs = PingSchema.parse(args);
560
+ const responseMessage = validatedArgs.message
561
+ ? `pong: ${validatedArgs.message}`
562
+ : "pong";
563
+
564
+ return {
565
+ content: [
566
+ {
567
+ type: "text",
568
+ text: responseMessage,
569
+ },
570
+ ],
571
+ };
572
+ }
573
+
574
+ if (name === "openBrowser") {
575
+ const validatedArgs = OpenBrowserSchema.parse(args);
576
+ const page = await getOrCreatePage(validatedArgs.url);
577
+ const title = await page.title();
578
+
579
+ return {
580
+ content: [
581
+ {
582
+ type: "text",
583
+ text: `Browser opened successfully!\nURL: ${validatedArgs.url}\nPage title: ${title}\n\nBrowser remains open for interaction.`,
584
+ },
585
+ ],
586
+ };
587
+ }
588
+
589
+ if (name === "click") {
590
+ const validatedArgs = ClickSchema.parse(args);
591
+ const page = await getLastOpenPage();
592
+
593
+ const element = await page.$(validatedArgs.selector);
594
+ if (!element) {
595
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
596
+ }
597
+
598
+ await element.click();
599
+ await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 1500));
600
+
601
+ const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
602
+
603
+ return {
604
+ content: [
605
+ { type: "text", text: `Clicked: ${validatedArgs.selector}` },
606
+ { type: "image", data: screenshot, mimeType: "image/png" }
607
+ ],
608
+ };
609
+ }
610
+
611
+ if (name === "type") {
612
+ const validatedArgs = TypeSchema.parse(args);
613
+ const page = await getLastOpenPage();
614
+
615
+ const element = await page.$(validatedArgs.selector);
616
+ if (!element) {
617
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
618
+ }
619
+
620
+ const clearFirst = validatedArgs.clearFirst !== undefined ? validatedArgs.clearFirst : true;
621
+ if (clearFirst) {
622
+ await element.click({ clickCount: 3 });
623
+ await page.keyboard.press('Backspace');
624
+ }
625
+
626
+ await element.type(validatedArgs.text, { delay: validatedArgs.delay || 0 });
627
+
628
+ return {
629
+ content: [
630
+ { type: "text", text: `Typed "${validatedArgs.text}" into ${validatedArgs.selector}` }
631
+ ],
632
+ };
633
+ }
634
+
635
+ if (name === "getElement") {
636
+ const validatedArgs = GetElementSchema.parse(args);
637
+ const page = await getLastOpenPage();
638
+
639
+ const client = await page.target().createCDPSession();
640
+ await client.send('DOM.enable');
641
+
642
+ const { root } = await client.send('DOM.getDocument');
643
+ const useSelector = (validatedArgs.selector && validatedArgs.selector.trim()) ? validatedArgs.selector : 'body';
644
+
645
+ const { nodeId } = await client.send('DOM.querySelector', {
646
+ selector: useSelector,
647
+ nodeId: root.nodeId
648
+ });
649
+
650
+ if (!nodeId) {
651
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
652
+ }
653
+
654
+ const { outerHTML } = await client.send('DOM.getOuterHTML', { nodeId });
655
+
656
+ return {
657
+ content: [{ type: "text", text: outerHTML }],
658
+ };
659
+ }
660
+
661
+ if (name === "getComputedCss") {
662
+ const validatedArgs = GetComputedCssSchema.parse(args);
663
+ const page = await getLastOpenPage();
664
+
665
+ const client = await page.target().createCDPSession();
666
+ await client.send('DOM.enable');
667
+ await client.send('CSS.enable');
668
+
669
+ const { root } = await client.send('DOM.getDocument');
670
+ const useSelector = (validatedArgs.selector && validatedArgs.selector.trim()) ? validatedArgs.selector : 'body';
671
+
672
+ const { nodeId } = await client.send('DOM.querySelector', {
673
+ selector: useSelector,
674
+ nodeId: root.nodeId
675
+ });
676
+
677
+ if (!nodeId) {
678
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
679
+ }
680
+
681
+ const { computedStyle } = await client.send('CSS.getComputedStyleForNode', { nodeId });
682
+
683
+ return {
684
+ content: [{ type: "text", text: JSON.stringify(computedStyle, null, 2) }],
685
+ };
686
+ }
687
+
688
+ if (name === "getBoxModel") {
689
+ const validatedArgs = GetBoxModelSchema.parse(args);
690
+ const page = await getLastOpenPage();
691
+
692
+ const client = await page.target().createCDPSession();
693
+ await client.send('DOM.enable');
694
+
695
+ const { root } = await client.send('DOM.getDocument');
696
+ const { nodeId } = await client.send('DOM.querySelector', {
697
+ selector: validatedArgs.selector,
698
+ nodeId: root.nodeId
699
+ });
700
+
701
+ if (!nodeId) {
702
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
703
+ }
704
+
705
+ const boxModel = await client.send('DOM.getBoxModel', { nodeId });
706
+ const metrics = await page.evaluate((sel) => {
707
+ const el = document.querySelector(sel);
708
+ if (!el) return null;
709
+ return {
710
+ offsetWidth: el.offsetWidth,
711
+ offsetHeight: el.offsetHeight,
712
+ scrollWidth: el.scrollWidth,
713
+ scrollHeight: el.scrollHeight
714
+ };
715
+ }, validatedArgs.selector);
716
+
717
+ if (!metrics) {
718
+ throw new Error(`Element not found (render): ${validatedArgs.selector}`);
719
+ }
720
+
721
+ return {
722
+ content: [{ type: "text", text: JSON.stringify({ boxModel, metrics }, null, 2) }],
723
+ };
724
+ }
725
+
726
+ if (name === "screenshot") {
727
+ const validatedArgs = ScreenshotSchema.parse(args);
728
+ const page = await getLastOpenPage();
729
+
730
+ const element = await page.$(validatedArgs.selector);
731
+ if (!element) {
732
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
733
+ }
734
+
735
+ const box = await element.boundingBox();
736
+ if (!box) {
737
+ throw new Error(`Element is not visible or has no bounding box: ${validatedArgs.selector}`);
738
+ }
739
+
740
+ const padding = validatedArgs.padding || 0;
741
+ const clip = {
742
+ x: Math.max(box.x - padding, 0),
743
+ y: Math.max(box.y - padding, 0),
744
+ width: Math.max(box.width + padding * 2, 1),
745
+ height: Math.max(box.height + padding * 2, 1)
746
+ };
747
+
748
+ const screenshot = await page.screenshot({ clip, encoding: 'base64' });
749
+
750
+ return {
751
+ content: [
752
+ {
753
+ type: "image",
754
+ data: screenshot,
755
+ mimeType: "image/png"
756
+ }
757
+ ],
758
+ };
759
+ }
760
+
761
+ if (name === "scrollTo") {
762
+ const validatedArgs = ScrollToSchema.parse(args);
763
+ const page = await getLastOpenPage();
764
+
765
+ const element = await page.$(validatedArgs.selector);
766
+ if (!element) {
767
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
768
+ }
769
+
770
+ await element.scrollIntoView({ behavior: validatedArgs.behavior || 'auto' });
771
+ await new Promise(resolve => setTimeout(resolve, 300));
772
+
773
+ const position = await page.evaluate(() => ({
774
+ x: window.scrollX,
775
+ y: window.scrollY
776
+ }));
777
+
778
+ return {
779
+ content: [
780
+ { type: "text", text: `Scrolled to ${validatedArgs.selector} (position: ${position.x}, ${position.y})` }
781
+ ],
782
+ };
783
+ }
784
+
785
+ if (name === "executeScript") {
786
+ const validatedArgs = ExecuteScriptSchema.parse(args);
787
+ const page = await getLastOpenPage();
788
+
789
+ const result = await page.evaluate((code) => {
790
+ try {
791
+ // eslint-disable-next-line no-eval
792
+ const evalResult = eval(code);
793
+ return { success: true, result: evalResult };
794
+ } catch (error) {
795
+ return { success: false, error: error.message };
796
+ }
797
+ }, validatedArgs.script);
798
+
799
+ await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 500));
800
+
801
+ const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
802
+
803
+ return {
804
+ content: [
805
+ {
806
+ type: "text",
807
+ text: result.success
808
+ ? `Script executed successfully.\nResult: ${JSON.stringify(result.result)}`
809
+ : `Script execution failed: ${result.error}`
810
+ },
811
+ { type: "image", data: screenshot, mimeType: "image/png" }
812
+ ],
813
+ };
814
+ }
815
+
816
+ if (name === "getConsoleLogs") {
817
+ const validatedArgs = GetConsoleLogsSchema.parse(args);
818
+
819
+ let logs = consoleLogs;
820
+
821
+ // Filter by types if specified
822
+ if (validatedArgs.types && validatedArgs.types.length > 0) {
823
+ logs = logs.filter(log => validatedArgs.types.includes(log.type));
824
+ }
825
+
826
+ const result = {
827
+ count: logs.length,
828
+ logs: logs.map(log => ({
829
+ type: log.type,
830
+ timestamp: log.timestamp,
831
+ message: log.message
832
+ }))
833
+ };
834
+
835
+ // Clear logs if requested
836
+ if (validatedArgs.clear) {
837
+ consoleLogs.length = 0;
838
+ }
839
+
840
+ return {
841
+ content: [{
842
+ type: "text",
843
+ text: JSON.stringify(result, null, 2)
844
+ }],
845
+ };
846
+ }
847
+
848
+ if (name === "hover") {
849
+ const validatedArgs = HoverSchema.parse(args);
850
+ const page = await getLastOpenPage();
851
+
852
+ const element = await page.$(validatedArgs.selector);
853
+ if (!element) {
854
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
855
+ }
856
+
857
+ await element.hover();
858
+ await new Promise(resolve => setTimeout(resolve, 100));
859
+
860
+ return {
861
+ content: [{
862
+ type: "text",
863
+ text: `Hovered over: ${validatedArgs.selector}`
864
+ }],
865
+ };
866
+ }
867
+
868
+ if (name === "setStyles") {
869
+ const validatedArgs = SetStylesSchema.parse(args);
870
+ const page = await getLastOpenPage();
871
+
872
+ const stylesObject = {};
873
+ for (const style of validatedArgs.styles) {
874
+ stylesObject[style.name] = style.value;
875
+ }
876
+
877
+ const success = await page.evaluate((sel, styles) => {
878
+ const el = document.querySelector(sel);
879
+ if (!el) return false;
880
+ Object.entries(styles).forEach(([key, value]) => {
881
+ el.style.setProperty(key, value);
882
+ });
883
+ return true;
884
+ }, validatedArgs.selector, stylesObject);
885
+
886
+ if (!success) {
887
+ throw new Error(`Element not found: ${validatedArgs.selector}`);
888
+ }
889
+
890
+ return {
891
+ content: [{
892
+ type: "text",
893
+ text: `Styles applied to ${validatedArgs.selector}:\n${JSON.stringify(stylesObject, null, 2)}`
894
+ }],
895
+ };
896
+ }
897
+
898
+ if (name === "setViewport") {
899
+ const validatedArgs = SetViewportSchema.parse(args);
900
+ const page = await getLastOpenPage();
901
+
902
+ await page.setViewport({
903
+ width: validatedArgs.width,
904
+ height: validatedArgs.height,
905
+ deviceScaleFactor: validatedArgs.deviceScaleFactor || 1
906
+ });
907
+
908
+ const actual = await page.evaluate(() => ({
909
+ width: window.innerWidth,
910
+ height: window.innerHeight,
911
+ devicePixelRatio: window.devicePixelRatio
912
+ }));
913
+
914
+ return {
915
+ content: [{
916
+ type: "text",
917
+ text: `Viewport set to ${validatedArgs.width}x${validatedArgs.height}\nActual: ${actual.width}x${actual.height} (DPR: ${actual.devicePixelRatio})`
918
+ }],
919
+ };
920
+ }
921
+
922
+ if (name === "getViewport") {
923
+ const page = await getLastOpenPage();
924
+
925
+ const viewport = await page.evaluate(() => ({
926
+ width: window.innerWidth,
927
+ height: window.innerHeight,
928
+ outerWidth: window.outerWidth,
929
+ outerHeight: window.outerHeight,
930
+ devicePixelRatio: window.devicePixelRatio
931
+ }));
932
+
933
+ return {
934
+ content: [{
935
+ type: "text",
936
+ text: JSON.stringify(viewport, null, 2)
937
+ }],
938
+ };
939
+ }
940
+
941
+ if (name === "navigateTo") {
942
+ const validatedArgs = NavigateToSchema.parse(args);
943
+ const page = await getOrCreatePage(validatedArgs.url);
944
+
945
+ if (validatedArgs.waitUntil) {
946
+ await page.goto(validatedArgs.url, { waitUntil: validatedArgs.waitUntil });
947
+ }
948
+
949
+ const title = await page.title();
950
+
951
+ return {
952
+ content: [{
953
+ type: "text",
954
+ text: `Navigated to: ${validatedArgs.url}\nPage title: ${title}`
955
+ }],
956
+ };
957
+ }
958
+
959
+ // Figma tools
960
+ if (name === "getFigmaFrame") {
961
+ const validatedArgs = GetFigmaFrameSchema.parse(args);
962
+ const token = validatedArgs.figmaToken || FIGMA_TOKEN;
963
+ if (!token) {
964
+ throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
965
+ }
966
+
967
+ const scale = validatedArgs.scale || 2;
968
+ const format = validatedArgs.format || 'png';
969
+
970
+ // Get export URL from Figma
971
+ const exportData = await fetchFigmaAPI(
972
+ `images/${validatedArgs.fileKey}?ids=${validatedArgs.nodeId}&scale=${scale}&format=${format}`,
973
+ token
974
+ );
975
+
976
+ if (!exportData.images || !exportData.images[validatedArgs.nodeId]) {
977
+ throw new Error(`Failed to export node ${validatedArgs.nodeId} from file ${validatedArgs.fileKey}`);
978
+ }
979
+
980
+ const imageUrl = exportData.images[validatedArgs.nodeId];
981
+
982
+ // Download image
983
+ const imageResponse = await fetch(imageUrl);
984
+ if (!imageResponse.ok) {
985
+ throw new Error(`Failed to download image: ${imageResponse.status}`);
986
+ }
987
+
988
+ const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
989
+
990
+ // Get frame info
991
+ const nodesData = await fetchFigmaAPI(`files/${validatedArgs.fileKey}/nodes?ids=${encodeURIComponent(validatedArgs.nodeId)}`, token);
992
+ const frameInfo = nodesData.nodes?.[validatedArgs.nodeId]?.document;
993
+
994
+ const result = {
995
+ figmaInfo: {
996
+ fileName: nodesData.name || 'Unknown',
997
+ frameId: validatedArgs.nodeId,
998
+ frameName: frameInfo?.name || 'Unknown',
999
+ dimensions: frameInfo ? {
1000
+ width: frameInfo.absoluteBoundingBox?.width,
1001
+ height: frameInfo.absoluteBoundingBox?.height
1002
+ } : null,
1003
+ exportSettings: {
1004
+ scale,
1005
+ format,
1006
+ fileSize: imageBuffer.length
1007
+ }
1008
+ }
1009
+ };
1010
+
1011
+ return {
1012
+ content: [
1013
+ { type: 'text', text: JSON.stringify(result, null, 2) },
1014
+ {
1015
+ type: 'image',
1016
+ data: imageBuffer.toString('base64'),
1017
+ mimeType: `image/${format}`
1018
+ }
1019
+ ]
1020
+ };
1021
+ }
1022
+
1023
+ if (name === "compareFigmaToElement") {
1024
+ const validatedArgs = CompareFigmaToElementSchema.parse(args);
1025
+ const token = validatedArgs.figmaToken || FIGMA_TOKEN;
1026
+ if (!token) {
1027
+ throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
1028
+ }
1029
+
1030
+ const page = await getLastOpenPage();
1031
+ const figmaScale = validatedArgs.figmaScale || 2;
1032
+ const threshold = validatedArgs.threshold || 0.05;
1033
+
1034
+ // Get Figma image
1035
+ const exportData = await fetchFigmaAPI(
1036
+ `images/${validatedArgs.fileKey}?ids=${validatedArgs.nodeId}&scale=${figmaScale}&format=png`,
1037
+ token
1038
+ );
1039
+
1040
+ if (!exportData.images || !exportData.images[validatedArgs.nodeId]) {
1041
+ throw new Error(`Failed to export Figma node ${validatedArgs.nodeId}`);
1042
+ }
1043
+
1044
+ const figmaImageUrl = exportData.images[validatedArgs.nodeId];
1045
+ const figmaResponse = await fetch(figmaImageUrl);
1046
+ const figmaBuffer = Buffer.from(await figmaResponse.arrayBuffer());
1047
+
1048
+ // Get page element screenshot
1049
+ const element = await page.$(validatedArgs.selector);
1050
+ if (!element) {
1051
+ throw new Error(`Selector not found: ${validatedArgs.selector}`);
1052
+ }
1053
+
1054
+ const pageBuffer = await element.screenshot();
1055
+
1056
+ // Load images for comparison
1057
+ const [figmaImg, pageImg] = await Promise.all([
1058
+ Jimp.read(figmaBuffer),
1059
+ Jimp.read(pageBuffer)
1060
+ ]);
1061
+
1062
+ // Resize to same dimensions (use larger dimensions)
1063
+ const targetWidth = Math.max(figmaImg.bitmap.width, pageImg.bitmap.width);
1064
+ const targetHeight = Math.max(figmaImg.bitmap.height, pageImg.bitmap.height);
1065
+
1066
+ figmaImg.resize(targetWidth, targetHeight);
1067
+ pageImg.resize(targetWidth, targetHeight);
1068
+
1069
+ // Compare images
1070
+ const figmaData = new Uint8ClampedArray(figmaImg.bitmap.data);
1071
+ const pageData = new Uint8ClampedArray(pageImg.bitmap.data);
1072
+ const diffData = new Uint8ClampedArray(targetWidth * targetHeight * 4);
1073
+
1074
+ const diffPixels = pixelmatch(figmaData, pageData, diffData, targetWidth, targetHeight, {
1075
+ threshold: 0.1,
1076
+ includeAA: false
1077
+ });
1078
+
1079
+ const ssimValue = calculateSSIM(figmaData, pageData, targetWidth, targetHeight);
1080
+ const totalPixels = targetWidth * targetHeight;
1081
+ const differencePercent = (diffPixels / totalPixels) * 100;
1082
+
1083
+ // Analysis
1084
+ const analysis = {
1085
+ figmaVsPage: {
1086
+ identical: diffPixels === 0,
1087
+ withinThreshold: differencePercent <= (threshold * 100),
1088
+ pixelDifferences: diffPixels,
1089
+ differencePercent: Math.round(differencePercent * 100) / 100,
1090
+ ssim: Math.round(ssimValue * 10000) / 10000,
1091
+ recommendation: differencePercent < 1 ? 'Pixel-perfect match' :
1092
+ differencePercent < 3 ? 'Very close to design' :
1093
+ differencePercent < 10 ? 'Minor differences detected' :
1094
+ 'Significant differences from design'
1095
+ },
1096
+ dimensions: {
1097
+ figma: { width: figmaImg.bitmap.width, height: figmaImg.bitmap.height },
1098
+ page: { width: pageImg.bitmap.width, height: pageImg.bitmap.height },
1099
+ comparison: { width: targetWidth, height: targetHeight }
1100
+ }
1101
+ };
1102
+
1103
+ const content = [
1104
+ { type: 'text', text: JSON.stringify(analysis, null, 2) },
1105
+ { type: 'image', data: figmaBuffer.toString('base64'), mimeType: 'image/png' },
1106
+ { type: 'image', data: pageBuffer.toString('base64'), mimeType: 'image/png' }
1107
+ ];
1108
+
1109
+ // Add difference map if there are differences
1110
+ if (diffPixels > 0) {
1111
+ const diffImg = new Jimp({ data: Buffer.from(diffData), width: targetWidth, height: targetHeight });
1112
+ const diffBuffer = await diffImg.getBufferAsync(Jimp.MIME_PNG);
1113
+ content.push({
1114
+ type: 'image',
1115
+ data: diffBuffer.toString('base64'),
1116
+ mimeType: 'image/png'
1117
+ });
1118
+ }
1119
+
1120
+ return { content };
1121
+ }
1122
+
1123
+ if (name === "getFigmaSpecs") {
1124
+ const validatedArgs = GetFigmaSpecsSchema.parse(args);
1125
+ const token = validatedArgs.figmaToken || FIGMA_TOKEN;
1126
+ if (!token) {
1127
+ throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
1128
+ }
1129
+
1130
+ // Get specific node via nodes API
1131
+ const nodesData = await fetchFigmaAPI(`files/${validatedArgs.fileKey}/nodes?ids=${encodeURIComponent(validatedArgs.nodeId)}`, token);
1132
+
1133
+ if (!nodesData.nodes || !nodesData.nodes[validatedArgs.nodeId]) {
1134
+ throw new Error(`Node ${validatedArgs.nodeId} not found in Figma file`);
1135
+ }
1136
+
1137
+ const node = nodesData.nodes[validatedArgs.nodeId].document;
1138
+
1139
+ // Extract specifications
1140
+ const specs = {
1141
+ general: {
1142
+ name: node.name,
1143
+ type: node.type,
1144
+ visible: node.visible !== false
1145
+ },
1146
+ dimensions: node.absoluteBoundingBox ? {
1147
+ width: node.absoluteBoundingBox.width,
1148
+ height: node.absoluteBoundingBox.height,
1149
+ x: node.absoluteBoundingBox.x,
1150
+ y: node.absoluteBoundingBox.y
1151
+ } : null,
1152
+ styling: {},
1153
+ children: []
1154
+ };
1155
+
1156
+ // Analyze styles
1157
+ if (node.fills && node.fills.length > 0) {
1158
+ specs.styling.fills = node.fills.map(fill => {
1159
+ if (fill.type === 'SOLID') {
1160
+ const r = Math.round(fill.color.r * 255);
1161
+ const g = Math.round(fill.color.g * 255);
1162
+ const b = Math.round(fill.color.b * 255);
1163
+ const a = fill.opacity || 1;
1164
+ return {
1165
+ type: fill.type,
1166
+ color: `rgba(${r}, ${g}, ${b}, ${a})`,
1167
+ hex: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`,
1168
+ opacity: a
1169
+ };
1170
+ }
1171
+ return fill;
1172
+ });
1173
+ }
1174
+
1175
+ if (node.strokes && node.strokes.length > 0) {
1176
+ specs.styling.strokes = node.strokes.map(stroke => {
1177
+ if (stroke.type === 'SOLID') {
1178
+ const r = Math.round(stroke.color.r * 255);
1179
+ const g = Math.round(stroke.color.g * 255);
1180
+ const b = Math.round(stroke.color.b * 255);
1181
+ const a = stroke.opacity || 1;
1182
+ return {
1183
+ type: stroke.type,
1184
+ color: `rgba(${r}, ${g}, ${b}, ${a})`,
1185
+ hex: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`,
1186
+ weight: node.strokeWeight || 1
1187
+ };
1188
+ }
1189
+ return stroke;
1190
+ });
1191
+ }
1192
+
1193
+ // Typography
1194
+ if (node.style) {
1195
+ specs.styling.typography = {
1196
+ fontFamily: node.style.fontFamily,
1197
+ fontSize: node.style.fontSize,
1198
+ fontWeight: node.style.fontWeight,
1199
+ lineHeight: node.style.lineHeightPx || node.style.lineHeightPercent,
1200
+ letterSpacing: node.style.letterSpacing,
1201
+ textAlign: node.style.textAlignHorizontal,
1202
+ textCase: node.style.textCase
1203
+ };
1204
+ }
1205
+
1206
+ // Effects (shadows, blur)
1207
+ if (node.effects && node.effects.length > 0) {
1208
+ specs.styling.effects = node.effects.map(effect => ({
1209
+ type: effect.type,
1210
+ visible: effect.visible !== false,
1211
+ radius: effect.radius,
1212
+ offset: effect.offset,
1213
+ color: effect.color ? {
1214
+ rgba: `rgba(${Math.round(effect.color.r * 255)}, ${Math.round(effect.color.g * 255)}, ${Math.round(effect.color.b * 255)}, ${effect.color.a || 1})`
1215
+ } : null
1216
+ }));
1217
+ }
1218
+
1219
+ // Border radius
1220
+ if (node.cornerRadius !== undefined) {
1221
+ specs.styling.borderRadius = node.cornerRadius;
1222
+ }
1223
+ if (node.rectangleCornerRadii) {
1224
+ specs.styling.borderRadius = {
1225
+ topLeft: node.rectangleCornerRadii[0],
1226
+ topRight: node.rectangleCornerRadii[1],
1227
+ bottomRight: node.rectangleCornerRadii[2],
1228
+ bottomLeft: node.rectangleCornerRadii[3]
1229
+ };
1230
+ }
1231
+
1232
+ // Analyze children
1233
+ if (node.children && node.children.length > 0) {
1234
+ specs.children = node.children.map(child => ({
1235
+ id: child.id,
1236
+ name: child.name,
1237
+ type: child.type,
1238
+ dimensions: child.absoluteBoundingBox,
1239
+ visible: child.visible !== false
1240
+ }));
1241
+ }
1242
+
1243
+ return {
1244
+ content: [
1245
+ { type: 'text', text: JSON.stringify(specs, null, 2) }
1246
+ ]
1247
+ };
1248
+ }
1249
+
1250
+ return {
1251
+ content: [
1252
+ {
1253
+ type: "text",
1254
+ text: `Unknown tool: ${name}`,
1255
+ },
1256
+ ],
1257
+ isError: true,
1258
+ };
1259
+ } catch (error) {
1260
+ return {
1261
+ content: [
1262
+ {
1263
+ type: "text",
1264
+ text: `Error: ${error.message}`,
1265
+ },
1266
+ ],
1267
+ isError: true,
1268
+ };
1269
+ }
1270
+ });
1271
+
1272
+ // Start server
1273
+ async function main() {
1274
+ console.error("Starting chrometools-mcp server...");
1275
+
1276
+ const transport = new StdioServerTransport();
1277
+ await server.connect(transport);
1278
+
1279
+ console.error("chrometools-mcp server running on stdio");
1280
+ console.error("Browser will be initialized on first openBrowser call");
1281
+ }
1282
+
1283
+ main().catch((error) => {
1284
+ console.error("Fatal error:", error);
1285
+ process.exit(1);
1286
+ });