@fragments-sdk/cli 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin.js +12 -12
- package/dist/{chunk-NOTYONHY.js → chunk-2DJH4F4P.js} +2 -2
- package/dist/{chunk-5CKYLCJH.js → chunk-2H2JAA3U.js} +35 -7
- package/dist/chunk-2H2JAA3U.js.map +1 -0
- package/dist/{chunk-G3M3MPQ6.js → chunk-B2TQKOLW.js} +157 -30
- package/dist/chunk-B2TQKOLW.js.map +1 -0
- package/dist/{chunk-AW7MWOUH.js → chunk-ICAIQ57V.js} +9 -5
- package/dist/chunk-ICAIQ57V.js.map +1 -0
- package/dist/{chunk-5ZYEOHYK.js → chunk-IOJE35DZ.js} +2 -2
- package/dist/{chunk-ZFKGX3QK.js → chunk-UXRGD3DM.js} +47 -14
- package/dist/chunk-UXRGD3DM.js.map +1 -0
- package/dist/{chunk-J4SI5RIH.js → chunk-XNWDI6UT.js} +4 -4
- package/dist/{core-LNXDLXDP.js → core-NJVKKLJ4.js} +11 -3
- package/dist/{generate-OIXXHOWR.js → generate-OVGMDKCJ.js} +4 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +6 -6
- package/dist/{init-EVPXIDW4.js → init-EOA7TTOR.js} +4 -4
- package/dist/mcp-bin.js +308 -48
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-YN4LUDKY.js +12 -0
- package/dist/{service-K52ORLCJ.js → service-2T26CBWE.js} +4 -4
- package/dist/{static-viewer-JNQIHA4B.js → static-viewer-CLJJRYHK.js} +4 -4
- package/dist/{test-USARUEFW.js → test-ECPEXFDN.js} +3 -3
- package/dist/{tokens-C6YHBOQE.js → tokens-FHA2DO22.js} +5 -5
- package/dist/{viewer-H7TVFT4E.js → viewer-XDPD52L7.js} +13 -13
- package/package.json +1 -1
- package/src/build.ts +53 -13
- package/src/core/constants.ts +4 -1
- package/src/core/context.ts +28 -28
- package/src/core/defineSegment.ts +21 -11
- package/src/core/discovery.ts +52 -4
- package/src/core/index.ts +14 -4
- package/src/core/loader.ts +3 -0
- package/src/core/node.ts +3 -1
- package/src/core/parser.ts +1 -1
- package/src/core/schema.ts +7 -2
- package/src/core/token-parser.ts +211 -0
- package/src/core/types.ts +46 -6
- package/src/mcp/server.ts +378 -56
- package/dist/chunk-5CKYLCJH.js.map +0 -1
- package/dist/chunk-AW7MWOUH.js.map +0 -1
- package/dist/chunk-G3M3MPQ6.js.map +0 -1
- package/dist/chunk-ZFKGX3QK.js.map +0 -1
- package/dist/scan-YVYD64GD.js +0 -12
- /package/dist/{chunk-NOTYONHY.js.map → chunk-2DJH4F4P.js.map} +0 -0
- /package/dist/{chunk-5ZYEOHYK.js.map → chunk-IOJE35DZ.js.map} +0 -0
- /package/dist/{chunk-J4SI5RIH.js.map → chunk-XNWDI6UT.js.map} +0 -0
- /package/dist/{core-LNXDLXDP.js.map → core-NJVKKLJ4.js.map} +0 -0
- /package/dist/{generate-OIXXHOWR.js.map → generate-OVGMDKCJ.js.map} +0 -0
- /package/dist/{init-EVPXIDW4.js.map → init-EOA7TTOR.js.map} +0 -0
- /package/dist/{scan-YVYD64GD.js.map → scan-YN4LUDKY.js.map} +0 -0
- /package/dist/{service-K52ORLCJ.js.map → service-2T26CBWE.js.map} +0 -0
- /package/dist/{static-viewer-JNQIHA4B.js.map → static-viewer-CLJJRYHK.js.map} +0 -0
- /package/dist/{test-USARUEFW.js.map → test-ECPEXFDN.js.map} +0 -0
- /package/dist/{tokens-C6YHBOQE.js.map → tokens-FHA2DO22.js.map} +0 -0
- /package/dist/{viewer-H7TVFT4E.js.map → viewer-XDPD52L7.js.map} +0 -0
package/src/mcp/server.ts
CHANGED
|
@@ -41,7 +41,9 @@ import { projectFields } from './utils.js';
|
|
|
41
41
|
const TOOL_NAMES = {
|
|
42
42
|
discover: `${BRAND.nameLower}_discover`,
|
|
43
43
|
inspect: `${BRAND.nameLower}_inspect`,
|
|
44
|
-
|
|
44
|
+
blocks: `${BRAND.nameLower}_blocks`,
|
|
45
|
+
tokens: `${BRAND.nameLower}_tokens`,
|
|
46
|
+
implement: `${BRAND.nameLower}_implement`,
|
|
45
47
|
render: `${BRAND.nameLower}_render`,
|
|
46
48
|
fix: `${BRAND.nameLower}_fix`,
|
|
47
49
|
} as const;
|
|
@@ -320,26 +322,61 @@ const TOOLS: Tool[] = [
|
|
|
320
322
|
},
|
|
321
323
|
},
|
|
322
324
|
{
|
|
323
|
-
name: TOOL_NAMES.
|
|
324
|
-
description: `Search and retrieve composition
|
|
325
|
+
name: TOOL_NAMES.blocks,
|
|
326
|
+
description: `Search and retrieve composition blocks — 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.`,
|
|
325
327
|
inputSchema: {
|
|
326
328
|
type: 'object' as const,
|
|
327
329
|
properties: {
|
|
328
330
|
name: {
|
|
329
331
|
type: 'string',
|
|
330
|
-
description: 'Exact
|
|
332
|
+
description: 'Exact block name to retrieve (e.g., "Login Form")',
|
|
331
333
|
},
|
|
332
334
|
search: {
|
|
333
335
|
type: 'string',
|
|
334
|
-
description: 'Free-text search across
|
|
336
|
+
description: 'Free-text search across block names, descriptions, tags, and components',
|
|
335
337
|
},
|
|
336
338
|
component: {
|
|
337
339
|
type: 'string',
|
|
338
|
-
description: 'Filter
|
|
340
|
+
description: 'Filter blocks that use a specific component (e.g., "Button")',
|
|
341
|
+
},
|
|
342
|
+
category: {
|
|
343
|
+
type: 'string',
|
|
344
|
+
description: 'Filter by category (e.g., "authentication", "marketing", "dashboard", "settings", "ecommerce", "ai")',
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: TOOL_NAMES.tokens,
|
|
351
|
+
description: `List available CSS design tokens (custom properties) by category. Use this when you need to style custom elements or override defaults — no more guessing variable names. Filter by category or search by keyword.`,
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: 'object' as const,
|
|
354
|
+
properties: {
|
|
355
|
+
category: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
description: 'Filter by category (e.g., "colors", "spacing", "typography", "surfaces", "shadows", "radius", "borders", "text", "focus", "layout", "code", "component-sizing")',
|
|
358
|
+
},
|
|
359
|
+
search: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'Search token names (e.g., "accent", "hover", "padding")',
|
|
339
362
|
},
|
|
340
363
|
},
|
|
341
364
|
},
|
|
342
365
|
},
|
|
366
|
+
{
|
|
367
|
+
name: TOOL_NAMES.implement,
|
|
368
|
+
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.`,
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: 'object' as const,
|
|
371
|
+
properties: {
|
|
372
|
+
useCase: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
description: 'What you want to implement (e.g., "login form", "data table with sorting", "streaming chat messages")',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
required: ['useCase'],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
343
380
|
{
|
|
344
381
|
name: TOOL_NAMES.render,
|
|
345
382
|
description: `Render a component and return a screenshot. Optionally compare against a stored baseline ('baseline: true') or against a Figma design ('figmaUrl'). Use this to verify your implementation looks correct.`,
|
|
@@ -430,8 +467,10 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
430
467
|
|
|
431
468
|
// Lazy-loaded resources
|
|
432
469
|
let segmentsData: CompiledSegmentsFile | null = null;
|
|
433
|
-
|
|
434
|
-
|
|
470
|
+
// Per-segment package name map (segment name → package name from its source fragments.json)
|
|
471
|
+
const segmentPackageMap = new Map<string, string>();
|
|
472
|
+
let defaultPackageName: string | null = null;
|
|
473
|
+
|
|
435
474
|
let browserPool: any = null;
|
|
436
475
|
let storageManager: any = null;
|
|
437
476
|
let diffEngine: any = null;
|
|
@@ -455,11 +494,31 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
455
494
|
const content = await readFile(paths[0], 'utf-8');
|
|
456
495
|
segmentsData = JSON.parse(content) as CompiledSegmentsFile;
|
|
457
496
|
|
|
497
|
+
// Normalize legacy "recipes" key to "blocks"
|
|
498
|
+
if (!segmentsData.blocks && segmentsData.recipes) {
|
|
499
|
+
segmentsData.blocks = segmentsData.recipes;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Track per-segment package names from each source file
|
|
503
|
+
if (segmentsData.packageName) {
|
|
504
|
+
for (const name of Object.keys(segmentsData.segments)) {
|
|
505
|
+
segmentPackageMap.set(name, segmentsData.packageName);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
458
509
|
for (let i = 1; i < paths.length; i++) {
|
|
459
510
|
const extra = JSON.parse(await readFile(paths[i], 'utf-8')) as CompiledSegmentsFile;
|
|
511
|
+
// Track package name for each segment from this file
|
|
512
|
+
if (extra.packageName) {
|
|
513
|
+
for (const name of Object.keys(extra.segments)) {
|
|
514
|
+
segmentPackageMap.set(name, extra.packageName);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
460
517
|
Object.assign(segmentsData.segments, extra.segments);
|
|
461
|
-
|
|
462
|
-
|
|
518
|
+
// Support both "blocks" (new) and "recipes" (legacy) keys
|
|
519
|
+
const extraBlocks = extra.blocks ?? extra.recipes;
|
|
520
|
+
if (extraBlocks) {
|
|
521
|
+
segmentsData.blocks = { ...segmentsData.blocks, ...extraBlocks };
|
|
463
522
|
}
|
|
464
523
|
}
|
|
465
524
|
|
|
@@ -467,20 +526,28 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
467
526
|
}
|
|
468
527
|
|
|
469
528
|
/**
|
|
470
|
-
* Get the package name for import statements.
|
|
471
|
-
*
|
|
472
|
-
* falls back to the project
|
|
529
|
+
* Get the package name for import statements for a specific component.
|
|
530
|
+
* Uses per-segment tracking when multiple fragments.json files are merged,
|
|
531
|
+
* falls back to the first fragments.json packageName, then project package.json.
|
|
473
532
|
*/
|
|
474
|
-
async function getPackageName(): Promise<string> {
|
|
475
|
-
|
|
476
|
-
|
|
533
|
+
async function getPackageName(segmentName?: string): Promise<string> {
|
|
534
|
+
// Ensure segments are loaded (populates segmentPackageMap)
|
|
535
|
+
await loadSegments();
|
|
536
|
+
|
|
537
|
+
// Check per-segment map first (handles multi-library merges correctly)
|
|
538
|
+
if (segmentName) {
|
|
539
|
+
const segPkg = segmentPackageMap.get(segmentName);
|
|
540
|
+
if (segPkg) return segPkg;
|
|
477
541
|
}
|
|
478
542
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
543
|
+
if (defaultPackageName) {
|
|
544
|
+
return defaultPackageName;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Prefer packageName from first fragments.json
|
|
548
|
+
if (segmentsData?.packageName) {
|
|
549
|
+
defaultPackageName = segmentsData.packageName;
|
|
550
|
+
return defaultPackageName;
|
|
484
551
|
}
|
|
485
552
|
|
|
486
553
|
// Fallback to project package.json
|
|
@@ -490,8 +557,8 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
490
557
|
const content = await readFile(packageJsonPath, 'utf-8');
|
|
491
558
|
const pkg = JSON.parse(content) as { name?: string };
|
|
492
559
|
if (pkg.name) {
|
|
493
|
-
|
|
494
|
-
return
|
|
560
|
+
defaultPackageName = pkg.name;
|
|
561
|
+
return defaultPackageName;
|
|
495
562
|
}
|
|
496
563
|
} catch {
|
|
497
564
|
// Fall through to default
|
|
@@ -499,8 +566,8 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
499
566
|
}
|
|
500
567
|
|
|
501
568
|
// Default fallback
|
|
502
|
-
|
|
503
|
-
return
|
|
569
|
+
defaultPackageName = 'your-component-library';
|
|
570
|
+
return defaultPackageName;
|
|
504
571
|
}
|
|
505
572
|
|
|
506
573
|
/**
|
|
@@ -594,7 +661,7 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
594
661
|
// --- Context mode: compact or format specified with no specific query ---
|
|
595
662
|
if (compact || (args?.format && !useCase && !componentForAlts && !category && !search && !status)) {
|
|
596
663
|
const segments = Object.values(data.segments);
|
|
597
|
-
const
|
|
664
|
+
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
598
665
|
|
|
599
666
|
const { content: ctxContent, tokenEstimate } = generateContext(segments, {
|
|
600
667
|
format,
|
|
@@ -603,7 +670,7 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
603
670
|
code: includeCode,
|
|
604
671
|
relations: includeRelations,
|
|
605
672
|
},
|
|
606
|
-
},
|
|
673
|
+
}, allBlocks);
|
|
607
674
|
|
|
608
675
|
return {
|
|
609
676
|
content: [{
|
|
@@ -757,6 +824,31 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
757
824
|
? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map(s => s.component).join(' and ')}.`
|
|
758
825
|
: undefined;
|
|
759
826
|
|
|
827
|
+
// Detect if query is about styling/tokens rather than components
|
|
828
|
+
const STYLE_KEYWORDS = ['color', 'spacing', 'padding', 'margin', 'font', 'border', 'radius', 'shadow', 'variable', 'token', 'css', 'theme', 'dark mode', 'background', 'hover'];
|
|
829
|
+
const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
|
|
830
|
+
|
|
831
|
+
// Determine no-match quality
|
|
832
|
+
const noMatch = suggestions.length === 0;
|
|
833
|
+
const weakMatch = !noMatch && suggestions.every((s) => s.confidence === 'low');
|
|
834
|
+
|
|
835
|
+
let recommendation: string;
|
|
836
|
+
let nextStep: string | undefined;
|
|
837
|
+
if (noMatch) {
|
|
838
|
+
recommendation = isStyleQuery
|
|
839
|
+
? `No matching components found. Your query seems styling-related — try ${TOOL_NAMES.tokens} to find CSS custom properties.`
|
|
840
|
+
: 'No matching components found. Try different keywords or browse all components with fragments_discover.';
|
|
841
|
+
nextStep = isStyleQuery
|
|
842
|
+
? `Use ${TOOL_NAMES.tokens}(search: "${searchTerms[0]}") to find design tokens.`
|
|
843
|
+
: undefined;
|
|
844
|
+
} else if (weakMatch) {
|
|
845
|
+
recommendation = `Weak matches only — ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${TOOL_NAMES.tokens}.` : ''}`;
|
|
846
|
+
nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
|
|
847
|
+
} else {
|
|
848
|
+
recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
|
|
849
|
+
nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
|
|
850
|
+
}
|
|
851
|
+
|
|
760
852
|
return {
|
|
761
853
|
content: [{
|
|
762
854
|
type: 'text' as const,
|
|
@@ -764,13 +856,11 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
764
856
|
useCase,
|
|
765
857
|
context: context || undefined,
|
|
766
858
|
suggestions: suggestions.map(({ score, ...rest }) => rest),
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
859
|
+
noMatch,
|
|
860
|
+
weakMatch,
|
|
861
|
+
recommendation,
|
|
770
862
|
compositionHint,
|
|
771
|
-
nextStep
|
|
772
|
-
? `Use fragments_inspect("${suggestions[0].component}") for full details.`
|
|
773
|
-
: undefined,
|
|
863
|
+
nextStep,
|
|
774
864
|
}, null, 2),
|
|
775
865
|
}],
|
|
776
866
|
};
|
|
@@ -893,16 +983,35 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
893
983
|
}
|
|
894
984
|
|
|
895
985
|
// Build the full inspect result combining get + guidelines + example
|
|
896
|
-
const pkgName = await getPackageName();
|
|
986
|
+
const pkgName = await getPackageName(segment.meta.name);
|
|
897
987
|
|
|
898
|
-
// Filter variants for examples
|
|
988
|
+
// Filter variants for examples — fuzzy match: exact → prefix → contains
|
|
899
989
|
let variants = segment.variants;
|
|
900
990
|
if (variantName) {
|
|
901
|
-
const
|
|
902
|
-
|
|
991
|
+
const query = variantName.toLowerCase();
|
|
992
|
+
// 1. Exact match
|
|
993
|
+
let filtered = variants.filter(
|
|
994
|
+
(v) => v.name.toLowerCase() === query
|
|
903
995
|
);
|
|
996
|
+
// 2. Prefix match (e.g. "Dots" matches "Dots (Default)")
|
|
997
|
+
if (filtered.length === 0) {
|
|
998
|
+
filtered = variants.filter(
|
|
999
|
+
(v) => v.name.toLowerCase().startsWith(query)
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
// 3. Contains match (e.g. "elapsed" matches "With Elapsed Time")
|
|
1003
|
+
if (filtered.length === 0) {
|
|
1004
|
+
filtered = variants.filter(
|
|
1005
|
+
(v) => v.name.toLowerCase().includes(query)
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
904
1008
|
if (filtered.length > 0) {
|
|
905
1009
|
variants = filtered;
|
|
1010
|
+
} else {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`Variant "${variantName}" not found for ${componentName}. ` +
|
|
1013
|
+
`Available: ${segment.variants.map((v) => v.name).join(', ')}`
|
|
1014
|
+
);
|
|
906
1015
|
}
|
|
907
1016
|
}
|
|
908
1017
|
if (maxExamples && maxExamples > 0) {
|
|
@@ -991,53 +1100,60 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
991
1100
|
}
|
|
992
1101
|
|
|
993
1102
|
// ================================================================
|
|
994
|
-
//
|
|
1103
|
+
// BLOCKS — composition patterns
|
|
995
1104
|
// ================================================================
|
|
996
|
-
case TOOL_NAMES.
|
|
1105
|
+
case TOOL_NAMES.blocks: {
|
|
997
1106
|
const data = await loadSegments();
|
|
998
|
-
const
|
|
1107
|
+
const blockName = args?.name as string | undefined;
|
|
999
1108
|
const search = (args?.search as string)?.toLowerCase() ?? undefined;
|
|
1000
1109
|
const component = (args?.component as string)?.toLowerCase() ?? undefined;
|
|
1110
|
+
const category = (args?.category as string)?.toLowerCase() ?? undefined;
|
|
1001
1111
|
|
|
1002
|
-
const
|
|
1112
|
+
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
1003
1113
|
|
|
1004
|
-
if (
|
|
1114
|
+
if (allBlocks.length === 0) {
|
|
1005
1115
|
return {
|
|
1006
1116
|
content: [{
|
|
1007
1117
|
type: 'text' as const,
|
|
1008
1118
|
text: JSON.stringify({
|
|
1009
1119
|
total: 0,
|
|
1010
|
-
|
|
1011
|
-
hint: `No
|
|
1120
|
+
blocks: [],
|
|
1121
|
+
hint: `No blocks found. Run \`${BRAND.cliCommand} build\` after adding .block.ts files.`,
|
|
1012
1122
|
}, null, 2),
|
|
1013
1123
|
}],
|
|
1014
1124
|
};
|
|
1015
1125
|
}
|
|
1016
1126
|
|
|
1017
|
-
let filtered =
|
|
1127
|
+
let filtered = allBlocks;
|
|
1018
1128
|
|
|
1019
|
-
if (
|
|
1129
|
+
if (blockName) {
|
|
1020
1130
|
filtered = filtered.filter(
|
|
1021
|
-
|
|
1131
|
+
b => b.name.toLowerCase() === blockName.toLowerCase()
|
|
1022
1132
|
);
|
|
1023
1133
|
}
|
|
1024
1134
|
|
|
1025
1135
|
if (search) {
|
|
1026
|
-
filtered = filtered.filter(
|
|
1136
|
+
filtered = filtered.filter(b => {
|
|
1027
1137
|
const haystack = [
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
...(
|
|
1031
|
-
...
|
|
1032
|
-
|
|
1138
|
+
b.name,
|
|
1139
|
+
b.description,
|
|
1140
|
+
...(b.tags ?? []),
|
|
1141
|
+
...b.components,
|
|
1142
|
+
b.category,
|
|
1033
1143
|
].join(' ').toLowerCase();
|
|
1034
1144
|
return haystack.includes(search);
|
|
1035
1145
|
});
|
|
1036
1146
|
}
|
|
1037
1147
|
|
|
1038
1148
|
if (component) {
|
|
1039
|
-
filtered = filtered.filter(
|
|
1040
|
-
|
|
1149
|
+
filtered = filtered.filter(b =>
|
|
1150
|
+
b.components.some(c => c.toLowerCase() === component)
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (category) {
|
|
1155
|
+
filtered = filtered.filter(b =>
|
|
1156
|
+
b.category.toLowerCase() === category
|
|
1041
1157
|
);
|
|
1042
1158
|
}
|
|
1043
1159
|
|
|
@@ -1046,7 +1162,213 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
1046
1162
|
type: 'text' as const,
|
|
1047
1163
|
text: JSON.stringify({
|
|
1048
1164
|
total: filtered.length,
|
|
1049
|
-
|
|
1165
|
+
blocks: filtered,
|
|
1166
|
+
}, null, 2),
|
|
1167
|
+
}],
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// ================================================================
|
|
1172
|
+
// TOKENS — list CSS custom properties by category
|
|
1173
|
+
// ================================================================
|
|
1174
|
+
case TOOL_NAMES.tokens: {
|
|
1175
|
+
const data = await loadSegments();
|
|
1176
|
+
const category = (args?.category as string)?.toLowerCase() ?? undefined;
|
|
1177
|
+
const search = (args?.search as string)?.toLowerCase() ?? undefined;
|
|
1178
|
+
|
|
1179
|
+
const tokenData = data.tokens;
|
|
1180
|
+
|
|
1181
|
+
if (!tokenData || tokenData.total === 0) {
|
|
1182
|
+
return {
|
|
1183
|
+
content: [{
|
|
1184
|
+
type: 'text' as const,
|
|
1185
|
+
text: JSON.stringify({
|
|
1186
|
+
total: 0,
|
|
1187
|
+
categories: {},
|
|
1188
|
+
hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`,
|
|
1189
|
+
}, null, 2),
|
|
1190
|
+
}],
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Filter by category and/or search
|
|
1195
|
+
let filteredCategories: Record<string, Array<{ name: string; description?: string }>> = {};
|
|
1196
|
+
let filteredTotal = 0;
|
|
1197
|
+
|
|
1198
|
+
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
1199
|
+
// Filter by category
|
|
1200
|
+
if (category && cat !== category) continue;
|
|
1201
|
+
|
|
1202
|
+
// Filter by search term within this category
|
|
1203
|
+
let filtered = tokens;
|
|
1204
|
+
if (search) {
|
|
1205
|
+
filtered = tokens.filter(
|
|
1206
|
+
(t) => t.name.toLowerCase().includes(search) ||
|
|
1207
|
+
(t.description && t.description.toLowerCase().includes(search))
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (filtered.length > 0) {
|
|
1212
|
+
filteredCategories[cat] = filtered;
|
|
1213
|
+
filteredTotal += filtered.length;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Build usage hint based on context
|
|
1218
|
+
let hint: string | undefined;
|
|
1219
|
+
if (filteredTotal === 0) {
|
|
1220
|
+
const availableCategories = Object.keys(tokenData.categories);
|
|
1221
|
+
hint = search
|
|
1222
|
+
? `No tokens matching "${search}". Try: ${availableCategories.join(', ')}`
|
|
1223
|
+
: category
|
|
1224
|
+
? `Category "${category}" not found. Available: ${availableCategories.join(', ')}`
|
|
1225
|
+
: undefined;
|
|
1226
|
+
} else if (!category && !search) {
|
|
1227
|
+
hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{
|
|
1232
|
+
type: 'text' as const,
|
|
1233
|
+
text: JSON.stringify({
|
|
1234
|
+
prefix: tokenData.prefix,
|
|
1235
|
+
total: filteredTotal,
|
|
1236
|
+
totalAvailable: tokenData.total,
|
|
1237
|
+
categories: filteredCategories,
|
|
1238
|
+
...(hint && { hint }),
|
|
1239
|
+
...((!category && !search) && {
|
|
1240
|
+
availableCategories: Object.entries(tokenData.categories).map(
|
|
1241
|
+
([cat, tokens]) => ({ category: cat, count: tokens.length })
|
|
1242
|
+
),
|
|
1243
|
+
}),
|
|
1244
|
+
}, null, 2),
|
|
1245
|
+
}],
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// ================================================================
|
|
1250
|
+
// IMPLEMENT — one-shot discover + inspect + blocks + tokens
|
|
1251
|
+
// ================================================================
|
|
1252
|
+
case TOOL_NAMES.implement: {
|
|
1253
|
+
const data = await loadSegments();
|
|
1254
|
+
const useCase = args?.useCase as string;
|
|
1255
|
+
if (!useCase) {
|
|
1256
|
+
throw new Error('useCase is required');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const useCaseLower = useCase.toLowerCase();
|
|
1260
|
+
const searchTerms = useCaseLower.split(/\s+/).filter(Boolean);
|
|
1261
|
+
|
|
1262
|
+
// 1. Score all segments (same logic as discover suggest)
|
|
1263
|
+
const synonymMap: Record<string, string[]> = {
|
|
1264
|
+
'form': ['input', 'field', 'submit', 'validation'],
|
|
1265
|
+
'input': ['form', 'field', 'text', 'entry'],
|
|
1266
|
+
'button': ['action', 'click', 'submit', 'trigger'],
|
|
1267
|
+
'alert': ['notification', 'message', 'warning', 'error', 'feedback'],
|
|
1268
|
+
'notification': ['alert', 'message', 'toast'],
|
|
1269
|
+
'card': ['container', 'panel', 'box', 'content'],
|
|
1270
|
+
'toggle': ['switch', 'checkbox', 'boolean'],
|
|
1271
|
+
'badge': ['tag', 'label', 'status', 'indicator'],
|
|
1272
|
+
'login': ['auth', 'signin', 'authentication', 'form'],
|
|
1273
|
+
'chat': ['message', 'conversation', 'ai'],
|
|
1274
|
+
'table': ['data', 'grid', 'list', 'rows'],
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
const expandedTerms = new Set(searchTerms);
|
|
1278
|
+
searchTerms.forEach((term) => {
|
|
1279
|
+
const synonyms = synonymMap[term];
|
|
1280
|
+
if (synonyms) synonyms.forEach((syn) => expandedTerms.add(syn));
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
const scored = Object.values(data.segments).map((s) => {
|
|
1284
|
+
let score = 0;
|
|
1285
|
+
const nameLower = s.meta.name.toLowerCase();
|
|
1286
|
+
if (searchTerms.some((t) => nameLower.includes(t))) score += 15;
|
|
1287
|
+
else if (Array.from(expandedTerms).some((t) => nameLower.includes(t))) score += 8;
|
|
1288
|
+
const desc = s.meta.description?.toLowerCase() ?? '';
|
|
1289
|
+
score += searchTerms.filter((t) => desc.includes(t)).length * 6;
|
|
1290
|
+
const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
|
|
1291
|
+
score += searchTerms.filter((t) => tags.some((tag) => tag.includes(t))).length * 4;
|
|
1292
|
+
const whenUsed = s.usage?.when?.join(' ').toLowerCase() ?? '';
|
|
1293
|
+
score += searchTerms.filter((t) => whenUsed.includes(t)).length * 10;
|
|
1294
|
+
score += Array.from(expandedTerms).filter((t) => !searchTerms.includes(t) && whenUsed.includes(t)).length * 5;
|
|
1295
|
+
if (s.meta.category && searchTerms.some((t) => s.meta.category!.toLowerCase().includes(t))) score += 8;
|
|
1296
|
+
if (s.meta.status === 'stable') score += 5;
|
|
1297
|
+
if (s.meta.status === 'deprecated') score -= 25;
|
|
1298
|
+
return { segment: s, score };
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
const topMatches = scored
|
|
1302
|
+
.filter((s) => s.score >= 8)
|
|
1303
|
+
.sort((a, b) => b.score - a.score)
|
|
1304
|
+
.slice(0, 3);
|
|
1305
|
+
|
|
1306
|
+
// 2. Build component details for top matches
|
|
1307
|
+
const components = await Promise.all(
|
|
1308
|
+
topMatches.map(async ({ segment: s, score }) => {
|
|
1309
|
+
const pkgName = await getPackageName(s.meta.name);
|
|
1310
|
+
const examples = s.variants.slice(0, 2).map((v) => ({
|
|
1311
|
+
variant: v.name,
|
|
1312
|
+
code: v.code ?? `<${s.meta.name} />`,
|
|
1313
|
+
}));
|
|
1314
|
+
const propsSummary = Object.entries(s.props ?? {}).slice(0, 10).map(
|
|
1315
|
+
([name, p]) => `${name}${p.required ? ' (required)' : ''}: ${p.type}${p.values ? ` = ${p.values.join('|')}` : ''}`
|
|
1316
|
+
);
|
|
1317
|
+
return {
|
|
1318
|
+
name: s.meta.name,
|
|
1319
|
+
category: s.meta.category,
|
|
1320
|
+
description: s.meta.description,
|
|
1321
|
+
confidence: score >= 25 ? 'high' : score >= 15 ? 'medium' : 'low',
|
|
1322
|
+
import: `import { ${s.meta.name} } from '${pkgName}';`,
|
|
1323
|
+
props: propsSummary,
|
|
1324
|
+
examples,
|
|
1325
|
+
guidelines: filterPlaceholders(s.usage?.when).slice(0, 3),
|
|
1326
|
+
accessibility: s.usage?.accessibility?.slice(0, 2) ?? [],
|
|
1327
|
+
};
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
// 3. Find relevant blocks
|
|
1332
|
+
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
1333
|
+
const matchingBlocks = allBlocks
|
|
1334
|
+
.filter((b) => {
|
|
1335
|
+
const haystack = [b.name, b.description, ...(b.tags ?? []), ...b.components, b.category].join(' ').toLowerCase();
|
|
1336
|
+
return searchTerms.some((t) => haystack.includes(t)) ||
|
|
1337
|
+
topMatches.some(({ segment }) => b.components.some((c) => c.toLowerCase() === segment.meta.name.toLowerCase()));
|
|
1338
|
+
})
|
|
1339
|
+
.slice(0, 2)
|
|
1340
|
+
.map((b) => ({ name: b.name, description: b.description, components: b.components, code: b.code }));
|
|
1341
|
+
|
|
1342
|
+
// 4. Find relevant tokens
|
|
1343
|
+
const tokenData = data.tokens;
|
|
1344
|
+
let relevantTokens: Record<string, string[]> | undefined;
|
|
1345
|
+
if (tokenData) {
|
|
1346
|
+
const STYLE_KEYWORDS = ['color', 'spacing', 'padding', 'margin', 'font', 'border', 'radius', 'shadow', 'background', 'hover', 'theme'];
|
|
1347
|
+
const styleTerms = searchTerms.filter((t) => STYLE_KEYWORDS.includes(t));
|
|
1348
|
+
if (styleTerms.length > 0) {
|
|
1349
|
+
relevantTokens = {};
|
|
1350
|
+
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
1351
|
+
const matching = tokens.filter((t) => styleTerms.some((st) => t.name.includes(st) || cat.includes(st)));
|
|
1352
|
+
if (matching.length > 0) {
|
|
1353
|
+
relevantTokens[cat] = matching.map((t) => t.name);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (Object.keys(relevantTokens).length === 0) relevantTokens = undefined;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return {
|
|
1361
|
+
content: [{
|
|
1362
|
+
type: 'text' as const,
|
|
1363
|
+
text: JSON.stringify({
|
|
1364
|
+
useCase,
|
|
1365
|
+
components,
|
|
1366
|
+
blocks: matchingBlocks.length > 0 ? matchingBlocks : undefined,
|
|
1367
|
+
tokens: relevantTokens,
|
|
1368
|
+
noMatch: components.length === 0,
|
|
1369
|
+
summary: components.length > 0
|
|
1370
|
+
? `Found ${components.length} component(s) for "${useCase}". ${matchingBlocks.length > 0 ? `Plus ${matchingBlocks.length} ready-to-use block(s).` : ''}`
|
|
1371
|
+
: `No components match "${useCase}". Try ${TOOL_NAMES.discover} with different terms${tokenData ? ` or ${TOOL_NAMES.tokens} for CSS variables` : ''}.`,
|
|
1050
1372
|
}, null, 2),
|
|
1051
1373
|
}],
|
|
1052
1374
|
};
|