@fragments-sdk/mcp 0.2.0 → 0.2.1
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/dist/bin.js +2 -1
- package/dist/bin.js.map +1 -1
- package/dist/chunk-7OMNY6JX.js +6799 -0
- package/dist/chunk-7OMNY6JX.js.map +1 -0
- package/dist/chunk-RUZV6VLE.js +1264 -0
- package/dist/chunk-RUZV6VLE.js.map +1 -0
- package/dist/{chunk-EN6JMJFP.js → dist-6B7Z5QLH.js} +6455 -8762
- package/dist/dist-6B7Z5QLH.js.map +1 -0
- package/dist/index.js +2 -1
- package/package.json +8 -2
- package/dist/chunk-EN6JMJFP.js.map +0 -1
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
BRAND,
|
|
4
|
+
DEFAULTS,
|
|
5
|
+
generateContext
|
|
6
|
+
} from "./chunk-7OMNY6JX.js";
|
|
7
|
+
|
|
8
|
+
// src/server.ts
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import {
|
|
12
|
+
CallToolRequestSchema,
|
|
13
|
+
ListToolsRequestSchema
|
|
14
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { readFile } from "fs/promises";
|
|
16
|
+
import { existsSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
|
|
19
|
+
// src/utils.ts
|
|
20
|
+
function projectFields(obj, fields) {
|
|
21
|
+
if (!fields || fields.length === 0) {
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const field of fields) {
|
|
26
|
+
const parts = field.split(".");
|
|
27
|
+
let source = obj;
|
|
28
|
+
let target = result;
|
|
29
|
+
for (let i = 0; i < parts.length; i++) {
|
|
30
|
+
const part = parts[i];
|
|
31
|
+
const isLast = i === parts.length - 1;
|
|
32
|
+
if (source === null || source === void 0 || typeof source !== "object") {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const sourceObj = source;
|
|
36
|
+
const value = sourceObj[part];
|
|
37
|
+
if (isLast) {
|
|
38
|
+
target[part] = value;
|
|
39
|
+
} else {
|
|
40
|
+
if (!(part in target)) {
|
|
41
|
+
target[part] = {};
|
|
42
|
+
}
|
|
43
|
+
target = target[part];
|
|
44
|
+
source = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/server.ts
|
|
52
|
+
var _service = null;
|
|
53
|
+
async function getService() {
|
|
54
|
+
if (!_service) {
|
|
55
|
+
try {
|
|
56
|
+
_service = await import("./dist-6B7Z5QLH.js");
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Visual tools require playwright. Install it with: npm install playwright"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return _service;
|
|
64
|
+
}
|
|
65
|
+
var TOOL_NAMES = {
|
|
66
|
+
list: `${BRAND.nameLower}_list`,
|
|
67
|
+
suggest: `${BRAND.nameLower}_suggest`,
|
|
68
|
+
get: `${BRAND.nameLower}_get`,
|
|
69
|
+
guidelines: `${BRAND.nameLower}_guidelines`,
|
|
70
|
+
alternatives: `${BRAND.nameLower}_alternatives`,
|
|
71
|
+
example: `${BRAND.nameLower}_example`,
|
|
72
|
+
verify: `${BRAND.nameLower}_verify`,
|
|
73
|
+
context: `${BRAND.nameLower}_context`,
|
|
74
|
+
render: `${BRAND.nameLower}_render`,
|
|
75
|
+
compare: `${BRAND.nameLower}_compare`,
|
|
76
|
+
fix: `${BRAND.nameLower}_fix`,
|
|
77
|
+
recipe: `${BRAND.nameLower}_recipe`
|
|
78
|
+
};
|
|
79
|
+
var PLACEHOLDER_PATTERNS = [
|
|
80
|
+
/^\w+ component is needed$/i,
|
|
81
|
+
/^Alternative component is more appropriate$/i,
|
|
82
|
+
/^Use \w+ when you need/i
|
|
83
|
+
];
|
|
84
|
+
function filterPlaceholders(items) {
|
|
85
|
+
if (!items) return [];
|
|
86
|
+
return items.filter(
|
|
87
|
+
(item) => !PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(item.trim()))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
var TOOLS = [
|
|
91
|
+
{
|
|
92
|
+
name: TOOL_NAMES.list,
|
|
93
|
+
description: `List all available component fragments in the design system. Returns component names, categories, descriptions, and variant counts. Use this FIRST to discover what components exist before implementing any UI.`,
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
category: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: 'Filter by category (e.g., "actions", "forms", "layout")'
|
|
100
|
+
},
|
|
101
|
+
search: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Search term to filter by name, description, or tags"
|
|
104
|
+
},
|
|
105
|
+
status: {
|
|
106
|
+
type: "string",
|
|
107
|
+
enum: ["stable", "beta", "deprecated", "experimental"],
|
|
108
|
+
description: "Filter by component status"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: TOOL_NAMES.suggest,
|
|
115
|
+
description: `Given a use case or UI requirement, suggest the most appropriate component(s) from the design system. Use this when you're unsure which component to use for a specific task. Returns ranked suggestions with explanations.`,
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
useCase: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: 'Description of what you want to build (e.g., "form for user email input", "button to submit data", "display a list of items")'
|
|
122
|
+
},
|
|
123
|
+
context: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: 'Additional context (e.g., "in a modal", "for mobile", "destructive action")'
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
required: ["useCase"]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: TOOL_NAMES.get,
|
|
133
|
+
description: `Get detailed information about a specific component fragment including props, usage guidelines, and variants. Use this AFTER fragments_list or fragments_suggest to understand a component's API and best practices before implementing. Use 'fields' to request only specific data for token efficiency.`,
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
component: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: 'Component name (e.g., "Button", "Input")'
|
|
140
|
+
},
|
|
141
|
+
fields: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: { type: "string" },
|
|
144
|
+
description: 'Specific fields to return (e.g., ["meta", "usage.when", "contract.propsSummary", "props"]). If omitted, returns all fields. Supports dot notation for nested fields.'
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
required: ["component"]
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: TOOL_NAMES.guidelines,
|
|
152
|
+
description: `Get the usage guidelines for a component - when to use it, when NOT to use it, and best practices. Use this to make the right component choice and avoid common mistakes. Use 'fields' to request only specific data for token efficiency.`,
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
component: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: 'Component name (e.g., "Button", "Input")'
|
|
159
|
+
},
|
|
160
|
+
fields: {
|
|
161
|
+
type: "array",
|
|
162
|
+
items: { type: "string" },
|
|
163
|
+
description: 'Specific fields to return (e.g., ["when", "whenNot", "accessibility"]). If omitted, returns all guideline fields.'
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
required: ["component"]
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: TOOL_NAMES.alternatives,
|
|
171
|
+
description: `Get alternative and related components for a given component. Use this when a component might not be the right fit, or to understand the component ecosystem.`,
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
component: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Component name to find alternatives for"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
required: ["component"]
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: TOOL_NAMES.example,
|
|
185
|
+
description: `Get example code for a component variant. Returns ready-to-use code with props demonstrated. Use this when you need to see HOW to implement a component, not just what props it accepts.`,
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
component: {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: 'Component name (e.g., "Button", "Input")'
|
|
192
|
+
},
|
|
193
|
+
variant: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description: 'Variant name (e.g., "Default", "Primary"). If omitted, returns all variants.'
|
|
196
|
+
},
|
|
197
|
+
maxExamples: {
|
|
198
|
+
type: "number",
|
|
199
|
+
description: "Maximum number of examples to return (default: all)"
|
|
200
|
+
},
|
|
201
|
+
maxLines: {
|
|
202
|
+
type: "number",
|
|
203
|
+
description: "Maximum lines per code example (truncates longer examples)"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
required: ["component"]
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: TOOL_NAMES.verify,
|
|
211
|
+
description: `Verify a component render against the baseline screenshot. Use this AFTER implementing or modifying a component to ensure visual consistency with the design system. Returns a diff percentage and visual comparison.`,
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
component: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "Component name to verify"
|
|
218
|
+
},
|
|
219
|
+
variant: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "Variant name to verify"
|
|
222
|
+
},
|
|
223
|
+
theme: {
|
|
224
|
+
type: "string",
|
|
225
|
+
enum: ["light", "dark"],
|
|
226
|
+
description: "Theme to verify (default: light)"
|
|
227
|
+
},
|
|
228
|
+
threshold: {
|
|
229
|
+
type: "number",
|
|
230
|
+
description: "Diff threshold percentage (default: 5)"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
required: ["component", "variant"]
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: TOOL_NAMES.context,
|
|
238
|
+
description: `Get the complete design system context in a single call. Use this FIRST before any implementation to understand all available components in a token-efficient format. Returns a summary of all components with their categories, usage guidelines, props, and variants.`,
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
format: {
|
|
243
|
+
type: "string",
|
|
244
|
+
enum: ["markdown", "json"],
|
|
245
|
+
description: "Output format (default: markdown)"
|
|
246
|
+
},
|
|
247
|
+
compact: {
|
|
248
|
+
type: "boolean",
|
|
249
|
+
description: "If true, returns minimal output (just component names and categories)"
|
|
250
|
+
},
|
|
251
|
+
includeCode: {
|
|
252
|
+
type: "boolean",
|
|
253
|
+
description: "If true, includes code examples for each variant"
|
|
254
|
+
},
|
|
255
|
+
includeRelations: {
|
|
256
|
+
type: "boolean",
|
|
257
|
+
description: "If true, includes component relationships"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: TOOL_NAMES.render,
|
|
264
|
+
description: `Render a design system component with specific props and return a screenshot. Use this to VERIFY your UI implementation looks correct. The AI can see what the component looks like and iterate until it's right.`,
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: "object",
|
|
267
|
+
properties: {
|
|
268
|
+
component: {
|
|
269
|
+
type: "string",
|
|
270
|
+
description: 'Component name (e.g., "Button", "Card", "Input")'
|
|
271
|
+
},
|
|
272
|
+
props: {
|
|
273
|
+
type: "object",
|
|
274
|
+
description: 'Props to pass to the component (e.g., { "variant": "primary", "children": "Click me" })'
|
|
275
|
+
},
|
|
276
|
+
viewport: {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: {
|
|
279
|
+
width: { type: "number", description: "Viewport width (default: 800)" },
|
|
280
|
+
height: { type: "number", description: "Viewport height (default: 600)" }
|
|
281
|
+
},
|
|
282
|
+
description: "Optional viewport size for the render"
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
required: ["component"]
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: TOOL_NAMES.compare,
|
|
290
|
+
description: `Compare a rendered component against its Figma design. Returns diff percentage and highlighted differences. Use this to verify your implementation matches the design spec. If the component has a figma URL in its fragment definition, no URL is needed.`,
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
component: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: 'Component name (e.g., "Button", "Input")'
|
|
297
|
+
},
|
|
298
|
+
variant: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "Variant name (optional, uses first variant if not specified)"
|
|
301
|
+
},
|
|
302
|
+
props: {
|
|
303
|
+
type: "object",
|
|
304
|
+
description: "Props to render with"
|
|
305
|
+
},
|
|
306
|
+
figmaUrl: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Figma frame URL (optional if segment has figma link)"
|
|
309
|
+
},
|
|
310
|
+
threshold: {
|
|
311
|
+
type: "number",
|
|
312
|
+
description: "Max acceptable diff percentage (default: 1.0)"
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
required: ["component"]
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: TOOL_NAMES.fix,
|
|
320
|
+
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_verify identifies issues to automatically fix them.`,
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
component: {
|
|
325
|
+
type: "string",
|
|
326
|
+
description: 'Component name to generate fixes for (e.g., "Button", "Card")'
|
|
327
|
+
},
|
|
328
|
+
variant: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "Specific variant to fix (optional, fixes all variants if omitted)"
|
|
331
|
+
},
|
|
332
|
+
fixType: {
|
|
333
|
+
type: "string",
|
|
334
|
+
enum: ["token", "all"],
|
|
335
|
+
description: 'Type of fixes to generate: "token" for hardcoded\u2192token replacements, "all" for all available fixes (default: "all")'
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
required: ["component"]
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: TOOL_NAMES.recipe,
|
|
343
|
+
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.`,
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: {
|
|
347
|
+
name: {
|
|
348
|
+
type: "string",
|
|
349
|
+
description: 'Exact recipe name to retrieve (e.g., "Login Form")'
|
|
350
|
+
},
|
|
351
|
+
search: {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "Free-text search across recipe names, descriptions, tags, and components"
|
|
354
|
+
},
|
|
355
|
+
component: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: 'Filter recipes that use a specific component (e.g., "Button")'
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
];
|
|
363
|
+
function createMcpServer(config) {
|
|
364
|
+
const server = new Server(
|
|
365
|
+
{
|
|
366
|
+
name: `${BRAND.nameLower}-mcp`,
|
|
367
|
+
version: "0.0.1"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
capabilities: {
|
|
371
|
+
tools: {}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
let segmentsData = null;
|
|
376
|
+
let packageName = null;
|
|
377
|
+
let browserPool = null;
|
|
378
|
+
let storageManager = null;
|
|
379
|
+
let diffEngine = null;
|
|
380
|
+
let isPoolWarming = false;
|
|
381
|
+
async function loadSegments() {
|
|
382
|
+
if (segmentsData) {
|
|
383
|
+
return segmentsData;
|
|
384
|
+
}
|
|
385
|
+
const segmentsPath = join(config.projectRoot, BRAND.outFile);
|
|
386
|
+
if (!existsSync(segmentsPath)) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`No ${BRAND.outFile} found. Run \`${BRAND.cliCommand} build\` first.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const content = await readFile(segmentsPath, "utf-8");
|
|
392
|
+
segmentsData = JSON.parse(content);
|
|
393
|
+
return segmentsData;
|
|
394
|
+
}
|
|
395
|
+
async function getPackageName() {
|
|
396
|
+
if (packageName) {
|
|
397
|
+
return packageName;
|
|
398
|
+
}
|
|
399
|
+
const packageJsonPath = join(config.projectRoot, "package.json");
|
|
400
|
+
if (existsSync(packageJsonPath)) {
|
|
401
|
+
try {
|
|
402
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
403
|
+
const pkg = JSON.parse(content);
|
|
404
|
+
if (pkg.name) {
|
|
405
|
+
packageName = pkg.name;
|
|
406
|
+
return packageName;
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
packageName = "your-component-library";
|
|
412
|
+
return packageName;
|
|
413
|
+
}
|
|
414
|
+
async function getBrowserPool() {
|
|
415
|
+
if (!browserPool) {
|
|
416
|
+
const { BrowserPool } = await getService();
|
|
417
|
+
browserPool = new BrowserPool({
|
|
418
|
+
viewport: DEFAULTS.viewport,
|
|
419
|
+
// 30 minute idle timeout for MCP - server runs continuously
|
|
420
|
+
idleTimeoutMs: 30 * 60 * 1e3,
|
|
421
|
+
poolSize: 2
|
|
422
|
+
// Keep 2 contexts warm for faster captures
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return browserPool;
|
|
426
|
+
}
|
|
427
|
+
function warmBrowserPool() {
|
|
428
|
+
if (isPoolWarming || browserPool?.isReady) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
isPoolWarming = true;
|
|
432
|
+
getBrowserPool().then((pool) => {
|
|
433
|
+
pool.warmup().then(() => {
|
|
434
|
+
isPoolWarming = false;
|
|
435
|
+
}).catch(() => {
|
|
436
|
+
isPoolWarming = false;
|
|
437
|
+
});
|
|
438
|
+
}).catch(() => {
|
|
439
|
+
isPoolWarming = false;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function getStorageManager() {
|
|
443
|
+
if (!storageManager) {
|
|
444
|
+
const { StorageManager } = await getService();
|
|
445
|
+
storageManager = new StorageManager({
|
|
446
|
+
projectRoot: config.projectRoot
|
|
447
|
+
});
|
|
448
|
+
await storageManager.initialize();
|
|
449
|
+
}
|
|
450
|
+
return storageManager;
|
|
451
|
+
}
|
|
452
|
+
async function getDiffEngine() {
|
|
453
|
+
if (!diffEngine) {
|
|
454
|
+
const { DiffEngine } = await getService();
|
|
455
|
+
diffEngine = new DiffEngine(config.threshold ?? DEFAULTS.diffThreshold);
|
|
456
|
+
}
|
|
457
|
+
return diffEngine;
|
|
458
|
+
}
|
|
459
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
460
|
+
return { tools: TOOLS };
|
|
461
|
+
});
|
|
462
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
463
|
+
const { name, arguments: args } = request.params;
|
|
464
|
+
try {
|
|
465
|
+
switch (name) {
|
|
466
|
+
case TOOL_NAMES.list: {
|
|
467
|
+
const data = await loadSegments();
|
|
468
|
+
const category = args?.category ?? void 0;
|
|
469
|
+
const search = args?.search?.toLowerCase() ?? void 0;
|
|
470
|
+
const status = args?.status ?? void 0;
|
|
471
|
+
const segments = Object.values(data.segments).filter((s) => {
|
|
472
|
+
if (category && s.meta.category !== category) return false;
|
|
473
|
+
if (status && (s.meta.status ?? "stable") !== status) return false;
|
|
474
|
+
if (search) {
|
|
475
|
+
const nameMatch = s.meta.name.toLowerCase().includes(search);
|
|
476
|
+
const descMatch = s.meta.description?.toLowerCase().includes(search);
|
|
477
|
+
const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search));
|
|
478
|
+
if (!nameMatch && !descMatch && !tagMatch) return false;
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
}).map((s) => ({
|
|
482
|
+
name: s.meta.name,
|
|
483
|
+
category: s.meta.category,
|
|
484
|
+
description: s.meta.description,
|
|
485
|
+
status: s.meta.status ?? "stable",
|
|
486
|
+
variantCount: s.variants.length,
|
|
487
|
+
tags: s.meta.tags ?? []
|
|
488
|
+
}));
|
|
489
|
+
return {
|
|
490
|
+
content: [
|
|
491
|
+
{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: JSON.stringify(
|
|
494
|
+
{
|
|
495
|
+
total: segments.length,
|
|
496
|
+
segments,
|
|
497
|
+
categories: [...new Set(segments.map((s) => s.category))],
|
|
498
|
+
hint: segments.length === 0 ? "No components found. Try broader search terms or check available categories." : segments.length > 5 ? "Use fragments_suggest for use-case based recommendations, or fragments_get for details on a specific component." : void 0
|
|
499
|
+
},
|
|
500
|
+
null,
|
|
501
|
+
2
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
case TOOL_NAMES.suggest: {
|
|
508
|
+
const data = await loadSegments();
|
|
509
|
+
const useCase = args?.useCase?.toLowerCase() ?? "";
|
|
510
|
+
const context = args?.context?.toLowerCase() ?? "";
|
|
511
|
+
if (!useCase) {
|
|
512
|
+
throw new Error("useCase is required");
|
|
513
|
+
}
|
|
514
|
+
const searchTerms = `${useCase} ${context}`.split(/\s+/).filter(Boolean);
|
|
515
|
+
const synonymMap = {
|
|
516
|
+
"form": ["input", "field", "submit", "validation"],
|
|
517
|
+
"input": ["form", "field", "text", "entry"],
|
|
518
|
+
"button": ["action", "click", "submit", "trigger"],
|
|
519
|
+
"action": ["button", "click", "trigger"],
|
|
520
|
+
"alert": ["notification", "message", "warning", "error", "feedback"],
|
|
521
|
+
"notification": ["alert", "message", "toast"],
|
|
522
|
+
"card": ["container", "panel", "box", "content"],
|
|
523
|
+
"toggle": ["switch", "checkbox", "boolean", "on/off"],
|
|
524
|
+
"switch": ["toggle", "checkbox", "boolean"],
|
|
525
|
+
"badge": ["tag", "label", "status", "indicator"],
|
|
526
|
+
"status": ["badge", "indicator", "state"],
|
|
527
|
+
"login": ["auth", "signin", "authentication", "form"],
|
|
528
|
+
"auth": ["login", "signin", "authentication"]
|
|
529
|
+
};
|
|
530
|
+
const expandedTerms = new Set(searchTerms);
|
|
531
|
+
searchTerms.forEach((term) => {
|
|
532
|
+
const synonyms = synonymMap[term];
|
|
533
|
+
if (synonyms) {
|
|
534
|
+
synonyms.forEach((syn) => expandedTerms.add(syn));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
const scored = Object.values(data.segments).map((s) => {
|
|
538
|
+
let score = 0;
|
|
539
|
+
const reasons = [];
|
|
540
|
+
const nameLower = s.meta.name.toLowerCase();
|
|
541
|
+
if (searchTerms.some((term) => nameLower.includes(term))) {
|
|
542
|
+
score += 15;
|
|
543
|
+
reasons.push(`Name matches search`);
|
|
544
|
+
} else if (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
|
|
545
|
+
score += 8;
|
|
546
|
+
reasons.push(`Name matches related term`);
|
|
547
|
+
}
|
|
548
|
+
const desc = s.meta.description?.toLowerCase() ?? "";
|
|
549
|
+
const descMatches = searchTerms.filter((term) => desc.includes(term));
|
|
550
|
+
if (descMatches.length > 0) {
|
|
551
|
+
score += descMatches.length * 6;
|
|
552
|
+
reasons.push(`Description matches: ${descMatches.join(", ")}`);
|
|
553
|
+
}
|
|
554
|
+
const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
|
|
555
|
+
const tagMatches = searchTerms.filter(
|
|
556
|
+
(term) => tags.some((tag) => tag.includes(term))
|
|
557
|
+
);
|
|
558
|
+
if (tagMatches.length > 0) {
|
|
559
|
+
score += tagMatches.length * 4;
|
|
560
|
+
reasons.push(`Tags match: ${tagMatches.join(", ")}`);
|
|
561
|
+
}
|
|
562
|
+
const whenUsed = s.usage?.when?.join(" ").toLowerCase() ?? "";
|
|
563
|
+
const whenMatches = searchTerms.filter((term) => whenUsed.includes(term));
|
|
564
|
+
if (whenMatches.length > 0) {
|
|
565
|
+
score += whenMatches.length * 10;
|
|
566
|
+
reasons.push(`Use cases match: "${whenMatches.join(", ")}"`);
|
|
567
|
+
}
|
|
568
|
+
const expandedWhenMatches = Array.from(expandedTerms).filter(
|
|
569
|
+
(term) => !searchTerms.includes(term) && whenUsed.includes(term)
|
|
570
|
+
);
|
|
571
|
+
if (expandedWhenMatches.length > 0) {
|
|
572
|
+
score += expandedWhenMatches.length * 5;
|
|
573
|
+
reasons.push(`Related use cases: "${expandedWhenMatches.join(", ")}"`);
|
|
574
|
+
}
|
|
575
|
+
const category = s.meta.category?.toLowerCase() ?? "";
|
|
576
|
+
if (searchTerms.some((term) => category.includes(term))) {
|
|
577
|
+
score += 8;
|
|
578
|
+
reasons.push(`Category: ${s.meta.category}`);
|
|
579
|
+
}
|
|
580
|
+
const variantText = s.variants.map((v) => `${v.name} ${v.description || ""}`.toLowerCase()).join(" ");
|
|
581
|
+
const variantMatches = searchTerms.filter((term) => variantText.includes(term));
|
|
582
|
+
if (variantMatches.length > 0) {
|
|
583
|
+
score += variantMatches.length * 3;
|
|
584
|
+
reasons.push(`Variants match: ${variantMatches.join(", ")}`);
|
|
585
|
+
}
|
|
586
|
+
if (s.meta.status === "stable") {
|
|
587
|
+
score += 5;
|
|
588
|
+
reasons.push("Stable component");
|
|
589
|
+
} else if (s.meta.status === "beta") {
|
|
590
|
+
score += 2;
|
|
591
|
+
}
|
|
592
|
+
if (s.meta.status === "deprecated") {
|
|
593
|
+
score -= 25;
|
|
594
|
+
reasons.push("Deprecated - consider alternatives");
|
|
595
|
+
}
|
|
596
|
+
const filteredWhen = filterPlaceholders(s.usage?.when).slice(0, 3);
|
|
597
|
+
const filteredWhenNot = filterPlaceholders(s.usage?.whenNot).slice(0, 2);
|
|
598
|
+
let confidence;
|
|
599
|
+
if (score >= 25) {
|
|
600
|
+
confidence = "high";
|
|
601
|
+
} else if (score >= 15) {
|
|
602
|
+
confidence = "medium";
|
|
603
|
+
} else {
|
|
604
|
+
confidence = "low";
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
component: s.meta.name,
|
|
608
|
+
category: s.meta.category,
|
|
609
|
+
description: s.meta.description,
|
|
610
|
+
score,
|
|
611
|
+
confidence,
|
|
612
|
+
reasons,
|
|
613
|
+
usage: {
|
|
614
|
+
when: filteredWhen,
|
|
615
|
+
whenNot: filteredWhenNot
|
|
616
|
+
},
|
|
617
|
+
variantCount: s.variants.length,
|
|
618
|
+
status: s.meta.status
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
const MIN_SCORE = 8;
|
|
622
|
+
const filtered = scored.filter((s) => s.score >= MIN_SCORE).sort((a, b) => b.score - a.score);
|
|
623
|
+
const suggestions = [];
|
|
624
|
+
const categoryCount = {};
|
|
625
|
+
for (const item of filtered) {
|
|
626
|
+
const cat = item.category || "uncategorized";
|
|
627
|
+
const count = categoryCount[cat] || 0;
|
|
628
|
+
if (count < 2 || suggestions.length < 3) {
|
|
629
|
+
suggestions.push(item);
|
|
630
|
+
categoryCount[cat] = count + 1;
|
|
631
|
+
if (suggestions.length >= 5) break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
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;
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: JSON.stringify(
|
|
640
|
+
{
|
|
641
|
+
useCase,
|
|
642
|
+
context: context || void 0,
|
|
643
|
+
suggestions: suggestions.map(({ score, ...rest }) => rest),
|
|
644
|
+
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_list.",
|
|
645
|
+
compositionHint,
|
|
646
|
+
nextStep: suggestions.length > 0 ? `Use fragments_get("${suggestions[0].component}") for full details, or fragments_guidelines("${suggestions[0].component}") for usage rules.` : void 0
|
|
647
|
+
},
|
|
648
|
+
null,
|
|
649
|
+
2
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
]
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
case TOOL_NAMES.get: {
|
|
656
|
+
const data = await loadSegments();
|
|
657
|
+
const componentName = args?.component;
|
|
658
|
+
const fields = args?.fields;
|
|
659
|
+
if (!componentName) {
|
|
660
|
+
throw new Error("component is required");
|
|
661
|
+
}
|
|
662
|
+
const segment = Object.values(data.segments).find(
|
|
663
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
664
|
+
);
|
|
665
|
+
if (!segment) {
|
|
666
|
+
throw new Error(`Component "${componentName}" not found. Use fragments_list to see available components.`);
|
|
667
|
+
}
|
|
668
|
+
const result = fields && fields.length > 0 ? projectFields(segment, fields) : segment;
|
|
669
|
+
return {
|
|
670
|
+
content: [
|
|
671
|
+
{
|
|
672
|
+
type: "text",
|
|
673
|
+
text: JSON.stringify(result, null, 2)
|
|
674
|
+
}
|
|
675
|
+
]
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
case TOOL_NAMES.guidelines: {
|
|
679
|
+
const data = await loadSegments();
|
|
680
|
+
const componentName = args?.component;
|
|
681
|
+
const fields = args?.fields;
|
|
682
|
+
if (!componentName) {
|
|
683
|
+
throw new Error("component is required");
|
|
684
|
+
}
|
|
685
|
+
const segment = Object.values(data.segments).find(
|
|
686
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
687
|
+
);
|
|
688
|
+
if (!segment) {
|
|
689
|
+
throw new Error(`Component "${componentName}" not found. Use fragments_list to see available components.`);
|
|
690
|
+
}
|
|
691
|
+
const guidelines = {
|
|
692
|
+
component: segment.meta.name,
|
|
693
|
+
description: segment.meta.description,
|
|
694
|
+
status: segment.meta.status ?? "stable",
|
|
695
|
+
when: filterPlaceholders(segment.usage?.when),
|
|
696
|
+
whenNot: filterPlaceholders(segment.usage?.whenNot),
|
|
697
|
+
guidelines: segment.usage?.guidelines ?? [],
|
|
698
|
+
accessibility: segment.usage?.accessibility ?? [],
|
|
699
|
+
// Include prop constraints as they're often important guidelines
|
|
700
|
+
propConstraints: Object.entries(segment.props ?? {}).filter(([, prop]) => prop.constraints && prop.constraints.length > 0).map(([name2, prop]) => ({
|
|
701
|
+
prop: name2,
|
|
702
|
+
constraints: prop.constraints
|
|
703
|
+
})),
|
|
704
|
+
// Include alternatives from relations
|
|
705
|
+
alternatives: segment.relations?.filter((r) => r.relationship === "alternative").map((r) => ({
|
|
706
|
+
component: r.component,
|
|
707
|
+
note: r.note
|
|
708
|
+
})) ?? []
|
|
709
|
+
};
|
|
710
|
+
const result = fields && fields.length > 0 ? projectFields(guidelines, fields) : guidelines;
|
|
711
|
+
return {
|
|
712
|
+
content: [
|
|
713
|
+
{
|
|
714
|
+
type: "text",
|
|
715
|
+
text: JSON.stringify(result, null, 2)
|
|
716
|
+
}
|
|
717
|
+
]
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
case TOOL_NAMES.alternatives: {
|
|
721
|
+
const data = await loadSegments();
|
|
722
|
+
const componentName = args?.component;
|
|
723
|
+
if (!componentName) {
|
|
724
|
+
throw new Error("component is required");
|
|
725
|
+
}
|
|
726
|
+
const segment = Object.values(data.segments).find(
|
|
727
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
728
|
+
);
|
|
729
|
+
if (!segment) {
|
|
730
|
+
throw new Error(`Component "${componentName}" not found. Use fragments_list to see available components.`);
|
|
731
|
+
}
|
|
732
|
+
const relations = segment.relations ?? [];
|
|
733
|
+
const referencedBy = Object.values(data.segments).filter(
|
|
734
|
+
(s) => s.relations?.some((r) => r.component.toLowerCase() === componentName.toLowerCase())
|
|
735
|
+
).map((s) => ({
|
|
736
|
+
component: s.meta.name,
|
|
737
|
+
relationship: s.relations?.find(
|
|
738
|
+
(r) => r.component.toLowerCase() === componentName.toLowerCase()
|
|
739
|
+
)?.relationship,
|
|
740
|
+
note: s.relations?.find(
|
|
741
|
+
(r) => r.component.toLowerCase() === componentName.toLowerCase()
|
|
742
|
+
)?.note
|
|
743
|
+
}));
|
|
744
|
+
const sameCategory = Object.values(data.segments).filter(
|
|
745
|
+
(s) => s.meta.category === segment.meta.category && s.meta.name.toLowerCase() !== componentName.toLowerCase()
|
|
746
|
+
).map((s) => ({
|
|
747
|
+
component: s.meta.name,
|
|
748
|
+
description: s.meta.description
|
|
749
|
+
}));
|
|
750
|
+
return {
|
|
751
|
+
content: [
|
|
752
|
+
{
|
|
753
|
+
type: "text",
|
|
754
|
+
text: JSON.stringify(
|
|
755
|
+
{
|
|
756
|
+
component: segment.meta.name,
|
|
757
|
+
category: segment.meta.category,
|
|
758
|
+
directRelations: relations,
|
|
759
|
+
referencedBy,
|
|
760
|
+
sameCategory,
|
|
761
|
+
suggestion: relations.find((r) => r.relationship === "alternative") ? `Consider ${relations.find((r) => r.relationship === "alternative")?.component}: ${relations.find((r) => r.relationship === "alternative")?.note}` : void 0
|
|
762
|
+
},
|
|
763
|
+
null,
|
|
764
|
+
2
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
]
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
case TOOL_NAMES.example: {
|
|
771
|
+
const data = await loadSegments();
|
|
772
|
+
const componentName = args?.component;
|
|
773
|
+
const variantName = args?.variant ?? void 0;
|
|
774
|
+
const maxExamples = args?.maxExamples;
|
|
775
|
+
const maxLines = args?.maxLines;
|
|
776
|
+
if (!componentName) {
|
|
777
|
+
throw new Error("component is required");
|
|
778
|
+
}
|
|
779
|
+
const segment = Object.values(data.segments).find(
|
|
780
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
781
|
+
);
|
|
782
|
+
if (!segment) {
|
|
783
|
+
throw new Error(`Component "${componentName}" not found. Use fragments_list to see available components.`);
|
|
784
|
+
}
|
|
785
|
+
let variants = segment.variants;
|
|
786
|
+
if (variantName) {
|
|
787
|
+
const filtered = variants.filter(
|
|
788
|
+
(v) => v.name.toLowerCase() === variantName.toLowerCase()
|
|
789
|
+
);
|
|
790
|
+
if (filtered.length === 0) {
|
|
791
|
+
throw new Error(
|
|
792
|
+
`Variant "${variantName}" not found for ${componentName}. Available: ${variants.map((v) => v.name).join(", ")}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
variants = filtered;
|
|
796
|
+
}
|
|
797
|
+
if (maxExamples && maxExamples > 0) {
|
|
798
|
+
variants = variants.slice(0, maxExamples);
|
|
799
|
+
}
|
|
800
|
+
const truncateCode = (code) => {
|
|
801
|
+
if (!maxLines || maxLines <= 0) return code;
|
|
802
|
+
const lines = code.split("\n");
|
|
803
|
+
if (lines.length <= maxLines) return code;
|
|
804
|
+
return lines.slice(0, maxLines).join("\n") + "\n// ... truncated";
|
|
805
|
+
};
|
|
806
|
+
const examples = variants.map((variant) => {
|
|
807
|
+
if (variant.code) {
|
|
808
|
+
return {
|
|
809
|
+
variant: variant.name,
|
|
810
|
+
description: variant.description,
|
|
811
|
+
code: truncateCode(variant.code)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
const basicCode = `<${segment.meta.name} />`;
|
|
815
|
+
return {
|
|
816
|
+
variant: variant.name,
|
|
817
|
+
description: variant.description,
|
|
818
|
+
code: basicCode,
|
|
819
|
+
note: "No code example provided in fragment. Refer to props for customization."
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
const pkgName = await getPackageName();
|
|
823
|
+
const importStatement = `import { ${segment.meta.name} } from '${pkgName}';`;
|
|
824
|
+
const propsReference = Object.entries(segment.props ?? {}).map(([propName, prop]) => ({
|
|
825
|
+
name: propName,
|
|
826
|
+
type: prop.type,
|
|
827
|
+
required: prop.required,
|
|
828
|
+
default: prop.default,
|
|
829
|
+
description: prop.description
|
|
830
|
+
}));
|
|
831
|
+
return {
|
|
832
|
+
content: [
|
|
833
|
+
{
|
|
834
|
+
type: "text",
|
|
835
|
+
text: JSON.stringify(
|
|
836
|
+
{
|
|
837
|
+
component: segment.meta.name,
|
|
838
|
+
import: importStatement,
|
|
839
|
+
examples,
|
|
840
|
+
propsReference,
|
|
841
|
+
tip: "Use the propsReference to customize components. Examples show documented variants."
|
|
842
|
+
},
|
|
843
|
+
null,
|
|
844
|
+
2
|
|
845
|
+
)
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
case TOOL_NAMES.context: {
|
|
851
|
+
const data = await loadSegments();
|
|
852
|
+
const format = args?.format ?? "markdown";
|
|
853
|
+
const compact = args?.compact ?? false;
|
|
854
|
+
const includeCode = args?.includeCode ?? false;
|
|
855
|
+
const includeRelations = args?.includeRelations ?? false;
|
|
856
|
+
const segments = Object.values(data.segments);
|
|
857
|
+
const recipes = Object.values(data.recipes ?? {});
|
|
858
|
+
const { content, tokenEstimate } = generateContext(segments, {
|
|
859
|
+
format,
|
|
860
|
+
compact,
|
|
861
|
+
include: {
|
|
862
|
+
code: includeCode,
|
|
863
|
+
relations: includeRelations
|
|
864
|
+
}
|
|
865
|
+
}, recipes);
|
|
866
|
+
return {
|
|
867
|
+
content: [
|
|
868
|
+
{
|
|
869
|
+
type: "text",
|
|
870
|
+
text: content
|
|
871
|
+
}
|
|
872
|
+
],
|
|
873
|
+
// Include token estimate in a way that doesn't pollute the main content
|
|
874
|
+
_meta: {
|
|
875
|
+
tokenEstimate
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
case TOOL_NAMES.render: {
|
|
880
|
+
const componentName = args?.component;
|
|
881
|
+
const props = args?.props ?? {};
|
|
882
|
+
const viewport = args?.viewport;
|
|
883
|
+
if (!componentName) {
|
|
884
|
+
return {
|
|
885
|
+
content: [
|
|
886
|
+
{
|
|
887
|
+
type: "text",
|
|
888
|
+
text: "Error: component name is required"
|
|
889
|
+
}
|
|
890
|
+
],
|
|
891
|
+
isError: true
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const baseUrl = config.viewerUrl ?? "http://localhost:6006";
|
|
895
|
+
const renderUrl = `${baseUrl}/fragments/render`;
|
|
896
|
+
try {
|
|
897
|
+
const response = await fetch(renderUrl, {
|
|
898
|
+
method: "POST",
|
|
899
|
+
headers: { "Content-Type": "application/json" },
|
|
900
|
+
body: JSON.stringify({
|
|
901
|
+
component: componentName,
|
|
902
|
+
props,
|
|
903
|
+
viewport: viewport ?? { width: 800, height: 600 }
|
|
904
|
+
})
|
|
905
|
+
});
|
|
906
|
+
const result = await response.json();
|
|
907
|
+
if (!response.ok || result.error) {
|
|
908
|
+
return {
|
|
909
|
+
content: [
|
|
910
|
+
{
|
|
911
|
+
type: "text",
|
|
912
|
+
text: `Render error: ${result.error ?? "Unknown error"}`
|
|
913
|
+
}
|
|
914
|
+
],
|
|
915
|
+
isError: true
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
content: [
|
|
920
|
+
{
|
|
921
|
+
type: "image",
|
|
922
|
+
data: result.screenshot.replace("data:image/png;base64,", ""),
|
|
923
|
+
mimeType: "image/png"
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
type: "text",
|
|
927
|
+
text: `Successfully rendered ${componentName} with props: ${JSON.stringify(props)}`
|
|
928
|
+
}
|
|
929
|
+
]
|
|
930
|
+
};
|
|
931
|
+
} catch (error) {
|
|
932
|
+
return {
|
|
933
|
+
content: [
|
|
934
|
+
{
|
|
935
|
+
type: "text",
|
|
936
|
+
text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
937
|
+
}
|
|
938
|
+
],
|
|
939
|
+
isError: true
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
case TOOL_NAMES.compare: {
|
|
944
|
+
const componentName = args?.component;
|
|
945
|
+
const variantName = args?.variant;
|
|
946
|
+
const props = args?.props ?? {};
|
|
947
|
+
const figmaUrl = args?.figmaUrl;
|
|
948
|
+
const threshold = args?.threshold ?? 1;
|
|
949
|
+
if (!componentName) {
|
|
950
|
+
return {
|
|
951
|
+
content: [
|
|
952
|
+
{
|
|
953
|
+
type: "text",
|
|
954
|
+
text: "Error: component name is required"
|
|
955
|
+
}
|
|
956
|
+
],
|
|
957
|
+
isError: true
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const baseUrl = config.viewerUrl ?? "http://localhost:6006";
|
|
961
|
+
const compareUrl = `${baseUrl}/fragments/compare`;
|
|
962
|
+
try {
|
|
963
|
+
const response = await fetch(compareUrl, {
|
|
964
|
+
method: "POST",
|
|
965
|
+
headers: { "Content-Type": "application/json" },
|
|
966
|
+
body: JSON.stringify({
|
|
967
|
+
component: componentName,
|
|
968
|
+
variant: variantName,
|
|
969
|
+
props,
|
|
970
|
+
figmaUrl,
|
|
971
|
+
threshold
|
|
972
|
+
})
|
|
973
|
+
});
|
|
974
|
+
const result = await response.json();
|
|
975
|
+
if (!response.ok || result.error) {
|
|
976
|
+
return {
|
|
977
|
+
content: [
|
|
978
|
+
{
|
|
979
|
+
type: "text",
|
|
980
|
+
text: `Compare error: ${result.error ?? "Unknown error"}${result.suggestion ? `
|
|
981
|
+
Suggestion: ${result.suggestion}` : ""}`
|
|
982
|
+
}
|
|
983
|
+
],
|
|
984
|
+
isError: true
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
const content = [];
|
|
988
|
+
const summaryText = result.match ? `\u2705 MATCH: ${componentName} matches Figma design (${result.diffPercentage}% diff, threshold: ${result.threshold}%)` : `\u274C MISMATCH: ${componentName} differs from Figma design by ${result.diffPercentage}% (threshold: ${result.threshold}%)`;
|
|
989
|
+
content.push({
|
|
990
|
+
type: "text",
|
|
991
|
+
text: summaryText
|
|
992
|
+
});
|
|
993
|
+
if (result.diff && !result.match) {
|
|
994
|
+
content.push({
|
|
995
|
+
type: "image",
|
|
996
|
+
data: result.diff.replace("data:image/png;base64,", ""),
|
|
997
|
+
mimeType: "image/png"
|
|
998
|
+
});
|
|
999
|
+
content.push({
|
|
1000
|
+
type: "text",
|
|
1001
|
+
text: `Diff image above shows visual differences (red highlights). Changed regions: ${result.changedRegions?.length ?? 0}`
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
content.push({
|
|
1005
|
+
type: "text",
|
|
1006
|
+
text: JSON.stringify({
|
|
1007
|
+
match: result.match,
|
|
1008
|
+
diffPercentage: result.diffPercentage,
|
|
1009
|
+
threshold: result.threshold,
|
|
1010
|
+
figmaUrl: result.figmaUrl,
|
|
1011
|
+
changedRegions: result.changedRegions
|
|
1012
|
+
}, null, 2)
|
|
1013
|
+
});
|
|
1014
|
+
return { content };
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
return {
|
|
1017
|
+
content: [
|
|
1018
|
+
{
|
|
1019
|
+
type: "text",
|
|
1020
|
+
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.`
|
|
1021
|
+
}
|
|
1022
|
+
],
|
|
1023
|
+
isError: true
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
case TOOL_NAMES.fix: {
|
|
1028
|
+
const data = await loadSegments();
|
|
1029
|
+
const componentName = args?.component;
|
|
1030
|
+
const variantName = args?.variant ?? void 0;
|
|
1031
|
+
const fixType = args?.fixType ?? "all";
|
|
1032
|
+
if (!componentName) {
|
|
1033
|
+
throw new Error("component is required");
|
|
1034
|
+
}
|
|
1035
|
+
const segment = Object.values(data.segments).find(
|
|
1036
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1037
|
+
);
|
|
1038
|
+
if (!segment) {
|
|
1039
|
+
throw new Error(`Component "${componentName}" not found. Use fragments_list to see available components.`);
|
|
1040
|
+
}
|
|
1041
|
+
const baseUrl = config.viewerUrl ?? "http://localhost:6006";
|
|
1042
|
+
const fixUrl = `${baseUrl}/fragments/fix`;
|
|
1043
|
+
try {
|
|
1044
|
+
const response = await fetch(fixUrl, {
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
headers: { "Content-Type": "application/json" },
|
|
1047
|
+
body: JSON.stringify({
|
|
1048
|
+
component: componentName,
|
|
1049
|
+
variant: variantName,
|
|
1050
|
+
fixType
|
|
1051
|
+
})
|
|
1052
|
+
});
|
|
1053
|
+
const result = await response.json();
|
|
1054
|
+
if (!response.ok || result.error) {
|
|
1055
|
+
return {
|
|
1056
|
+
content: [
|
|
1057
|
+
{
|
|
1058
|
+
type: "text",
|
|
1059
|
+
text: `Fix generation error: ${result.error ?? "Unknown error"}`
|
|
1060
|
+
}
|
|
1061
|
+
],
|
|
1062
|
+
isError: true
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
content: [
|
|
1067
|
+
{
|
|
1068
|
+
type: "text",
|
|
1069
|
+
text: JSON.stringify({
|
|
1070
|
+
component: componentName,
|
|
1071
|
+
variant: variantName ?? "all",
|
|
1072
|
+
fixType,
|
|
1073
|
+
patches: result.patches,
|
|
1074
|
+
summary: result.summary,
|
|
1075
|
+
patchCount: result.patches.length,
|
|
1076
|
+
nextStep: result.patches.length > 0 ? "Apply patches using your editor or `patch` command, then run fragments_verify to confirm fixes." : void 0
|
|
1077
|
+
}, null, 2)
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
};
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
return {
|
|
1083
|
+
content: [
|
|
1084
|
+
{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
1087
|
+
}
|
|
1088
|
+
],
|
|
1089
|
+
isError: true
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
case TOOL_NAMES.recipe: {
|
|
1094
|
+
const data = await loadSegments();
|
|
1095
|
+
const recipeName = args?.name;
|
|
1096
|
+
const search = args?.search?.toLowerCase() ?? void 0;
|
|
1097
|
+
const component = args?.component?.toLowerCase() ?? void 0;
|
|
1098
|
+
const allRecipes = Object.values(data.recipes ?? {});
|
|
1099
|
+
if (allRecipes.length === 0) {
|
|
1100
|
+
return {
|
|
1101
|
+
content: [{
|
|
1102
|
+
type: "text",
|
|
1103
|
+
text: JSON.stringify({
|
|
1104
|
+
total: 0,
|
|
1105
|
+
recipes: [],
|
|
1106
|
+
hint: `No recipes found. Run \`${BRAND.cliCommand} build\` after adding .recipe.ts files.`
|
|
1107
|
+
}, null, 2)
|
|
1108
|
+
}]
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
let filtered = allRecipes;
|
|
1112
|
+
if (recipeName) {
|
|
1113
|
+
filtered = filtered.filter(
|
|
1114
|
+
(r) => r.name.toLowerCase() === recipeName.toLowerCase()
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (search) {
|
|
1118
|
+
filtered = filtered.filter((r) => {
|
|
1119
|
+
const haystack = [
|
|
1120
|
+
r.name,
|
|
1121
|
+
r.description,
|
|
1122
|
+
...r.tags ?? [],
|
|
1123
|
+
...r.components,
|
|
1124
|
+
r.category
|
|
1125
|
+
].join(" ").toLowerCase();
|
|
1126
|
+
return haystack.includes(search);
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
if (component) {
|
|
1130
|
+
filtered = filtered.filter(
|
|
1131
|
+
(r) => r.components.some((c) => c.toLowerCase() === component)
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
content: [{
|
|
1136
|
+
type: "text",
|
|
1137
|
+
text: JSON.stringify({
|
|
1138
|
+
total: filtered.length,
|
|
1139
|
+
recipes: filtered
|
|
1140
|
+
}, null, 2)
|
|
1141
|
+
}]
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
case TOOL_NAMES.verify: {
|
|
1145
|
+
const { Timer, CaptureEngine: CE, bufferToBase64Url: toBase64 } = await getService();
|
|
1146
|
+
const timer = new Timer();
|
|
1147
|
+
const componentName = args?.component;
|
|
1148
|
+
const variantName = args?.variant;
|
|
1149
|
+
const theme = args?.theme ?? config.theme ?? DEFAULTS.theme;
|
|
1150
|
+
const threshold = args?.threshold ?? config.threshold ?? DEFAULTS.diffThreshold;
|
|
1151
|
+
if (!componentName || !variantName) {
|
|
1152
|
+
throw new Error("component and variant are required");
|
|
1153
|
+
}
|
|
1154
|
+
const storage = await getStorageManager();
|
|
1155
|
+
const pool = await getBrowserPool();
|
|
1156
|
+
const diff = await getDiffEngine();
|
|
1157
|
+
const baseline = await storage.loadBaseline(componentName, variantName, theme);
|
|
1158
|
+
if (!baseline) {
|
|
1159
|
+
return {
|
|
1160
|
+
content: [
|
|
1161
|
+
{
|
|
1162
|
+
type: "text",
|
|
1163
|
+
text: JSON.stringify(
|
|
1164
|
+
{
|
|
1165
|
+
verdict: "error",
|
|
1166
|
+
matches: false,
|
|
1167
|
+
diffPercentage: 0,
|
|
1168
|
+
screenshot: "",
|
|
1169
|
+
baseline: "",
|
|
1170
|
+
notes: [],
|
|
1171
|
+
error: `No baseline found for ${componentName}/${variantName}. Run \`${BRAND.cliCommand} screenshot\` first.`,
|
|
1172
|
+
timing: { renderMs: 0, captureMs: 0, diffMs: 0, totalMs: timer.elapsed() }
|
|
1173
|
+
},
|
|
1174
|
+
null,
|
|
1175
|
+
2
|
|
1176
|
+
)
|
|
1177
|
+
}
|
|
1178
|
+
]
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
const viewerUrl = config.viewerUrl ?? `http://localhost:${DEFAULTS.port}`;
|
|
1182
|
+
const captureEngine = new CE(pool, viewerUrl);
|
|
1183
|
+
const current = await captureEngine.captureVariant(componentName, variantName, {
|
|
1184
|
+
theme,
|
|
1185
|
+
delay: DEFAULTS.captureDelayMs
|
|
1186
|
+
});
|
|
1187
|
+
let diffResult;
|
|
1188
|
+
let matches = false;
|
|
1189
|
+
if (diff.areIdentical(current, baseline)) {
|
|
1190
|
+
matches = true;
|
|
1191
|
+
diffResult = {
|
|
1192
|
+
matches: true,
|
|
1193
|
+
diffPercentage: 0,
|
|
1194
|
+
diffPixelCount: 0,
|
|
1195
|
+
totalPixels: current.viewport.width * current.viewport.height,
|
|
1196
|
+
changedRegions: [],
|
|
1197
|
+
diffTimeMs: 0
|
|
1198
|
+
};
|
|
1199
|
+
} else {
|
|
1200
|
+
diffResult = diff.compare(current, baseline, { threshold });
|
|
1201
|
+
matches = diffResult.matches;
|
|
1202
|
+
}
|
|
1203
|
+
const result = {
|
|
1204
|
+
verdict: matches ? "pass" : "fail",
|
|
1205
|
+
matches,
|
|
1206
|
+
diffPercentage: diffResult.diffPercentage,
|
|
1207
|
+
screenshot: toBase64(current.data),
|
|
1208
|
+
baseline: toBase64(baseline.data),
|
|
1209
|
+
diffImage: diffResult.diffImage ? toBase64(diffResult.diffImage) : void 0,
|
|
1210
|
+
notes: matches ? ["Screenshot matches baseline within threshold"] : [
|
|
1211
|
+
`Diff percentage (${diffResult.diffPercentage}%) exceeds threshold (${threshold}%)`,
|
|
1212
|
+
`${diffResult.changedRegions.length} changed region(s) detected`
|
|
1213
|
+
],
|
|
1214
|
+
timing: {
|
|
1215
|
+
renderMs: current.metadata.renderTimeMs,
|
|
1216
|
+
captureMs: current.metadata.captureTimeMs,
|
|
1217
|
+
diffMs: diffResult.diffTimeMs,
|
|
1218
|
+
totalMs: timer.elapsed()
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
return {
|
|
1222
|
+
content: [
|
|
1223
|
+
{
|
|
1224
|
+
type: "text",
|
|
1225
|
+
text: JSON.stringify(result, null, 2)
|
|
1226
|
+
}
|
|
1227
|
+
]
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
default:
|
|
1231
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1232
|
+
}
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
return {
|
|
1235
|
+
content: [
|
|
1236
|
+
{
|
|
1237
|
+
type: "text",
|
|
1238
|
+
text: JSON.stringify({
|
|
1239
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1240
|
+
})
|
|
1241
|
+
}
|
|
1242
|
+
],
|
|
1243
|
+
isError: true
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
server.onclose = async () => {
|
|
1248
|
+
if (browserPool) {
|
|
1249
|
+
await browserPool.shutdown();
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
return server;
|
|
1253
|
+
}
|
|
1254
|
+
async function startMcpServer(config) {
|
|
1255
|
+
const server = createMcpServer(config);
|
|
1256
|
+
const transport = new StdioServerTransport();
|
|
1257
|
+
await server.connect(transport);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export {
|
|
1261
|
+
createMcpServer,
|
|
1262
|
+
startMcpServer
|
|
1263
|
+
};
|
|
1264
|
+
//# sourceMappingURL=chunk-RUZV6VLE.js.map
|