chrometools-mcp 2.4.2 → 2.5.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/CHANGELOG.md +120 -0
- package/README.md +92 -4
- package/RELEASE_NOTES_v2.5.0.md +109 -0
- package/element-finder-utils.js +138 -28
- package/figma-tools.js +120 -0
- package/index.js +391 -8
- package/npm_publish_output.txt +0 -0
- package/package.json +1 -1
- package/server/tool-definitions.js +62 -5
- package/server/tool-groups.js +3 -2
- package/server/tool-schemas.js +30 -0
package/figma-tools.js
CHANGED
|
@@ -377,3 +377,123 @@ export function collectAllText(node, texts = []) {
|
|
|
377
377
|
}
|
|
378
378
|
return texts;
|
|
379
379
|
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Simplify Figma node structure for code generation
|
|
383
|
+
* Extracts only essential properties: layout, styling, text, and children
|
|
384
|
+
*/
|
|
385
|
+
export function simplifyNode(node) {
|
|
386
|
+
if (!node) return null;
|
|
387
|
+
|
|
388
|
+
const simplified = {
|
|
389
|
+
type: node.type,
|
|
390
|
+
name: node.name,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Dimensions
|
|
394
|
+
if (node.absoluteBoundingBox) {
|
|
395
|
+
simplified.size = {
|
|
396
|
+
width: Math.round(node.absoluteBoundingBox.width),
|
|
397
|
+
height: Math.round(node.absoluteBoundingBox.height),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Layout properties (Auto Layout / Flexbox)
|
|
402
|
+
if (node.layoutMode) {
|
|
403
|
+
simplified.layout = {
|
|
404
|
+
mode: node.layoutMode, // HORIZONTAL or VERTICAL
|
|
405
|
+
padding: (node.paddingLeft || node.paddingTop || node.paddingRight || node.paddingBottom) ? {
|
|
406
|
+
top: node.paddingTop || 0,
|
|
407
|
+
right: node.paddingRight || 0,
|
|
408
|
+
bottom: node.paddingBottom || 0,
|
|
409
|
+
left: node.paddingLeft || 0,
|
|
410
|
+
} : undefined,
|
|
411
|
+
gap: node.itemSpacing,
|
|
412
|
+
align: node.primaryAxisAlignItems,
|
|
413
|
+
justify: node.counterAxisAlignItems,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Border radius
|
|
418
|
+
if (node.cornerRadius) {
|
|
419
|
+
simplified.borderRadius = node.cornerRadius;
|
|
420
|
+
} else if (node.rectangleCornerRadii) {
|
|
421
|
+
simplified.borderRadius = node.rectangleCornerRadii;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fills (backgrounds)
|
|
425
|
+
if (node.fills && node.fills.length > 0) {
|
|
426
|
+
simplified.fills = node.fills
|
|
427
|
+
.filter(fill => fill.visible !== false)
|
|
428
|
+
.map(fill => ({
|
|
429
|
+
type: fill.type,
|
|
430
|
+
color: fill.color ? {
|
|
431
|
+
r: Math.round(fill.color.r * 255),
|
|
432
|
+
g: Math.round(fill.color.g * 255),
|
|
433
|
+
b: Math.round(fill.color.b * 255),
|
|
434
|
+
a: fill.color.a !== undefined ? Math.round(fill.color.a * 100) / 100 : 1,
|
|
435
|
+
} : undefined,
|
|
436
|
+
opacity: fill.opacity,
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Strokes (borders)
|
|
441
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
442
|
+
simplified.strokes = node.strokes
|
|
443
|
+
.filter(stroke => stroke.visible !== false)
|
|
444
|
+
.map(stroke => ({
|
|
445
|
+
type: stroke.type,
|
|
446
|
+
color: stroke.color ? {
|
|
447
|
+
r: Math.round(stroke.color.r * 255),
|
|
448
|
+
g: Math.round(stroke.color.g * 255),
|
|
449
|
+
b: Math.round(stroke.color.b * 255),
|
|
450
|
+
a: stroke.color.a !== undefined ? Math.round(stroke.color.a * 100) / 100 : 1,
|
|
451
|
+
} : undefined,
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
if (node.strokeWeight) {
|
|
455
|
+
simplified.strokeWeight = node.strokeWeight;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Effects (shadows, blurs)
|
|
460
|
+
if (node.effects && node.effects.length > 0) {
|
|
461
|
+
simplified.effects = node.effects
|
|
462
|
+
.filter(effect => effect.visible !== false)
|
|
463
|
+
.map(effect => ({
|
|
464
|
+
type: effect.type,
|
|
465
|
+
radius: effect.radius,
|
|
466
|
+
offset: effect.offset,
|
|
467
|
+
color: effect.color ? {
|
|
468
|
+
r: Math.round(effect.color.r * 255),
|
|
469
|
+
g: Math.round(effect.color.g * 255),
|
|
470
|
+
b: Math.round(effect.color.b * 255),
|
|
471
|
+
a: Math.round(effect.color.a * 100) / 100,
|
|
472
|
+
} : undefined,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Text properties
|
|
477
|
+
if (node.type === 'TEXT') {
|
|
478
|
+
simplified.text = node.characters;
|
|
479
|
+
if (node.style) {
|
|
480
|
+
simplified.textStyle = {
|
|
481
|
+
fontFamily: node.style.fontFamily,
|
|
482
|
+
fontWeight: node.style.fontWeight,
|
|
483
|
+
fontSize: node.style.fontSize,
|
|
484
|
+
lineHeight: node.style.lineHeightPx,
|
|
485
|
+
letterSpacing: node.style.letterSpacing,
|
|
486
|
+
textAlign: node.style.textAlignHorizontal,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Recursively simplify children
|
|
492
|
+
if (node.children && node.children.length > 0) {
|
|
493
|
+
simplified.children = node.children
|
|
494
|
+
.map(child => simplifyNode(child))
|
|
495
|
+
.filter(Boolean); // Remove null values
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return simplified;
|
|
499
|
+
}
|
package/index.js
CHANGED
|
@@ -63,7 +63,8 @@ import {
|
|
|
63
63
|
listFigmaPages,
|
|
64
64
|
normalizeFigmaNodeId,
|
|
65
65
|
parseFigmaUrl,
|
|
66
|
-
searchFigmaFrames
|
|
66
|
+
searchFigmaFrames,
|
|
67
|
+
simplifyNode
|
|
67
68
|
} from './figma-tools.js';
|
|
68
69
|
|
|
69
70
|
// Debug mode - only use stderr for actual errors, not debug info
|
|
@@ -908,6 +909,240 @@ async function executeToolInternal(name, args) {
|
|
|
908
909
|
};
|
|
909
910
|
}
|
|
910
911
|
|
|
912
|
+
if (name === "selectOption") {
|
|
913
|
+
const validatedArgs = schemas.SelectOptionSchema.parse(args);
|
|
914
|
+
const page = await getLastOpenPage();
|
|
915
|
+
|
|
916
|
+
// Select option with priority: value > text > index
|
|
917
|
+
const result = await page.evaluate((selector, value, text, index) => {
|
|
918
|
+
const selectElement = document.querySelector(selector);
|
|
919
|
+
if (!selectElement || selectElement.tagName !== 'SELECT') {
|
|
920
|
+
return { success: false, error: `Select element not found: ${selector}` };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
let selectedOption = null;
|
|
924
|
+
|
|
925
|
+
// Priority 1: Select by value
|
|
926
|
+
if (value !== undefined && value !== null) {
|
|
927
|
+
const option = Array.from(selectElement.options).find(opt => opt.value === value);
|
|
928
|
+
if (option) {
|
|
929
|
+
selectElement.value = value;
|
|
930
|
+
selectedOption = option;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Priority 2: Select by text
|
|
935
|
+
if (!selectedOption && text !== undefined && text !== null) {
|
|
936
|
+
const option = Array.from(selectElement.options).find(opt => opt.textContent.trim() === text);
|
|
937
|
+
if (option) {
|
|
938
|
+
selectElement.value = option.value;
|
|
939
|
+
selectedOption = option;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Priority 3: Select by index
|
|
944
|
+
if (!selectedOption && index !== undefined && index !== null) {
|
|
945
|
+
if (index >= 0 && index < selectElement.options.length) {
|
|
946
|
+
selectElement.selectedIndex = index;
|
|
947
|
+
selectedOption = selectElement.options[index];
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (!selectedOption) {
|
|
952
|
+
return { success: false, error: 'No matching option found' };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Trigger events for React and other frameworks
|
|
956
|
+
selectElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
957
|
+
selectElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
success: true,
|
|
961
|
+
selectedValue: selectElement.value,
|
|
962
|
+
selectedText: selectedOption.textContent.trim(),
|
|
963
|
+
selectedIndex: selectElement.selectedIndex
|
|
964
|
+
};
|
|
965
|
+
}, validatedArgs.selector, validatedArgs.value, validatedArgs.text, validatedArgs.index);
|
|
966
|
+
|
|
967
|
+
if (!result.success) {
|
|
968
|
+
throw new Error(result.error);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
content: [{
|
|
973
|
+
type: "text",
|
|
974
|
+
text: `Selected option in ${validatedArgs.selector}:\n` +
|
|
975
|
+
` Value: ${result.selectedValue}\n` +
|
|
976
|
+
` Text: ${result.selectedText}\n` +
|
|
977
|
+
` Index: ${result.selectedIndex}`
|
|
978
|
+
}],
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (name === "drag") {
|
|
983
|
+
const validatedArgs = schemas.DragSchema.parse(args);
|
|
984
|
+
const page = await getLastOpenPage();
|
|
985
|
+
|
|
986
|
+
const distance = validatedArgs.distance || 100;
|
|
987
|
+
const duration = validatedArgs.duration || 500;
|
|
988
|
+
|
|
989
|
+
// Calculate drag deltas based on direction
|
|
990
|
+
let deltaX = 0;
|
|
991
|
+
let deltaY = 0;
|
|
992
|
+
|
|
993
|
+
switch (validatedArgs.direction) {
|
|
994
|
+
case 'up':
|
|
995
|
+
deltaY = -distance;
|
|
996
|
+
break;
|
|
997
|
+
case 'down':
|
|
998
|
+
deltaY = distance;
|
|
999
|
+
break;
|
|
1000
|
+
case 'left':
|
|
1001
|
+
deltaX = -distance;
|
|
1002
|
+
break;
|
|
1003
|
+
case 'right':
|
|
1004
|
+
deltaX = distance;
|
|
1005
|
+
break;
|
|
1006
|
+
case 'up-left':
|
|
1007
|
+
deltaY = -distance;
|
|
1008
|
+
deltaX = -distance;
|
|
1009
|
+
break;
|
|
1010
|
+
case 'up-right':
|
|
1011
|
+
deltaY = -distance;
|
|
1012
|
+
deltaX = distance;
|
|
1013
|
+
break;
|
|
1014
|
+
case 'down-left':
|
|
1015
|
+
deltaY = distance;
|
|
1016
|
+
deltaX = -distance;
|
|
1017
|
+
break;
|
|
1018
|
+
case 'down-right':
|
|
1019
|
+
deltaY = distance;
|
|
1020
|
+
deltaX = distance;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Get element center position for drag start
|
|
1025
|
+
const elementInfo = await page.evaluate((selector) => {
|
|
1026
|
+
const element = document.querySelector(selector);
|
|
1027
|
+
if (!element) {
|
|
1028
|
+
return { success: false, error: `Element not found: ${selector}` };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const rect = element.getBoundingClientRect();
|
|
1032
|
+
return {
|
|
1033
|
+
success: true,
|
|
1034
|
+
centerX: rect.left + rect.width / 2,
|
|
1035
|
+
centerY: rect.top + rect.height / 2,
|
|
1036
|
+
width: rect.width,
|
|
1037
|
+
height: rect.height
|
|
1038
|
+
};
|
|
1039
|
+
}, validatedArgs.selector);
|
|
1040
|
+
|
|
1041
|
+
if (!elementInfo.success) {
|
|
1042
|
+
throw new Error(elementInfo.error);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Perform drag: mousedown → mousemove → mouseup
|
|
1046
|
+
const startX = elementInfo.centerX;
|
|
1047
|
+
const startY = elementInfo.centerY;
|
|
1048
|
+
const endX = startX + deltaX;
|
|
1049
|
+
const endY = startY + deltaY;
|
|
1050
|
+
|
|
1051
|
+
// Move to start position
|
|
1052
|
+
await page.mouse.move(startX, startY);
|
|
1053
|
+
|
|
1054
|
+
// Press mouse button (start drag)
|
|
1055
|
+
await page.mouse.down();
|
|
1056
|
+
|
|
1057
|
+
// Wait a bit to ensure drag is registered
|
|
1058
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1059
|
+
|
|
1060
|
+
// Move mouse to end position (drag)
|
|
1061
|
+
const steps = Math.max(10, Math.floor(duration / 20)); // Smooth movement
|
|
1062
|
+
await page.mouse.move(endX, endY, { steps });
|
|
1063
|
+
|
|
1064
|
+
// Wait for duration
|
|
1065
|
+
await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
|
|
1066
|
+
|
|
1067
|
+
// Release mouse button (end drag)
|
|
1068
|
+
await page.mouse.up();
|
|
1069
|
+
|
|
1070
|
+
return {
|
|
1071
|
+
content: [{
|
|
1072
|
+
type: "text",
|
|
1073
|
+
text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px:\n` +
|
|
1074
|
+
` Start position: (${Math.round(startX)}, ${Math.round(startY)})\n` +
|
|
1075
|
+
` End position: (${Math.round(endX)}, ${Math.round(endY)})\n` +
|
|
1076
|
+
` Delta: (${deltaX}px, ${deltaY}px)\n` +
|
|
1077
|
+
` Duration: ${duration}ms`
|
|
1078
|
+
}],
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (name === "scrollHorizontal") {
|
|
1083
|
+
const validatedArgs = schemas.ScrollHorizontalSchema.parse(args);
|
|
1084
|
+
const page = await getLastOpenPage();
|
|
1085
|
+
|
|
1086
|
+
const behavior = validatedArgs.behavior || 'auto';
|
|
1087
|
+
|
|
1088
|
+
const result = await page.evaluate((selector, direction, amount, behavior) => {
|
|
1089
|
+
const element = document.querySelector(selector);
|
|
1090
|
+
if (!element) {
|
|
1091
|
+
return { success: false, error: `Element not found: ${selector}` };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Determine scroll amount
|
|
1095
|
+
let scrollAmount;
|
|
1096
|
+
if (amount === 'full') {
|
|
1097
|
+
// Scroll to the end
|
|
1098
|
+
scrollAmount = direction === 'right'
|
|
1099
|
+
? element.scrollWidth - element.clientWidth
|
|
1100
|
+
: 0;
|
|
1101
|
+
} else {
|
|
1102
|
+
// Relative scroll
|
|
1103
|
+
scrollAmount = direction === 'right'
|
|
1104
|
+
? element.scrollLeft + amount
|
|
1105
|
+
: element.scrollLeft - amount;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Perform scroll
|
|
1109
|
+
element.scrollTo({
|
|
1110
|
+
left: scrollAmount,
|
|
1111
|
+
behavior: behavior
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Wait a bit for scroll to complete (if smooth)
|
|
1115
|
+
return new Promise(resolve => {
|
|
1116
|
+
setTimeout(() => {
|
|
1117
|
+
resolve({
|
|
1118
|
+
success: true,
|
|
1119
|
+
scrollLeft: element.scrollLeft,
|
|
1120
|
+
scrollWidth: element.scrollWidth,
|
|
1121
|
+
clientWidth: element.clientWidth,
|
|
1122
|
+
canScrollRight: element.scrollLeft < (element.scrollWidth - element.clientWidth),
|
|
1123
|
+
canScrollLeft: element.scrollLeft > 0
|
|
1124
|
+
});
|
|
1125
|
+
}, behavior === 'smooth' ? 300 : 50);
|
|
1126
|
+
});
|
|
1127
|
+
}, validatedArgs.selector, validatedArgs.direction, validatedArgs.amount, behavior);
|
|
1128
|
+
|
|
1129
|
+
if (!result.success) {
|
|
1130
|
+
throw new Error(result.error);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
content: [{
|
|
1135
|
+
type: "text",
|
|
1136
|
+
text: `Scrolled ${validatedArgs.selector} ${validatedArgs.direction}:\n` +
|
|
1137
|
+
` Scroll position: ${result.scrollLeft}px\n` +
|
|
1138
|
+
` Total width: ${result.scrollWidth}px\n` +
|
|
1139
|
+
` Visible width: ${result.clientWidth}px\n` +
|
|
1140
|
+
` Can scroll right: ${result.canScrollRight}\n` +
|
|
1141
|
+
` Can scroll left: ${result.canScrollLeft}`
|
|
1142
|
+
}],
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
911
1146
|
if (name === "setStyles") {
|
|
912
1147
|
const validatedArgs = schemas.SetStylesSchema.parse(args);
|
|
913
1148
|
const page = await getLastOpenPage();
|
|
@@ -1464,6 +1699,111 @@ async function executeToolInternal(name, args) {
|
|
|
1464
1699
|
};
|
|
1465
1700
|
}
|
|
1466
1701
|
|
|
1702
|
+
if (name === "convertFigmaToCode") {
|
|
1703
|
+
const validatedArgs = schemas.ConvertFigmaToCodeSchema.parse(args);
|
|
1704
|
+
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
1705
|
+
if (!token) {
|
|
1706
|
+
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Normalize node ID
|
|
1710
|
+
const nodeId = normalizeFigmaNodeId(validatedArgs.nodeId);
|
|
1711
|
+
const framework = validatedArgs.framework || 'react';
|
|
1712
|
+
const includeComments = validatedArgs.includeComments !== false; // default true
|
|
1713
|
+
|
|
1714
|
+
// Fetch node structure
|
|
1715
|
+
const nodesData = await fetchFigmaAPI(
|
|
1716
|
+
`files/${validatedArgs.fileKey}/nodes?ids=${encodeURIComponent(nodeId)}`,
|
|
1717
|
+
token
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
if (!nodesData.nodes || !nodesData.nodes[nodeId]) {
|
|
1721
|
+
throw new Error(`Node ${nodeId} not found in Figma file ${validatedArgs.fileKey}`);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Fetch rendered image at 2x scale
|
|
1725
|
+
const exportData = await fetchFigmaAPI(
|
|
1726
|
+
`images/${validatedArgs.fileKey}?ids=${nodeId}&scale=2&format=png`,
|
|
1727
|
+
token
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
if (!exportData.images || !exportData.images[nodeId]) {
|
|
1731
|
+
throw new Error(`Failed to export image for node ${nodeId}`);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const imageUrl = exportData.images[nodeId];
|
|
1735
|
+
|
|
1736
|
+
// Simplify node structure
|
|
1737
|
+
const nodeInfo = nodesData.nodes[nodeId];
|
|
1738
|
+
const simplifiedNode = simplifyNode(nodeInfo.document);
|
|
1739
|
+
|
|
1740
|
+
// Build AI instruction based on framework
|
|
1741
|
+
const frameworkInstructions = {
|
|
1742
|
+
'react': 'React (JavaScript) with Tailwind CSS',
|
|
1743
|
+
'react-typescript': 'React (TypeScript) with Tailwind CSS',
|
|
1744
|
+
'html': 'Pure HTML with Tailwind CSS classes'
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
const instruction = `# Figma to Code Conversion
|
|
1748
|
+
|
|
1749
|
+
## Design Image
|
|
1750
|
+

|
|
1751
|
+
|
|
1752
|
+
## Task
|
|
1753
|
+
Convert this Figma design to ${frameworkInstructions[framework]}.
|
|
1754
|
+
|
|
1755
|
+
## Design Structure (Simplified)
|
|
1756
|
+
\`\`\`json
|
|
1757
|
+
${JSON.stringify(simplifiedNode, null, 2)}
|
|
1758
|
+
\`\`\`
|
|
1759
|
+
|
|
1760
|
+
## Instructions
|
|
1761
|
+
|
|
1762
|
+
### Framework: ${framework.toUpperCase()}
|
|
1763
|
+
${framework.startsWith('react') ? `
|
|
1764
|
+
- Create a functional React component
|
|
1765
|
+
- Use Tailwind CSS for all styling
|
|
1766
|
+
- Props: Accept any necessary data as props
|
|
1767
|
+
- Use semantic HTML elements (div, section, button, h1-h6, p, etc.)
|
|
1768
|
+
${framework === 'react-typescript' ? '- Add TypeScript type definitions for props' : ''}
|
|
1769
|
+
` : `
|
|
1770
|
+
- Create clean, semantic HTML structure
|
|
1771
|
+
- Use Tailwind CSS classes for styling
|
|
1772
|
+
- No JavaScript required unless interactive elements present
|
|
1773
|
+
`}
|
|
1774
|
+
|
|
1775
|
+
### Styling Guidelines
|
|
1776
|
+
1. **Colors**: Convert RGB values to Tailwind colors or use arbitrary values: \`bg-[rgb(r,g,b)]\`
|
|
1777
|
+
2. **Spacing**: Use Tailwind spacing scale (p-4, m-2, gap-4) matching design padding/gaps
|
|
1778
|
+
3. **Layout**:
|
|
1779
|
+
- HORIZONTAL → \`flex flex-row\`
|
|
1780
|
+
- VERTICAL → \`flex flex-col\`
|
|
1781
|
+
- Use \`justify-*\` and \`items-*\` for alignment
|
|
1782
|
+
4. **Typography**: Match font families, weights, sizes from textStyle properties
|
|
1783
|
+
5. **Border Radius**: \`rounded-[Npx]\` for exact values
|
|
1784
|
+
6. **Shadows**: Use Tailwind shadow utilities or arbitrary values
|
|
1785
|
+
7. **Responsive**: Add responsive variants if design suggests multiple breakpoints
|
|
1786
|
+
|
|
1787
|
+
### Quality Requirements
|
|
1788
|
+
- **Clean code**: No unnecessary divs, proper semantic structure
|
|
1789
|
+
- **Accurate spacing**: Match design padding, gaps, and margins
|
|
1790
|
+
- **Proper hierarchy**: Respect component nesting from design structure
|
|
1791
|
+
${includeComments ? '- **Comments**: Add brief comments explaining complex layout decisions' : ''}
|
|
1792
|
+
- **Accessibility**: Use proper ARIA labels where needed
|
|
1793
|
+
|
|
1794
|
+
### Output Format
|
|
1795
|
+
Return ONLY the code, no explanations. ${framework.startsWith('react') ? 'Export the component as default.' : 'Provide complete HTML structure.'}
|
|
1796
|
+
|
|
1797
|
+
Start coding now.`;
|
|
1798
|
+
|
|
1799
|
+
return {
|
|
1800
|
+
content: [{
|
|
1801
|
+
type: "text",
|
|
1802
|
+
text: instruction
|
|
1803
|
+
}],
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1467
1807
|
// New AI optimization tools
|
|
1468
1808
|
if (name === "smartFindElement") {
|
|
1469
1809
|
const validatedArgs = schemas.SmartFindElementSchema.parse(args);
|
|
@@ -1629,9 +1969,9 @@ async function executeToolInternal(name, args) {
|
|
|
1629
1969
|
form.querySelectorAll('input, textarea, select').forEach(field => {
|
|
1630
1970
|
if (field.type === 'submit' || field.type === 'button') return;
|
|
1631
1971
|
|
|
1632
|
-
|
|
1972
|
+
const fieldData = {
|
|
1633
1973
|
selector: getUniqueSelectorInPage(field),
|
|
1634
|
-
type: field.type || 'text',
|
|
1974
|
+
type: field.type || (field.tagName === 'SELECT' ? 'select' : 'text'),
|
|
1635
1975
|
name: field.name,
|
|
1636
1976
|
id: field.id,
|
|
1637
1977
|
placeholder: field.placeholder,
|
|
@@ -1640,7 +1980,23 @@ async function executeToolInternal(name, args) {
|
|
|
1640
1980
|
return label ? label.textContent.trim() : null;
|
|
1641
1981
|
})(),
|
|
1642
1982
|
required: field.required,
|
|
1643
|
-
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
// Add select-specific information
|
|
1986
|
+
if (field.tagName === 'SELECT') {
|
|
1987
|
+
fieldData.options = Array.from(field.options).map((opt, idx) => ({
|
|
1988
|
+
value: opt.value,
|
|
1989
|
+
text: opt.textContent.trim(),
|
|
1990
|
+
index: idx,
|
|
1991
|
+
selected: opt.selected,
|
|
1992
|
+
disabled: opt.disabled
|
|
1993
|
+
}));
|
|
1994
|
+
fieldData.selectedIndex = field.selectedIndex;
|
|
1995
|
+
fieldData.selectedValue = field.value;
|
|
1996
|
+
fieldData.selectedText = field.options[field.selectedIndex]?.textContent.trim() || null;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
formData.fields.push(fieldData);
|
|
1644
2000
|
});
|
|
1645
2001
|
|
|
1646
2002
|
// Find submit button
|
|
@@ -1672,12 +2028,28 @@ async function executeToolInternal(name, args) {
|
|
|
1672
2028
|
if (input.type === 'submit' || input.type === 'button' || input.type === 'hidden') return;
|
|
1673
2029
|
if (input.offsetWidth === 0 && input.offsetHeight === 0) return;
|
|
1674
2030
|
|
|
1675
|
-
|
|
2031
|
+
const inputData = {
|
|
1676
2032
|
selector: getUniqueSelectorInPage(input),
|
|
1677
|
-
type: input.type || 'text',
|
|
2033
|
+
type: input.type || (input.tagName === 'SELECT' ? 'select' : 'text'),
|
|
1678
2034
|
name: input.name,
|
|
1679
2035
|
placeholder: input.placeholder,
|
|
1680
|
-
}
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
// Add select-specific information
|
|
2039
|
+
if (input.tagName === 'SELECT') {
|
|
2040
|
+
inputData.options = Array.from(input.options).map((opt, idx) => ({
|
|
2041
|
+
value: opt.value,
|
|
2042
|
+
text: opt.textContent.trim(),
|
|
2043
|
+
index: idx,
|
|
2044
|
+
selected: opt.selected,
|
|
2045
|
+
disabled: opt.disabled
|
|
2046
|
+
}));
|
|
2047
|
+
inputData.selectedIndex = input.selectedIndex;
|
|
2048
|
+
inputData.selectedValue = input.value;
|
|
2049
|
+
inputData.selectedText = input.options[input.selectedIndex]?.textContent.trim() || null;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
result.inputs.push(inputData);
|
|
1681
2053
|
});
|
|
1682
2054
|
|
|
1683
2055
|
// All links
|
|
@@ -1740,7 +2112,18 @@ async function executeToolInternal(name, args) {
|
|
|
1740
2112
|
selector: getUniqueSelectorInPage(el),
|
|
1741
2113
|
tag: el.tagName.toLowerCase(),
|
|
1742
2114
|
text: ownText.substring(0, 100),
|
|
1743
|
-
classes:
|
|
2115
|
+
classes: (() => {
|
|
2116
|
+
// Handle both string className (HTML) and SVGAnimatedString (SVG)
|
|
2117
|
+
if (!el.className) return [];
|
|
2118
|
+
if (typeof el.className === 'string') {
|
|
2119
|
+
return el.className.split(' ').filter(c => c);
|
|
2120
|
+
}
|
|
2121
|
+
// SVG elements have className.baseVal
|
|
2122
|
+
if (el.className.baseVal) {
|
|
2123
|
+
return el.className.baseVal.split(' ').filter(c => c);
|
|
2124
|
+
}
|
|
2125
|
+
return [];
|
|
2126
|
+
})(),
|
|
1744
2127
|
id: el.id || null,
|
|
1745
2128
|
attributes: {
|
|
1746
2129
|
role: el.getAttribute('role') || null,
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrometools-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, visual testing, Figma comparison, and design validation. Works seamlessly in WSL, Linux, and macOS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|