diff --git a/packages/mcp/src/tools/inspect.ts b/packages/mcp/src/tools/inspect.ts new file mode 100644 index 0000000..44d24bb --- /dev/null +++ b/packages/mcp/src/tools/inspect.ts @@ -0,0 +1,41 @@ +import type { ComponentRegistry } from "../registry.js"; + +interface InspectInput { component: string; } +interface InspectResult { + name: string; description: string; exportPath: string; + parts: readonly string[]; requiredParts: readonly string[]; + props: Record; hasStyledVersion: boolean; example: string; +} + +function toKebab(str: string): string { + return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +} + +function generateExample(name: string, parts: readonly string[], requiredParts: readonly string[]): string { + const lines: string[] = []; + lines.push(`import { ${name} } from "pettyui/${toKebab(name)}";`); + lines.push(""); + if (requiredParts.includes("Root") || parts.includes("Root")) { + lines.push(`<${name}>`); + for (const part of requiredParts) { + if (part === "Root") continue; + lines.push(` <${name}.${part}>...`); + } + lines.push(``); + } else { + lines.push(`<${name} />`); + } + return lines.join("\n"); +} + +/** Returns full component schema, parts, and usage example. */ +export function handleInspect(registry: ComponentRegistry, input: InspectInput): InspectResult | null { + const comp = registry.getComponent(input.component); + if (!comp) return null; + return { + name: comp.meta.name, description: comp.meta.description, exportPath: comp.exportPath, + parts: comp.meta.parts, requiredParts: comp.meta.requiredParts, + props: comp.jsonSchemas, hasStyledVersion: comp.hasStyledVersion, + example: generateExample(comp.meta.name, comp.meta.parts, comp.meta.requiredParts), + }; +} diff --git a/packages/mcp/tests/tools/inspect.test.ts b/packages/mcp/tests/tools/inspect.test.ts new file mode 100644 index 0000000..41dc4cb --- /dev/null +++ b/packages/mcp/tests/tools/inspect.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { handleInspect } from "../../src/tools/inspect.js"; +import { ComponentRegistry } from "../../src/registry.js"; + +describe("pettyui.inspect", () => { + const registry = new ComponentRegistry(); + + it("returns full component info", () => { + const result = handleInspect(registry, { component: "Dialog" }); + expect(result).not.toBeNull(); + expect(result?.name).toBe("Dialog"); + expect(result?.parts).toContain("Root"); + expect(result?.parts).toContain("Content"); + expect(result?.exportPath).toBe("pettyui/dialog"); + expect(typeof result?.description).toBe("string"); + expect(result?.description.length).toBeGreaterThan(0); + }); + + it("includes JSON schema for props", () => { + const result = handleInspect(registry, { component: "Dialog" }); + expect(result).not.toBeNull(); + expect(result?.props).toBeDefined(); + const dialogRoot = result?.props["dialogRoot"] as { type?: string } | undefined; + expect(dialogRoot).toBeDefined(); + expect(dialogRoot?.type).toBe("object"); + }); + + it("includes minimal example", () => { + const result = handleInspect(registry, { component: "Dialog" }); + expect(result).not.toBeNull(); + expect(result?.example).toContain("Dialog"); + expect(result?.example).toContain('from "pettyui/dialog"'); + }); + + it("returns null for unknown component", () => { + const result = handleInspect(registry, { component: "FakeComponent" }); + expect(result).toBeNull(); + }); + + it("is case-insensitive", () => { + const result = handleInspect(registry, { component: "dialog" }); + expect(result).not.toBeNull(); + expect(result?.name).toBe("Dialog"); + }); +});