From 6ea0c597e3ae42fc75dbfd5f2edfa2d0b17e1563 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 22 Apr 2026 02:21:23 +0530 Subject: [PATCH 1/4] feat: add corpus-backed shadcn component loader + migrate Button Adds corpus support to `src/plugins/shadcn/tools/get-component.ts`. The tool now reads per-component slices from `corpus/frontend/shadcn/.yaml`, validates required fields (category, basePrimitive, dataSlots, usageSnippet, requiresUseClient, pairsWith), and falls back to the in-file `SHADCN_COMPONENTS` registry when the corpus entry is missing or invalid. Creates the shadcn corpus namespace: - `corpus/frontend/shadcn/index.yaml` registers the `frontend.shadcn` namespace and component files - `corpus/frontend/shadcn/button.yaml` captures the Button contract (category, variants, sizes, usage snippet, pairs) - `corpus/index.yaml` registers the new namespace New `tests/shadcn-component-corpus-backed-tools-behaviour.test.ts` asserts corpus source for Button and fallback for Dialog. Co-Authored-By: Claude Sonnet 4.6 --- corpus/frontend/shadcn/button.yaml | 35 ++++++ corpus/frontend/shadcn/index.yaml | 4 + corpus/index.yaml | 2 + src/plugins/shadcn/tools/get-component.ts | 114 +++++++++++++++++- ...nent-corpus-backed-tools-behaviour.test.ts | 22 ++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 corpus/frontend/shadcn/button.yaml create mode 100644 corpus/frontend/shadcn/index.yaml create mode 100644 tests/shadcn-component-corpus-backed-tools-behaviour.test.ts diff --git a/corpus/frontend/shadcn/button.yaml b/corpus/frontend/shadcn/button.yaml new file mode 100644 index 0000000..ef71039 --- /dev/null +++ b/corpus/frontend/shadcn/button.yaml @@ -0,0 +1,35 @@ +name: Button +category: button +description: Interactive button with variant + size support. Uses CVA for variant composition. +basePrimitive: native + + // With variants and sizes + + + + // Icon usage (data-slot automatic in component) + +pairsWith: + - Dialog + - Form + - DropdownMenu diff --git a/corpus/frontend/shadcn/index.yaml b/corpus/frontend/shadcn/index.yaml new file mode 100644 index 0000000..9726209 --- /dev/null +++ b/corpus/frontend/shadcn/index.yaml @@ -0,0 +1,4 @@ +namespace: frontend.shadcn +components: + button: + file: button.yaml diff --git a/corpus/index.yaml b/corpus/index.yaml index 888611c..2f1978e 100644 --- a/corpus/index.yaml +++ b/corpus/index.yaml @@ -9,3 +9,5 @@ namespaces: index: frontend/designer/index.yaml frontend.design-tokens: index: frontend/design-tokens/index.yaml + frontend.shadcn: + index: frontend/shadcn/index.yaml diff --git a/src/plugins/shadcn/tools/get-component.ts b/src/plugins/shadcn/tools/get-component.ts index c68ce8a..e21761f 100644 --- a/src/plugins/shadcn/tools/get-component.ts +++ b/src/plugins/shadcn/tools/get-component.ts @@ -1,6 +1,111 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import YAML from "yaml"; import { z } from "zod"; -import { getComponentByName, SHADCN_COMPONENTS } from "../data.js"; +import { getComponentByName, SHADCN_COMPONENTS, type ShadcnComponent } from "../data.js"; + +type CorpusIndex = { + namespaces?: { + "frontend.shadcn"?: { + index?: string; + }; + }; +}; + +type CorpusNamespaceIndex = { + namespace?: string; + components?: Record; +}; + +type CorpusComponentEntry = { + name?: string; + category?: string; + description?: string; + basePrimitive?: string; + dataSlots?: string[]; + variants?: string[]; + sizes?: string[]; + requiresUseClient?: boolean; + usageSnippet?: string; + pairsWith?: string[]; +}; + +type LoadedCorpusComponent = { component: ShadcnComponent; source: string }; + +const moduleDir = dirname(fileURLToPath(import.meta.url)); +const corpusRoot = join(moduleDir, "../../../../corpus"); +const corpusNamespace = "frontend.shadcn"; + +const cachedCorpusComponents = new Map(); + +function normalize(name: string): string { + return name.toLowerCase().trim(); +} + +function loadCorpusComponent(name: string): LoadedCorpusComponent | null { + const key = normalize(name); + if (cachedCorpusComponents.has(key)) { + return cachedCorpusComponents.get(key) ?? null; + } + + try { + const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8"); + const index = YAML.parse(indexRaw) as CorpusIndex | null; + const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index; + if (!namespaceIndexPath) { + cachedCorpusComponents.set(key, null); + return null; + } + + const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8"); + const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null; + const componentPath = namespaceIndex?.components?.[key]?.file; + if (!componentPath) { + cachedCorpusComponents.set(key, null); + return null; + } + + const raw = readFileSync(join(corpusRoot, "frontend/shadcn", componentPath), "utf8"); + const entry = YAML.parse(raw) as CorpusComponentEntry | null; + if ( + !entry || + normalize(entry.name ?? "") !== key || + !entry.category || + !entry.description || + !entry.basePrimitive || + !Array.isArray(entry.dataSlots) || + !entry.usageSnippet || + typeof entry.requiresUseClient !== "boolean" || + !Array.isArray(entry.pairsWith) + ) { + cachedCorpusComponents.set(key, null); + return null; + } + + const loaded: LoadedCorpusComponent = { + component: { + name: entry.name ?? name, + category: entry.category as ShadcnComponent["category"], + description: entry.description, + basePrimitive: entry.basePrimitive, + dataSlots: entry.dataSlots, + variants: entry.variants, + sizes: entry.sizes, + requiresUseClient: entry.requiresUseClient, + usageSnippet: entry.usageSnippet, + pairsWith: entry.pairsWith, + }, + source: corpusNamespace, + }; + cachedCorpusComponents.set(key, loaded); + return loaded; + } catch { + cachedCorpusComponents.set(key, null); + return null; + } +} export function register(server: McpServer): void { server.tool( @@ -10,7 +115,8 @@ export function register(server: McpServer): void { name: z.string().describe("Component name (e.g., 'Button', 'Dialog', 'Field', 'Select'). Case-insensitive."), }, async ({ name }) => { - const component = getComponentByName(name); + const corpusEntry = loadCorpusComponent(name); + const component = corpusEntry?.component ?? getComponentByName(name); if (!component) { const available = SHADCN_COMPONENTS.map((c) => c.name).join(", "); return { @@ -58,6 +164,10 @@ export function register(server: McpServer): void { text += `- [ ] Props spread (...props) to underlying primitive\n`; text += `- [ ] OKLCH color tokens from design system (not hardcoded hex)\n`; + if (corpusEntry) { + text += `\n**Corpus Source:** ${corpusEntry.source}`; + } + return { content: [{ type: "text" as const, text }] }; } ); diff --git a/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts b/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts new file mode 100644 index 0000000..de38e19 --- /dev/null +++ b/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from "bun:test"; +import { captureTool, extractTextContent } from "./helpers"; +import { register as registerShadcnGetComponent } from "../src/plugins/shadcn/tools/get-component.ts"; + +const shadcnGetComponent = captureTool(registerShadcnGetComponent); + +test("shadcn_get_component prefers corpus metadata for Button", async () => { + const result = await shadcnGetComponent.invoke({ name: "Button" }); + const text = extractTextContent(result); + + expect(text).toContain("# Button"); + expect(text).toContain("**Corpus Source:** frontend.shadcn"); + expect(text).toContain(' + + + +pairsWith: + - Button + - Form + - Select diff --git a/corpus/frontend/shadcn/index.yaml b/corpus/frontend/shadcn/index.yaml index 9726209..a3359ba 100644 --- a/corpus/frontend/shadcn/index.yaml +++ b/corpus/frontend/shadcn/index.yaml @@ -2,3 +2,5 @@ namespace: frontend.shadcn components: button: file: button.yaml + dialog: + file: dialog.yaml diff --git a/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts b/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts index de38e19..dfa509d 100644 --- a/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts +++ b/tests/shadcn-component-corpus-backed-tools-behaviour.test.ts @@ -13,10 +13,19 @@ test("shadcn_get_component prefers corpus metadata for Button", async () => { expect(text).toContain('