@cascivo/mcp 0.1.2 → 0.1.5

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 CHANGED
@@ -35,17 +35,19 @@ The server speaks the MCP stdio transport. By default it reads the `registry.jso
35
35
 
36
36
  ## Tools
37
37
 
38
- | Tool | Input | Returns |
39
- | ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
40
- | `list_components` | `{ category? }` | All component manifests, optionally filtered by category |
41
- | `get_component` | `{ name }` | The full manifest for one component |
42
- | `search_components` | `{ query }` | Components matching name, tags, or description |
43
- | `add_to_project` | `{ name, outputDir? }` | Runs `cascivo add <name>` as a child process |
44
- | `create_theme` | `{ primary, neutral, accent, name? }` | A custom theme as CSS (semantic token layer) |
45
- | `scaffold_page` | `{ description, components? }` | A JSX page layout string |
46
- | `scaffold_view` | `{ description, components? }` | A validated starter `ViewConfig` + the bound-vocabulary `grammar` for its components |
47
- | `validate_view` | `{ config }` | Validation errors (component, prop type/enum, refs) with exact paths |
48
- | `get_view_grammar` | `{ components? }` | Bound-vocabulary grammar + generation prompt for valid `ViewConfig` JSON |
38
+ | Tool | Input | Returns |
39
+ | -------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `list_components` | `{ category? }` | All component manifests, optionally filtered by category |
41
+ | `get_component` | `{ name }` | The full manifest for one component |
42
+ | `search_components` | `{ query }` | Components matching name, tags, or description |
43
+ | `add_to_project` | `{ name, outputDir? }` | Runs `cascivo add <name>` as a child process |
44
+ | `create_theme` | `{ primary, neutral, accent, name? }` | A custom theme as CSS (semantic token layer) |
45
+ | `scaffold_page` | `{ description, components? }` | A JSX page layout string |
46
+ | `scaffold_view` | `{ description, components? }` | A validated starter `ViewConfig` + the bound-vocabulary `grammar` for its components |
47
+ | `validate_view` | `{ config }` | Validation errors (component, prop type/enum, refs) with exact paths |
48
+ | `get_view_grammar` | `{ components? }` | Bound-vocabulary grammar + generation prompt for valid `ViewConfig` JSON |
49
+ | `get_variant_matrix` | `{ role?, theme? }` | Deterministic intent→token map (role + state slot) + every token resolved per theme |
50
+ | `validate_component` | `{ tsx?, css?, name? }` | Static structural-invariant check of generated source (banned hooks, off-scale breakpoints, missing CSS fallbacks, hallucinated tokens) |
49
51
 
50
52
  `category` is one of `inputs`, `display`, `overlay`, `navigation`, `feedback`.
51
53
 
package/dist/index.d.mts CHANGED
@@ -69,7 +69,7 @@ interface ComponentManifest {
69
69
  }
70
70
  interface RegistryComponent {
71
71
  name: string;
72
- type?: 'component' | 'layout' | 'block' | 'chart' | 'section';
72
+ type?: 'component' | 'layout' | 'block' | 'chart' | 'section' | 'flow';
73
73
  description: string;
74
74
  category: string;
75
75
  version: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/theme.ts","../src/scaffold.ts","../src/registry.ts","../src/grammar.ts","../src/prompt.ts","../src/index.ts"],"mappings":";;;KAwBK,OAAA,IAAW,GAAA,UAAa,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA,UAE3C,aAAA;EACf,YAAA;EACA,OAAA;;EAEA,OAAA,GAAU,OAAO;AAAA;;iBAeH,YAAA,CAAa,OAAA,GAAS,aAAA,GAAqB,SAAS;;;UC7CnD,WAAA;;EAEf,OAAA;EDsBG;ECpBH,OAAA;;EAEA,MAAA;AAAA;;;;;iBAiBc,gBAAA,CAAiB,MAAA,EAAQ,WAAW,EAAE,IAAA;;;UCvBrC,eAAA;EACf,WAAA;EACA,UAAU;AAAA;;;;;iBAgCI,YAAA,CAAa,OAAwB,EAAf,eAAe;;;UC9BpC,YAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,WAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,KAAA;EACA,KAAA,EAAO,YAAY;EACnB,MAAA;EACA,aAAA;IAAiB,IAAA;IAAc,IAAA;IAAc,QAAA;EAAA;EAC7C,QAAA;IAAY,KAAA;IAAe,IAAA;IAAc,WAAA;EAAA;EACzC,YAAA;EACA,IAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,IAAA;EACA,IAAA,EAAM,iBAAiB;AAAA;AAAA,UAGR,QAAA;EACf,OAAA;EACA,WAAA;EACA,UAAA,EAAY,iBAAiB;AAAA;AAAA,iBAwBf,YAAA,CAAa,IAAA,YAAgB,QAAQ;;iBAUrC,cAAA,CACd,QAAA,EAAU,QAAA,EACV,QAAA,WACA,IAAA,YACC,iBAAiB;;iBAQJ,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,IAAA,WAAe,iBAAiB;;iBAoGjE,gBAAA,CAAiB,QAAA,EAAU,QAAA,EAAU,KAAA,WAAgB,iBAAiB;;;UChLrE,WAAA;EACf,IAAA;EACA,QAAA;EJUU;EIRV,IAAA;EJQkC;EINlC,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EACA,QAAA;EACA,KAAA,EAAO,WAAW;EAClB,QAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,UAAA,EAAY,gBAAgB;AAAA;;;;;;iBAQd,SAAA,CAAU,IAAY;AAAA,iBA8BtB,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,MAAA,cAAoB,WAAW;;AJtC7D;AAenB;;;;iBI8EgB,aAAA,CAAc,OAAoB,EAAX,WAAW;;;UChHjC,uBAAA;;EAEf,UAAU;AAAA;;;;;;iBAsCI,qBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,uBAA4B;;;cChD1B,OAAA;;iBA0BS,IAAA,IAAQ,OAAO"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/theme.ts","../src/scaffold.ts","../src/registry.ts","../src/grammar.ts","../src/prompt.ts","../src/index.ts"],"mappings":";;;KA0BK,OAAA,IAAW,GAAA,UAAa,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA,UAE3C,aAAA;EACf,YAAA;EACA,OAAA;;EAEA,OAAA,GAAU,OAAO;AAAA;;iBAeH,YAAA,CAAa,OAAA,GAAS,aAAA,GAAqB,SAAS;;;UC/CnD,WAAA;;EAEf,OAAA;EDwBG;ECtBH,OAAA;;EAEA,MAAA;AAAA;;;;;iBAiBc,gBAAA,CAAiB,MAAA,EAAQ,WAAW,EAAE,IAAA;;;UCvBrC,eAAA;EACf,WAAA;EACA,UAAU;AAAA;;;;;iBAgCI,YAAA,CAAa,OAAwB,EAAf,eAAe;;;UC9BpC,YAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,WAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,KAAA;EACA,KAAA,EAAO,YAAY;EACnB,MAAA;EACA,aAAA;IAAiB,IAAA;IAAc,IAAA;IAAc,QAAA;EAAA;EAC7C,QAAA;IAAY,KAAA;IAAe,IAAA;IAAc,WAAA;EAAA;EACzC,YAAA;EACA,IAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,IAAA;EACA,IAAA,EAAM,iBAAiB;AAAA;AAAA,UAGR,QAAA;EACf,OAAA;EACA,WAAA;EACA,UAAA,EAAY,iBAAiB;AAAA;AAAA,iBAwBf,YAAA,CAAa,IAAA,YAAgB,QAAQ;;iBAUrC,cAAA,CACd,QAAA,EAAU,QAAA,EACV,QAAA,WACA,IAAA,YACC,iBAAiB;;iBAQJ,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,IAAA,WAAe,iBAAiB;;iBAoGjE,gBAAA,CAAiB,QAAA,EAAU,QAAA,EAAU,KAAA,WAAgB,iBAAiB;;;UChLrE,WAAA;EACf,IAAA;EACA,QAAA;EJYU;EIVV,IAAA;EJUkC;EIRlC,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EACA,QAAA;EACA,KAAA,EAAO,WAAW;EAClB,QAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,UAAA,EAAY,gBAAgB;AAAA;;;;;;iBAQd,SAAA,CAAU,IAAY;AAAA,iBA8BtB,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,MAAA,cAAoB,WAAW;;AJpC7D;AAenB;;;;iBI4EgB,aAAA,CAAc,OAAoB,EAAX,WAAW;;;UChHjC,uBAAA;;EAEf,UAAU;AAAA;;;;;;iBAsCI,qBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,uBAA4B;;;cChD1B,OAAA;;iBA0BS,IAAA,IAAQ,OAAO"}
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { z } from "zod";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  //#region src/registry.ts
11
- const HERE$2 = dirname(fileURLToPath(import.meta.url));
11
+ const HERE$3 = dirname(fileURLToPath(import.meta.url));
12
12
  /**
13
13
  * Resolve the registry.json location. Checks, in order: an explicit path, the
14
14
  * `CASCIVO_REGISTRY_PATH` env var, a copy bundled next to the built server
@@ -18,9 +18,9 @@ function resolveRegistryPath(explicit) {
18
18
  const candidates = [
19
19
  explicit,
20
20
  process.env.CASCIVO_REGISTRY_PATH,
21
- join(HERE$2, "registry.json"),
22
- join(HERE$2, "..", "..", "..", "registry.json"),
23
- join(HERE$2, "..", "..", "registry.json")
21
+ join(HERE$3, "registry.json"),
22
+ join(HERE$3, "..", "..", "..", "registry.json"),
23
+ join(HERE$3, "..", "..", "registry.json")
24
24
  ].filter((p) => typeof p === "string" && p.length > 0);
25
25
  return candidates.find((p) => existsSync(p)) ?? candidates[candidates.length - 1] ?? "registry.json";
26
26
  }
@@ -496,16 +496,174 @@ function buildGenerationPrompt(registry, options = {}) {
496
496
  }
497
497
  //#endregion
498
498
  //#region src/tokens.ts
499
- const HERE$1 = dirname(fileURLToPath(import.meta.url));
499
+ const HERE$2 = dirname(fileURLToPath(import.meta.url));
500
500
  const CATALOG_BASE_URL = "https://cascivo.com";
501
501
  async function loadTokenCatalog(fetchFn) {
502
- const localPaths = [join(HERE$1, "..", "..", "..", "apps", "docs", "public", "tokens.catalog.json"), join(HERE$1, "tokens.catalog.json")];
502
+ const localPaths = [join(HERE$2, "..", "..", "..", "apps", "docs", "public", "tokens.catalog.json"), join(HERE$2, "tokens.catalog.json")];
503
503
  for (const p of localPaths) if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
504
504
  const res = await (fetchFn ?? fetch)(`${CATALOG_BASE_URL}/tokens.catalog.json`);
505
505
  if (!res.ok) throw new Error(`Failed to fetch token catalog: ${res.status}`);
506
506
  return res.json();
507
507
  }
508
508
  //#endregion
509
+ //#region src/variants.ts
510
+ const HERE$1 = dirname(fileURLToPath(import.meta.url));
511
+ const BASE_URL = "https://cascivo.com";
512
+ async function loadVariantMatrix(fetchFn) {
513
+ const localPaths = [join(HERE$1, "..", "..", "..", "apps", "docs", "public", "tokens.variants.json"), join(HERE$1, "tokens.variants.json")];
514
+ for (const p of localPaths) if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
515
+ const res = await (fetchFn ?? fetch)(`${BASE_URL}/tokens.variants.json`);
516
+ if (!res.ok) throw new Error(`Failed to fetch variant matrix: ${res.status}`);
517
+ return res.json();
518
+ }
519
+ //#endregion
520
+ //#region src/validate-component.ts
521
+ const BANNED_HOOKS = [
522
+ "useState",
523
+ "useEffect",
524
+ "useLayoutEffect",
525
+ "useContext",
526
+ "useReducer"
527
+ ];
528
+ const CANONICAL_WIDTHS = new Set([
529
+ "30rem",
530
+ "40rem",
531
+ "64rem",
532
+ "80rem",
533
+ "480px",
534
+ "640px",
535
+ "1024px",
536
+ "1280px"
537
+ ]);
538
+ function lineOf(source, index) {
539
+ let line = 1;
540
+ for (let i = 0; i < index && i < source.length; i++) if (source[i] === "\n") line++;
541
+ return line;
542
+ }
543
+ /** Detect banned React hooks in TSX (usage or import). */
544
+ function checkBannedHooks(tsx) {
545
+ const violations = [];
546
+ for (const hook of BANNED_HOOKS) {
547
+ const re = new RegExp(`\\b${hook}\\b`, "g");
548
+ let m;
549
+ while ((m = re.exec(tsx)) !== null) violations.push({
550
+ rule: "banned-hook",
551
+ severity: "error",
552
+ line: lineOf(tsx, m.index),
553
+ detail: hook === "useEffect" ? `${hook} is banned — use useSignalEffect for DOM side effects` : `${hook} is banned — use useSignal/useComputed from @cascivo/core`
554
+ });
555
+ }
556
+ return violations;
557
+ }
558
+ /** Detect off-scale width literals in @media / @container conditions. */
559
+ function checkBreakpoints(css) {
560
+ const violations = [];
561
+ const widthCondRe = /(?:min-width|max-width|width|min-inline-size|max-inline-size|inline-size)\s*:\s*([\d.]+(?:rem|px|em))/g;
562
+ const lines = css.split("\n");
563
+ for (let i = 0; i < lines.length; i++) {
564
+ const trimmed = (lines[i] ?? "").trim();
565
+ if (!trimmed.startsWith("@media") && !trimmed.startsWith("@container")) continue;
566
+ if (/prefers|forced|pointer|hover|aspect|color|resolution|orientation|print|screen/.test(trimmed)) continue;
567
+ let m;
568
+ widthCondRe.lastIndex = 0;
569
+ while ((m = widthCondRe.exec(trimmed)) !== null) {
570
+ const value = m[1] ?? "";
571
+ if (!CANONICAL_WIDTHS.has(value)) violations.push({
572
+ rule: "off-scale-breakpoint",
573
+ severity: "error",
574
+ line: i + 1,
575
+ detail: `Off-scale width "${value}" — use 30rem/40rem/64rem/80rem`
576
+ });
577
+ }
578
+ }
579
+ return violations;
580
+ }
581
+ const isFunctionCall = (value) => /--[\w-]+\(/.test(value);
582
+ const isIfExpression = (value) => /\bif\s*\(/.test(value);
583
+ /** Enforce the @function / if() static-fallback contract. */
584
+ function checkFallbacks(css) {
585
+ const violations = [];
586
+ const lines = css.split("\n");
587
+ let inSupports = false;
588
+ let supportsDepth = 0;
589
+ let baseSupportsDepth = 0;
590
+ const stack = [];
591
+ for (let i = 0; i < lines.length; i++) {
592
+ const line = (lines[i] ?? "").trim();
593
+ if (/^@supports\b/.test(line)) {
594
+ inSupports = true;
595
+ baseSupportsDepth = supportsDepth;
596
+ }
597
+ const opens = (line.match(/\{/g) ?? []).length;
598
+ const closes = (line.match(/\}/g) ?? []).length;
599
+ for (let j = 0; j < opens; j++) {
600
+ stack.push({ properties: /* @__PURE__ */ new Map() });
601
+ supportsDepth++;
602
+ }
603
+ for (let j = 0; j < closes; j++) {
604
+ if (stack.length > 0) stack.pop();
605
+ supportsDepth--;
606
+ if (supportsDepth <= baseSupportsDepth) inSupports = false;
607
+ }
608
+ const declMatch = line.match(/^([\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
609
+ if (!declMatch) continue;
610
+ const property = declMatch[1];
611
+ const value = declMatch[2];
612
+ if (isFunctionCall(value) || isIfExpression(value)) {
613
+ let hasFallback = inSupports;
614
+ if (!hasFallback) {
615
+ for (const block of stack) if (block.properties.get(property)) {
616
+ hasFallback = true;
617
+ break;
618
+ }
619
+ }
620
+ if (!hasFallback) violations.push({
621
+ rule: "missing-css-fallback",
622
+ severity: "error",
623
+ line: i + 1,
624
+ detail: `"${property}" uses @function/if() with no preceding static fallback`
625
+ });
626
+ } else if (stack.length > 0) stack[stack.length - 1].properties.set(property, true);
627
+ }
628
+ return violations;
629
+ }
630
+ /** Flag `var(--cascivo-…)` references that are not in the closed token set. */
631
+ function checkTokens(css, tokenNames) {
632
+ if (tokenNames.size === 0) return [];
633
+ const violations = [];
634
+ const re = /var\(\s*(--cascivo-[\w-]+)/g;
635
+ let m;
636
+ const seen = /* @__PURE__ */ new Set();
637
+ while ((m = re.exec(css)) !== null) {
638
+ const token = m[1];
639
+ if (tokenNames.has(token) || seen.has(token)) continue;
640
+ seen.add(token);
641
+ violations.push({
642
+ rule: "unknown-token",
643
+ severity: "error",
644
+ line: lineOf(css, m.index),
645
+ detail: `"${token}" is not in the cascivo token catalog (hallucinated token)`
646
+ });
647
+ }
648
+ return violations;
649
+ }
650
+ /** Run all structural invariants over a candidate component's source. */
651
+ function validateComponentSource(input, options = {}) {
652
+ const violations = [];
653
+ if (input.tsx) violations.push(...checkBannedHooks(input.tsx));
654
+ if (input.css) {
655
+ violations.push(...checkBreakpoints(input.css));
656
+ violations.push(...checkFallbacks(input.css));
657
+ if (options.tokenNames) violations.push(...checkTokens(input.css, options.tokenNames));
658
+ }
659
+ violations.sort((a, b) => a.line - b.line);
660
+ return {
661
+ valid: violations.every((v) => v.severity !== "error"),
662
+ ...input.name ? { name: input.name } : {},
663
+ violations
664
+ };
665
+ }
666
+ //#endregion
509
667
  //#region src/context.ts
510
668
  const HERE = dirname(fileURLToPath(import.meta.url));
511
669
  const CONTEXT_BASE_URL = "https://cascivo.com";
@@ -634,7 +792,8 @@ function createServer(options = {}) {
634
792
  "component",
635
793
  "layout",
636
794
  "block",
637
- "chart"
795
+ "chart",
796
+ "flow"
638
797
  ]).optional().describe("Filter by entry type")
639
798
  }
640
799
  }, ({ category, type }) => json(listComponents(registry, category, type)));
@@ -762,6 +921,60 @@ function createServer(options = {}) {
762
921
  grammar
763
922
  });
764
923
  });
924
+ server.registerTool("scaffold_flow", {
925
+ title: "Scaffold a flow diagram",
926
+ description: "Generate a starter declarative <Flow nodes edges /> (from @cascivo/flow) for a node/edge diagram — flowchart, DAG, or pipeline. Returns serializable nodes/edges plus ready-to-paste JSX. Edit the labels/edges to match the intent.",
927
+ inputSchema: {
928
+ description: z.string().describe("What the diagram represents"),
929
+ steps: z.array(z.string()).optional().describe("Ordered step labels; consecutive steps are connected. Omit for a 3-step starter."),
930
+ layout: z.enum([
931
+ "none",
932
+ "grid",
933
+ "layered"
934
+ ]).optional().describe("Optional auto-layout. Default \"layered\".")
935
+ }
936
+ }, ({ description, steps, layout }) => {
937
+ const labels = steps && steps.length > 0 ? steps : [
938
+ "Source",
939
+ "Process",
940
+ "Sink"
941
+ ];
942
+ const nodes = labels.map((label, i) => ({
943
+ id: `n${i + 1}`,
944
+ position: {
945
+ x: i * 240,
946
+ y: 0
947
+ },
948
+ data: { label }
949
+ }));
950
+ const edges = labels.slice(1).map((_, i) => ({
951
+ id: `e${i + 1}`,
952
+ source: `n${i + 1}`,
953
+ target: `n${i + 2}`,
954
+ animated: true
955
+ }));
956
+ const lay = layout ?? "layered";
957
+ return json({
958
+ nodes,
959
+ edges,
960
+ layout: lay,
961
+ code: `import { Flow } from '@cascivo/flow'
962
+ import '@cascivo/flow/styles.css'
963
+
964
+ // ${description}
965
+ export function Diagram() {
966
+ return (
967
+ <Flow
968
+ style={{ height: 360 }}
969
+ background
970
+ controls${lay === "none" ? "" : ` layout="${lay}"`}
971
+ nodes={${JSON.stringify(nodes)}}
972
+ edges={${JSON.stringify(edges)}}
973
+ />
974
+ )
975
+ }`
976
+ });
977
+ });
765
978
  server.registerTool("get_view_grammar", {
766
979
  title: "Get view grammar",
767
980
  description: "Get the bound-vocabulary grammar + system prompt for generating valid ViewConfig JSON, derived from the component manifests. Use this to constrain an LLM to cascivo's real components, props, and enum values (anti-hallucination). Optionally scope to a subset of components.",
@@ -798,6 +1011,57 @@ function createServer(options = {}) {
798
1011
  return error(`Token catalog unavailable: ${e instanceof Error ? e.message : String(e)}`);
799
1012
  }
800
1013
  });
1014
+ server.registerTool("get_variant_matrix", {
1015
+ title: "Get variant matrix",
1016
+ description: "Get the deterministic token variant matrix: a map from design intent (a colour role + state slot, e.g. accent + hover) to the exact token name, plus every semantic/component token resolved to a concrete value in each theme. Use this to answer \"what is the warm theme's accent-hover value?\" or \"which token is the active state of primary?\" without guessing how layered theme CSS resolves.",
1017
+ inputSchema: {
1018
+ role: z.string().optional().describe("Filter to one colour role, e.g. \"accent\", \"primary\", \"destructive\""),
1019
+ theme: z.string().optional().describe("Restrict the per-theme values to a single theme, e.g. \"warm\"")
1020
+ }
1021
+ }, async ({ role, theme }) => {
1022
+ try {
1023
+ const matrix = await loadVariantMatrix(fetchFn);
1024
+ if (theme && !matrix.themes.includes(theme)) return error(`Unknown theme "${theme}". Available: ${matrix.themes.join(", ")}`);
1025
+ const pickTheme = (byTheme) => theme ? { [theme]: byTheme[theme] ?? null } : byTheme;
1026
+ let tokens = matrix.tokens;
1027
+ let families = matrix.families;
1028
+ if (role) {
1029
+ tokens = tokens.filter((t) => t.role === role);
1030
+ families = matrix.families[role] ? { [role]: matrix.families[role] } : {};
1031
+ if (tokens.length === 0) return error(`No colour role "${role}". Available roles: ${Object.keys(matrix.families).join(", ")}`);
1032
+ }
1033
+ return json({
1034
+ themes: theme ? [theme] : matrix.themes,
1035
+ families,
1036
+ tokens: tokens.map((t) => ({
1037
+ ...t,
1038
+ byTheme: pickTheme(t.byTheme)
1039
+ }))
1040
+ });
1041
+ } catch (e) {
1042
+ return error(`Variant matrix unavailable: ${e instanceof Error ? e.message : String(e)}`);
1043
+ }
1044
+ });
1045
+ server.registerTool("validate_component", {
1046
+ title: "Validate component (shadow sandbox)",
1047
+ description: "Run cascivo's structural invariants over candidate component source (TSX and/or CSS) before writing it to disk. Catches banned React hooks, off-scale @media/@container breakpoints, missing static fallbacks for progressive CSS (@function/if()), and hallucinated --cascivo-* tokens. Use this to self-correct generated code in a loop until `valid` is true.",
1048
+ inputSchema: {
1049
+ tsx: z.string().optional().describe("Component TSX source"),
1050
+ css: z.string().optional().describe("Component CSS (module or plain) source"),
1051
+ name: z.string().optional().describe("Component name (for messages only)")
1052
+ }
1053
+ }, async ({ tsx, css, name }) => {
1054
+ let tokenNames;
1055
+ try {
1056
+ const catalog = await loadTokenCatalog(fetchFn);
1057
+ tokenNames = new Set(catalog.tokens.map((t) => t.name));
1058
+ } catch {}
1059
+ return json(validateComponentSource({
1060
+ ...tsx ? { tsx } : {},
1061
+ ...css ? { css } : {},
1062
+ ...name ? { name } : {}
1063
+ }, tokenNames ? { tokenNames } : {}));
1064
+ });
801
1065
  server.registerTool("get_context", {
802
1066
  title: "Get context",
803
1067
  description: "Get intent, whenToUse/whenNotToUse, and authoring guidance for one component by name.",
@@ -815,6 +1079,7 @@ function createServer(options = {}) {
815
1079
  description: component.description,
816
1080
  intent: component.intent,
817
1081
  contextUrl: component.contextUrl,
1082
+ ...component.aiPrompt ? { aiPrompt: component.aiPrompt } : {},
818
1083
  ...md ? { markdown: md } : {}
819
1084
  });
820
1085
  } catch (e) {