@cascivo/mcp 0.1.1 → 0.1.4

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
@@ -1,10 +1,20 @@
1
1
  <!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
2
2
 
3
- # @cascivo/mcp
3
+ <div align="center">
4
+ <a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="72" height="72" alt="cascivo logo"></a>
5
+ <h1>@cascivo/mcp</h1>
6
+ <p><strong>MCP server exposing the cascivo component registry to AI agents</strong></p>
4
7
 
5
- > MCP server exposing the cascivo component registry to AI agents
8
+ [![npm](https://img.shields.io/npm/v/%40cascivo%2Fmcp?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/mcp)
9
+ [![downloads](https://img.shields.io/npm/dm/%40cascivo%2Fmcp?style=flat-square&color=0079bf)](https://www.npmjs.com/package/@cascivo/mcp)
10
+ [![license](https://img.shields.io/npm/l/%40cascivo%2Fmcp?style=flat-square&color=0079bf)](https://github.com/cascivo/cascivo/blob/main/LICENSE)
11
+ ![types](https://img.shields.io/badge/types-included-0079bf?style=flat-square&logo=typescript&logoColor=white)
6
12
 
7
- [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo)
13
+ [npm](https://www.npmjs.com/package/@cascivo/mcp) · [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo)
14
+
15
+ </div>
16
+
17
+ ---
8
18
 
9
19
  ## Usage
10
20
 
@@ -25,17 +35,19 @@ The server speaks the MCP stdio transport. By default it reads the `registry.jso
25
35
 
26
36
  ## Tools
27
37
 
28
- | Tool | Input | Returns |
29
- | ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
30
- | `list_components` | `{ category? }` | All component manifests, optionally filtered by category |
31
- | `get_component` | `{ name }` | The full manifest for one component |
32
- | `search_components` | `{ query }` | Components matching name, tags, or description |
33
- | `add_to_project` | `{ name, outputDir? }` | Runs `cascivo add <name>` as a child process |
34
- | `create_theme` | `{ primary, neutral, accent, name? }` | A custom theme as CSS (semantic token layer) |
35
- | `scaffold_page` | `{ description, components? }` | A JSX page layout string |
36
- | `scaffold_view` | `{ description, components? }` | A validated starter `ViewConfig` + the bound-vocabulary `grammar` for its components |
37
- | `validate_view` | `{ config }` | Validation errors (component, prop type/enum, refs) with exact paths |
38
- | `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) |
39
51
 
40
52
  `category` is one of `inputs`, `display`, `overlay`, `navigation`, `feedback`.
41
53
 
@@ -79,4 +91,6 @@ pnpm add @cascivo/mcp
79
91
 
80
92
  ---
81
93
 
82
- [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/urbanisierung/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/urbanisierung/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/urbanisierung/cascivo/blob/main/registry.json) · MIT
94
+ [cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo) · AI agents: use [`@cascivo/mcp`](https://github.com/cascivo/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/cascivo/cascivo/blob/main/registry.json) · MIT
95
+
96
+ <div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="28" height="28" alt="cascivo"></a></div>
@@ -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
  }
@@ -436,7 +436,7 @@ function scaffoldView(input, registry) {
436
436
  const layout = pickLayout(input.description);
437
437
  const componentNames = pickComponents(input.description, registry, input.components);
438
438
  const config = {
439
- $schema: "https://raw.githubusercontent.com/urbanisierung/cascivo/main/packages/render/schema/view.v1.json",
439
+ $schema: "https://raw.githubusercontent.com/cascivo/cascivo/main/packages/render/schema/view.v1.json",
440
440
  version: 1,
441
441
  view: {
442
442
  ...layout !== "none" ? { layout } : {},
@@ -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";
@@ -798,6 +956,57 @@ function createServer(options = {}) {
798
956
  return error(`Token catalog unavailable: ${e instanceof Error ? e.message : String(e)}`);
799
957
  }
800
958
  });
959
+ server.registerTool("get_variant_matrix", {
960
+ title: "Get variant matrix",
961
+ 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.",
962
+ inputSchema: {
963
+ role: z.string().optional().describe("Filter to one colour role, e.g. \"accent\", \"primary\", \"destructive\""),
964
+ theme: z.string().optional().describe("Restrict the per-theme values to a single theme, e.g. \"warm\"")
965
+ }
966
+ }, async ({ role, theme }) => {
967
+ try {
968
+ const matrix = await loadVariantMatrix(fetchFn);
969
+ if (theme && !matrix.themes.includes(theme)) return error(`Unknown theme "${theme}". Available: ${matrix.themes.join(", ")}`);
970
+ const pickTheme = (byTheme) => theme ? { [theme]: byTheme[theme] ?? null } : byTheme;
971
+ let tokens = matrix.tokens;
972
+ let families = matrix.families;
973
+ if (role) {
974
+ tokens = tokens.filter((t) => t.role === role);
975
+ families = matrix.families[role] ? { [role]: matrix.families[role] } : {};
976
+ if (tokens.length === 0) return error(`No colour role "${role}". Available roles: ${Object.keys(matrix.families).join(", ")}`);
977
+ }
978
+ return json({
979
+ themes: theme ? [theme] : matrix.themes,
980
+ families,
981
+ tokens: tokens.map((t) => ({
982
+ ...t,
983
+ byTheme: pickTheme(t.byTheme)
984
+ }))
985
+ });
986
+ } catch (e) {
987
+ return error(`Variant matrix unavailable: ${e instanceof Error ? e.message : String(e)}`);
988
+ }
989
+ });
990
+ server.registerTool("validate_component", {
991
+ title: "Validate component (shadow sandbox)",
992
+ 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.",
993
+ inputSchema: {
994
+ tsx: z.string().optional().describe("Component TSX source"),
995
+ css: z.string().optional().describe("Component CSS (module or plain) source"),
996
+ name: z.string().optional().describe("Component name (for messages only)")
997
+ }
998
+ }, async ({ tsx, css, name }) => {
999
+ let tokenNames;
1000
+ try {
1001
+ const catalog = await loadTokenCatalog(fetchFn);
1002
+ tokenNames = new Set(catalog.tokens.map((t) => t.name));
1003
+ } catch {}
1004
+ return json(validateComponentSource({
1005
+ ...tsx ? { tsx } : {},
1006
+ ...css ? { css } : {},
1007
+ ...name ? { name } : {}
1008
+ }, tokenNames ? { tokenNames } : {}));
1009
+ });
801
1010
  server.registerTool("get_context", {
802
1011
  title: "Get context",
803
1012
  description: "Get intent, whenToUse/whenNotToUse, and authoring guidance for one component by name.",
@@ -815,6 +1024,7 @@ function createServer(options = {}) {
815
1024
  description: component.description,
816
1025
  intent: component.intent,
817
1026
  contextUrl: component.contextUrl,
1027
+ ...component.aiPrompt ? { aiPrompt: component.aiPrompt } : {},
818
1028
  ...md ? { markdown: md } : {}
819
1029
  });
820
1030
  } catch (e) {