@fragments-sdk/cli 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,1117 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
3
+ import {
4
+ generateContext
5
+ } from "./chunk-LY2CFFPY.js";
6
+ import {
7
+ BRAND,
8
+ DEFAULTS
9
+ } from "./chunk-XHNKNI6J.js";
10
+
11
+ // src/mcp/server.ts
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema
17
+ } from "@modelcontextprotocol/sdk/types.js";
18
+ import { readFile } from "fs/promises";
19
+ import { existsSync, readFileSync } from "fs";
20
+ import { join, dirname, resolve } from "path";
21
+
22
+ // src/mcp/utils.ts
23
+ function projectFields(obj, fields) {
24
+ if (!fields || fields.length === 0) {
25
+ return obj;
26
+ }
27
+ const result = {};
28
+ for (const field of fields) {
29
+ const parts = field.split(".");
30
+ let source = obj;
31
+ let target = result;
32
+ for (let i = 0; i < parts.length; i++) {
33
+ const part = parts[i];
34
+ const isLast = i === parts.length - 1;
35
+ if (source === null || source === void 0 || typeof source !== "object") {
36
+ break;
37
+ }
38
+ const sourceObj = source;
39
+ const value = sourceObj[part];
40
+ if (isLast) {
41
+ target[part] = value;
42
+ } else {
43
+ if (!(part in target)) {
44
+ target[part] = {};
45
+ }
46
+ target = target[part];
47
+ source = value;
48
+ }
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
54
+ // src/mcp/server.ts
55
+ var _service = null;
56
+ async function getService() {
57
+ if (!_service) {
58
+ try {
59
+ _service = await import("./service-QSZMZJBJ.js");
60
+ } catch {
61
+ throw new Error(
62
+ "Visual tools require playwright. Install it with: npm install playwright"
63
+ );
64
+ }
65
+ }
66
+ return _service;
67
+ }
68
+ var TOOL_NAMES = {
69
+ discover: `${BRAND.nameLower}_discover`,
70
+ inspect: `${BRAND.nameLower}_inspect`,
71
+ recipe: `${BRAND.nameLower}_recipe`,
72
+ render: `${BRAND.nameLower}_render`,
73
+ fix: `${BRAND.nameLower}_fix`
74
+ };
75
+ var PLACEHOLDER_PATTERNS = [
76
+ /^\w+ component is needed$/i,
77
+ /^Alternative component is more appropriate$/i,
78
+ /^Use \w+ when you need/i
79
+ ];
80
+ function filterPlaceholders(items) {
81
+ if (!items) return [];
82
+ return items.filter(
83
+ (item) => !PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(item.trim()))
84
+ );
85
+ }
86
+ var TOOLS = [
87
+ {
88
+ name: TOOL_NAMES.discover,
89
+ description: `Discover components in the design system. Use with no params to list all components. Use 'useCase' for AI-powered suggestions. Use 'component' to find alternatives. Use 'compact' for a token-efficient overview.`,
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ useCase: {
94
+ type: "string",
95
+ description: 'Description of what you want to build \u2014 returns ranked suggestions (e.g., "form for user email input", "button to submit data")'
96
+ },
97
+ component: {
98
+ type: "string",
99
+ description: 'Component name to find alternatives for (e.g., "Button")'
100
+ },
101
+ category: {
102
+ type: "string",
103
+ description: 'Filter by category (e.g., "actions", "forms", "layout")'
104
+ },
105
+ search: {
106
+ type: "string",
107
+ description: "Search term to filter by name, description, or tags"
108
+ },
109
+ status: {
110
+ type: "string",
111
+ enum: ["stable", "beta", "deprecated", "experimental"],
112
+ description: "Filter by component status"
113
+ },
114
+ format: {
115
+ type: "string",
116
+ enum: ["markdown", "json"],
117
+ description: "Output format for context mode (default: markdown)"
118
+ },
119
+ compact: {
120
+ type: "boolean",
121
+ description: "If true, returns minimal output (just component names and categories)"
122
+ },
123
+ includeCode: {
124
+ type: "boolean",
125
+ description: "If true, includes code examples for each variant"
126
+ },
127
+ includeRelations: {
128
+ type: "boolean",
129
+ description: "If true, includes component relationships"
130
+ }
131
+ }
132
+ }
133
+ },
134
+ {
135
+ name: TOOL_NAMES.inspect,
136
+ description: `Get detailed information about a specific component: props, usage guidelines, code examples, accessibility \u2014 all in one call. Use 'fields' to request only specific data for token efficiency.`,
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ component: {
141
+ type: "string",
142
+ description: 'Component name (e.g., "Button", "Input")'
143
+ },
144
+ fields: {
145
+ type: "array",
146
+ items: { type: "string" },
147
+ description: 'Specific fields to return (e.g., ["meta", "usage.when", "contract.propsSummary", "props", "examples", "guidelines"]). If omitted, returns everything. Supports dot notation.'
148
+ },
149
+ variant: {
150
+ type: "string",
151
+ description: 'Filter examples to a specific variant name (e.g., "Default", "Primary")'
152
+ },
153
+ maxExamples: {
154
+ type: "number",
155
+ description: "Maximum number of code examples to return (default: all)"
156
+ },
157
+ maxLines: {
158
+ type: "number",
159
+ description: "Maximum lines per code example (truncates longer examples)"
160
+ }
161
+ },
162
+ required: ["component"]
163
+ }
164
+ },
165
+ {
166
+ name: TOOL_NAMES.recipe,
167
+ description: `Search and retrieve composition recipes \u2014 named patterns showing how design system components wire together for common use cases (e.g., "Login Form", "Settings Page"). Returns the recipe with its code pattern.`,
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ name: {
172
+ type: "string",
173
+ description: 'Exact recipe name to retrieve (e.g., "Login Form")'
174
+ },
175
+ search: {
176
+ type: "string",
177
+ description: "Free-text search across recipe names, descriptions, tags, and components"
178
+ },
179
+ component: {
180
+ type: "string",
181
+ description: 'Filter recipes that use a specific component (e.g., "Button")'
182
+ }
183
+ }
184
+ }
185
+ },
186
+ {
187
+ name: TOOL_NAMES.render,
188
+ description: `Render a component and return a screenshot. Optionally compare against a stored baseline ('baseline: true') or against a Figma design ('figmaUrl'). Use this to verify your implementation looks correct.`,
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ component: {
193
+ type: "string",
194
+ description: 'Component name (e.g., "Button", "Card", "Input")'
195
+ },
196
+ variant: {
197
+ type: "string",
198
+ description: "Variant name for baseline/compare modes"
199
+ },
200
+ props: {
201
+ type: "object",
202
+ description: 'Props to pass to the component (e.g., { "variant": "primary", "children": "Click me" })'
203
+ },
204
+ viewport: {
205
+ type: "object",
206
+ properties: {
207
+ width: { type: "number", description: "Viewport width (default: 800)" },
208
+ height: { type: "number", description: "Viewport height (default: 600)" }
209
+ },
210
+ description: "Optional viewport size for the render"
211
+ },
212
+ baseline: {
213
+ type: "boolean",
214
+ description: "If true, compares the render against the stored baseline screenshot (requires variant)"
215
+ },
216
+ figmaUrl: {
217
+ type: "string",
218
+ description: "Figma frame URL \u2014 if provided, compares the render against the Figma design"
219
+ },
220
+ theme: {
221
+ type: "string",
222
+ enum: ["light", "dark"],
223
+ description: "Theme for baseline verification (default: light)"
224
+ },
225
+ threshold: {
226
+ type: "number",
227
+ description: "Diff threshold percentage (default: 5 for baseline, 1 for Figma)"
228
+ }
229
+ },
230
+ required: ["component"]
231
+ }
232
+ },
233
+ {
234
+ name: TOOL_NAMES.fix,
235
+ description: `Generate patches to fix token compliance issues in a component. Returns unified diff patches that replace hardcoded CSS values with design token references. Use this after fragments_render identifies issues to automatically fix them.`,
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ component: {
240
+ type: "string",
241
+ description: 'Component name to generate fixes for (e.g., "Button", "Card")'
242
+ },
243
+ variant: {
244
+ type: "string",
245
+ description: "Specific variant to fix (optional, fixes all variants if omitted)"
246
+ },
247
+ fixType: {
248
+ type: "string",
249
+ enum: ["token", "all"],
250
+ description: 'Type of fixes to generate: "token" for hardcoded\u2192token replacements, "all" for all available fixes (default: "all")'
251
+ }
252
+ },
253
+ required: ["component"]
254
+ }
255
+ }
256
+ ];
257
+ function createMcpServer(config) {
258
+ const server = new Server(
259
+ {
260
+ name: `${BRAND.nameLower}-mcp`,
261
+ version: "0.0.1"
262
+ },
263
+ {
264
+ capabilities: {
265
+ tools: {}
266
+ }
267
+ }
268
+ );
269
+ let segmentsData = null;
270
+ let packageName = null;
271
+ let browserPool = null;
272
+ let storageManager = null;
273
+ let diffEngine = null;
274
+ let isPoolWarming = false;
275
+ function findFragmentsJson(startDir) {
276
+ const found = [];
277
+ const resolvedStart = resolve(startDir);
278
+ let dir = resolvedStart;
279
+ while (true) {
280
+ const candidate = join(dir, BRAND.outFile);
281
+ if (existsSync(candidate)) {
282
+ found.push(candidate);
283
+ break;
284
+ }
285
+ const parent = dirname(dir);
286
+ if (parent === dir) break;
287
+ dir = parent;
288
+ }
289
+ const pkgJsonPath = join(resolvedStart, "package.json");
290
+ if (existsSync(pkgJsonPath)) {
291
+ try {
292
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
293
+ const allDeps = {
294
+ ...pkgJson.dependencies,
295
+ ...pkgJson.devDependencies
296
+ };
297
+ for (const depName of Object.keys(allDeps)) {
298
+ const depPkgPath = join(resolvedStart, "node_modules", depName, "package.json");
299
+ if (!existsSync(depPkgPath)) continue;
300
+ try {
301
+ const depPkg = JSON.parse(readFileSync(depPkgPath, "utf-8"));
302
+ if (depPkg.fragments) {
303
+ const fragmentsPath = join(resolvedStart, "node_modules", depName, depPkg.fragments);
304
+ if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
305
+ found.push(fragmentsPath);
306
+ }
307
+ }
308
+ } catch {
309
+ }
310
+ }
311
+ } catch {
312
+ }
313
+ }
314
+ return found;
315
+ }
316
+ async function loadSegments() {
317
+ if (segmentsData) {
318
+ return segmentsData;
319
+ }
320
+ const paths = findFragmentsJson(config.projectRoot);
321
+ if (paths.length === 0) {
322
+ throw new Error(
323
+ `No ${BRAND.outFile} found. Searched ${config.projectRoot} and package.json dependencies. Either run \`${BRAND.cliCommand} build\` or install a package with a "fragments" field in its package.json.`
324
+ );
325
+ }
326
+ const content = await readFile(paths[0], "utf-8");
327
+ segmentsData = JSON.parse(content);
328
+ for (let i = 1; i < paths.length; i++) {
329
+ const extra = JSON.parse(await readFile(paths[i], "utf-8"));
330
+ Object.assign(segmentsData.segments, extra.segments);
331
+ if (extra.recipes) {
332
+ segmentsData.recipes = { ...segmentsData.recipes, ...extra.recipes };
333
+ }
334
+ }
335
+ return segmentsData;
336
+ }
337
+ async function getPackageName() {
338
+ if (packageName) {
339
+ return packageName;
340
+ }
341
+ const packageJsonPath = join(config.projectRoot, "package.json");
342
+ if (existsSync(packageJsonPath)) {
343
+ try {
344
+ const content = await readFile(packageJsonPath, "utf-8");
345
+ const pkg = JSON.parse(content);
346
+ if (pkg.name) {
347
+ packageName = pkg.name;
348
+ return packageName;
349
+ }
350
+ } catch {
351
+ }
352
+ }
353
+ packageName = "your-component-library";
354
+ return packageName;
355
+ }
356
+ async function getBrowserPool() {
357
+ if (!browserPool) {
358
+ const { BrowserPool } = await getService();
359
+ browserPool = new BrowserPool({
360
+ viewport: DEFAULTS.viewport,
361
+ // 30 minute idle timeout for MCP - server runs continuously
362
+ idleTimeoutMs: 30 * 60 * 1e3,
363
+ poolSize: 2
364
+ // Keep 2 contexts warm for faster captures
365
+ });
366
+ }
367
+ return browserPool;
368
+ }
369
+ function warmBrowserPool() {
370
+ if (isPoolWarming || browserPool?.isReady) {
371
+ return;
372
+ }
373
+ isPoolWarming = true;
374
+ getBrowserPool().then((pool) => {
375
+ pool.warmup().then(() => {
376
+ isPoolWarming = false;
377
+ }).catch(() => {
378
+ isPoolWarming = false;
379
+ });
380
+ }).catch(() => {
381
+ isPoolWarming = false;
382
+ });
383
+ }
384
+ async function getStorageManager() {
385
+ if (!storageManager) {
386
+ const { StorageManager } = await getService();
387
+ storageManager = new StorageManager({
388
+ projectRoot: config.projectRoot
389
+ });
390
+ await storageManager.initialize();
391
+ }
392
+ return storageManager;
393
+ }
394
+ async function getDiffEngine() {
395
+ if (!diffEngine) {
396
+ const { DiffEngine } = await getService();
397
+ diffEngine = new DiffEngine(config.threshold ?? DEFAULTS.diffThreshold);
398
+ }
399
+ return diffEngine;
400
+ }
401
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
402
+ return { tools: TOOLS };
403
+ });
404
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
405
+ const { name, arguments: args2 } = request.params;
406
+ try {
407
+ switch (name) {
408
+ // ================================================================
409
+ // DISCOVER — list, suggest, context, alternatives
410
+ // ================================================================
411
+ case TOOL_NAMES.discover: {
412
+ const data = await loadSegments();
413
+ const useCase = args2?.useCase ?? void 0;
414
+ const componentForAlts = args2?.component ?? void 0;
415
+ const category = args2?.category ?? void 0;
416
+ const search = args2?.search?.toLowerCase() ?? void 0;
417
+ const status = args2?.status ?? void 0;
418
+ const format = args2?.format ?? "markdown";
419
+ const compact = args2?.compact ?? false;
420
+ const includeCode = args2?.includeCode ?? false;
421
+ const includeRelations = args2?.includeRelations ?? false;
422
+ if (compact || args2?.format && !useCase && !componentForAlts && !category && !search && !status) {
423
+ const segments2 = Object.values(data.segments);
424
+ const recipes = Object.values(data.recipes ?? {});
425
+ const { content: ctxContent, tokenEstimate } = generateContext(segments2, {
426
+ format,
427
+ compact,
428
+ include: {
429
+ code: includeCode,
430
+ relations: includeRelations
431
+ }
432
+ }, recipes);
433
+ return {
434
+ content: [{
435
+ type: "text",
436
+ text: ctxContent
437
+ }],
438
+ _meta: { tokenEstimate }
439
+ };
440
+ }
441
+ if (useCase) {
442
+ const useCaseLower = useCase.toLowerCase();
443
+ const context = args2?.context?.toLowerCase() ?? "";
444
+ const searchTerms = `${useCaseLower} ${context}`.split(/\s+/).filter(Boolean);
445
+ const synonymMap = {
446
+ "form": ["input", "field", "submit", "validation"],
447
+ "input": ["form", "field", "text", "entry"],
448
+ "button": ["action", "click", "submit", "trigger"],
449
+ "action": ["button", "click", "trigger"],
450
+ "alert": ["notification", "message", "warning", "error", "feedback"],
451
+ "notification": ["alert", "message", "toast"],
452
+ "card": ["container", "panel", "box", "content"],
453
+ "toggle": ["switch", "checkbox", "boolean", "on/off"],
454
+ "switch": ["toggle", "checkbox", "boolean"],
455
+ "badge": ["tag", "label", "status", "indicator"],
456
+ "status": ["badge", "indicator", "state"],
457
+ "login": ["auth", "signin", "authentication", "form"],
458
+ "auth": ["login", "signin", "authentication"]
459
+ };
460
+ const expandedTerms = new Set(searchTerms);
461
+ searchTerms.forEach((term) => {
462
+ const synonyms = synonymMap[term];
463
+ if (synonyms) {
464
+ synonyms.forEach((syn) => expandedTerms.add(syn));
465
+ }
466
+ });
467
+ const scored = Object.values(data.segments).map((s) => {
468
+ let score = 0;
469
+ const reasons = [];
470
+ const nameLower = s.meta.name.toLowerCase();
471
+ if (searchTerms.some((term) => nameLower.includes(term))) {
472
+ score += 15;
473
+ reasons.push(`Name matches search`);
474
+ } else if (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
475
+ score += 8;
476
+ reasons.push(`Name matches related term`);
477
+ }
478
+ const desc = s.meta.description?.toLowerCase() ?? "";
479
+ const descMatches = searchTerms.filter((term) => desc.includes(term));
480
+ if (descMatches.length > 0) {
481
+ score += descMatches.length * 6;
482
+ reasons.push(`Description matches: ${descMatches.join(", ")}`);
483
+ }
484
+ const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
485
+ const tagMatches = searchTerms.filter(
486
+ (term) => tags.some((tag) => tag.includes(term))
487
+ );
488
+ if (tagMatches.length > 0) {
489
+ score += tagMatches.length * 4;
490
+ reasons.push(`Tags match: ${tagMatches.join(", ")}`);
491
+ }
492
+ const whenUsed = s.usage?.when?.join(" ").toLowerCase() ?? "";
493
+ const whenMatches = searchTerms.filter((term) => whenUsed.includes(term));
494
+ if (whenMatches.length > 0) {
495
+ score += whenMatches.length * 10;
496
+ reasons.push(`Use cases match: "${whenMatches.join(", ")}"`);
497
+ }
498
+ const expandedWhenMatches = Array.from(expandedTerms).filter(
499
+ (term) => !searchTerms.includes(term) && whenUsed.includes(term)
500
+ );
501
+ if (expandedWhenMatches.length > 0) {
502
+ score += expandedWhenMatches.length * 5;
503
+ reasons.push(`Related use cases: "${expandedWhenMatches.join(", ")}"`);
504
+ }
505
+ const cat = s.meta.category?.toLowerCase() ?? "";
506
+ if (searchTerms.some((term) => cat.includes(term))) {
507
+ score += 8;
508
+ reasons.push(`Category: ${s.meta.category}`);
509
+ }
510
+ const variantText = s.variants.map((v) => `${v.name} ${v.description || ""}`.toLowerCase()).join(" ");
511
+ const variantMatches = searchTerms.filter((term) => variantText.includes(term));
512
+ if (variantMatches.length > 0) {
513
+ score += variantMatches.length * 3;
514
+ reasons.push(`Variants match: ${variantMatches.join(", ")}`);
515
+ }
516
+ if (s.meta.status === "stable") {
517
+ score += 5;
518
+ reasons.push("Stable component");
519
+ } else if (s.meta.status === "beta") {
520
+ score += 2;
521
+ }
522
+ if (s.meta.status === "deprecated") {
523
+ score -= 25;
524
+ reasons.push("Deprecated - consider alternatives");
525
+ }
526
+ const filteredWhen = filterPlaceholders(s.usage?.when).slice(0, 3);
527
+ const filteredWhenNot = filterPlaceholders(s.usage?.whenNot).slice(0, 2);
528
+ let confidence;
529
+ if (score >= 25) confidence = "high";
530
+ else if (score >= 15) confidence = "medium";
531
+ else confidence = "low";
532
+ return {
533
+ component: s.meta.name,
534
+ category: s.meta.category,
535
+ description: s.meta.description,
536
+ score,
537
+ confidence,
538
+ reasons,
539
+ usage: { when: filteredWhen, whenNot: filteredWhenNot },
540
+ variantCount: s.variants.length,
541
+ status: s.meta.status
542
+ };
543
+ });
544
+ const MIN_SCORE = 8;
545
+ const filtered = scored.filter((s) => s.score >= MIN_SCORE).sort((a, b) => b.score - a.score);
546
+ const suggestions = [];
547
+ const categoryCount = {};
548
+ for (const item of filtered) {
549
+ const cat = item.category || "uncategorized";
550
+ const count = categoryCount[cat] || 0;
551
+ if (count < 2 || suggestions.length < 3) {
552
+ suggestions.push(item);
553
+ categoryCount[cat] = count + 1;
554
+ if (suggestions.length >= 5) break;
555
+ }
556
+ }
557
+ const compositionHint = suggestions.length >= 2 ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map((s) => s.component).join(" and ")}.` : void 0;
558
+ return {
559
+ content: [{
560
+ type: "text",
561
+ text: JSON.stringify({
562
+ useCase,
563
+ context: context || void 0,
564
+ suggestions: suggestions.map(({ score, ...rest }) => rest),
565
+ recommendation: suggestions.length > 0 ? `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}` : "No matching components found. Try different keywords or browse with fragments_discover.",
566
+ compositionHint,
567
+ nextStep: suggestions.length > 0 ? `Use fragments_inspect("${suggestions[0].component}") for full details.` : void 0
568
+ }, null, 2)
569
+ }]
570
+ };
571
+ }
572
+ if (componentForAlts) {
573
+ const segment = Object.values(data.segments).find(
574
+ (s) => s.meta.name.toLowerCase() === componentForAlts.toLowerCase()
575
+ );
576
+ if (!segment) {
577
+ throw new Error(`Component "${componentForAlts}" not found. Use fragments_discover to see available components.`);
578
+ }
579
+ const relations = segment.relations ?? [];
580
+ const referencedBy = Object.values(data.segments).filter(
581
+ (s) => s.relations?.some((r) => r.component.toLowerCase() === componentForAlts.toLowerCase())
582
+ ).map((s) => ({
583
+ component: s.meta.name,
584
+ relationship: s.relations?.find(
585
+ (r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
586
+ )?.relationship,
587
+ note: s.relations?.find(
588
+ (r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
589
+ )?.note
590
+ }));
591
+ const sameCategory = Object.values(data.segments).filter(
592
+ (s) => s.meta.category === segment.meta.category && s.meta.name.toLowerCase() !== componentForAlts.toLowerCase()
593
+ ).map((s) => ({
594
+ component: s.meta.name,
595
+ description: s.meta.description
596
+ }));
597
+ return {
598
+ content: [{
599
+ type: "text",
600
+ text: JSON.stringify({
601
+ component: segment.meta.name,
602
+ category: segment.meta.category,
603
+ directRelations: relations,
604
+ referencedBy,
605
+ sameCategory,
606
+ suggestion: relations.find((r) => r.relationship === "alternative") ? `Consider ${relations.find((r) => r.relationship === "alternative")?.component}: ${relations.find((r) => r.relationship === "alternative")?.note}` : void 0
607
+ }, null, 2)
608
+ }]
609
+ };
610
+ }
611
+ const segments = Object.values(data.segments).filter((s) => {
612
+ if (category && s.meta.category !== category) return false;
613
+ if (status && (s.meta.status ?? "stable") !== status) return false;
614
+ if (search) {
615
+ const nameMatch = s.meta.name.toLowerCase().includes(search);
616
+ const descMatch = s.meta.description?.toLowerCase().includes(search);
617
+ const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search));
618
+ if (!nameMatch && !descMatch && !tagMatch) return false;
619
+ }
620
+ return true;
621
+ }).map((s) => ({
622
+ name: s.meta.name,
623
+ category: s.meta.category,
624
+ description: s.meta.description,
625
+ status: s.meta.status ?? "stable",
626
+ variantCount: s.variants.length,
627
+ tags: s.meta.tags ?? []
628
+ }));
629
+ return {
630
+ content: [{
631
+ type: "text",
632
+ text: JSON.stringify({
633
+ total: segments.length,
634
+ segments,
635
+ categories: [...new Set(segments.map((s) => s.category))],
636
+ hint: segments.length === 0 ? "No components found. Try broader search terms or check available categories." : segments.length > 5 ? "Use fragments_discover with useCase for recommendations, or fragments_inspect for details on a specific component." : void 0
637
+ }, null, 2)
638
+ }]
639
+ };
640
+ }
641
+ // ================================================================
642
+ // INSPECT — get + guidelines + example in one call
643
+ // ================================================================
644
+ case TOOL_NAMES.inspect: {
645
+ const data = await loadSegments();
646
+ const componentName = args2?.component;
647
+ const fields = args2?.fields;
648
+ const variantName = args2?.variant ?? void 0;
649
+ const maxExamples = args2?.maxExamples;
650
+ const maxLines = args2?.maxLines;
651
+ if (!componentName) {
652
+ throw new Error("component is required");
653
+ }
654
+ const segment = Object.values(data.segments).find(
655
+ (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
656
+ );
657
+ if (!segment) {
658
+ throw new Error(`Component "${componentName}" not found. Use fragments_discover to see available components.`);
659
+ }
660
+ const pkgName = await getPackageName();
661
+ let variants = segment.variants;
662
+ if (variantName) {
663
+ const filtered = variants.filter(
664
+ (v) => v.name.toLowerCase() === variantName.toLowerCase()
665
+ );
666
+ if (filtered.length > 0) {
667
+ variants = filtered;
668
+ }
669
+ }
670
+ if (maxExamples && maxExamples > 0) {
671
+ variants = variants.slice(0, maxExamples);
672
+ }
673
+ const truncateCode = (code) => {
674
+ if (!maxLines || maxLines <= 0) return code;
675
+ const lines = code.split("\n");
676
+ if (lines.length <= maxLines) return code;
677
+ return lines.slice(0, maxLines).join("\n") + "\n// ... truncated";
678
+ };
679
+ const examples = variants.map((variant) => {
680
+ if (variant.code) {
681
+ return {
682
+ variant: variant.name,
683
+ description: variant.description,
684
+ code: truncateCode(variant.code)
685
+ };
686
+ }
687
+ return {
688
+ variant: variant.name,
689
+ description: variant.description,
690
+ code: `<${segment.meta.name} />`,
691
+ note: "No code example provided in fragment. Refer to props for customization."
692
+ };
693
+ });
694
+ const propsReference = Object.entries(segment.props ?? {}).map(([propName, prop]) => ({
695
+ name: propName,
696
+ type: prop.type,
697
+ required: prop.required,
698
+ default: prop.default,
699
+ description: prop.description
700
+ }));
701
+ const propConstraints = Object.entries(segment.props ?? {}).filter(([, prop]) => prop.constraints && prop.constraints.length > 0).map(([pName, prop]) => ({
702
+ prop: pName,
703
+ constraints: prop.constraints
704
+ }));
705
+ const fullResult = {
706
+ // Component data (from old "get")
707
+ meta: segment.meta,
708
+ props: segment.props,
709
+ variants: segment.variants,
710
+ relations: segment.relations,
711
+ contract: segment.contract,
712
+ generated: segment._generated,
713
+ // Guidelines (from old "guidelines")
714
+ guidelines: {
715
+ when: filterPlaceholders(segment.usage?.when),
716
+ whenNot: filterPlaceholders(segment.usage?.whenNot),
717
+ guidelines: segment.usage?.guidelines ?? [],
718
+ accessibility: segment.usage?.accessibility ?? [],
719
+ propConstraints,
720
+ alternatives: segment.relations?.filter((r) => r.relationship === "alternative").map((r) => ({
721
+ component: r.component,
722
+ note: r.note
723
+ })) ?? []
724
+ },
725
+ // Examples (from old "example")
726
+ examples: {
727
+ import: `import { ${segment.meta.name} } from '${pkgName}';`,
728
+ code: examples,
729
+ propsReference
730
+ }
731
+ };
732
+ const result = fields && fields.length > 0 ? projectFields(fullResult, fields) : fullResult;
733
+ return {
734
+ content: [{
735
+ type: "text",
736
+ text: JSON.stringify(result, null, 2)
737
+ }]
738
+ };
739
+ }
740
+ // ================================================================
741
+ // RECIPE — unchanged
742
+ // ================================================================
743
+ case TOOL_NAMES.recipe: {
744
+ const data = await loadSegments();
745
+ const recipeName = args2?.name;
746
+ const search = args2?.search?.toLowerCase() ?? void 0;
747
+ const component = args2?.component?.toLowerCase() ?? void 0;
748
+ const allRecipes = Object.values(data.recipes ?? {});
749
+ if (allRecipes.length === 0) {
750
+ return {
751
+ content: [{
752
+ type: "text",
753
+ text: JSON.stringify({
754
+ total: 0,
755
+ recipes: [],
756
+ hint: `No recipes found. Run \`${BRAND.cliCommand} build\` after adding .recipe.ts files.`
757
+ }, null, 2)
758
+ }]
759
+ };
760
+ }
761
+ let filtered = allRecipes;
762
+ if (recipeName) {
763
+ filtered = filtered.filter(
764
+ (r) => r.name.toLowerCase() === recipeName.toLowerCase()
765
+ );
766
+ }
767
+ if (search) {
768
+ filtered = filtered.filter((r) => {
769
+ const haystack = [
770
+ r.name,
771
+ r.description,
772
+ ...r.tags ?? [],
773
+ ...r.components,
774
+ r.category
775
+ ].join(" ").toLowerCase();
776
+ return haystack.includes(search);
777
+ });
778
+ }
779
+ if (component) {
780
+ filtered = filtered.filter(
781
+ (r) => r.components.some((c) => c.toLowerCase() === component)
782
+ );
783
+ }
784
+ return {
785
+ content: [{
786
+ type: "text",
787
+ text: JSON.stringify({
788
+ total: filtered.length,
789
+ recipes: filtered
790
+ }, null, 2)
791
+ }]
792
+ };
793
+ }
794
+ // ================================================================
795
+ // RENDER — render + verify + compare
796
+ // ================================================================
797
+ case TOOL_NAMES.render: {
798
+ const componentName = args2?.component;
799
+ const variantName = args2?.variant;
800
+ const props = args2?.props ?? {};
801
+ const viewport = args2?.viewport;
802
+ const useBaseline = args2?.baseline ?? false;
803
+ const figmaUrl = args2?.figmaUrl;
804
+ const theme = args2?.theme ?? config.theme ?? DEFAULTS.theme;
805
+ const threshold = args2?.threshold ?? (figmaUrl ? 1 : config.threshold ?? DEFAULTS.diffThreshold);
806
+ if (!componentName) {
807
+ return {
808
+ content: [{
809
+ type: "text",
810
+ text: "Error: component name is required"
811
+ }],
812
+ isError: true
813
+ };
814
+ }
815
+ if (useBaseline) {
816
+ if (!variantName) {
817
+ throw new Error("variant is required when baseline is true");
818
+ }
819
+ const { Timer, CaptureEngine: CE, bufferToBase64Url: toBase64 } = await getService();
820
+ const timer = new Timer();
821
+ const storage = await getStorageManager();
822
+ const pool = await getBrowserPool();
823
+ const diff = await getDiffEngine();
824
+ const baseline = await storage.loadBaseline(componentName, variantName, theme);
825
+ if (!baseline) {
826
+ return {
827
+ content: [{
828
+ type: "text",
829
+ text: JSON.stringify({
830
+ verdict: "error",
831
+ matches: false,
832
+ diffPercentage: 0,
833
+ screenshot: "",
834
+ baseline: "",
835
+ notes: [],
836
+ error: `No baseline found for ${componentName}/${variantName}. Run \`${BRAND.cliCommand} screenshot\` first.`,
837
+ timing: { renderMs: 0, captureMs: 0, diffMs: 0, totalMs: timer.elapsed() }
838
+ }, null, 2)
839
+ }]
840
+ };
841
+ }
842
+ const viewerUrl2 = config.viewerUrl ?? `http://localhost:${DEFAULTS.port}`;
843
+ const captureEngine = new CE(pool, viewerUrl2);
844
+ const current = await captureEngine.captureVariant(componentName, variantName, {
845
+ theme,
846
+ delay: DEFAULTS.captureDelayMs
847
+ });
848
+ let diffResult;
849
+ let matches = false;
850
+ if (diff.areIdentical(current, baseline)) {
851
+ matches = true;
852
+ diffResult = {
853
+ matches: true,
854
+ diffPercentage: 0,
855
+ diffPixelCount: 0,
856
+ totalPixels: current.viewport.width * current.viewport.height,
857
+ changedRegions: [],
858
+ diffTimeMs: 0
859
+ };
860
+ } else {
861
+ diffResult = diff.compare(current, baseline, { threshold });
862
+ matches = diffResult.matches;
863
+ }
864
+ const result = {
865
+ verdict: matches ? "pass" : "fail",
866
+ matches,
867
+ diffPercentage: diffResult.diffPercentage,
868
+ screenshot: toBase64(current.data),
869
+ baseline: toBase64(baseline.data),
870
+ diffImage: diffResult.diffImage ? toBase64(diffResult.diffImage) : void 0,
871
+ notes: matches ? ["Screenshot matches baseline within threshold"] : [
872
+ `Diff percentage (${diffResult.diffPercentage}%) exceeds threshold (${threshold}%)`,
873
+ `${diffResult.changedRegions.length} changed region(s) detected`
874
+ ],
875
+ timing: {
876
+ renderMs: current.metadata.renderTimeMs,
877
+ captureMs: current.metadata.captureTimeMs,
878
+ diffMs: diffResult.diffTimeMs,
879
+ totalMs: timer.elapsed()
880
+ }
881
+ };
882
+ return {
883
+ content: [{
884
+ type: "text",
885
+ text: JSON.stringify(result, null, 2)
886
+ }]
887
+ };
888
+ }
889
+ if (figmaUrl) {
890
+ const baseUrl2 = config.viewerUrl ?? "http://localhost:6006";
891
+ const compareUrl = `${baseUrl2}/fragments/compare`;
892
+ try {
893
+ const response = await fetch(compareUrl, {
894
+ method: "POST",
895
+ headers: { "Content-Type": "application/json" },
896
+ body: JSON.stringify({
897
+ component: componentName,
898
+ variant: variantName,
899
+ props,
900
+ figmaUrl,
901
+ threshold
902
+ })
903
+ });
904
+ const result = await response.json();
905
+ if (!response.ok || result.error) {
906
+ return {
907
+ content: [{
908
+ type: "text",
909
+ text: `Compare error: ${result.error ?? "Unknown error"}${result.suggestion ? `
910
+ Suggestion: ${result.suggestion}` : ""}`
911
+ }],
912
+ isError: true
913
+ };
914
+ }
915
+ const content = [];
916
+ const summaryText = result.match ? `MATCH: ${componentName} matches Figma design (${result.diffPercentage}% diff, threshold: ${result.threshold}%)` : `MISMATCH: ${componentName} differs from Figma design by ${result.diffPercentage}% (threshold: ${result.threshold}%)`;
917
+ content.push({ type: "text", text: summaryText });
918
+ if (result.diff && !result.match) {
919
+ content.push({
920
+ type: "image",
921
+ data: result.diff.replace("data:image/png;base64,", ""),
922
+ mimeType: "image/png"
923
+ });
924
+ content.push({
925
+ type: "text",
926
+ text: `Diff image above shows visual differences (red highlights). Changed regions: ${result.changedRegions?.length ?? 0}`
927
+ });
928
+ }
929
+ content.push({
930
+ type: "text",
931
+ text: JSON.stringify({
932
+ match: result.match,
933
+ diffPercentage: result.diffPercentage,
934
+ threshold: result.threshold,
935
+ figmaUrl: result.figmaUrl,
936
+ changedRegions: result.changedRegions
937
+ }, null, 2)
938
+ });
939
+ return { content };
940
+ } catch (error) {
941
+ return {
942
+ content: [{
943
+ type: "text",
944
+ text: `Failed to compare component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running and FIGMA_ACCESS_TOKEN is set.`
945
+ }],
946
+ isError: true
947
+ };
948
+ }
949
+ }
950
+ const baseUrl = config.viewerUrl ?? "http://localhost:6006";
951
+ const renderUrl = `${baseUrl}/fragments/render`;
952
+ try {
953
+ const response = await fetch(renderUrl, {
954
+ method: "POST",
955
+ headers: { "Content-Type": "application/json" },
956
+ body: JSON.stringify({
957
+ component: componentName,
958
+ props,
959
+ viewport: viewport ?? { width: 800, height: 600 }
960
+ })
961
+ });
962
+ const result = await response.json();
963
+ if (!response.ok || result.error) {
964
+ return {
965
+ content: [{
966
+ type: "text",
967
+ text: `Render error: ${result.error ?? "Unknown error"}`
968
+ }],
969
+ isError: true
970
+ };
971
+ }
972
+ return {
973
+ content: [
974
+ {
975
+ type: "image",
976
+ data: result.screenshot.replace("data:image/png;base64,", ""),
977
+ mimeType: "image/png"
978
+ },
979
+ {
980
+ type: "text",
981
+ text: `Successfully rendered ${componentName} with props: ${JSON.stringify(props)}`
982
+ }
983
+ ]
984
+ };
985
+ } catch (error) {
986
+ return {
987
+ content: [{
988
+ type: "text",
989
+ text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
990
+ }],
991
+ isError: true
992
+ };
993
+ }
994
+ }
995
+ // ================================================================
996
+ // FIX — unchanged
997
+ // ================================================================
998
+ case TOOL_NAMES.fix: {
999
+ const data = await loadSegments();
1000
+ const componentName = args2?.component;
1001
+ const variantName = args2?.variant ?? void 0;
1002
+ const fixType = args2?.fixType ?? "all";
1003
+ if (!componentName) {
1004
+ throw new Error("component is required");
1005
+ }
1006
+ const segment = Object.values(data.segments).find(
1007
+ (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1008
+ );
1009
+ if (!segment) {
1010
+ throw new Error(`Component "${componentName}" not found. Use fragments_discover to see available components.`);
1011
+ }
1012
+ const baseUrl = config.viewerUrl ?? "http://localhost:6006";
1013
+ const fixUrl = `${baseUrl}/fragments/fix`;
1014
+ try {
1015
+ const response = await fetch(fixUrl, {
1016
+ method: "POST",
1017
+ headers: { "Content-Type": "application/json" },
1018
+ body: JSON.stringify({
1019
+ component: componentName,
1020
+ variant: variantName,
1021
+ fixType
1022
+ })
1023
+ });
1024
+ const result = await response.json();
1025
+ if (!response.ok || result.error) {
1026
+ return {
1027
+ content: [{
1028
+ type: "text",
1029
+ text: `Fix generation error: ${result.error ?? "Unknown error"}`
1030
+ }],
1031
+ isError: true
1032
+ };
1033
+ }
1034
+ return {
1035
+ content: [{
1036
+ type: "text",
1037
+ text: JSON.stringify({
1038
+ component: componentName,
1039
+ variant: variantName ?? "all",
1040
+ fixType,
1041
+ patches: result.patches,
1042
+ summary: result.summary,
1043
+ patchCount: result.patches.length,
1044
+ nextStep: result.patches.length > 0 ? "Apply patches using your editor or `patch` command, then run fragments_render with baseline:true to confirm fixes." : void 0
1045
+ }, null, 2)
1046
+ }]
1047
+ };
1048
+ } catch (error) {
1049
+ return {
1050
+ content: [{
1051
+ type: "text",
1052
+ text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
1053
+ }],
1054
+ isError: true
1055
+ };
1056
+ }
1057
+ }
1058
+ default:
1059
+ throw new Error(`Unknown tool: ${name}`);
1060
+ }
1061
+ } catch (error) {
1062
+ return {
1063
+ content: [
1064
+ {
1065
+ type: "text",
1066
+ text: JSON.stringify({
1067
+ error: error instanceof Error ? error.message : String(error)
1068
+ })
1069
+ }
1070
+ ],
1071
+ isError: true
1072
+ };
1073
+ }
1074
+ });
1075
+ server.onclose = async () => {
1076
+ if (browserPool) {
1077
+ await browserPool.shutdown();
1078
+ }
1079
+ };
1080
+ return server;
1081
+ }
1082
+ async function startMcpServer(config) {
1083
+ const server = createMcpServer(config);
1084
+ const transport = new StdioServerTransport();
1085
+ await server.connect(transport);
1086
+ }
1087
+
1088
+ // src/mcp-bin.ts
1089
+ var args = process.argv.slice(2);
1090
+ var projectRoot = process.cwd();
1091
+ var viewerUrl;
1092
+ for (let i = 0; i < args.length; i++) {
1093
+ const arg = args[i];
1094
+ if (arg === "--project-root" || arg === "-p") {
1095
+ projectRoot = args[++i] ?? projectRoot;
1096
+ } else if (arg === "--viewer-url" || arg === "-u") {
1097
+ viewerUrl = args[++i];
1098
+ } else if (arg === "--help" || arg === "-h") {
1099
+ console.log(`
1100
+ Usage: fragments-mcp [options]
1101
+
1102
+ Options:
1103
+ -p, --project-root <path> Project root directory (default: cwd)
1104
+ -u, --viewer-url <url> Viewer URL (default: http://localhost:6006)
1105
+ -h, --help Show this help message
1106
+ `);
1107
+ process.exit(0);
1108
+ }
1109
+ }
1110
+ startMcpServer({
1111
+ projectRoot,
1112
+ viewerUrl
1113
+ }).catch((error) => {
1114
+ console.error("Failed to start MCP server:", error);
1115
+ process.exit(1);
1116
+ });
1117
+ //# sourceMappingURL=mcp-bin.js.map