@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 +29 -15
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +217 -7
- package/dist/index.mjs.map +1 -1
- package/dist/registry.json +360 -318
- package/package.json +4 -4
- package/readme.body.md +13 -11
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
|
-
|
|
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
|
-
|
|
8
|
+
[](https://www.npmjs.com/package/@cascivo/mcp)
|
|
9
|
+
[](https://www.npmjs.com/package/@cascivo/mcp)
|
|
10
|
+
[](https://github.com/cascivo/cascivo/blob/main/LICENSE)
|
|
11
|
+

|
|
6
12
|
|
|
7
|
-
[cascivo.com](https://cascivo.com) · [Docs](https://docs.cascivo.com) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/
|
|
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
|
|
29
|
-
|
|
|
30
|
-
| `list_components`
|
|
31
|
-
| `get_component`
|
|
32
|
-
| `search_components`
|
|
33
|
-
| `add_to_project`
|
|
34
|
-
| `create_theme`
|
|
35
|
-
| `scaffold_page`
|
|
36
|
-
| `scaffold_view`
|
|
37
|
-
| `validate_view`
|
|
38
|
-
| `get_view_grammar`
|
|
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/
|
|
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>
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;;
|
|
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$
|
|
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$
|
|
22
|
-
join(HERE$
|
|
23
|
-
join(HERE$
|
|
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/
|
|
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$
|
|
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$
|
|
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) {
|