chrometools-mcp 2.4.0 → 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 +139 -0
- package/README.md +102 -5
- package/RELEASE_NOTES_v2.5.0.md +109 -0
- package/VIDEO_SCRIPTS.md +2116 -0
- package/element-finder-utils.js +138 -28
- package/figma-tools.js +120 -0
- package/index.js +430 -9
- package/npm_publish_output.txt +0 -0
- package/package.json +1 -1
- package/server/tool-definitions.js +63 -5
- package/server/tool-groups.js +4 -5
- package/server/tool-schemas.js +31 -0
- package/tools/tool-schemas.js +1 -0
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);
|
|
@@ -1595,7 +1935,7 @@ async function executeToolInternal(name, args) {
|
|
|
1595
1935
|
}
|
|
1596
1936
|
|
|
1597
1937
|
// Perform comprehensive analysis
|
|
1598
|
-
const analysis = await page.evaluate((utilsCode) => {
|
|
1938
|
+
const analysis = await page.evaluate((includeAll, utilsCode) => {
|
|
1599
1939
|
// Inject utilities
|
|
1600
1940
|
eval(utilsCode);
|
|
1601
1941
|
|
|
@@ -1610,6 +1950,11 @@ async function executeToolInternal(name, args) {
|
|
|
1610
1950
|
navigation: [],
|
|
1611
1951
|
};
|
|
1612
1952
|
|
|
1953
|
+
// Add allElements array if includeAll is true
|
|
1954
|
+
if (includeAll) {
|
|
1955
|
+
result.allElements = [];
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1613
1958
|
// Analyze forms
|
|
1614
1959
|
document.querySelectorAll('form').forEach((form, idx) => {
|
|
1615
1960
|
const formData = {
|
|
@@ -1624,9 +1969,9 @@ async function executeToolInternal(name, args) {
|
|
|
1624
1969
|
form.querySelectorAll('input, textarea, select').forEach(field => {
|
|
1625
1970
|
if (field.type === 'submit' || field.type === 'button') return;
|
|
1626
1971
|
|
|
1627
|
-
|
|
1972
|
+
const fieldData = {
|
|
1628
1973
|
selector: getUniqueSelectorInPage(field),
|
|
1629
|
-
type: field.type || 'text',
|
|
1974
|
+
type: field.type || (field.tagName === 'SELECT' ? 'select' : 'text'),
|
|
1630
1975
|
name: field.name,
|
|
1631
1976
|
id: field.id,
|
|
1632
1977
|
placeholder: field.placeholder,
|
|
@@ -1635,7 +1980,23 @@ async function executeToolInternal(name, args) {
|
|
|
1635
1980
|
return label ? label.textContent.trim() : null;
|
|
1636
1981
|
})(),
|
|
1637
1982
|
required: field.required,
|
|
1638
|
-
}
|
|
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);
|
|
1639
2000
|
});
|
|
1640
2001
|
|
|
1641
2002
|
// Find submit button
|
|
@@ -1667,12 +2028,28 @@ async function executeToolInternal(name, args) {
|
|
|
1667
2028
|
if (input.type === 'submit' || input.type === 'button' || input.type === 'hidden') return;
|
|
1668
2029
|
if (input.offsetWidth === 0 && input.offsetHeight === 0) return;
|
|
1669
2030
|
|
|
1670
|
-
|
|
2031
|
+
const inputData = {
|
|
1671
2032
|
selector: getUniqueSelectorInPage(input),
|
|
1672
|
-
type: input.type || 'text',
|
|
2033
|
+
type: input.type || (input.tagName === 'SELECT' ? 'select' : 'text'),
|
|
1673
2034
|
name: input.name,
|
|
1674
2035
|
placeholder: input.placeholder,
|
|
1675
|
-
}
|
|
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);
|
|
1676
2053
|
});
|
|
1677
2054
|
|
|
1678
2055
|
// All links
|
|
@@ -1712,8 +2089,52 @@ async function executeToolInternal(name, args) {
|
|
|
1712
2089
|
});
|
|
1713
2090
|
});
|
|
1714
2091
|
|
|
2092
|
+
// Collect all elements if requested
|
|
2093
|
+
if (includeAll) {
|
|
2094
|
+
const skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD', 'TITLE'];
|
|
2095
|
+
|
|
2096
|
+
document.querySelectorAll('body *').forEach(el => {
|
|
2097
|
+
if (skipTags.includes(el.tagName)) return;
|
|
2098
|
+
|
|
2099
|
+
// Skip elements with no dimensions (hidden)
|
|
2100
|
+
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
|
2101
|
+
|
|
2102
|
+
// Get text content (own text, not children)
|
|
2103
|
+
let ownText = '';
|
|
2104
|
+
for (const node of el.childNodes) {
|
|
2105
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2106
|
+
ownText += node.textContent;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
ownText = ownText.trim();
|
|
2110
|
+
|
|
2111
|
+
result.allElements.push({
|
|
2112
|
+
selector: getUniqueSelectorInPage(el),
|
|
2113
|
+
tag: el.tagName.toLowerCase(),
|
|
2114
|
+
text: ownText.substring(0, 100),
|
|
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
|
+
})(),
|
|
2127
|
+
id: el.id || null,
|
|
2128
|
+
attributes: {
|
|
2129
|
+
role: el.getAttribute('role') || null,
|
|
2130
|
+
'aria-label': el.getAttribute('aria-label') || null,
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1715
2136
|
return result;
|
|
1716
|
-
}, elementFinderUtils);
|
|
2137
|
+
}, validatedArgs.includeAll || false, elementFinderUtils);
|
|
1717
2138
|
|
|
1718
2139
|
// Cache the result
|
|
1719
2140
|
pageAnalysisCache.set(pageUrl, analysis);
|
|
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",
|
|
@@ -28,7 +28,7 @@ export const toolDefinitions = [
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: "click",
|
|
31
|
-
description: "
|
|
31
|
+
description: "PRIMARY tool for clicking elements. Works correctly with React/Vue/Angular synthetic events. DO NOT use executeScript for clicks - use this tool instead. Waits for animations and navigation.",
|
|
32
32
|
inputSchema: {
|
|
33
33
|
type: "object",
|
|
34
34
|
properties: {
|
|
@@ -42,7 +42,7 @@ export const toolDefinitions = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: "type",
|
|
45
|
-
description: "
|
|
45
|
+
description: "PRIMARY tool for filling input fields. Works correctly with React/Vue/Angular state management. DO NOT use executeScript for typing - use this tool instead. Automatically updates framework state (React hooks, Vue reactive data).",
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: "object",
|
|
48
48
|
properties: {
|
|
@@ -159,7 +159,7 @@ export const toolDefinitions = [
|
|
|
159
159
|
},
|
|
160
160
|
{
|
|
161
161
|
name: "executeScript",
|
|
162
|
-
description: "
|
|
162
|
+
description: "⚠️ LAST RESORT tool - use ONLY when ALL specialized tools failed. NEVER use for: clicking (use click), typing (use type), reading page (use analyzePage), finding elements (use findElementsByText). May break React/Vue/Angular synthetic events. ALWAYS try specialized tools first.",
|
|
163
163
|
inputSchema: {
|
|
164
164
|
type: "object",
|
|
165
165
|
properties: {
|
|
@@ -231,6 +231,48 @@ export const toolDefinitions = [
|
|
|
231
231
|
required: ["selector"],
|
|
232
232
|
},
|
|
233
233
|
},
|
|
234
|
+
{
|
|
235
|
+
name: "selectOption",
|
|
236
|
+
description: "Select option in dropdown. Works with HTML select elements. Specify value, text, or index to choose option.",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
selector: { type: "string", description: "CSS selector for select element" },
|
|
241
|
+
value: { type: "string", description: "Option value attribute (priority 1)" },
|
|
242
|
+
text: { type: "string", description: "Option text content (priority 2)" },
|
|
243
|
+
index: { type: "number", description: "Option index, 0-based (priority 3)" },
|
|
244
|
+
},
|
|
245
|
+
required: ["selector"],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "drag",
|
|
250
|
+
description: "Drag element by mouse (click-hold-move-release). Simulates real mouse drag in any direction. Works with interactive maps, Gantt charts, SVG diagrams, canvas, sliders. Does NOT work with standard overflow scrollbars - use scrollTo/scrollHorizontal instead.",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: {
|
|
254
|
+
selector: { type: "string", description: "CSS selector for element to drag" },
|
|
255
|
+
direction: { type: "string", enum: ["up", "down", "left", "right", "up-left", "up-right", "down-left", "down-right"], description: "Drag direction" },
|
|
256
|
+
distance: { type: "number", description: "Distance in pixels (default: 100)" },
|
|
257
|
+
duration: { type: "number", description: "Drag duration in ms (default: 500)" },
|
|
258
|
+
},
|
|
259
|
+
required: ["selector", "direction"],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "scrollHorizontal",
|
|
264
|
+
description: "Scroll element horizontally. For tables, carousels, and horizontally scrollable containers. Can scroll by pixels or to the end.",
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: "object",
|
|
267
|
+
properties: {
|
|
268
|
+
selector: { type: "string", description: "CSS selector for element to scroll" },
|
|
269
|
+
direction: { type: "string", enum: ["left", "right"], description: "Scroll direction" },
|
|
270
|
+
amount: { description: "Pixels to scroll or 'full' for end" },
|
|
271
|
+
behavior: { type: "string", enum: ["auto", "smooth"], description: "Scroll behavior (default: auto)" },
|
|
272
|
+
},
|
|
273
|
+
required: ["selector", "direction", "amount"],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
234
276
|
{
|
|
235
277
|
name: "setStyles",
|
|
236
278
|
description: "Apply inline CSS to element. For live editing and prototyping.",
|
|
@@ -403,6 +445,21 @@ export const toolDefinitions = [
|
|
|
403
445
|
required: ["fileKey"],
|
|
404
446
|
},
|
|
405
447
|
},
|
|
448
|
+
{
|
|
449
|
+
name: "convertFigmaToCode",
|
|
450
|
+
description: "Convert Figma design to React/Tailwind code. Fetches node structure and rendered image, returns simplified design data with AI instructions for generating clean, semantic code. Focuses on React components with Tailwind CSS styling.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
figmaToken: { type: "string", description: "API token (optional)" },
|
|
455
|
+
fileKey: { type: "string", description: "File key" },
|
|
456
|
+
nodeId: { type: "string", description: "Frame/component ID (formats: '123:456' or '123-456')" },
|
|
457
|
+
framework: { type: "string", enum: ["react", "react-typescript", "html"], description: "Target framework (default: react)" },
|
|
458
|
+
includeComments: { type: "boolean", description: "Include comments (default: true)" },
|
|
459
|
+
},
|
|
460
|
+
required: ["fileKey", "nodeId"],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
406
463
|
{
|
|
407
464
|
name: "smartFindElement",
|
|
408
465
|
description: "Find elements with natural language. Returns ranked candidates. Prefer analyzePage for better performance.",
|
|
@@ -429,11 +486,12 @@ export const toolDefinitions = [
|
|
|
429
486
|
},
|
|
430
487
|
{
|
|
431
488
|
name: "analyzePage",
|
|
432
|
-
description: "
|
|
489
|
+
description: "PRIMARY tool for reading page state (forms, inputs, buttons, links, values). Use this INSTEAD of executeScript for reading page content. Use refresh:true after clicks/submissions to see updated state. Efficient: 2-5k tokens vs screenshot 15-25k. includeAll:true gets ALL elements including non-interactive.",
|
|
433
490
|
inputSchema: {
|
|
434
491
|
type: "object",
|
|
435
492
|
properties: {
|
|
436
493
|
refresh: { type: "boolean", description: "Refresh cache (default: false)" },
|
|
494
|
+
includeAll: { type: "boolean", description: "Include all elements on page, not just interactive ones (default: false)" },
|
|
437
495
|
},
|
|
438
496
|
},
|
|
439
497
|
},
|
|
@@ -449,7 +507,7 @@ export const toolDefinitions = [
|
|
|
449
507
|
},
|
|
450
508
|
{
|
|
451
509
|
name: "findElementsByText",
|
|
452
|
-
description: "Find elements by text.
|
|
510
|
+
description: "Find elements by visible text content and get their selectors. Use this INSTEAD of executeScript when you need to find elements. Returns working selectors that can be used with click/type tools. Can optionally perform actions directly.",
|
|
453
511
|
inputSchema: {
|
|
454
512
|
type: "object",
|
|
455
513
|
properties: {
|
package/server/tool-groups.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export const toolGroups = {
|
|
8
|
-
core: ['ping', 'openBrowser'],
|
|
8
|
+
core: ['ping', 'openBrowser', 'executeScript', 'navigateTo'],
|
|
9
9
|
|
|
10
|
-
interaction: ['click', 'type', 'scrollTo', 'waitForElement', 'hover'],
|
|
10
|
+
interaction: ['click', 'type', 'scrollTo', 'waitForElement', 'hover', 'selectOption', 'drag', 'scrollHorizontal'],
|
|
11
11
|
|
|
12
12
|
inspection: ['getElement', 'getComputedCss', 'getBoxModel', 'screenshot', 'saveScreenshot'],
|
|
13
13
|
|
|
@@ -19,11 +19,9 @@ export const toolGroups = {
|
|
|
19
19
|
],
|
|
20
20
|
|
|
21
21
|
advanced: [
|
|
22
|
-
'executeScript',
|
|
23
22
|
'setStyles',
|
|
24
23
|
'setViewport',
|
|
25
24
|
'getViewport',
|
|
26
|
-
'navigateTo',
|
|
27
25
|
'smartFindElement',
|
|
28
26
|
'analyzePage',
|
|
29
27
|
'getAllInteractiveElements',
|
|
@@ -51,7 +49,8 @@ export const toolGroups = {
|
|
|
51
49
|
'searchFigmaFrames',
|
|
52
50
|
'getFigmaComponents',
|
|
53
51
|
'getFigmaStyles',
|
|
54
|
-
'getFigmaColorPalette'
|
|
52
|
+
'getFigmaColorPalette',
|
|
53
|
+
'convertFigmaToCode'
|
|
55
54
|
]
|
|
56
55
|
};
|
|
57
56
|
|