@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 +13 -11
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +272 -7
- package/dist/index.mjs.map +1 -1
- package/dist/registry.json +1836 -59
- package/package.json +1 -1
- package/readme.body.md +13 -11
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
|
|
39
|
-
|
|
|
40
|
-
| `list_components`
|
|
41
|
-
| `get_component`
|
|
42
|
-
| `search_components`
|
|
43
|
-
| `add_to_project`
|
|
44
|
-
| `create_theme`
|
|
45
|
-
| `scaffold_page`
|
|
46
|
-
| `scaffold_view`
|
|
47
|
-
| `validate_view`
|
|
48
|
-
| `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) |
|
|
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;
|
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
|
}
|
|
@@ -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";
|
|
@@ -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) {
|