@fragments-sdk/mcp 0.5.1 → 0.5.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.
@@ -0,0 +1,2103 @@
1
+ import {
2
+ ComponentGraphEngine,
3
+ deserializeGraph
4
+ } from "./chunk-WXUZ55XQ.js";
5
+
6
+ // src/server.ts
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema
12
+ } from "@modelcontextprotocol/sdk/types.js";
13
+ import { readFile } from "fs/promises";
14
+ import { existsSync as existsSync2 } from "fs";
15
+ import { readFileSync as readFileSync2 } from "fs";
16
+ import { join as join2 } from "path";
17
+
18
+ // src/constants.ts
19
+ var BRAND = {
20
+ /** Display name (e.g., "Fragments") */
21
+ name: "Fragments",
22
+ /** Lowercase name for file paths and CLI (e.g., "fragments") */
23
+ nameLower: "fragments",
24
+ /** File extension for fragment definition files (e.g., ".fragment.tsx") */
25
+ fileExtension: ".fragment.tsx",
26
+ /** Legacy file extension for segments (still supported for migration) */
27
+ legacyFileExtension: ".segment.tsx",
28
+ /** JSON file extension for compiled output */
29
+ jsonExtension: ".fragment.json",
30
+ /** Default output file name (e.g., "fragments.json") */
31
+ outFile: "fragments.json",
32
+ /** Config file name (e.g., "fragments.config.ts") */
33
+ configFile: "fragments.config.ts",
34
+ /** Legacy config file name (still supported for migration) */
35
+ legacyConfigFile: "segments.config.ts",
36
+ /** CLI command name (e.g., "fragments") */
37
+ cliCommand: "fragments",
38
+ /** Package scope (e.g., "@fragments") */
39
+ packageScope: "@fragments",
40
+ /** Directory for storing fragments, registry, and cache */
41
+ dataDir: ".fragments",
42
+ /** Components subdirectory within .fragments/ */
43
+ componentsDir: "components",
44
+ /** Registry file name */
45
+ registryFile: "registry.json",
46
+ /** Context file name (AI-ready markdown) */
47
+ contextFile: "context.md",
48
+ /** Screenshots subdirectory */
49
+ screenshotsDir: "screenshots",
50
+ /** Cache subdirectory (gitignored) */
51
+ cacheDir: "cache",
52
+ /** Diff output subdirectory (gitignored) */
53
+ diffDir: "diff",
54
+ /** Manifest filename */
55
+ manifestFile: "manifest.json",
56
+ /** Prefix for localStorage keys (e.g., "fragments-") */
57
+ storagePrefix: "fragments-",
58
+ /** Static viewer HTML file name */
59
+ viewerHtmlFile: "fragments-viewer.html",
60
+ /** MCP tool name prefix (e.g., "fragments_") */
61
+ mcpToolPrefix: "fragments_",
62
+ /** File extension for block definition files */
63
+ blockFileExtension: ".block.ts",
64
+ /** @deprecated Use blockFileExtension instead */
65
+ recipeFileExtension: ".recipe.ts",
66
+ /** Vite plugin namespace */
67
+ vitePluginNamespace: "fragments-core-shim"
68
+ };
69
+ var DEFAULTS = {
70
+ /** Default viewport dimensions */
71
+ viewport: {
72
+ width: 1280,
73
+ height: 800
74
+ },
75
+ /** Default diff threshold (percentage) */
76
+ diffThreshold: 5,
77
+ /** Browser pool size */
78
+ poolSize: 3,
79
+ /** Idle timeout before browser shutdown (ms) - 5 minutes */
80
+ idleTimeoutMs: 5 * 60 * 1e3,
81
+ /** Delay after render before capture (ms) */
82
+ captureDelayMs: 100,
83
+ /** Font loading timeout (ms) */
84
+ fontTimeoutMs: 3e3,
85
+ /** Default theme */
86
+ theme: "light",
87
+ /** Dev server port */
88
+ port: 6006
89
+ };
90
+
91
+ // ../context/dist/chunk-2UQY4VNM.js
92
+ var PLACEHOLDER_PATTERNS = [
93
+ /^\w+ component is needed$/i,
94
+ /^Alternative component is more appropriate$/i,
95
+ /^Use \w+ when you need/i
96
+ ];
97
+ function filterPlaceholders(items) {
98
+ if (!items) return [];
99
+ return items.filter(
100
+ (item) => !PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(item.trim()))
101
+ );
102
+ }
103
+ function generateContext(fragments, options = {}, blocks) {
104
+ const format = options.format ?? "markdown";
105
+ const compact = options.compact ?? false;
106
+ const include = {
107
+ props: options.include?.props ?? true,
108
+ variants: options.include?.variants ?? true,
109
+ usage: options.include?.usage ?? true,
110
+ relations: options.include?.relations ?? false,
111
+ code: options.include?.code ?? false
112
+ };
113
+ const sorted = [...fragments].sort((a, b) => {
114
+ const catCompare = a.meta.category.localeCompare(b.meta.category);
115
+ if (catCompare !== 0) return catCompare;
116
+ return a.meta.name.localeCompare(b.meta.name);
117
+ });
118
+ if (format === "json") {
119
+ return generateJsonContext(sorted, include, compact, blocks);
120
+ }
121
+ return generateMarkdownContext(sorted, include, compact, blocks);
122
+ }
123
+ function generateMarkdownContext(fragments, include, compact, blocks) {
124
+ const lines = [];
125
+ lines.push("# Design System Reference");
126
+ lines.push("");
127
+ lines.push("## Quick Reference");
128
+ lines.push("");
129
+ lines.push("| Component | Category | Use For |");
130
+ lines.push("|-----------|----------|---------|");
131
+ for (const fragment of fragments) {
132
+ const filteredWhen = filterPlaceholders(fragment.usage.when);
133
+ const useFor = filteredWhen.slice(0, 2).join(", ") || fragment.meta.description;
134
+ lines.push(`| ${fragment.meta.name} | ${fragment.meta.category} | ${truncate(useFor, 50)} |`);
135
+ }
136
+ lines.push("");
137
+ if (compact) {
138
+ const content2 = lines.join("\n");
139
+ return { content: content2, tokenEstimate: estimateTokens(content2) };
140
+ }
141
+ lines.push("## Components");
142
+ lines.push("");
143
+ for (const fragment of fragments) {
144
+ lines.push(`### ${fragment.meta.name}`);
145
+ lines.push("");
146
+ const statusParts = [`**Category:** ${fragment.meta.category}`];
147
+ if (fragment.meta.status) {
148
+ statusParts.push(`**Status:** ${fragment.meta.status}`);
149
+ }
150
+ lines.push(statusParts.join(" | "));
151
+ lines.push("");
152
+ if (fragment.meta.description) {
153
+ lines.push(fragment.meta.description);
154
+ lines.push("");
155
+ }
156
+ const whenFiltered = filterPlaceholders(fragment.usage.when);
157
+ const whenNotFiltered = filterPlaceholders(fragment.usage.whenNot);
158
+ if (include.usage && (whenFiltered.length > 0 || whenNotFiltered.length > 0)) {
159
+ if (whenFiltered.length > 0) {
160
+ lines.push("**When to use:**");
161
+ for (const when of whenFiltered) {
162
+ lines.push(`- ${when}`);
163
+ }
164
+ lines.push("");
165
+ }
166
+ if (whenNotFiltered.length > 0) {
167
+ lines.push("**When NOT to use:**");
168
+ for (const whenNot of whenNotFiltered) {
169
+ lines.push(`- ${whenNot}`);
170
+ }
171
+ lines.push("");
172
+ }
173
+ }
174
+ if (include.props && Object.keys(fragment.props).length > 0) {
175
+ lines.push("**Props:**");
176
+ for (const [name, prop] of Object.entries(fragment.props)) {
177
+ lines.push(`- \`${name}\`: ${formatPropType(prop)}${prop.required ? " (required)" : ""}`);
178
+ }
179
+ lines.push("");
180
+ }
181
+ if (include.variants && fragment.variants.length > 0) {
182
+ const variantNames = fragment.variants.map((v) => v.name).join(", ");
183
+ lines.push(`**Variants:** ${variantNames}`);
184
+ lines.push("");
185
+ if (include.code) {
186
+ for (const variant of fragment.variants) {
187
+ if (variant.code) {
188
+ lines.push(`*${variant.name}:*`);
189
+ lines.push("```tsx");
190
+ lines.push(variant.code);
191
+ lines.push("```");
192
+ lines.push("");
193
+ }
194
+ }
195
+ }
196
+ }
197
+ if (include.relations && fragment.relations && fragment.relations.length > 0) {
198
+ lines.push("**Related:**");
199
+ for (const relation of fragment.relations) {
200
+ lines.push(`- ${relation.component} (${relation.relationship}): ${relation.note}`);
201
+ }
202
+ lines.push("");
203
+ }
204
+ lines.push("---");
205
+ lines.push("");
206
+ }
207
+ if (blocks && blocks.length > 0) {
208
+ lines.push("## Blocks");
209
+ lines.push("");
210
+ lines.push("Composition patterns showing how components wire together.");
211
+ lines.push("");
212
+ for (const block of blocks) {
213
+ lines.push(`### ${block.name}`);
214
+ lines.push("");
215
+ lines.push(block.description);
216
+ lines.push("");
217
+ lines.push(`**Category:** ${block.category}`);
218
+ lines.push(`**Components:** ${block.components.join(", ")}`);
219
+ if (block.tags && block.tags.length > 0) {
220
+ lines.push(`**Tags:** ${block.tags.join(", ")}`);
221
+ }
222
+ lines.push("");
223
+ lines.push("```tsx");
224
+ lines.push(block.code);
225
+ lines.push("```");
226
+ lines.push("");
227
+ lines.push("---");
228
+ lines.push("");
229
+ }
230
+ }
231
+ const content = lines.join("\n");
232
+ return { content, tokenEstimate: estimateTokens(content) };
233
+ }
234
+ function generateJsonContext(fragments, include, compact, blocks) {
235
+ const categories = [...new Set(fragments.map((s) => s.meta.category))].sort();
236
+ const components = {};
237
+ for (const fragment of fragments) {
238
+ const component = {
239
+ category: fragment.meta.category,
240
+ description: fragment.meta.description
241
+ };
242
+ if (fragment.meta.status) {
243
+ component.status = fragment.meta.status;
244
+ }
245
+ if (!compact) {
246
+ if (include.usage) {
247
+ const whenFiltered = filterPlaceholders(fragment.usage.when);
248
+ const whenNotFiltered = filterPlaceholders(fragment.usage.whenNot);
249
+ if (whenFiltered.length > 0) component.whenToUse = whenFiltered;
250
+ if (whenNotFiltered.length > 0) component.whenNotToUse = whenNotFiltered;
251
+ }
252
+ if (include.props && Object.keys(fragment.props).length > 0) {
253
+ component.props = {};
254
+ for (const [name, prop] of Object.entries(fragment.props)) {
255
+ component.props[name] = {
256
+ type: formatPropType(prop),
257
+ description: prop.description
258
+ };
259
+ if (prop.required) component.props[name].required = true;
260
+ if (prop.default !== void 0) component.props[name].default = prop.default;
261
+ }
262
+ }
263
+ if (include.variants && fragment.variants.length > 0) {
264
+ component.variants = fragment.variants.map((v) => v.name);
265
+ }
266
+ if (include.relations && fragment.relations && fragment.relations.length > 0) {
267
+ component.relations = fragment.relations.map((r) => ({
268
+ component: r.component,
269
+ relationship: r.relationship,
270
+ note: r.note
271
+ }));
272
+ }
273
+ }
274
+ components[fragment.meta.name] = component;
275
+ }
276
+ const blocksMap = blocks && blocks.length > 0 ? Object.fromEntries(blocks.map((b) => [b.name, {
277
+ description: b.description,
278
+ category: b.category,
279
+ components: b.components,
280
+ code: b.code,
281
+ tags: b.tags
282
+ }])) : void 0;
283
+ const output = {
284
+ version: "1.0",
285
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
286
+ summary: {
287
+ totalComponents: fragments.length,
288
+ categories,
289
+ ...blocksMap && { totalBlocks: blocks.length }
290
+ },
291
+ components,
292
+ ...blocksMap && { blocks: blocksMap }
293
+ };
294
+ const content = JSON.stringify(output, null, 2);
295
+ return { content, tokenEstimate: estimateTokens(content) };
296
+ }
297
+ function formatPropType(prop) {
298
+ if (prop.type === "enum" && prop.values) {
299
+ return prop.values.map((v) => `"${v}"`).join(" | ");
300
+ }
301
+ if (prop.default !== void 0) {
302
+ return `${prop.type} (default: ${JSON.stringify(prop.default)})`;
303
+ }
304
+ return prop.type;
305
+ }
306
+ function truncate(str, maxLength) {
307
+ if (str.length <= maxLength) return str;
308
+ return str.slice(0, maxLength - 3) + "...";
309
+ }
310
+ function estimateTokens(text) {
311
+ return Math.ceil(text.length / 4);
312
+ }
313
+
314
+ // ../context/dist/chunk-HAJWPNLU.js
315
+ var MCP_TOOL_DEFINITIONS = [
316
+ {
317
+ key: "discover",
318
+ 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.",
319
+ params: {
320
+ useCase: {
321
+ type: "string",
322
+ description: 'Description of what you want to build \u2014 returns ranked suggestions (e.g., "form for user email input", "button to submit data")'
323
+ },
324
+ component: {
325
+ type: "string",
326
+ description: 'Component name to find alternatives for (e.g., "Button")'
327
+ },
328
+ category: {
329
+ type: "string",
330
+ description: 'Filter by category (e.g., "actions", "forms", "layout")'
331
+ },
332
+ search: {
333
+ type: "string",
334
+ description: "Search term to filter by name, description, or tags"
335
+ },
336
+ status: {
337
+ type: "string",
338
+ enum: ["stable", "beta", "deprecated", "experimental"],
339
+ description: "Filter by component status"
340
+ },
341
+ format: {
342
+ type: "string",
343
+ enum: ["markdown", "json"],
344
+ description: "Output format for context mode (default: markdown)"
345
+ },
346
+ compact: {
347
+ type: "boolean",
348
+ description: "If true, returns minimal output (just component names and categories)"
349
+ },
350
+ includeCode: {
351
+ type: "boolean",
352
+ description: "If true, includes code examples for each variant"
353
+ },
354
+ includeRelations: {
355
+ type: "boolean",
356
+ description: "If true, includes component relationships"
357
+ }
358
+ }
359
+ },
360
+ {
361
+ key: "inspect",
362
+ 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.",
363
+ params: {
364
+ component: {
365
+ type: "string",
366
+ description: 'Component name (e.g., "Button", "Input")'
367
+ },
368
+ fields: {
369
+ type: "array",
370
+ items: { type: "string" },
371
+ description: 'Specific fields to return (e.g., ["meta", "usage.when", "contract.propsSummary", "props", "examples", "guidelines"]). If omitted, returns everything. Supports dot notation.'
372
+ },
373
+ variant: {
374
+ type: "string",
375
+ description: 'Filter examples to a specific variant name (e.g., "Default", "Primary")'
376
+ },
377
+ maxExamples: {
378
+ type: "number",
379
+ description: "Maximum number of code examples to return (default: all)"
380
+ },
381
+ maxLines: {
382
+ type: "number",
383
+ description: "Maximum lines per code example (truncates longer examples)"
384
+ }
385
+ },
386
+ required: ["component"]
387
+ },
388
+ {
389
+ key: "blocks",
390
+ description: 'Search and retrieve composition blocks \u2014 named patterns showing how design system components wire together for common use cases (e.g., "Login Form", "Settings Page"). Returns the block with its code pattern.',
391
+ params: {
392
+ name: {
393
+ type: "string",
394
+ description: 'Exact block name to retrieve (e.g., "Login Form")'
395
+ },
396
+ search: {
397
+ type: "string",
398
+ description: "Free-text search across block names, descriptions, tags, and components"
399
+ },
400
+ component: {
401
+ type: "string",
402
+ description: 'Filter blocks that use a specific component (e.g., "Button")'
403
+ },
404
+ category: {
405
+ type: "string",
406
+ description: 'Filter by category (e.g., "authentication", "marketing", "dashboard", "settings", "ecommerce", "ai")'
407
+ }
408
+ }
409
+ },
410
+ {
411
+ key: "tokens",
412
+ description: "List available CSS design tokens (custom properties) by category. Use this when you need to style custom elements or override defaults \u2014 no more guessing variable names. Filter by category or search by keyword.",
413
+ params: {
414
+ category: {
415
+ type: "string",
416
+ description: 'Filter by category (e.g., "colors", "spacing", "typography", "surfaces", "shadows", "radius", "borders", "text", "focus", "layout", "code", "component-sizing")'
417
+ },
418
+ search: {
419
+ type: "string",
420
+ description: 'Search token names (e.g., "accent", "hover", "padding")'
421
+ }
422
+ }
423
+ },
424
+ {
425
+ key: "implement",
426
+ description: "One-shot implementation helper. Describe what you want to build and get everything needed in a single call: best-matching component(s) with full props and code examples, relevant composition blocks, and applicable CSS tokens. Saves multiple round-trips.",
427
+ params: {
428
+ useCase: {
429
+ type: "string",
430
+ description: 'What you want to implement (e.g., "login form", "data table with sorting", "streaming chat messages")'
431
+ }
432
+ },
433
+ required: ["useCase"]
434
+ },
435
+ {
436
+ key: "render",
437
+ description: "Render a component and return a screenshot. Optionally compare against a Figma design ('figmaUrl'). Requires a running Fragments dev server (viewer URL). Use this to verify your implementation looks correct.",
438
+ params: {
439
+ component: {
440
+ type: "string",
441
+ description: 'Component name (e.g., "Button", "Card", "Input")'
442
+ },
443
+ props: {
444
+ type: "object",
445
+ description: 'Props to pass to the component (e.g., { "variant": "primary", "children": "Click me" })'
446
+ },
447
+ viewport: {
448
+ type: "object",
449
+ properties: {
450
+ width: {
451
+ type: "number",
452
+ description: "Viewport width (default: 800)"
453
+ },
454
+ height: {
455
+ type: "number",
456
+ description: "Viewport height (default: 600)"
457
+ }
458
+ },
459
+ description: "Optional viewport size for the render"
460
+ },
461
+ figmaUrl: {
462
+ type: "string",
463
+ description: "Figma frame URL \u2014 if provided, compares the render against the Figma design"
464
+ },
465
+ variant: {
466
+ type: "string",
467
+ description: "Variant name for compare mode"
468
+ },
469
+ threshold: {
470
+ type: "number",
471
+ description: "Diff threshold percentage (default: 1 for Figma)"
472
+ }
473
+ },
474
+ required: ["component"]
475
+ },
476
+ {
477
+ key: "fix",
478
+ description: "Generate patches to fix token compliance issues in a component. Returns unified diff patches that replace hardcoded CSS values with design token references. Requires a running Fragments dev server.",
479
+ params: {
480
+ component: {
481
+ type: "string",
482
+ description: 'Component name to generate fixes for (e.g., "Button", "Card")'
483
+ },
484
+ variant: {
485
+ type: "string",
486
+ description: "Specific variant to fix (optional, fixes all variants if omitted)"
487
+ },
488
+ fixType: {
489
+ type: "string",
490
+ enum: ["token", "all"],
491
+ description: 'Type of fixes to generate: "token" for hardcoded\u2192token replacements, "all" for all available fixes (default: "all")'
492
+ }
493
+ },
494
+ required: ["component"]
495
+ },
496
+ {
497
+ key: "graph",
498
+ description: 'Query the component relationship graph. Understand dependencies, impact analysis, composition trees, alternatives, and design system health. Use "health" for an overview, "dependencies"/"dependents" for direct relationships, "impact" for change analysis, "composition" for compound component trees.',
499
+ params: {
500
+ mode: {
501
+ type: "string",
502
+ enum: ["dependencies", "dependents", "impact", "path", "composition", "alternatives", "islands", "health"],
503
+ description: "Query mode"
504
+ },
505
+ component: {
506
+ type: "string",
507
+ description: "Component name (required for most modes)"
508
+ },
509
+ target: {
510
+ type: "string",
511
+ description: 'Target component for "path" mode'
512
+ },
513
+ edgeTypes: {
514
+ type: "array",
515
+ items: { type: "string" },
516
+ description: "Filter by edge types (imports, hook-depends, renders, composes, parent-of, alternative-to, sibling-of)"
517
+ },
518
+ maxDepth: {
519
+ type: "number",
520
+ description: "Max traversal depth for impact mode (default: 3)"
521
+ }
522
+ },
523
+ required: ["mode"]
524
+ },
525
+ {
526
+ key: "a11y",
527
+ description: "Run an accessibility audit on a component. Returns axe-core violations, a WCAG compliance score, and optional fix suggestions. Requires a running Fragments dev server.",
528
+ params: {
529
+ component: {
530
+ type: "string",
531
+ description: 'Component name to audit (e.g., "Button", "Card")'
532
+ },
533
+ variant: {
534
+ type: "string",
535
+ description: "Specific variant to audit (optional, audits all variants if omitted)"
536
+ },
537
+ standard: {
538
+ type: "string",
539
+ enum: ["AA", "AAA"],
540
+ description: "WCAG compliance level to check against (default: AA)"
541
+ },
542
+ includeFixPatches: {
543
+ type: "boolean",
544
+ description: "If true, includes auto-fix suggestions for each violation"
545
+ }
546
+ },
547
+ required: ["component"]
548
+ }
549
+ ];
550
+ function buildToolNames(prefix) {
551
+ const map = {};
552
+ for (const def of MCP_TOOL_DEFINITIONS) {
553
+ map[def.key] = `${prefix}_${def.key}`;
554
+ }
555
+ return map;
556
+ }
557
+ function buildMcpTools(prefix, extensions) {
558
+ const extMap = /* @__PURE__ */ new Map();
559
+ if (extensions) {
560
+ for (const ext of extensions) {
561
+ extMap.set(ext.key, ext);
562
+ }
563
+ }
564
+ return MCP_TOOL_DEFINITIONS.map((def) => {
565
+ const ext = extMap.get(def.key);
566
+ const mergedParams = ext ? { ...def.params, ...ext.params } : def.params;
567
+ const properties = {};
568
+ for (const [name, param] of Object.entries(mergedParams)) {
569
+ const prop = {
570
+ type: param.type,
571
+ description: param.description
572
+ };
573
+ if (param.enum) prop.enum = param.enum;
574
+ if (param.items) prop.items = param.items;
575
+ if (param.properties) {
576
+ const nested = {};
577
+ for (const [k, v] of Object.entries(param.properties)) {
578
+ nested[k] = { type: v.type, description: v.description };
579
+ }
580
+ prop.properties = nested;
581
+ }
582
+ properties[name] = prop;
583
+ }
584
+ return {
585
+ name: `${prefix}_${def.key}`,
586
+ description: ext?.description ?? def.description,
587
+ inputSchema: {
588
+ type: "object",
589
+ properties,
590
+ ...def.required && { required: def.required }
591
+ }
592
+ };
593
+ });
594
+ }
595
+
596
+ // src/discovery.ts
597
+ import { existsSync, readFileSync, readdirSync } from "fs";
598
+ import { join, dirname, resolve } from "path";
599
+ import { createRequire } from "module";
600
+ function resolveWorkspaceGlob(baseDir, pattern) {
601
+ const parts = pattern.split("/");
602
+ let dirs = [baseDir];
603
+ for (const part of parts) {
604
+ if (part === "**") continue;
605
+ const next = [];
606
+ for (const d of dirs) {
607
+ if (part === "*") {
608
+ try {
609
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
610
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
611
+ next.push(join(d, entry.name));
612
+ }
613
+ }
614
+ } catch {
615
+ }
616
+ } else {
617
+ const candidate = join(d, part);
618
+ if (existsSync(candidate)) next.push(candidate);
619
+ }
620
+ }
621
+ dirs = next;
622
+ }
623
+ return dirs;
624
+ }
625
+ function getWorkspaceDirs(rootDir) {
626
+ const dirs = [];
627
+ const rootPkgPath = join(rootDir, "package.json");
628
+ if (existsSync(rootPkgPath)) {
629
+ try {
630
+ const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
631
+ const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
632
+ if (Array.isArray(workspaces)) {
633
+ for (const pattern of workspaces) {
634
+ dirs.push(...resolveWorkspaceGlob(rootDir, pattern));
635
+ }
636
+ return dirs;
637
+ }
638
+ } catch {
639
+ }
640
+ }
641
+ const pnpmWsPath = join(rootDir, "pnpm-workspace.yaml");
642
+ if (existsSync(pnpmWsPath)) {
643
+ try {
644
+ const content = readFileSync(pnpmWsPath, "utf-8");
645
+ const lines = content.split("\n");
646
+ let inPackages = false;
647
+ for (const line of lines) {
648
+ if (/^packages\s*:/.test(line)) {
649
+ inPackages = true;
650
+ continue;
651
+ }
652
+ if (inPackages) {
653
+ const match = line.match(/^\s+-\s+['"]?([^'"#\n]+)['"]?/);
654
+ if (match) {
655
+ dirs.push(...resolveWorkspaceGlob(rootDir, match[1].trim()));
656
+ } else if (/^\S/.test(line) && line.trim()) {
657
+ break;
658
+ }
659
+ }
660
+ }
661
+ } catch {
662
+ }
663
+ }
664
+ return dirs;
665
+ }
666
+ function resolveDepPackageJson(localRequire, depName) {
667
+ try {
668
+ return localRequire.resolve(`${depName}/package.json`);
669
+ } catch {
670
+ }
671
+ try {
672
+ const mainPath = localRequire.resolve(depName);
673
+ let dir = dirname(mainPath);
674
+ while (true) {
675
+ const candidate = join(dir, "package.json");
676
+ if (existsSync(candidate)) {
677
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
678
+ if (pkg.name === depName) return candidate;
679
+ }
680
+ const parent = dirname(dir);
681
+ if (parent === dir) break;
682
+ dir = parent;
683
+ }
684
+ } catch {
685
+ }
686
+ return null;
687
+ }
688
+ function findFragmentsInDeps(dir, found) {
689
+ const pkgJsonPath = join(dir, "package.json");
690
+ if (!existsSync(pkgJsonPath)) return;
691
+ try {
692
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
693
+ const allDeps = {
694
+ ...pkgJson.dependencies,
695
+ ...pkgJson.devDependencies
696
+ };
697
+ const localRequire = createRequire(join(dir, "noop.js"));
698
+ for (const depName of Object.keys(allDeps)) {
699
+ try {
700
+ const depPkgPath = resolveDepPackageJson(localRequire, depName);
701
+ if (!depPkgPath) continue;
702
+ const depPkg = JSON.parse(readFileSync(depPkgPath, "utf-8"));
703
+ if (depPkg.fragments) {
704
+ const fragmentsPath = join(dirname(depPkgPath), depPkg.fragments);
705
+ if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
706
+ found.push(fragmentsPath);
707
+ }
708
+ }
709
+ } catch {
710
+ }
711
+ }
712
+ } catch {
713
+ }
714
+ }
715
+ function findFragmentsJson(startDir) {
716
+ const found = [];
717
+ const resolvedStart = resolve(startDir);
718
+ let dir = resolvedStart;
719
+ while (true) {
720
+ const candidate = join(dir, BRAND.outFile);
721
+ if (existsSync(candidate)) {
722
+ found.push(candidate);
723
+ break;
724
+ }
725
+ const parent = dirname(dir);
726
+ if (parent === dir) break;
727
+ dir = parent;
728
+ }
729
+ findFragmentsInDeps(resolvedStart, found);
730
+ if (found.length === 0 || existsSync(join(resolvedStart, "pnpm-workspace.yaml"))) {
731
+ const workspaceDirs = getWorkspaceDirs(resolvedStart);
732
+ for (const wsDir of workspaceDirs) {
733
+ findFragmentsInDeps(wsDir, found);
734
+ }
735
+ }
736
+ return found;
737
+ }
738
+
739
+ // src/search.ts
740
+ var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
741
+ var CONVEX_TIMEOUT_MS = 3e3;
742
+ var SYNONYM_MAP = {
743
+ "form": ["input", "field", "submit", "validation"],
744
+ "input": ["form", "field", "text", "entry"],
745
+ "button": ["action", "click", "submit", "trigger"],
746
+ "action": ["button", "click", "trigger"],
747
+ "alert": ["notification", "message", "warning", "error", "feedback"],
748
+ "notification": ["alert", "message", "toast"],
749
+ "card": ["container", "panel", "box", "content"],
750
+ "toggle": ["switch", "checkbox", "boolean", "on/off"],
751
+ "switch": ["toggle", "checkbox", "boolean"],
752
+ "badge": ["tag", "label", "status", "indicator"],
753
+ "status": ["badge", "indicator", "state"],
754
+ "login": ["auth", "signin", "authentication", "form"],
755
+ "auth": ["login", "signin", "authentication"],
756
+ "chat": ["message", "conversation", "ai"],
757
+ "table": ["data", "grid", "list", "rows"]
758
+ };
759
+ async function searchConvex(query, limit = 10, kind) {
760
+ try {
761
+ const controller = new AbortController();
762
+ const timeout = setTimeout(() => controller.abort(), CONVEX_TIMEOUT_MS);
763
+ const response = await fetch(CONVEX_SEARCH_URL, {
764
+ method: "POST",
765
+ headers: { "Content-Type": "application/json" },
766
+ body: JSON.stringify({ query, limit, ...kind && { kind } }),
767
+ signal: controller.signal
768
+ });
769
+ clearTimeout(timeout);
770
+ if (!response.ok) {
771
+ return [];
772
+ }
773
+ const data = await response.json();
774
+ return data.results.map((r, i) => ({
775
+ name: r.name,
776
+ kind: r.kind ?? "component",
777
+ rank: i,
778
+ score: r.score
779
+ }));
780
+ } catch {
781
+ return [];
782
+ }
783
+ }
784
+ function keywordScoreComponents(query, fragments) {
785
+ const searchTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
786
+ const expandedTerms = new Set(searchTerms);
787
+ for (const term of searchTerms) {
788
+ const synonyms = SYNONYM_MAP[term];
789
+ if (synonyms) {
790
+ for (const syn of synonyms) expandedTerms.add(syn);
791
+ }
792
+ }
793
+ const scored = fragments.map((s) => {
794
+ let score = 0;
795
+ const nameLower = s.meta.name.toLowerCase();
796
+ if (searchTerms.some((term) => nameLower.includes(term))) {
797
+ score += 15;
798
+ } else if (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
799
+ score += 8;
800
+ }
801
+ const desc = s.meta.description?.toLowerCase() ?? "";
802
+ score += searchTerms.filter((term) => desc.includes(term)).length * 6;
803
+ const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
804
+ score += searchTerms.filter((term) => tags.some((tag) => tag.includes(term))).length * 4;
805
+ const whenUsed = s.usage?.when?.join(" ").toLowerCase() ?? "";
806
+ score += searchTerms.filter((term) => whenUsed.includes(term)).length * 10;
807
+ score += Array.from(expandedTerms).filter((term) => !searchTerms.includes(term) && whenUsed.includes(term)).length * 5;
808
+ const cat = s.meta.category?.toLowerCase() ?? "";
809
+ if (searchTerms.some((term) => cat.includes(term))) {
810
+ score += 8;
811
+ }
812
+ const variantText = s.variants.map((v) => `${v.name} ${v.description || ""}`.toLowerCase()).join(" ");
813
+ score += searchTerms.filter((term) => variantText.includes(term)).length * 3;
814
+ if (s.meta.status === "stable") score += 5;
815
+ else if (s.meta.status === "beta") score += 2;
816
+ if (s.meta.status === "deprecated") score -= 25;
817
+ return { name: s.meta.name, kind: "component", score };
818
+ });
819
+ return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).map((s, i) => ({ ...s, rank: i }));
820
+ }
821
+ function keywordScoreBlocks(query, blocks) {
822
+ const searchTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
823
+ const expandedTerms = new Set(searchTerms);
824
+ for (const term of searchTerms) {
825
+ const synonyms = SYNONYM_MAP[term];
826
+ if (synonyms) {
827
+ for (const syn of synonyms) expandedTerms.add(syn);
828
+ }
829
+ }
830
+ const scored = blocks.map((b) => {
831
+ let score = 0;
832
+ const nameLower = b.name.toLowerCase();
833
+ if (searchTerms.some((term) => nameLower.includes(term))) {
834
+ score += 15;
835
+ } else if (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
836
+ score += 8;
837
+ }
838
+ const desc = b.description.toLowerCase();
839
+ score += searchTerms.filter((term) => desc.includes(term)).length * 6;
840
+ const tags = b.tags?.map((t) => t.toLowerCase()) ?? [];
841
+ score += searchTerms.filter((term) => tags.some((tag) => tag.includes(term))).length * 4;
842
+ const componentText = b.components.join(" ").toLowerCase();
843
+ score += searchTerms.filter((term) => componentText.includes(term)).length * 5;
844
+ const cat = b.category.toLowerCase();
845
+ if (searchTerms.some((term) => cat.includes(term))) {
846
+ score += 8;
847
+ }
848
+ return { name: b.name, kind: "block", score };
849
+ });
850
+ return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).map((s, i) => ({ ...s, rank: i }));
851
+ }
852
+ function keywordScoreTokens(query, tokenData) {
853
+ const searchTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
854
+ const scored = [];
855
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
856
+ const catLower = cat.toLowerCase();
857
+ const catBonus = searchTerms.some((term) => catLower.includes(term)) ? 8 : 0;
858
+ for (const token of tokens) {
859
+ let score = catBonus;
860
+ const nameLower = token.name.toLowerCase();
861
+ score += searchTerms.filter((term) => nameLower.includes(term)).length * 10;
862
+ if (token.description) {
863
+ const descLower = token.description.toLowerCase();
864
+ score += searchTerms.filter((term) => descLower.includes(term)).length * 6;
865
+ }
866
+ if (score > 0) {
867
+ scored.push({ name: token.name, kind: "token", score });
868
+ }
869
+ }
870
+ }
871
+ return scored.sort((a, b) => b.score - a.score).map((s, i) => ({ ...s, rank: i }));
872
+ }
873
+ function reciprocalRankFusion(resultSets, k = 60) {
874
+ const scoreMap = /* @__PURE__ */ new Map();
875
+ for (const { results } of resultSets) {
876
+ for (let rank = 0; rank < results.length; rank++) {
877
+ const result = results[rank];
878
+ const key = `${result.kind}:${result.name}`;
879
+ const rrfScore = 1 / (k + rank + 1);
880
+ const existing = scoreMap.get(key);
881
+ if (existing) {
882
+ existing.score += rrfScore;
883
+ } else {
884
+ scoreMap.set(key, { score: rrfScore, kind: result.kind, name: result.name });
885
+ }
886
+ }
887
+ }
888
+ const fused = [];
889
+ for (const [, { score, kind, name }] of scoreMap) {
890
+ fused.push({ name, kind, rank: 0, score });
891
+ }
892
+ fused.sort((a, b) => b.score - a.score);
893
+ fused.forEach((r, i) => {
894
+ r.rank = i;
895
+ });
896
+ return fused;
897
+ }
898
+ async function hybridSearch(query, data, limit = 10, kind) {
899
+ const keywordResults = [];
900
+ if (!kind || kind === "component") {
901
+ keywordResults.push(...keywordScoreComponents(query, data.fragments));
902
+ }
903
+ if ((!kind || kind === "block") && data.blocks) {
904
+ keywordResults.push(...keywordScoreBlocks(query, data.blocks));
905
+ }
906
+ if ((!kind || kind === "token") && data.tokenData) {
907
+ keywordResults.push(...keywordScoreTokens(query, data.tokenData));
908
+ }
909
+ keywordResults.sort((a, b) => b.score - a.score);
910
+ keywordResults.forEach((r, i) => {
911
+ r.rank = i;
912
+ });
913
+ const vectorResults = await searchConvex(query, limit, kind);
914
+ if (vectorResults.length === 0) {
915
+ return keywordResults.slice(0, limit);
916
+ }
917
+ const graphBoostResults = [];
918
+ if (data.graph) {
919
+ try {
920
+ const { ComponentGraphEngine: ComponentGraphEngine2, deserializeGraph: deserializeGraph2 } = await import("./graph-6PMJ6XCF.js");
921
+ const graph = deserializeGraph2(data.graph);
922
+ const engine = new ComponentGraphEngine2(graph);
923
+ const topComponents = [...keywordResults, ...vectorResults].filter((r) => r.kind === "component").slice(0, 5);
924
+ const neighborSet = /* @__PURE__ */ new Set();
925
+ for (const result of topComponents) {
926
+ const neighbors = engine.neighbors(result.name, 1);
927
+ for (const n of neighbors.neighbors) {
928
+ if (!neighborSet.has(n.component)) {
929
+ neighborSet.add(n.component);
930
+ graphBoostResults.push({
931
+ name: n.component,
932
+ kind: "component",
933
+ rank: graphBoostResults.length,
934
+ score: 1
935
+ // Will be normalized through RRF
936
+ });
937
+ }
938
+ }
939
+ }
940
+ } catch {
941
+ }
942
+ }
943
+ const resultSets = [
944
+ { label: "vector", results: vectorResults },
945
+ { label: "keyword", results: keywordResults }
946
+ ];
947
+ if (graphBoostResults.length > 0) {
948
+ resultSets.push({ label: "graph", results: graphBoostResults });
949
+ }
950
+ const fused = reciprocalRankFusion(resultSets);
951
+ return fused.slice(0, limit);
952
+ }
953
+
954
+ // src/service.ts
955
+ async function renderComponent(viewerUrl, request) {
956
+ const renderUrl = `${viewerUrl}/fragments/render`;
957
+ const response = await fetch(renderUrl, {
958
+ method: "POST",
959
+ headers: { "Content-Type": "application/json" },
960
+ body: JSON.stringify({
961
+ component: request.component,
962
+ props: request.props ?? {},
963
+ viewport: request.viewport ?? { width: 800, height: 600 }
964
+ })
965
+ });
966
+ return await response.json();
967
+ }
968
+ async function compareComponent(viewerUrl, request) {
969
+ const compareUrl = `${viewerUrl}/fragments/compare`;
970
+ const response = await fetch(compareUrl, {
971
+ method: "POST",
972
+ headers: { "Content-Type": "application/json" },
973
+ body: JSON.stringify(request)
974
+ });
975
+ return await response.json();
976
+ }
977
+ async function fixComponent(viewerUrl, request) {
978
+ const fixUrl = `${viewerUrl}/fragments/fix`;
979
+ const response = await fetch(fixUrl, {
980
+ method: "POST",
981
+ headers: { "Content-Type": "application/json" },
982
+ body: JSON.stringify(request)
983
+ });
984
+ return await response.json();
985
+ }
986
+ async function auditComponent(viewerUrl, request) {
987
+ const a11yUrl = `${viewerUrl}/fragments/a11y`;
988
+ const response = await fetch(a11yUrl, {
989
+ method: "POST",
990
+ headers: { "Content-Type": "application/json" },
991
+ body: JSON.stringify({
992
+ component: request.component,
993
+ variant: request.variant
994
+ })
995
+ });
996
+ const raw = await response.json();
997
+ if (raw.error) {
998
+ return {
999
+ component: request.component,
1000
+ results: [],
1001
+ score: 0,
1002
+ aaPercent: 0,
1003
+ aaaPercent: 0,
1004
+ passed: false,
1005
+ standard: request.standard ?? "AA",
1006
+ error: raw.error
1007
+ };
1008
+ }
1009
+ const results = raw.results ?? [];
1010
+ const standard = request.standard ?? "AA";
1011
+ let totalCritical = 0;
1012
+ let totalSerious = 0;
1013
+ let totalModerate = 0;
1014
+ let totalMinor = 0;
1015
+ for (const r of results) {
1016
+ totalCritical += r.summary.critical;
1017
+ totalSerious += r.summary.serious;
1018
+ totalModerate += r.summary.moderate;
1019
+ totalMinor += r.summary.minor;
1020
+ }
1021
+ const deductions = totalCritical * 10 + totalSerious * 5 + totalModerate * 2 + totalMinor * 1;
1022
+ const score = Math.max(0, 100 - deductions);
1023
+ const totalViolations = totalCritical + totalSerious + totalModerate + totalMinor;
1024
+ const aaPass = totalCritical === 0 && totalSerious === 0;
1025
+ const aaaPass = totalViolations === 0;
1026
+ const aaPercent = aaPass ? 100 : 0;
1027
+ const aaaPercent = aaaPass ? 100 : 0;
1028
+ const passed = standard === "AAA" ? aaaPass : aaPass;
1029
+ return {
1030
+ component: request.component,
1031
+ results,
1032
+ score,
1033
+ aaPercent,
1034
+ aaaPercent,
1035
+ passed,
1036
+ standard
1037
+ };
1038
+ }
1039
+
1040
+ // src/utils.ts
1041
+ function projectFields(obj, fields) {
1042
+ if (!fields || fields.length === 0) {
1043
+ return obj;
1044
+ }
1045
+ const result = {};
1046
+ for (const field of fields) {
1047
+ const parts = field.split(".");
1048
+ let source = obj;
1049
+ let target = result;
1050
+ for (let i = 0; i < parts.length; i++) {
1051
+ const part = parts[i];
1052
+ const isLast = i === parts.length - 1;
1053
+ if (source === null || source === void 0 || typeof source !== "object") {
1054
+ break;
1055
+ }
1056
+ const sourceObj = source;
1057
+ const value = sourceObj[part];
1058
+ if (isLast) {
1059
+ target[part] = value;
1060
+ } else {
1061
+ if (!(part in target)) {
1062
+ target[part] = {};
1063
+ }
1064
+ target = target[part];
1065
+ source = value;
1066
+ }
1067
+ }
1068
+ }
1069
+ return result;
1070
+ }
1071
+
1072
+ // src/graph-handler.ts
1073
+ function handleGraphTool(args, serializedGraph, blocks) {
1074
+ if (!serializedGraph) {
1075
+ return {
1076
+ text: JSON.stringify({
1077
+ error: "No graph data available. Run `fragments build` to generate the component graph.",
1078
+ hint: "The graph is built automatically during `fragments build` and embedded in fragments.json."
1079
+ }),
1080
+ isError: true
1081
+ };
1082
+ }
1083
+ const graph = deserializeGraph(serializedGraph);
1084
+ const blockData = blocks ? Object.fromEntries(
1085
+ Object.entries(blocks).map(([k, v]) => [k, { components: v.components }])
1086
+ ) : void 0;
1087
+ const engine = new ComponentGraphEngine(graph, blockData);
1088
+ const edgeTypes = args.edgeTypes;
1089
+ switch (args.mode) {
1090
+ case "health": {
1091
+ const health = engine.getHealth();
1092
+ return {
1093
+ text: JSON.stringify({
1094
+ mode: "health",
1095
+ ...health,
1096
+ summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
1097
+ }, null, 2)
1098
+ };
1099
+ }
1100
+ case "dependencies": {
1101
+ if (!args.component) {
1102
+ return { text: JSON.stringify({ error: "component is required for dependencies mode" }), isError: true };
1103
+ }
1104
+ if (!engine.hasNode(args.component)) {
1105
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1106
+ }
1107
+ const deps = engine.dependencies(args.component, edgeTypes);
1108
+ return {
1109
+ text: JSON.stringify({
1110
+ mode: "dependencies",
1111
+ component: args.component,
1112
+ count: deps.length,
1113
+ dependencies: deps.map((e) => ({
1114
+ component: e.target,
1115
+ type: e.type,
1116
+ weight: e.weight,
1117
+ note: e.note,
1118
+ provenance: e.provenance
1119
+ }))
1120
+ }, null, 2)
1121
+ };
1122
+ }
1123
+ case "dependents": {
1124
+ if (!args.component) {
1125
+ return { text: JSON.stringify({ error: "component is required for dependents mode" }), isError: true };
1126
+ }
1127
+ if (!engine.hasNode(args.component)) {
1128
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1129
+ }
1130
+ const deps = engine.dependents(args.component, edgeTypes);
1131
+ return {
1132
+ text: JSON.stringify({
1133
+ mode: "dependents",
1134
+ component: args.component,
1135
+ count: deps.length,
1136
+ dependents: deps.map((e) => ({
1137
+ component: e.source,
1138
+ type: e.type,
1139
+ weight: e.weight,
1140
+ note: e.note,
1141
+ provenance: e.provenance
1142
+ }))
1143
+ }, null, 2)
1144
+ };
1145
+ }
1146
+ case "impact": {
1147
+ if (!args.component) {
1148
+ return { text: JSON.stringify({ error: "component is required for impact mode" }), isError: true };
1149
+ }
1150
+ if (!engine.hasNode(args.component)) {
1151
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1152
+ }
1153
+ const result = engine.impact(args.component, args.maxDepth ?? 3);
1154
+ return {
1155
+ text: JSON.stringify({
1156
+ mode: "impact",
1157
+ ...result,
1158
+ summary: `Changing ${args.component} affects ${result.totalAffected} component(s) and ${result.affectedBlocks.length} block(s)`
1159
+ }, null, 2)
1160
+ };
1161
+ }
1162
+ case "path": {
1163
+ if (!args.component || !args.target) {
1164
+ return { text: JSON.stringify({ error: "component and target are required for path mode" }), isError: true };
1165
+ }
1166
+ const result = engine.path(args.component, args.target);
1167
+ return {
1168
+ text: JSON.stringify({
1169
+ mode: "path",
1170
+ from: args.component,
1171
+ to: args.target,
1172
+ ...result,
1173
+ edges: result.edges.map((e) => ({
1174
+ source: e.source,
1175
+ target: e.target,
1176
+ type: e.type
1177
+ }))
1178
+ }, null, 2)
1179
+ };
1180
+ }
1181
+ case "composition": {
1182
+ if (!args.component) {
1183
+ return { text: JSON.stringify({ error: "component is required for composition mode" }), isError: true };
1184
+ }
1185
+ if (!engine.hasNode(args.component)) {
1186
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1187
+ }
1188
+ const tree = engine.composition(args.component);
1189
+ return {
1190
+ text: JSON.stringify({
1191
+ mode: "composition",
1192
+ ...tree
1193
+ }, null, 2)
1194
+ };
1195
+ }
1196
+ case "alternatives": {
1197
+ if (!args.component) {
1198
+ return { text: JSON.stringify({ error: "component is required for alternatives mode" }), isError: true };
1199
+ }
1200
+ if (!engine.hasNode(args.component)) {
1201
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1202
+ }
1203
+ const alts = engine.alternatives(args.component);
1204
+ return {
1205
+ text: JSON.stringify({
1206
+ mode: "alternatives",
1207
+ component: args.component,
1208
+ count: alts.length,
1209
+ alternatives: alts
1210
+ }, null, 2)
1211
+ };
1212
+ }
1213
+ case "islands": {
1214
+ const islands = engine.islands();
1215
+ return {
1216
+ text: JSON.stringify({
1217
+ mode: "islands",
1218
+ count: islands.length,
1219
+ islands: islands.map((island, i) => ({
1220
+ id: i + 1,
1221
+ size: island.length,
1222
+ components: island
1223
+ }))
1224
+ }, null, 2)
1225
+ };
1226
+ }
1227
+ default:
1228
+ return {
1229
+ text: JSON.stringify({
1230
+ error: `Unknown mode: "${args.mode}". Valid modes: dependencies, dependents, impact, path, composition, alternatives, islands, health`
1231
+ }),
1232
+ isError: true
1233
+ };
1234
+ }
1235
+ }
1236
+
1237
+ // src/server.ts
1238
+ var TOOL_NAMES = buildToolNames(BRAND.nameLower);
1239
+ var TOOLS = buildMcpTools(BRAND.nameLower);
1240
+ function createMcpServer(config) {
1241
+ const server = new Server(
1242
+ {
1243
+ name: `${BRAND.nameLower}-mcp`,
1244
+ version: "0.3.0"
1245
+ },
1246
+ {
1247
+ capabilities: {
1248
+ tools: {}
1249
+ }
1250
+ }
1251
+ );
1252
+ let fragmentsData = null;
1253
+ const fragmentPackageMap = /* @__PURE__ */ new Map();
1254
+ let defaultPackageName = null;
1255
+ async function loadFragments() {
1256
+ if (fragmentsData) return fragmentsData;
1257
+ const paths = findFragmentsJson(config.projectRoot);
1258
+ if (paths.length === 0) {
1259
+ throw new Error(
1260
+ `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.`
1261
+ );
1262
+ }
1263
+ const content = await readFile(paths[0], "utf-8");
1264
+ fragmentsData = JSON.parse(content);
1265
+ if (!fragmentsData.blocks && fragmentsData.recipes) {
1266
+ fragmentsData.blocks = fragmentsData.recipes;
1267
+ }
1268
+ if (fragmentsData.packageName) {
1269
+ for (const name of Object.keys(fragmentsData.fragments)) {
1270
+ fragmentPackageMap.set(name, fragmentsData.packageName);
1271
+ }
1272
+ }
1273
+ for (let i = 1; i < paths.length; i++) {
1274
+ const extra = JSON.parse(await readFile(paths[i], "utf-8"));
1275
+ if (extra.packageName) {
1276
+ for (const name of Object.keys(extra.fragments)) {
1277
+ fragmentPackageMap.set(name, extra.packageName);
1278
+ }
1279
+ }
1280
+ Object.assign(fragmentsData.fragments, extra.fragments);
1281
+ const extraBlocks = extra.blocks ?? extra.recipes;
1282
+ if (extraBlocks) {
1283
+ fragmentsData.blocks = { ...fragmentsData.blocks, ...extraBlocks };
1284
+ }
1285
+ }
1286
+ return fragmentsData;
1287
+ }
1288
+ async function getPackageName(fragmentName) {
1289
+ await loadFragments();
1290
+ if (fragmentName) {
1291
+ const segPkg = fragmentPackageMap.get(fragmentName);
1292
+ if (segPkg) return segPkg;
1293
+ }
1294
+ if (defaultPackageName) return defaultPackageName;
1295
+ if (fragmentsData?.packageName) {
1296
+ defaultPackageName = fragmentsData.packageName;
1297
+ return defaultPackageName;
1298
+ }
1299
+ const packageJsonPath = join2(config.projectRoot, "package.json");
1300
+ if (existsSync2(packageJsonPath)) {
1301
+ try {
1302
+ const content = readFileSync2(packageJsonPath, "utf-8");
1303
+ const pkg = JSON.parse(content);
1304
+ if (pkg.name) {
1305
+ defaultPackageName = pkg.name;
1306
+ return defaultPackageName;
1307
+ }
1308
+ } catch {
1309
+ }
1310
+ }
1311
+ defaultPackageName = "your-component-library";
1312
+ return defaultPackageName;
1313
+ }
1314
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1315
+ return { tools: TOOLS };
1316
+ });
1317
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1318
+ const { name, arguments: args } = request.params;
1319
+ try {
1320
+ switch (name) {
1321
+ // ================================================================
1322
+ // DISCOVER — list, suggest, context, alternatives
1323
+ // ================================================================
1324
+ case TOOL_NAMES.discover: {
1325
+ const data = await loadFragments();
1326
+ const useCase = args?.useCase ?? void 0;
1327
+ const componentForAlts = args?.component ?? void 0;
1328
+ const category = args?.category ?? void 0;
1329
+ const search = args?.search?.toLowerCase() ?? void 0;
1330
+ const status = args?.status ?? void 0;
1331
+ const format = args?.format ?? "markdown";
1332
+ const compact = args?.compact ?? false;
1333
+ const includeCode = args?.includeCode ?? false;
1334
+ const includeRelations = args?.includeRelations ?? false;
1335
+ if (compact || args?.format && !useCase && !componentForAlts && !category && !search && !status) {
1336
+ const fragments2 = Object.values(data.fragments);
1337
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1338
+ const { content: ctxContent, tokenEstimate } = generateContext(fragments2, {
1339
+ format,
1340
+ compact,
1341
+ include: {
1342
+ code: includeCode,
1343
+ relations: includeRelations
1344
+ }
1345
+ }, allBlocks);
1346
+ return {
1347
+ content: [{ type: "text", text: ctxContent }],
1348
+ _meta: { tokenEstimate }
1349
+ };
1350
+ }
1351
+ if (useCase) {
1352
+ const allFragments = Object.values(data.fragments);
1353
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1354
+ const context = args?.context?.toLowerCase() ?? "";
1355
+ const fullQuery = context ? `${useCase} ${context}` : useCase;
1356
+ const localData = {
1357
+ fragments: allFragments,
1358
+ blocks: allBlocks,
1359
+ tokenData: data.tokens
1360
+ };
1361
+ const searchResults = await hybridSearch(fullQuery, localData, 10, "component");
1362
+ const scored = searchResults.map((result) => {
1363
+ const fragment = allFragments.find(
1364
+ (s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
1365
+ );
1366
+ if (!fragment) return null;
1367
+ const filteredWhen = filterPlaceholders(fragment.usage?.when).slice(0, 3);
1368
+ const filteredWhenNot = filterPlaceholders(fragment.usage?.whenNot).slice(0, 2);
1369
+ let confidence;
1370
+ if (result.score >= 0.025) confidence = "high";
1371
+ else if (result.score >= 0.015) confidence = "medium";
1372
+ else confidence = "low";
1373
+ return {
1374
+ component: fragment.meta.name,
1375
+ category: fragment.meta.category,
1376
+ description: fragment.meta.description,
1377
+ confidence,
1378
+ reasons: [`Matched via hybrid search (score: ${result.score.toFixed(4)})`],
1379
+ usage: { when: filteredWhen, whenNot: filteredWhenNot },
1380
+ variantCount: fragment.variants.length,
1381
+ status: fragment.meta.status
1382
+ };
1383
+ }).filter(Boolean);
1384
+ const suggestions = [];
1385
+ const categoryCount = {};
1386
+ for (const item of scored) {
1387
+ if (!item) continue;
1388
+ const cat = item.category || "uncategorized";
1389
+ const count = categoryCount[cat] || 0;
1390
+ if (count < 2 || suggestions.length < 3) {
1391
+ suggestions.push(item);
1392
+ categoryCount[cat] = count + 1;
1393
+ if (suggestions.length >= 5) break;
1394
+ }
1395
+ }
1396
+ 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;
1397
+ const useCaseLower = useCase.toLowerCase();
1398
+ const STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "variable", "token", "css", "theme", "dark mode", "background", "hover"];
1399
+ const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
1400
+ const noMatch = suggestions.length === 0;
1401
+ const weakMatch = !noMatch && suggestions.every((s) => s.confidence === "low");
1402
+ let recommendation;
1403
+ let nextStep;
1404
+ if (noMatch) {
1405
+ recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${TOOL_NAMES.tokens} to find CSS custom properties.` : "No matching components found. Try different keywords or browse all components with fragments_discover.";
1406
+ nextStep = isStyleQuery ? `Use ${TOOL_NAMES.tokens}(search: "${useCaseLower.split(/\s+/)[0]}") to find design tokens.` : void 0;
1407
+ } else if (weakMatch) {
1408
+ recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${TOOL_NAMES.tokens}.` : ""}`;
1409
+ nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
1410
+ } else {
1411
+ recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
1412
+ nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
1413
+ }
1414
+ return {
1415
+ content: [{
1416
+ type: "text",
1417
+ text: JSON.stringify({
1418
+ useCase,
1419
+ context: context || void 0,
1420
+ suggestions,
1421
+ noMatch,
1422
+ weakMatch,
1423
+ recommendation,
1424
+ compositionHint,
1425
+ nextStep
1426
+ }, null, 2)
1427
+ }]
1428
+ };
1429
+ }
1430
+ if (componentForAlts) {
1431
+ const fragment = Object.values(data.fragments).find(
1432
+ (s) => s.meta.name.toLowerCase() === componentForAlts.toLowerCase()
1433
+ );
1434
+ if (!fragment) {
1435
+ throw new Error(`Component "${componentForAlts}" not found. Use fragments_discover to see available components.`);
1436
+ }
1437
+ const relations = fragment.relations ?? [];
1438
+ const referencedBy = Object.values(data.fragments).filter(
1439
+ (s) => s.relations?.some((r) => r.component.toLowerCase() === componentForAlts.toLowerCase())
1440
+ ).map((s) => ({
1441
+ component: s.meta.name,
1442
+ relationship: s.relations?.find(
1443
+ (r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
1444
+ )?.relationship,
1445
+ note: s.relations?.find(
1446
+ (r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
1447
+ )?.note
1448
+ }));
1449
+ const sameCategory = Object.values(data.fragments).filter(
1450
+ (s) => s.meta.category === fragment.meta.category && s.meta.name.toLowerCase() !== componentForAlts.toLowerCase()
1451
+ ).map((s) => ({
1452
+ component: s.meta.name,
1453
+ description: s.meta.description
1454
+ }));
1455
+ return {
1456
+ content: [{
1457
+ type: "text",
1458
+ text: JSON.stringify({
1459
+ component: fragment.meta.name,
1460
+ category: fragment.meta.category,
1461
+ directRelations: relations,
1462
+ referencedBy,
1463
+ sameCategory,
1464
+ suggestion: relations.find((r) => r.relationship === "alternative") ? `Consider ${relations.find((r) => r.relationship === "alternative")?.component}: ${relations.find((r) => r.relationship === "alternative")?.note}` : void 0
1465
+ }, null, 2)
1466
+ }]
1467
+ };
1468
+ }
1469
+ const fragments = Object.values(data.fragments).filter((s) => {
1470
+ if (category && s.meta.category !== category) return false;
1471
+ if (status && (s.meta.status ?? "stable") !== status) return false;
1472
+ if (search) {
1473
+ const nameMatch = s.meta.name.toLowerCase().includes(search);
1474
+ const descMatch = s.meta.description?.toLowerCase().includes(search);
1475
+ const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search));
1476
+ if (!nameMatch && !descMatch && !tagMatch) return false;
1477
+ }
1478
+ return true;
1479
+ }).map((s) => ({
1480
+ name: s.meta.name,
1481
+ category: s.meta.category,
1482
+ description: s.meta.description,
1483
+ status: s.meta.status ?? "stable",
1484
+ variantCount: s.variants.length,
1485
+ tags: s.meta.tags ?? []
1486
+ }));
1487
+ return {
1488
+ content: [{
1489
+ type: "text",
1490
+ text: JSON.stringify({
1491
+ total: fragments.length,
1492
+ fragments,
1493
+ categories: [...new Set(fragments.map((s) => s.category))],
1494
+ hint: fragments.length === 0 ? "No components found. Try broader search terms or check available categories." : fragments.length > 5 ? "Use fragments_discover with useCase for recommendations, or fragments_inspect for details on a specific component." : void 0
1495
+ }, null, 2)
1496
+ }]
1497
+ };
1498
+ }
1499
+ // ================================================================
1500
+ // INSPECT
1501
+ // ================================================================
1502
+ case TOOL_NAMES.inspect: {
1503
+ const data = await loadFragments();
1504
+ const componentName = args?.component;
1505
+ const fields = args?.fields;
1506
+ const variantName = args?.variant ?? void 0;
1507
+ const maxExamples = args?.maxExamples;
1508
+ const maxLines = args?.maxLines;
1509
+ if (!componentName) {
1510
+ throw new Error("component is required");
1511
+ }
1512
+ const fragment = Object.values(data.fragments).find(
1513
+ (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1514
+ );
1515
+ if (!fragment) {
1516
+ throw new Error(`Component "${componentName}" not found. Use fragments_discover to see available components.`);
1517
+ }
1518
+ const pkgName = await getPackageName(fragment.meta.name);
1519
+ let variants = fragment.variants;
1520
+ if (variantName) {
1521
+ const query = variantName.toLowerCase();
1522
+ let filtered = variants.filter((v) => v.name.toLowerCase() === query);
1523
+ if (filtered.length === 0) {
1524
+ filtered = variants.filter((v) => v.name.toLowerCase().startsWith(query));
1525
+ }
1526
+ if (filtered.length === 0) {
1527
+ filtered = variants.filter((v) => v.name.toLowerCase().includes(query));
1528
+ }
1529
+ if (filtered.length > 0) {
1530
+ variants = filtered;
1531
+ } else {
1532
+ throw new Error(
1533
+ `Variant "${variantName}" not found for ${componentName}. Available: ${fragment.variants.map((v) => v.name).join(", ")}`
1534
+ );
1535
+ }
1536
+ }
1537
+ if (maxExamples && maxExamples > 0) {
1538
+ variants = variants.slice(0, maxExamples);
1539
+ }
1540
+ const truncateCode = (code) => {
1541
+ if (!maxLines || maxLines <= 0) return code;
1542
+ const lines = code.split("\n");
1543
+ if (lines.length <= maxLines) return code;
1544
+ return lines.slice(0, maxLines).join("\n") + "\n// ... truncated";
1545
+ };
1546
+ const examples = variants.map((variant) => {
1547
+ if (variant.code) {
1548
+ return {
1549
+ variant: variant.name,
1550
+ description: variant.description,
1551
+ code: truncateCode(variant.code)
1552
+ };
1553
+ }
1554
+ return {
1555
+ variant: variant.name,
1556
+ description: variant.description,
1557
+ code: `<${fragment.meta.name} />`,
1558
+ note: "No code example provided in fragment. Refer to props for customization."
1559
+ };
1560
+ });
1561
+ const propsReference = Object.entries(fragment.props ?? {}).map(([propName, prop]) => ({
1562
+ name: propName,
1563
+ type: prop.type,
1564
+ required: prop.required,
1565
+ default: prop.default,
1566
+ description: prop.description
1567
+ }));
1568
+ const propConstraints = Object.entries(fragment.props ?? {}).filter(([, prop]) => prop.constraints && prop.constraints.length > 0).map(([pName, prop]) => ({
1569
+ prop: pName,
1570
+ constraints: prop.constraints
1571
+ }));
1572
+ const fullResult = {
1573
+ meta: fragment.meta,
1574
+ props: fragment.props,
1575
+ variants: fragment.variants,
1576
+ relations: fragment.relations,
1577
+ contract: fragment.contract,
1578
+ generated: fragment._generated,
1579
+ guidelines: {
1580
+ when: filterPlaceholders(fragment.usage?.when),
1581
+ whenNot: filterPlaceholders(fragment.usage?.whenNot),
1582
+ guidelines: fragment.usage?.guidelines ?? [],
1583
+ accessibility: fragment.usage?.accessibility ?? [],
1584
+ propConstraints,
1585
+ alternatives: fragment.relations?.filter((r) => r.relationship === "alternative").map((r) => ({
1586
+ component: r.component,
1587
+ note: r.note
1588
+ })) ?? []
1589
+ },
1590
+ examples: {
1591
+ import: `import { ${fragment.meta.name} } from '${pkgName}';`,
1592
+ code: examples,
1593
+ propsReference
1594
+ }
1595
+ };
1596
+ const result = fields && fields.length > 0 ? projectFields(fullResult, fields) : fullResult;
1597
+ return {
1598
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1599
+ };
1600
+ }
1601
+ // ================================================================
1602
+ // BLOCKS
1603
+ // ================================================================
1604
+ case TOOL_NAMES.blocks: {
1605
+ const data = await loadFragments();
1606
+ const blockName = args?.name;
1607
+ const search = args?.search?.toLowerCase() ?? void 0;
1608
+ const component = args?.component?.toLowerCase() ?? void 0;
1609
+ const category = args?.category?.toLowerCase() ?? void 0;
1610
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1611
+ if (allBlocks.length === 0) {
1612
+ return {
1613
+ content: [{
1614
+ type: "text",
1615
+ text: JSON.stringify({
1616
+ total: 0,
1617
+ blocks: [],
1618
+ hint: `No blocks found. Run \`${BRAND.cliCommand} build\` after adding .block.ts files.`
1619
+ }, null, 2)
1620
+ }]
1621
+ };
1622
+ }
1623
+ let filtered = allBlocks;
1624
+ if (blockName) {
1625
+ filtered = filtered.filter(
1626
+ (b) => b.name.toLowerCase() === blockName.toLowerCase()
1627
+ );
1628
+ }
1629
+ if (search) {
1630
+ filtered = filtered.filter((b) => {
1631
+ const haystack = [
1632
+ b.name,
1633
+ b.description,
1634
+ ...b.tags ?? [],
1635
+ ...b.components,
1636
+ b.category
1637
+ ].join(" ").toLowerCase();
1638
+ return haystack.includes(search);
1639
+ });
1640
+ }
1641
+ if (component) {
1642
+ filtered = filtered.filter(
1643
+ (b) => b.components.some((c) => c.toLowerCase() === component)
1644
+ );
1645
+ }
1646
+ if (category) {
1647
+ filtered = filtered.filter(
1648
+ (b) => b.category.toLowerCase() === category
1649
+ );
1650
+ }
1651
+ return {
1652
+ content: [{
1653
+ type: "text",
1654
+ text: JSON.stringify({
1655
+ total: filtered.length,
1656
+ blocks: filtered
1657
+ }, null, 2)
1658
+ }]
1659
+ };
1660
+ }
1661
+ // ================================================================
1662
+ // TOKENS
1663
+ // ================================================================
1664
+ case TOOL_NAMES.tokens: {
1665
+ const data = await loadFragments();
1666
+ const category = args?.category?.toLowerCase() ?? void 0;
1667
+ const search = args?.search?.toLowerCase() ?? void 0;
1668
+ const tokenData = data.tokens;
1669
+ if (!tokenData || tokenData.total === 0) {
1670
+ return {
1671
+ content: [{
1672
+ type: "text",
1673
+ text: JSON.stringify({
1674
+ total: 0,
1675
+ categories: {},
1676
+ hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
1677
+ }, null, 2)
1678
+ }]
1679
+ };
1680
+ }
1681
+ let filteredCategories = {};
1682
+ let filteredTotal = 0;
1683
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1684
+ if (category && cat !== category) continue;
1685
+ let filtered = tokens;
1686
+ if (search) {
1687
+ filtered = tokens.filter(
1688
+ (t) => t.name.toLowerCase().includes(search) || t.description && t.description.toLowerCase().includes(search)
1689
+ );
1690
+ }
1691
+ if (filtered.length > 0) {
1692
+ filteredCategories[cat] = filtered;
1693
+ filteredTotal += filtered.length;
1694
+ }
1695
+ }
1696
+ let hint;
1697
+ if (filteredTotal === 0) {
1698
+ const availableCategories = Object.keys(tokenData.categories);
1699
+ hint = search ? `No tokens matching "${search}". Try: ${availableCategories.join(", ")}` : category ? `Category "${category}" not found. Available: ${availableCategories.join(", ")}` : void 0;
1700
+ } else if (!category && !search) {
1701
+ hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
1702
+ }
1703
+ return {
1704
+ content: [{
1705
+ type: "text",
1706
+ text: JSON.stringify({
1707
+ prefix: tokenData.prefix,
1708
+ total: filteredTotal,
1709
+ totalAvailable: tokenData.total,
1710
+ categories: filteredCategories,
1711
+ ...hint && { hint },
1712
+ ...!category && !search && {
1713
+ availableCategories: Object.entries(tokenData.categories).map(
1714
+ ([cat, tokens]) => ({ category: cat, count: tokens.length })
1715
+ )
1716
+ }
1717
+ }, null, 2)
1718
+ }]
1719
+ };
1720
+ }
1721
+ // ================================================================
1722
+ // IMPLEMENT — one-shot discover + inspect + blocks + tokens
1723
+ // ================================================================
1724
+ case TOOL_NAMES.implement: {
1725
+ const data = await loadFragments();
1726
+ const useCase = args?.useCase;
1727
+ if (!useCase) {
1728
+ throw new Error("useCase is required");
1729
+ }
1730
+ const allFragments = Object.values(data.fragments);
1731
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1732
+ const tokenData = data.tokens;
1733
+ const localData = {
1734
+ fragments: allFragments,
1735
+ blocks: allBlocks,
1736
+ tokenData
1737
+ };
1738
+ const searchResults = await hybridSearch(useCase, localData, 15);
1739
+ const componentResults = searchResults.filter((r) => r.kind === "component").slice(0, 3);
1740
+ const blockResults = searchResults.filter((r) => r.kind === "block").slice(0, 2);
1741
+ const tokenResults = searchResults.filter((r) => r.kind === "token").slice(0, 5);
1742
+ const topMatches = componentResults.map((result) => {
1743
+ const fragment = allFragments.find(
1744
+ (s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
1745
+ );
1746
+ return fragment ? { fragment, score: result.score } : null;
1747
+ }).filter(Boolean);
1748
+ const components = await Promise.all(
1749
+ topMatches.map(async ({ fragment: s, score }) => {
1750
+ const pkgName = await getPackageName(s.meta.name);
1751
+ const examples = s.variants.slice(0, 2).map((v) => ({
1752
+ variant: v.name,
1753
+ code: v.code ?? `<${s.meta.name} />`
1754
+ }));
1755
+ const propsSummary = Object.entries(s.props ?? {}).slice(0, 10).map(
1756
+ ([pName, p]) => `${pName}${p.required ? " (required)" : ""}: ${p.type}${p.values ? ` = ${p.values.join("|")}` : ""}`
1757
+ );
1758
+ return {
1759
+ name: s.meta.name,
1760
+ category: s.meta.category,
1761
+ description: s.meta.description,
1762
+ confidence: score >= 0.025 ? "high" : score >= 0.015 ? "medium" : "low",
1763
+ import: `import { ${s.meta.name} } from '${pkgName}';`,
1764
+ props: propsSummary,
1765
+ examples,
1766
+ guidelines: filterPlaceholders(s.usage?.when).slice(0, 3),
1767
+ accessibility: s.usage?.accessibility?.slice(0, 2) ?? []
1768
+ };
1769
+ })
1770
+ );
1771
+ const matchingBlocks = blockResults.map((result) => {
1772
+ const block = allBlocks.find(
1773
+ (b) => b.name.toLowerCase() === result.name.toLowerCase()
1774
+ );
1775
+ return block ? { name: block.name, description: block.description, components: block.components, code: block.code } : null;
1776
+ }).filter(Boolean);
1777
+ let relevantTokens;
1778
+ if (tokenResults.length > 0 && tokenData) {
1779
+ relevantTokens = {};
1780
+ for (const result of tokenResults) {
1781
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1782
+ if (tokens.some((t) => t.name === result.name)) {
1783
+ if (!relevantTokens[cat]) relevantTokens[cat] = [];
1784
+ relevantTokens[cat].push(result.name);
1785
+ break;
1786
+ }
1787
+ }
1788
+ }
1789
+ if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
1790
+ }
1791
+ return {
1792
+ content: [{
1793
+ type: "text",
1794
+ text: JSON.stringify({
1795
+ useCase,
1796
+ components,
1797
+ blocks: matchingBlocks.length > 0 ? matchingBlocks : void 0,
1798
+ tokens: relevantTokens,
1799
+ noMatch: components.length === 0,
1800
+ summary: components.length > 0 ? `Found ${components.length} component(s) for "${useCase}". ${matchingBlocks.length > 0 ? `Plus ${matchingBlocks.length} ready-to-use block(s).` : ""}` : `No components match "${useCase}". Try ${TOOL_NAMES.discover} with different terms${tokenData ? ` or ${TOOL_NAMES.tokens} for CSS variables` : ""}.`
1801
+ }, null, 2)
1802
+ }]
1803
+ };
1804
+ }
1805
+ // ================================================================
1806
+ // RENDER — HTTP-only (no Playwright)
1807
+ // ================================================================
1808
+ case TOOL_NAMES.render: {
1809
+ const componentName = args?.component;
1810
+ const variantName = args?.variant;
1811
+ const props = args?.props ?? {};
1812
+ const viewport = args?.viewport;
1813
+ const figmaUrl = args?.figmaUrl;
1814
+ const threshold = args?.threshold ?? (figmaUrl ? 1 : config.threshold ?? DEFAULTS.diffThreshold);
1815
+ if (!componentName) {
1816
+ return {
1817
+ content: [{ type: "text", text: "Error: component name is required" }],
1818
+ isError: true
1819
+ };
1820
+ }
1821
+ const viewerUrl = config.viewerUrl;
1822
+ if (!viewerUrl) {
1823
+ return {
1824
+ content: [{
1825
+ type: "text",
1826
+ text: "Render requires a running Fragments dev server. Start it with `fragments dev` and pass --viewer-url, or use the CLI MCP server which includes Playwright."
1827
+ }],
1828
+ isError: true
1829
+ };
1830
+ }
1831
+ if (figmaUrl) {
1832
+ try {
1833
+ const result = await compareComponent(viewerUrl, {
1834
+ component: componentName,
1835
+ variant: variantName,
1836
+ props,
1837
+ figmaUrl,
1838
+ threshold
1839
+ });
1840
+ if (result.error) {
1841
+ return {
1842
+ content: [{
1843
+ type: "text",
1844
+ text: `Compare error: ${result.error}${result.suggestion ? `
1845
+ Suggestion: ${result.suggestion}` : ""}`
1846
+ }],
1847
+ isError: true
1848
+ };
1849
+ }
1850
+ const content = [];
1851
+ 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}%)`;
1852
+ content.push({ type: "text", text: summaryText });
1853
+ if (result.diff && !result.match) {
1854
+ content.push({
1855
+ type: "image",
1856
+ data: result.diff.replace("data:image/png;base64,", ""),
1857
+ mimeType: "image/png"
1858
+ });
1859
+ content.push({
1860
+ type: "text",
1861
+ text: `Diff image above shows visual differences (red highlights). Changed regions: ${result.changedRegions?.length ?? 0}`
1862
+ });
1863
+ }
1864
+ content.push({
1865
+ type: "text",
1866
+ text: JSON.stringify({
1867
+ match: result.match,
1868
+ diffPercentage: result.diffPercentage,
1869
+ threshold: result.threshold,
1870
+ figmaUrl: result.figmaUrl,
1871
+ changedRegions: result.changedRegions
1872
+ }, null, 2)
1873
+ });
1874
+ return { content };
1875
+ } catch (error) {
1876
+ return {
1877
+ content: [{
1878
+ type: "text",
1879
+ 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.`
1880
+ }],
1881
+ isError: true
1882
+ };
1883
+ }
1884
+ }
1885
+ try {
1886
+ const result = await renderComponent(viewerUrl, {
1887
+ component: componentName,
1888
+ props,
1889
+ viewport: viewport ?? { width: 800, height: 600 }
1890
+ });
1891
+ if (result.error) {
1892
+ return {
1893
+ content: [{ type: "text", text: `Render error: ${result.error}` }],
1894
+ isError: true
1895
+ };
1896
+ }
1897
+ return {
1898
+ content: [
1899
+ {
1900
+ type: "image",
1901
+ data: result.screenshot.replace("data:image/png;base64,", ""),
1902
+ mimeType: "image/png"
1903
+ },
1904
+ {
1905
+ type: "text",
1906
+ text: `Successfully rendered ${componentName} with props: ${JSON.stringify(props)}`
1907
+ }
1908
+ ]
1909
+ };
1910
+ } catch (error) {
1911
+ return {
1912
+ content: [{
1913
+ type: "text",
1914
+ text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
1915
+ }],
1916
+ isError: true
1917
+ };
1918
+ }
1919
+ }
1920
+ // ================================================================
1921
+ // FIX — HTTP-only (no Playwright)
1922
+ // ================================================================
1923
+ case TOOL_NAMES.fix: {
1924
+ const data = await loadFragments();
1925
+ const componentName = args?.component;
1926
+ const variantName = args?.variant ?? void 0;
1927
+ const fixType = args?.fixType ?? "all";
1928
+ if (!componentName) {
1929
+ throw new Error("component is required");
1930
+ }
1931
+ const fragment = Object.values(data.fragments).find(
1932
+ (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1933
+ );
1934
+ if (!fragment) {
1935
+ throw new Error(`Component "${componentName}" not found. Use fragments_discover to see available components.`);
1936
+ }
1937
+ const viewerUrl = config.viewerUrl;
1938
+ if (!viewerUrl) {
1939
+ return {
1940
+ content: [{
1941
+ type: "text",
1942
+ text: "Fix requires a running Fragments dev server. Start it with `fragments dev` and pass --viewer-url."
1943
+ }],
1944
+ isError: true
1945
+ };
1946
+ }
1947
+ try {
1948
+ const result = await fixComponent(viewerUrl, {
1949
+ component: componentName,
1950
+ variant: variantName,
1951
+ fixType
1952
+ });
1953
+ if (result.error) {
1954
+ return {
1955
+ content: [{
1956
+ type: "text",
1957
+ text: `Fix generation error: ${result.error}`
1958
+ }],
1959
+ isError: true
1960
+ };
1961
+ }
1962
+ return {
1963
+ content: [{
1964
+ type: "text",
1965
+ text: JSON.stringify({
1966
+ component: componentName,
1967
+ variant: variantName ?? "all",
1968
+ fixType,
1969
+ patches: result.patches,
1970
+ summary: result.summary,
1971
+ patchCount: result.patches.length,
1972
+ nextStep: result.patches.length > 0 ? "Apply patches using your editor or `patch` command, then run fragments_render to confirm fixes." : void 0
1973
+ }, null, 2)
1974
+ }]
1975
+ };
1976
+ } catch (error) {
1977
+ return {
1978
+ content: [{
1979
+ type: "text",
1980
+ text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
1981
+ }],
1982
+ isError: true
1983
+ };
1984
+ }
1985
+ }
1986
+ // ================================================================
1987
+ // A11Y — accessibility audit
1988
+ // ================================================================
1989
+ case TOOL_NAMES.a11y: {
1990
+ const componentName = args?.component;
1991
+ const variantName = args?.variant ?? void 0;
1992
+ const standard = args?.standard ?? "AA";
1993
+ const includeFixPatches = args?.includeFixPatches ?? false;
1994
+ if (!componentName) {
1995
+ throw new Error("component is required");
1996
+ }
1997
+ const viewerUrl = config.viewerUrl;
1998
+ if (!viewerUrl) {
1999
+ return {
2000
+ content: [{
2001
+ type: "text",
2002
+ text: "A11y audit requires a running Fragments dev server. Start it with `fragments dev` and pass --viewer-url."
2003
+ }],
2004
+ isError: true
2005
+ };
2006
+ }
2007
+ try {
2008
+ const result = await auditComponent(viewerUrl, {
2009
+ component: componentName,
2010
+ variant: variantName,
2011
+ standard,
2012
+ includeFixPatches
2013
+ });
2014
+ if (result.error) {
2015
+ return {
2016
+ content: [{
2017
+ type: "text",
2018
+ text: `A11y audit error: ${result.error}`
2019
+ }],
2020
+ isError: true
2021
+ };
2022
+ }
2023
+ return {
2024
+ content: [{
2025
+ type: "text",
2026
+ text: JSON.stringify({
2027
+ component: componentName,
2028
+ variant: variantName ?? "all",
2029
+ standard,
2030
+ score: result.score,
2031
+ aaCompliance: `${result.aaPercent}%`,
2032
+ aaaCompliance: `${result.aaaPercent}%`,
2033
+ passed: result.passed,
2034
+ results: result.results,
2035
+ nextStep: result.passed ? 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.' : `Fix the violations above, then re-run ${TOOL_NAMES.a11y} to verify. Use ${TOOL_NAMES.fix} for automated fixes.`
2036
+ }, null, 2)
2037
+ }]
2038
+ };
2039
+ } catch (error) {
2040
+ return {
2041
+ content: [{
2042
+ type: "text",
2043
+ text: `Failed to audit component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
2044
+ }],
2045
+ isError: true
2046
+ };
2047
+ }
2048
+ }
2049
+ // ================================================================
2050
+ // GRAPH — component relationship queries
2051
+ // ================================================================
2052
+ case TOOL_NAMES.graph: {
2053
+ const data = await loadFragments();
2054
+ const graphArgs = {
2055
+ mode: args?.mode ?? "health",
2056
+ component: args?.component,
2057
+ target: args?.target,
2058
+ edgeTypes: args?.edgeTypes,
2059
+ maxDepth: args?.maxDepth
2060
+ };
2061
+ const result = handleGraphTool(
2062
+ graphArgs,
2063
+ data.graph,
2064
+ data.blocks ?? data.recipes
2065
+ );
2066
+ if (result.isError) {
2067
+ return {
2068
+ content: [{ type: "text", text: result.text }],
2069
+ isError: true
2070
+ };
2071
+ }
2072
+ return {
2073
+ content: [{ type: "text", text: result.text }]
2074
+ };
2075
+ }
2076
+ default:
2077
+ throw new Error(`Unknown tool: ${name}`);
2078
+ }
2079
+ } catch (error) {
2080
+ return {
2081
+ content: [{
2082
+ type: "text",
2083
+ text: JSON.stringify({
2084
+ error: error instanceof Error ? error.message : String(error)
2085
+ })
2086
+ }],
2087
+ isError: true
2088
+ };
2089
+ }
2090
+ });
2091
+ return server;
2092
+ }
2093
+ async function startMcpServer(config) {
2094
+ const server = createMcpServer(config);
2095
+ const transport = new StdioServerTransport();
2096
+ await server.connect(transport);
2097
+ }
2098
+
2099
+ export {
2100
+ createMcpServer,
2101
+ startMcpServer
2102
+ };
2103
+ //# sourceMappingURL=chunk-EMMM6LTH.js.map