9 tasks: package scaffold, component registry, 5 MCP tools (discover, inspect, validate, add, compose), CLI wrapper, final verification.
1375 lines
41 KiB
Markdown
1375 lines
41 KiB
Markdown
# Phase 3: MCP Server + CLI — Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Build an MCP server that reads Zod schemas and Meta objects from core/ and exposes 5 tools for AI agents (discover, inspect, validate, add, compose), plus a CLI wrapper for humans.
|
|
|
|
**Architecture:** The MCP server imports component schemas/meta at startup, builds a component registry in memory, and exposes tools via `@modelcontextprotocol/sdk`. Each tool is a separate module. The CLI (`pettyui`) is a thin wrapper that calls the same registry logic without MCP transport.
|
|
|
|
**Tech Stack:** @modelcontextprotocol/sdk, Zod v4 (with `z.toJsonSchema()`), TypeScript, Vitest, Node.js
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-29-pettyui-ai-first-architecture.md`
|
|
|
|
**Parallelism:** Tasks 1-2 sequential (foundation). Tasks 3-7 parallel (each tool is independent). Task 8 (CLI) after tools. Task 9 last.
|
|
|
|
```
|
|
Wave 1 (sequential): Task 1 (package scaffold)
|
|
Task 2 (component registry)
|
|
|
|
Wave 2 (parallel): Task 3 (pettyui.discover)
|
|
Task 4 (pettyui.inspect)
|
|
Task 5 (pettyui.validate)
|
|
Task 6 (pettyui.add)
|
|
Task 7 (pettyui.compose)
|
|
|
|
Wave 3 (sequential): Task 8 (CLI wrapper)
|
|
Task 9 (build config + verification)
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1: MCP Package Scaffold
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/package.json`
|
|
- Create: `packages/mcp/tsconfig.json`
|
|
- Create: `packages/mcp/src/index.ts`
|
|
- Create: `packages/mcp/src/server.ts`
|
|
|
|
- [ ] **Step 1: Create package.json**
|
|
|
|
Create `packages/mcp/package.json`:
|
|
|
|
```json
|
|
{
|
|
"name": "pettyui-mcp",
|
|
"version": "0.1.0",
|
|
"description": "MCP server for PettyUI — AI's interface to the component library",
|
|
"type": "module",
|
|
"bin": {
|
|
"pettyui": "./dist/cli.js",
|
|
"pettyui-mcp": "./dist/index.js"
|
|
},
|
|
"exports": {
|
|
".": {
|
|
"import": "./dist/index.js"
|
|
},
|
|
"./registry": {
|
|
"import": "./dist/registry.js"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"dev": "tsc --watch",
|
|
"test": "vitest run",
|
|
"start": "node dist/index.js",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
"pettyui": "workspace:*",
|
|
"zod": "^4.3.6"
|
|
},
|
|
"devDependencies": {
|
|
"typescript": "^6.0.2",
|
|
"vitest": "^4.1.2"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create tsconfig.json**
|
|
|
|
Create `packages/mcp/tsconfig.json`:
|
|
|
|
```json
|
|
{
|
|
"extends": "../../tsconfig.base.json",
|
|
"compilerOptions": {
|
|
"outDir": "dist",
|
|
"rootDir": "src",
|
|
"module": "Node16",
|
|
"moduleResolution": "Node16",
|
|
"declaration": true,
|
|
"emitDeclarationMaps": true
|
|
},
|
|
"include": ["src"]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create server.ts — MCP server setup**
|
|
|
|
Create `packages/mcp/src/server.ts`:
|
|
|
|
```typescript
|
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
|
/** Creates and configures the PettyUI MCP server with all tools registered. */
|
|
export function createPettyUIServer(): McpServer {
|
|
const server = new McpServer({
|
|
name: "pettyui",
|
|
version: "0.1.0",
|
|
});
|
|
|
|
return server;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create index.ts — entry point with stdio transport**
|
|
|
|
Create `packages/mcp/src/index.ts`:
|
|
|
|
```typescript
|
|
#!/usr/bin/env node
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import { createPettyUIServer } from "./server.js";
|
|
|
|
const server = createPettyUIServer();
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
```
|
|
|
|
- [ ] **Step 5: Install dependencies**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm install
|
|
```
|
|
|
|
- [ ] **Step 6: Build and verify**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm build
|
|
```
|
|
|
|
Expected: Compiles without errors.
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add packages/mcp/
|
|
git commit -m "feat(mcp): scaffold MCP server package with stdio transport"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Component Registry
|
|
|
|
The registry is the brain — it loads all component schemas and meta objects into an in-memory index that the tools query.
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/registry.ts`
|
|
- Create: `packages/mcp/tests/registry.test.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/registry.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "vitest";
|
|
import { ComponentRegistry } from "../src/registry.js";
|
|
|
|
describe("ComponentRegistry", () => {
|
|
it("loads all components", () => {
|
|
const registry = new ComponentRegistry();
|
|
const components = registry.listComponents();
|
|
expect(components.length).toBeGreaterThan(30);
|
|
});
|
|
|
|
it("finds component by exact name", () => {
|
|
const registry = new ComponentRegistry();
|
|
const dialog = registry.getComponent("Dialog");
|
|
expect(dialog).toBeDefined();
|
|
expect(dialog!.meta.name).toBe("Dialog");
|
|
expect(dialog!.meta.parts).toContain("Content");
|
|
expect(dialog!.meta.requiredParts).toContain("Title");
|
|
});
|
|
|
|
it("finds component by case-insensitive name", () => {
|
|
const registry = new ComponentRegistry();
|
|
const dialog = registry.getComponent("dialog");
|
|
expect(dialog).toBeDefined();
|
|
expect(dialog!.meta.name).toBe("Dialog");
|
|
});
|
|
|
|
it("returns undefined for unknown component", () => {
|
|
const registry = new ComponentRegistry();
|
|
expect(registry.getComponent("NonExistent")).toBeUndefined();
|
|
});
|
|
|
|
it("searches by description text", () => {
|
|
const registry = new ComponentRegistry();
|
|
const results = registry.search("modal overlay");
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].meta.name).toBe("Dialog");
|
|
});
|
|
|
|
it("searches by intent matching", () => {
|
|
const registry = new ComponentRegistry();
|
|
const results = registry.search("searchable dropdown");
|
|
const names = results.map((r) => r.meta.name);
|
|
expect(names).toContain("Combobox");
|
|
});
|
|
|
|
it("gets JSON schema for a component", () => {
|
|
const registry = new ComponentRegistry();
|
|
const dialog = registry.getComponent("Dialog");
|
|
expect(dialog!.jsonSchemas).toBeDefined();
|
|
expect(dialog!.jsonSchemas.root).toBeDefined();
|
|
});
|
|
|
|
it("has styled registry info when available", () => {
|
|
const registry = new ComponentRegistry();
|
|
const dialog = registry.getComponent("Dialog");
|
|
expect(dialog!.hasStyledVersion).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/registry.test.ts
|
|
```
|
|
|
|
Expected: FAIL — module not found.
|
|
|
|
- [ ] **Step 3: Implement ComponentRegistry**
|
|
|
|
Create `packages/mcp/src/registry.ts`:
|
|
|
|
```typescript
|
|
import { z } from "zod/v4";
|
|
import type { ComponentMeta } from "pettyui/dialog";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
/** A registered component with schema, meta, and optional styled version info. */
|
|
export interface RegisteredComponent {
|
|
meta: ComponentMeta;
|
|
schemas: Record<string, z.ZodType>;
|
|
jsonSchemas: Record<string, unknown>;
|
|
hasStyledVersion: boolean;
|
|
exportPath: string;
|
|
}
|
|
|
|
/** Search result with relevance score. */
|
|
export interface SearchResult extends RegisteredComponent {
|
|
score: number;
|
|
}
|
|
|
|
/** Registry of all PettyUI components — loaded at startup, queried by tools. */
|
|
export class ComponentRegistry {
|
|
private components = new Map<string, RegisteredComponent>();
|
|
|
|
constructor() {
|
|
this.loadComponents();
|
|
}
|
|
|
|
private loadComponents(): void {
|
|
const componentImports = this.getComponentImports();
|
|
for (const entry of componentImports) {
|
|
this.registerComponent(entry);
|
|
}
|
|
}
|
|
|
|
private getComponentImports(): Array<{ name: string; mod: Record<string, unknown>; exportPath: string }> {
|
|
const entries: Array<{ name: string; mod: Record<string, unknown>; exportPath: string }> = [];
|
|
const componentsDir = this.resolveComponentsDir();
|
|
if (!componentsDir) return entries;
|
|
|
|
const dirs = fs.readdirSync(componentsDir, { withFileTypes: true })
|
|
.filter((d) => d.isDirectory())
|
|
.map((d) => d.name);
|
|
|
|
for (const dir of dirs) {
|
|
const propsFile = path.join(componentsDir, dir, `${dir}.props.ts`);
|
|
const propsFileAlt = path.join(componentsDir, dir, `${dir}.props.js`);
|
|
if (!fs.existsSync(propsFile) && !fs.existsSync(propsFileAlt)) continue;
|
|
|
|
try {
|
|
const mod = this.requireComponent(dir);
|
|
if (mod) {
|
|
const kebabName = dir;
|
|
entries.push({ name: kebabName, mod, exportPath: `pettyui/${kebabName}` });
|
|
}
|
|
} catch {
|
|
// Skip components that fail to load
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
private resolveComponentsDir(): string | null {
|
|
try {
|
|
const corePkgPath = require.resolve("pettyui/dialog");
|
|
const coreDir = path.dirname(path.dirname(corePkgPath));
|
|
const srcDir = path.join(coreDir, "src", "components");
|
|
if (fs.existsSync(srcDir)) return srcDir;
|
|
return null;
|
|
} catch {
|
|
const fallback = path.resolve(import.meta.dirname, "../../core/src/components");
|
|
if (fs.existsSync(fallback)) return fallback;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private requireComponent(name: string): Record<string, unknown> | null {
|
|
try {
|
|
const componentsDir = this.resolveComponentsDir();
|
|
if (!componentsDir) return null;
|
|
const propsPath = path.join(componentsDir, name, `${name}.props.ts`);
|
|
if (!fs.existsSync(propsPath)) return null;
|
|
// Dynamic import would be async — for now, we statically register known components
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private registerComponent(entry: { name: string; mod: Record<string, unknown>; exportPath: string }): void {
|
|
const meta = this.findMeta(entry.mod);
|
|
if (!meta) return;
|
|
|
|
const schemas = this.findSchemas(entry.mod);
|
|
const jsonSchemas: Record<string, unknown> = {};
|
|
for (const [key, schema] of Object.entries(schemas)) {
|
|
try {
|
|
jsonSchemas[key] = z.toJsonSchema(schema);
|
|
} catch {
|
|
jsonSchemas[key] = { type: "object", description: "Schema conversion failed" };
|
|
}
|
|
}
|
|
|
|
const hasStyledVersion = this.checkStyledVersion(entry.name);
|
|
|
|
this.components.set(meta.name.toLowerCase(), {
|
|
meta,
|
|
schemas,
|
|
jsonSchemas,
|
|
hasStyledVersion,
|
|
exportPath: entry.exportPath,
|
|
});
|
|
}
|
|
|
|
private findMeta(mod: Record<string, unknown>): ComponentMeta | undefined {
|
|
for (const value of Object.values(mod)) {
|
|
if (value && typeof value === "object" && "name" in value && "description" in value && "parts" in value && "requiredParts" in value) {
|
|
return value as ComponentMeta;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private findSchemas(mod: Record<string, unknown>): Record<string, z.ZodType> {
|
|
const schemas: Record<string, z.ZodType> = {};
|
|
for (const [key, value] of Object.entries(mod)) {
|
|
if (key.endsWith("Schema") && value instanceof z.ZodType) {
|
|
const shortKey = key.replace(/PropsSchema$/, "").replace(/Schema$/, "");
|
|
schemas[shortKey.charAt(0).toLowerCase() + shortKey.slice(1)] = value;
|
|
}
|
|
}
|
|
return schemas;
|
|
}
|
|
|
|
private checkStyledVersion(kebabName: string): boolean {
|
|
try {
|
|
const registryDir = path.resolve(import.meta.dirname, "../../registry/src/components");
|
|
return fs.existsSync(path.join(registryDir, `${kebabName}.tsx`));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Get all registered components. */
|
|
listComponents(): RegisteredComponent[] {
|
|
return Array.from(this.components.values());
|
|
}
|
|
|
|
/** Get a component by name (case-insensitive). */
|
|
getComponent(name: string): RegisteredComponent | undefined {
|
|
return this.components.get(name.toLowerCase());
|
|
}
|
|
|
|
/** Search components by intent/description text. Simple word-match scoring. */
|
|
search(query: string): SearchResult[] {
|
|
const terms = query.toLowerCase().split(/\s+/);
|
|
const results: SearchResult[] = [];
|
|
|
|
for (const component of this.components.values()) {
|
|
const searchText = `${component.meta.name} ${component.meta.description}`.toLowerCase();
|
|
let score = 0;
|
|
for (const term of terms) {
|
|
if (searchText.includes(term)) score += 1;
|
|
if (component.meta.name.toLowerCase() === term) score += 3;
|
|
if (component.meta.name.toLowerCase().includes(term)) score += 1;
|
|
}
|
|
if (score > 0) {
|
|
results.push({ ...component, score });
|
|
}
|
|
}
|
|
|
|
return results.sort((a, b) => b.score - a.score);
|
|
}
|
|
}
|
|
```
|
|
|
|
**NOTE TO IMPLEMENTER:** The dynamic import approach above won't work directly with TypeScript source files. The actual implementation should use a **static registration approach** — explicitly importing each component's props module at build time. Create a `packages/mcp/src/component-imports.ts` file that does:
|
|
|
|
```typescript
|
|
import { DialogRootPropsSchema, DialogContentPropsSchema, DialogMeta } from "pettyui/dialog";
|
|
import { ButtonPropsSchema, ButtonMeta } from "pettyui/button";
|
|
// ... all 39 components
|
|
|
|
export const COMPONENT_REGISTRY = [
|
|
{ exportPath: "pettyui/dialog", meta: DialogMeta, schemas: { dialogRoot: DialogRootPropsSchema, dialogContent: DialogContentPropsSchema } },
|
|
{ exportPath: "pettyui/button", meta: ButtonMeta, schemas: { button: ButtonPropsSchema } },
|
|
// ... all components
|
|
];
|
|
```
|
|
|
|
Then `ComponentRegistry` loads from this static list instead of filesystem scanning.
|
|
|
|
- [ ] **Step 4: Create component-imports.ts with static imports**
|
|
|
|
Create `packages/mcp/src/component-imports.ts` that statically imports all 39 component schemas and meta objects from `pettyui/*` exports. Use the pattern:
|
|
|
|
```typescript
|
|
import { DialogRootPropsSchema, DialogContentPropsSchema, DialogMeta } from "pettyui/dialog";
|
|
// ... repeat for all 39 components
|
|
|
|
export interface ComponentEntry {
|
|
exportPath: string;
|
|
meta: ComponentMeta;
|
|
schemas: Record<string, z.ZodType>;
|
|
}
|
|
|
|
export const COMPONENT_ENTRIES: ComponentEntry[] = [
|
|
{ exportPath: "pettyui/dialog", meta: DialogMeta, schemas: { dialogRoot: DialogRootPropsSchema, dialogContent: DialogContentPropsSchema } },
|
|
// ... all 39
|
|
];
|
|
```
|
|
|
|
Read `packages/core/package.json` exports to get the full list of component names.
|
|
|
|
- [ ] **Step 5: Update registry to use static imports**
|
|
|
|
Modify `packages/mcp/src/registry.ts` to load from `COMPONENT_ENTRIES` instead of filesystem scanning.
|
|
|
|
- [ ] **Step 6: Run tests**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/registry.test.ts
|
|
```
|
|
|
|
Expected: All 8 tests pass.
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add packages/mcp/src/registry.ts packages/mcp/src/component-imports.ts packages/mcp/tests/registry.test.ts
|
|
git commit -m "feat(mcp): add ComponentRegistry with static imports and search"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: pettyui.discover Tool
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/tools/discover.ts`
|
|
- Create: `packages/mcp/tests/tools/discover.test.ts`
|
|
- Modify: `packages/mcp/src/server.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/tools/discover.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "vitest";
|
|
import { handleDiscover } from "../../src/tools/discover.js";
|
|
import { ComponentRegistry } from "../../src/registry.js";
|
|
|
|
describe("pettyui.discover", () => {
|
|
const registry = new ComponentRegistry();
|
|
|
|
it("finds components matching intent", async () => {
|
|
const result = await handleDiscover(registry, { intent: "modal overlay" });
|
|
expect(result.length).toBeGreaterThan(0);
|
|
expect(result[0].name).toBe("Dialog");
|
|
});
|
|
|
|
it("returns multiple matches ranked by relevance", async () => {
|
|
const result = await handleDiscover(registry, { intent: "select option from list" });
|
|
expect(result.length).toBeGreaterThan(1);
|
|
const names = result.map((r) => r.name);
|
|
expect(names).toContain("Select");
|
|
});
|
|
|
|
it("returns empty array for no matches", async () => {
|
|
const result = await handleDiscover(registry, { intent: "xyznonexistent" });
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("limits results to maxResults", async () => {
|
|
const result = await handleDiscover(registry, { intent: "input", maxResults: 3 });
|
|
expect(result.length).toBeLessThanOrEqual(3);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Implement discover tool**
|
|
|
|
Create `packages/mcp/src/tools/discover.ts`:
|
|
|
|
```typescript
|
|
import type { ComponentRegistry } from "../registry.js";
|
|
|
|
interface DiscoverInput {
|
|
intent: string;
|
|
maxResults?: number;
|
|
}
|
|
|
|
interface DiscoverResult {
|
|
name: string;
|
|
description: string;
|
|
exportPath: string;
|
|
parts: readonly string[];
|
|
hasStyledVersion: boolean;
|
|
score: number;
|
|
}
|
|
|
|
/** Handles pettyui.discover — semantic search by intent. */
|
|
export async function handleDiscover(registry: ComponentRegistry, input: DiscoverInput): Promise<DiscoverResult[]> {
|
|
const maxResults = input.maxResults ?? 10;
|
|
const results = registry.search(input.intent);
|
|
|
|
return results.slice(0, maxResults).map((r) => ({
|
|
name: r.meta.name,
|
|
description: r.meta.description,
|
|
exportPath: r.exportPath,
|
|
parts: r.meta.parts,
|
|
hasStyledVersion: r.hasStyledVersion,
|
|
score: r.score,
|
|
}));
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Register tool in server.ts**
|
|
|
|
Update `packages/mcp/src/server.ts` to register the discover tool:
|
|
|
|
```typescript
|
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod/v4";
|
|
import { ComponentRegistry } from "./registry.js";
|
|
import { handleDiscover } from "./tools/discover.js";
|
|
|
|
export function createPettyUIServer(): McpServer {
|
|
const server = new McpServer({ name: "pettyui", version: "0.1.0" });
|
|
const registry = new ComponentRegistry();
|
|
|
|
server.tool(
|
|
"pettyui.discover",
|
|
"Search for components by describing what you need. Returns matching components ranked by relevance.",
|
|
{ intent: z.string().describe("Natural language description of what you need"), maxResults: z.number().optional().describe("Max results to return. Defaults to 10") },
|
|
async ({ intent, maxResults }) => {
|
|
const results = await handleDiscover(registry, { intent, maxResults });
|
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
return server;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests and commit**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/tools/discover.test.ts
|
|
git add packages/mcp/src/tools/discover.ts packages/mcp/tests/tools/discover.test.ts packages/mcp/src/server.ts
|
|
git commit -m "feat(mcp): add pettyui.discover tool for semantic component search"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: pettyui.inspect Tool
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/tools/inspect.ts`
|
|
- Create: `packages/mcp/tests/tools/inspect.test.ts`
|
|
- Modify: `packages/mcp/src/server.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/tools/inspect.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } 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", async () => {
|
|
const result = await handleInspect(registry, { component: "Dialog" });
|
|
expect(result).toBeDefined();
|
|
expect(result!.name).toBe("Dialog");
|
|
expect(result!.description).toBeTruthy();
|
|
expect(result!.parts.length).toBeGreaterThan(0);
|
|
expect(result!.requiredParts.length).toBeGreaterThan(0);
|
|
expect(result!.props).toBeDefined();
|
|
expect(result!.exportPath).toBe("pettyui/dialog");
|
|
});
|
|
|
|
it("includes JSON schema for props", async () => {
|
|
const result = await handleInspect(registry, { component: "Dialog" });
|
|
expect(result!.props.dialogRoot).toBeDefined();
|
|
expect(result!.props.dialogRoot.type).toBe("object");
|
|
});
|
|
|
|
it("includes minimal example", async () => {
|
|
const result = await handleInspect(registry, { component: "Dialog" });
|
|
expect(result!.example).toContain("Dialog");
|
|
});
|
|
|
|
it("returns null for unknown component", async () => {
|
|
const result = await handleInspect(registry, { component: "FakeComponent" });
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("is case-insensitive", async () => {
|
|
const result = await handleInspect(registry, { component: "dialog" });
|
|
expect(result).toBeDefined();
|
|
expect(result!.name).toBe("Dialog");
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Implement inspect tool**
|
|
|
|
Create `packages/mcp/src/tools/inspect.ts`:
|
|
|
|
```typescript
|
|
import type { ComponentRegistry } from "../registry.js";
|
|
|
|
interface InspectInput {
|
|
component: string;
|
|
includeSource?: boolean;
|
|
}
|
|
|
|
interface InspectResult {
|
|
name: string;
|
|
description: string;
|
|
exportPath: string;
|
|
parts: readonly string[];
|
|
requiredParts: readonly string[];
|
|
props: Record<string, unknown>;
|
|
hasStyledVersion: boolean;
|
|
example: string;
|
|
}
|
|
|
|
/** Generates a minimal valid usage example from component meta. */
|
|
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}>...</${name}.${part}>`);
|
|
}
|
|
lines.push(`</${name}>`);
|
|
} else {
|
|
lines.push(`<${name} />`);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/** Converts PascalCase to kebab-case. */
|
|
function toKebab(str: string): string {
|
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
}
|
|
|
|
/** Handles pettyui.inspect — returns full schema + parts + example for a component. */
|
|
export async function handleInspect(registry: ComponentRegistry, input: InspectInput): Promise<InspectResult | null> {
|
|
const component = registry.getComponent(input.component);
|
|
if (!component) return null;
|
|
|
|
return {
|
|
name: component.meta.name,
|
|
description: component.meta.description,
|
|
exportPath: component.exportPath,
|
|
parts: component.meta.parts,
|
|
requiredParts: component.meta.requiredParts,
|
|
props: component.jsonSchemas,
|
|
hasStyledVersion: component.hasStyledVersion,
|
|
example: generateExample(component.meta.name, component.meta.parts, component.meta.requiredParts),
|
|
};
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Register in server.ts and commit**
|
|
|
|
Add the inspect tool registration to `server.ts`, run tests, commit.
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/tools/inspect.test.ts
|
|
git add packages/mcp/src/tools/inspect.ts packages/mcp/tests/tools/inspect.test.ts packages/mcp/src/server.ts
|
|
git commit -m "feat(mcp): add pettyui.inspect tool for component schema inspection"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: pettyui.validate Tool
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/tools/validate.ts`
|
|
- Create: `packages/mcp/tests/tools/validate.test.ts`
|
|
- Modify: `packages/mcp/src/server.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/tools/validate.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "vitest";
|
|
import { handleValidate } from "../../src/tools/validate.js";
|
|
import { ComponentRegistry } from "../../src/registry.js";
|
|
|
|
describe("pettyui.validate", () => {
|
|
const registry = new ComponentRegistry();
|
|
|
|
it("validates correct props", async () => {
|
|
const result = await handleValidate(registry, {
|
|
component: "Dialog",
|
|
schema: "dialogRoot",
|
|
props: { open: true, modal: false },
|
|
});
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it("rejects invalid props", async () => {
|
|
const result = await handleValidate(registry, {
|
|
component: "Dialog",
|
|
schema: "dialogRoot",
|
|
props: { open: "yes" },
|
|
});
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("validates empty props for optional schemas", async () => {
|
|
const result = await handleValidate(registry, {
|
|
component: "Dialog",
|
|
schema: "dialogRoot",
|
|
props: {},
|
|
});
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it("checks required parts are present", async () => {
|
|
const result = await handleValidate(registry, {
|
|
component: "Dialog",
|
|
parts: ["Root", "Content"],
|
|
});
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain("Missing required part: Title");
|
|
});
|
|
|
|
it("passes when all required parts present", async () => {
|
|
const result = await handleValidate(registry, {
|
|
component: "Dialog",
|
|
parts: ["Root", "Content", "Title"],
|
|
});
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it("returns error for unknown component", async () => {
|
|
const result = await handleValidate(registry, {
|
|
component: "FakeComponent",
|
|
props: {},
|
|
});
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Component "FakeComponent" not found');
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Implement validate tool**
|
|
|
|
Create `packages/mcp/src/tools/validate.ts`:
|
|
|
|
```typescript
|
|
import type { ComponentRegistry } from "../registry.js";
|
|
|
|
interface ValidateInput {
|
|
component: string;
|
|
schema?: string;
|
|
props?: Record<string, unknown>;
|
|
parts?: string[];
|
|
}
|
|
|
|
interface ValidateResult {
|
|
valid: boolean;
|
|
errors: string[];
|
|
}
|
|
|
|
/** Handles pettyui.validate — validates props against schema and checks required parts. */
|
|
export async function handleValidate(registry: ComponentRegistry, input: ValidateInput): Promise<ValidateResult> {
|
|
const component = registry.getComponent(input.component);
|
|
if (!component) {
|
|
return { valid: false, errors: [`Component "${input.component}" not found`] };
|
|
}
|
|
|
|
const errors: string[] = [];
|
|
|
|
if (input.props !== undefined && input.schema) {
|
|
const schema = component.schemas[input.schema];
|
|
if (!schema) {
|
|
errors.push(`Schema "${input.schema}" not found on ${component.meta.name}`);
|
|
} else {
|
|
const result = schema.safeParse(input.props);
|
|
if (!result.success) {
|
|
for (const issue of result.error.issues) {
|
|
const path = issue.path.length > 0 ? ` at "${issue.path.join(".")}"` : "";
|
|
errors.push(`${issue.message}${path}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (input.parts !== undefined) {
|
|
for (const required of component.meta.requiredParts) {
|
|
if (!input.parts.includes(required)) {
|
|
errors.push(`Missing required part: ${required}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Register in server.ts and commit**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/tools/validate.test.ts
|
|
git add packages/mcp/src/tools/validate.ts packages/mcp/tests/tools/validate.test.ts packages/mcp/src/server.ts
|
|
git commit -m "feat(mcp): add pettyui.validate tool for props and parts validation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: pettyui.add Tool
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/tools/add.ts`
|
|
- Create: `packages/mcp/tests/tools/add.test.ts`
|
|
- Modify: `packages/mcp/src/server.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/tools/add.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "vitest";
|
|
import { handleAdd } from "../../src/tools/add.js";
|
|
import { ComponentRegistry } from "../../src/registry.js";
|
|
|
|
describe("pettyui.add", () => {
|
|
const registry = new ComponentRegistry();
|
|
|
|
it("returns styled component source for Dialog", async () => {
|
|
const result = await handleAdd(registry, { component: "Dialog" });
|
|
expect(result).toBeDefined();
|
|
expect(result!.files.length).toBeGreaterThan(0);
|
|
expect(result!.files[0].path).toContain("dialog");
|
|
expect(result!.files[0].content).toContain("Dialog");
|
|
expect(result!.dependencies).toContain("pettyui/dialog");
|
|
});
|
|
|
|
it("returns null for component without styled version", async () => {
|
|
const result = await handleAdd(registry, { component: "Checkbox" });
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns null for unknown component", async () => {
|
|
const result = await handleAdd(registry, { component: "FakeComponent" });
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("includes target path", async () => {
|
|
const result = await handleAdd(registry, { component: "Dialog", targetDir: "src/ui" });
|
|
expect(result!.files[0].path).toContain("src/ui");
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Implement add tool**
|
|
|
|
Create `packages/mcp/src/tools/add.ts`:
|
|
|
|
```typescript
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import type { ComponentRegistry } from "../registry.js";
|
|
|
|
interface AddInput {
|
|
component: string;
|
|
targetDir?: string;
|
|
}
|
|
|
|
interface AddResult {
|
|
files: Array<{ path: string; content: string }>;
|
|
dependencies: string[];
|
|
}
|
|
|
|
/** Resolves the registry components directory. */
|
|
function getRegistryDir(): string {
|
|
return path.resolve(import.meta.dirname, "../../registry/src/components");
|
|
}
|
|
|
|
/** Converts PascalCase to kebab-case. */
|
|
function toKebab(str: string): string {
|
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
}
|
|
|
|
/** Handles pettyui.add — copies styled component from registry. */
|
|
export async function handleAdd(registry: ComponentRegistry, input: AddInput): Promise<AddResult | null> {
|
|
const component = registry.getComponent(input.component);
|
|
if (!component || !component.hasStyledVersion) return null;
|
|
|
|
const kebabName = toKebab(component.meta.name);
|
|
const registryDir = getRegistryDir();
|
|
const sourcePath = path.join(registryDir, `${kebabName}.tsx`);
|
|
|
|
if (!fs.existsSync(sourcePath)) return null;
|
|
|
|
const content = fs.readFileSync(sourcePath, "utf-8");
|
|
const targetDir = input.targetDir ?? "src/components/ui";
|
|
const targetPath = path.join(targetDir, `${kebabName}.tsx`);
|
|
|
|
return {
|
|
files: [{ path: targetPath, content }],
|
|
dependencies: [component.exportPath],
|
|
};
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Register in server.ts and commit**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/tools/add.test.ts
|
|
git add packages/mcp/src/tools/add.ts packages/mcp/tests/tools/add.test.ts packages/mcp/src/server.ts
|
|
git commit -m "feat(mcp): add pettyui.add tool for copying styled components from registry"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: pettyui.compose Tool
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/tools/compose.ts`
|
|
- Create: `packages/mcp/tests/tools/compose.test.ts`
|
|
- Modify: `packages/mcp/src/server.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/tools/compose.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "vitest";
|
|
import { handleCompose } from "../../src/tools/compose.js";
|
|
import { ComponentRegistry } from "../../src/registry.js";
|
|
|
|
describe("pettyui.compose", () => {
|
|
const registry = new ComponentRegistry();
|
|
|
|
it("generates composed JSX for specified components", async () => {
|
|
const result = await handleCompose(registry, {
|
|
components: ["Dialog", "Button"],
|
|
});
|
|
expect(result.jsx).toContain("Dialog");
|
|
expect(result.jsx).toContain("Button");
|
|
expect(result.imports).toContain("pettyui/dialog");
|
|
expect(result.imports).toContain("pettyui/button");
|
|
});
|
|
|
|
it("generates imports for all components", async () => {
|
|
const result = await handleCompose(registry, {
|
|
components: ["Form", "TextField", "Button"],
|
|
});
|
|
expect(result.imports.length).toBe(3);
|
|
});
|
|
|
|
it("uses required parts in generated JSX", async () => {
|
|
const result = await handleCompose(registry, {
|
|
components: ["Dialog"],
|
|
});
|
|
expect(result.jsx).toContain("Dialog.Content");
|
|
expect(result.jsx).toContain("Dialog.Title");
|
|
});
|
|
|
|
it("skips unknown components with warning", async () => {
|
|
const result = await handleCompose(registry, {
|
|
components: ["Dialog", "FakeComponent"],
|
|
});
|
|
expect(result.jsx).toContain("Dialog");
|
|
expect(result.warnings).toContain('Component "FakeComponent" not found');
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Implement compose tool**
|
|
|
|
Create `packages/mcp/src/tools/compose.ts`:
|
|
|
|
```typescript
|
|
import type { ComponentRegistry } from "../registry.js";
|
|
|
|
interface ComposeInput {
|
|
components: string[];
|
|
intent?: string;
|
|
}
|
|
|
|
interface ComposeResult {
|
|
jsx: string;
|
|
imports: string[];
|
|
warnings: string[];
|
|
}
|
|
|
|
/** Converts PascalCase to kebab-case. */
|
|
function toKebab(str: string): string {
|
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
}
|
|
|
|
/** Generates JSX snippet for a single component using its required parts. */
|
|
function generateComponentJsx(name: string, parts: readonly string[], requiredParts: readonly string[], indent: number): string {
|
|
const pad = " ".repeat(indent);
|
|
const lines: string[] = [];
|
|
|
|
const hasRoot = parts.includes("Root");
|
|
if (hasRoot) {
|
|
lines.push(`${pad}<${name}>`);
|
|
for (const part of requiredParts) {
|
|
if (part === "Root") continue;
|
|
lines.push(`${pad} <${name}.${part}>...</${name}.${part}>`);
|
|
}
|
|
lines.push(`${pad}</${name}>`);
|
|
} else {
|
|
lines.push(`${pad}<${name} />`);
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/** Handles pettyui.compose — generates composed multi-component JSX. */
|
|
export async function handleCompose(registry: ComponentRegistry, input: ComposeInput): Promise<ComposeResult> {
|
|
const imports: string[] = [];
|
|
const jsxParts: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
for (const name of input.components) {
|
|
const component = registry.getComponent(name);
|
|
if (!component) {
|
|
warnings.push(`Component "${name}" not found`);
|
|
continue;
|
|
}
|
|
|
|
imports.push(component.exportPath);
|
|
jsxParts.push(generateComponentJsx(component.meta.name, component.meta.parts, component.meta.requiredParts, 1));
|
|
}
|
|
|
|
const importLines = imports.map((imp) => {
|
|
const name = imp.replace("pettyui/", "").split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
return `import { ${name} } from "${imp}";`;
|
|
});
|
|
|
|
const jsx = [
|
|
...importLines,
|
|
"",
|
|
"function Component() {",
|
|
" return (",
|
|
" <>",
|
|
jsxParts.join("\n"),
|
|
" </>",
|
|
" );",
|
|
"}",
|
|
].join("\n");
|
|
|
|
return { jsx, imports, warnings };
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Register in server.ts and commit**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/tools/compose.test.ts
|
|
git add packages/mcp/src/tools/compose.ts packages/mcp/tests/tools/compose.test.ts packages/mcp/src/server.ts
|
|
git commit -m "feat(mcp): add pettyui.compose tool for multi-component JSX generation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: CLI Wrapper
|
|
|
|
**Files:**
|
|
- Create: `packages/mcp/src/cli.ts`
|
|
- Create: `packages/mcp/tests/cli.test.ts`
|
|
|
|
- [ ] **Step 1: Write failing test**
|
|
|
|
Create `packages/mcp/tests/cli.test.ts`:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "vitest";
|
|
import { parseCommand } from "../src/cli.js";
|
|
|
|
describe("CLI parser", () => {
|
|
it("parses init command", () => {
|
|
const cmd = parseCommand(["init"]);
|
|
expect(cmd.command).toBe("init");
|
|
});
|
|
|
|
it("parses add command with component", () => {
|
|
const cmd = parseCommand(["add", "dialog"]);
|
|
expect(cmd.command).toBe("add");
|
|
expect(cmd.args.component).toBe("dialog");
|
|
});
|
|
|
|
it("parses add with target dir", () => {
|
|
const cmd = parseCommand(["add", "dialog", "--dir", "src/ui"]);
|
|
expect(cmd.command).toBe("add");
|
|
expect(cmd.args.component).toBe("dialog");
|
|
expect(cmd.args.dir).toBe("src/ui");
|
|
});
|
|
|
|
it("parses inspect command", () => {
|
|
const cmd = parseCommand(["inspect", "dialog"]);
|
|
expect(cmd.command).toBe("inspect");
|
|
expect(cmd.args.component).toBe("dialog");
|
|
});
|
|
|
|
it("parses list command", () => {
|
|
const cmd = parseCommand(["list"]);
|
|
expect(cmd.command).toBe("list");
|
|
});
|
|
|
|
it("returns help for unknown command", () => {
|
|
const cmd = parseCommand(["unknown"]);
|
|
expect(cmd.command).toBe("help");
|
|
});
|
|
|
|
it("returns help for empty args", () => {
|
|
const cmd = parseCommand([]);
|
|
expect(cmd.command).toBe("help");
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Implement CLI**
|
|
|
|
Create `packages/mcp/src/cli.ts`:
|
|
|
|
```typescript
|
|
#!/usr/bin/env node
|
|
import { ComponentRegistry } from "./registry.js";
|
|
import { handleDiscover } from "./tools/discover.js";
|
|
import { handleInspect } from "./tools/inspect.js";
|
|
import { handleAdd } from "./tools/add.js";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
interface ParsedCommand {
|
|
command: string;
|
|
args: Record<string, string>;
|
|
}
|
|
|
|
/** Parses CLI arguments into a command + args. */
|
|
export function parseCommand(argv: string[]): ParsedCommand {
|
|
if (argv.length === 0) return { command: "help", args: {} };
|
|
|
|
const command = argv[0];
|
|
const args: Record<string, string> = {};
|
|
|
|
if (["add", "inspect", "discover"].includes(command) && argv.length > 1) {
|
|
args.component = argv[1];
|
|
}
|
|
|
|
for (let i = 1; i < argv.length; i++) {
|
|
if (argv[i] === "--dir" && argv[i + 1]) {
|
|
args.dir = argv[i + 1];
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (!["init", "add", "inspect", "list", "discover", "help"].includes(command)) {
|
|
return { command: "help", args: {} };
|
|
}
|
|
|
|
return { command, args };
|
|
}
|
|
|
|
const HELP_TEXT = `
|
|
pettyui — AI-native component library CLI
|
|
|
|
Commands:
|
|
pettyui init Set up tokens, utils, tsconfig paths
|
|
pettyui add <component> Copy styled component to your project
|
|
pettyui inspect <component> Show component schema and usage
|
|
pettyui list List all available components
|
|
pettyui discover <intent> Search components by intent
|
|
|
|
Options:
|
|
--dir <path> Target directory for add (default: src/components/ui)
|
|
`;
|
|
|
|
/** Runs the CLI with given arguments. */
|
|
export async function runCli(argv: string[]): Promise<void> {
|
|
const { command, args } = parseCommand(argv);
|
|
const registry = new ComponentRegistry();
|
|
|
|
switch (command) {
|
|
case "help": {
|
|
console.log(HELP_TEXT);
|
|
break;
|
|
}
|
|
case "list": {
|
|
const components = registry.listComponents();
|
|
console.log(`\n${components.length} components available:\n`);
|
|
for (const c of components) {
|
|
const styled = c.hasStyledVersion ? " [styled]" : "";
|
|
console.log(` ${c.meta.name}${styled} — ${c.meta.description}`);
|
|
}
|
|
console.log("");
|
|
break;
|
|
}
|
|
case "inspect": {
|
|
if (!args.component) { console.error("Usage: pettyui inspect <component>"); break; }
|
|
const result = await handleInspect(registry, { component: args.component });
|
|
if (!result) { console.error(`Component "${args.component}" not found`); break; }
|
|
console.log(JSON.stringify(result, null, 2));
|
|
break;
|
|
}
|
|
case "discover": {
|
|
if (!args.component) { console.error("Usage: pettyui discover <intent>"); break; }
|
|
const results = await handleDiscover(registry, { intent: args.component });
|
|
if (results.length === 0) { console.log("No matching components found."); break; }
|
|
for (const r of results) {
|
|
console.log(` ${r.name} (${r.score}) — ${r.description}`);
|
|
}
|
|
break;
|
|
}
|
|
case "add": {
|
|
if (!args.component) { console.error("Usage: pettyui add <component>"); break; }
|
|
const result = await handleAdd(registry, { component: args.component, targetDir: args.dir });
|
|
if (!result) { console.error(`No styled version available for "${args.component}"`); break; }
|
|
for (const file of result.files) {
|
|
const dir = path.dirname(file.path);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(file.path, file.content, "utf-8");
|
|
console.log(` Created ${file.path}`);
|
|
}
|
|
console.log(`\n Dependencies: ${result.dependencies.join(", ")}`);
|
|
break;
|
|
}
|
|
case "init": {
|
|
console.log("pettyui init — setting up project...");
|
|
const tokensPath = "src/styles/tokens.css";
|
|
const utilsPath = "src/lib/utils.ts";
|
|
const registryDir = path.resolve(import.meta.dirname, "../../registry/src");
|
|
const tokensSource = path.join(registryDir, "tokens.css");
|
|
const utilsSource = path.join(registryDir, "utils.ts");
|
|
|
|
if (fs.existsSync(tokensSource)) {
|
|
fs.mkdirSync(path.dirname(tokensPath), { recursive: true });
|
|
fs.copyFileSync(tokensSource, tokensPath);
|
|
console.log(` Created ${tokensPath}`);
|
|
}
|
|
if (fs.existsSync(utilsSource)) {
|
|
fs.mkdirSync(path.dirname(utilsPath), { recursive: true });
|
|
fs.copyFileSync(utilsSource, utilsPath);
|
|
console.log(` Created ${utilsPath}`);
|
|
}
|
|
console.log("\n Done! Add pettyui as a dependency and import tokens.css in your app.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run if called directly
|
|
const args = process.argv.slice(2);
|
|
if (args.length > 0 || !process.stdin.isTTY) {
|
|
runCli(args);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Run tests and commit**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run tests/cli.test.ts
|
|
git add packages/mcp/src/cli.ts packages/mcp/tests/cli.test.ts
|
|
git commit -m "feat(mcp): add CLI wrapper with init, add, inspect, list, discover commands"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Final Server Assembly + Verification
|
|
|
|
**Files:**
|
|
- Modify: `packages/mcp/src/server.ts` — ensure all 5 tools registered
|
|
- Modify: `packages/mcp/package.json` — verify bin entries
|
|
|
|
- [ ] **Step 1: Verify server.ts has all 5 tools registered**
|
|
|
|
Read `packages/mcp/src/server.ts` and ensure it registers: `pettyui.discover`, `pettyui.inspect`, `pettyui.validate`, `pettyui.add`, `pettyui.compose`.
|
|
|
|
- [ ] **Step 2: Run full test suite**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm vitest run
|
|
```
|
|
|
|
Expected: All tests pass across registry, tools, and CLI.
|
|
|
|
- [ ] **Step 3: Build the MCP package**
|
|
|
|
```bash
|
|
cd packages/mcp && pnpm build
|
|
```
|
|
|
|
Expected: Clean build.
|
|
|
|
- [ ] **Step 4: Verify MCP server starts**
|
|
|
|
```bash
|
|
cd packages/mcp && echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' | node dist/index.js
|
|
```
|
|
|
|
Expected: Returns JSON-RPC response with server capabilities listing all 5 tools.
|
|
|
|
- [ ] **Step 5: Verify CLI works**
|
|
|
|
```bash
|
|
cd packages/mcp && node dist/cli.js list
|
|
```
|
|
|
|
Expected: Lists all 39 components with descriptions.
|
|
|
|
- [ ] **Step 6: Run full monorepo test suite**
|
|
|
|
```bash
|
|
cd /Users/matsbosson/Documents/StayThree/PettyUI && pnpm test
|
|
```
|
|
|
|
Expected: All packages pass (core: 445 tests, registry: 3 tests, mcp: all tests).
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add packages/mcp/
|
|
git commit -m "feat(mcp): complete MCP server with 5 tools + CLI, Phase 3 done"
|
|
```
|