capy-mcp 1.0.1 → 1.0.2

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 CHANGED
@@ -6,9 +6,10 @@ It does not generate the preview page itself. It gives an AI coding agent the re
6
6
 
7
7
  ## What it exposes
8
8
 
9
- The server currently registers one MCP tool:
9
+ The server currently registers two MCP tools:
10
10
 
11
11
  - `get_preview_brief`
12
+ - `get_design_system`
12
13
 
13
14
  The tool accepts:
14
15
 
@@ -26,6 +27,8 @@ The tool returns structured content with:
26
27
  - `warnings`
27
28
  - `instructions`
28
29
 
30
+ `get_design_system` writes a stable JSON artifact, by default at `.capy/design-system.json`, and returns a summary plus the artifact contents. This file is meant to be the machine-readable source of truth that later coding sessions can load before making UI changes.
31
+
29
32
  ## Install
30
33
 
31
34
  ```bash
@@ -56,12 +59,19 @@ Example stdio MCP config:
56
59
  ## Library usage
57
60
 
58
61
  ```ts
59
- import { buildPreviewBrief, detectFramework } from "capy-mcp";
62
+ import {
63
+ buildDesignSystemArtifact,
64
+ buildPreviewBrief,
65
+ detectFramework,
66
+ writeDesignSystemArtifact,
67
+ } from "capy-mcp";
60
68
 
61
69
  const framework = await detectFramework(process.cwd());
62
70
  const brief = await buildPreviewBrief(process.cwd(), {
63
71
  task: "build_preview",
64
72
  });
73
+ const designSystem = await buildDesignSystemArtifact(process.cwd());
74
+ await writeDesignSystemArtifact(process.cwd());
65
75
  ```
66
76
 
67
77
  ## Local development
package/dist/brief.js CHANGED
@@ -1,7 +1,5 @@
1
- import { glob } from "glob";
2
- import { join } from "path";
3
1
  import { detectFramework } from "./framework.js";
4
- import { fileExists, toPosixPath } from "./files.js";
2
+ import { buildProjectFacts } from "./project.js";
5
3
  const SECTION_ORDER = [
6
4
  "Foundations",
7
5
  "Colors",
@@ -39,37 +37,6 @@ export async function buildPreviewBrief(projectRoot, input) {
39
37
  instructions: buildInstructions(projectFacts, input),
40
38
  };
41
39
  }
42
- async function buildProjectFacts(projectRoot, framework) {
43
- const componentDirs = await findExistingDirectories(projectRoot, [
44
- "src/components",
45
- "src/ui",
46
- "src/features",
47
- "components",
48
- "ui",
49
- "features",
50
- ]);
51
- const pageDirs = await findExistingDirectories(projectRoot, [
52
- "src/app",
53
- "app",
54
- "src/pages",
55
- "pages",
56
- "src/routes",
57
- "routes",
58
- ]);
59
- const styleFiles = await findStyleFiles(projectRoot);
60
- const likelyUiDirs = Array.from(new Set([...componentDirs, ...pageDirs.filter((dir) => !dir.endsWith("/preview"))]));
61
- return {
62
- framework: framework.kind,
63
- routingStyle: framework.routingStyle,
64
- previewRoute: framework.previewRoute,
65
- previewEntryFile: framework.previewEntryFile,
66
- packageManager: framework.packageManager,
67
- likelyComponentDirs: componentDirs,
68
- likelyStyleFiles: styleFiles,
69
- likelyPageDirs: pageDirs,
70
- likelyUiDirs,
71
- };
72
- }
73
40
  function buildInspectionPlan(projectFacts) {
74
41
  const appShellTargets = uniqueCompact([
75
42
  firstMatching(projectFacts.likelyPageDirs, "src/app")
@@ -154,33 +121,6 @@ function buildInstructions(projectFacts, input) {
154
121
  const userGoal = input.userGoal ? ` User goal: ${input.userGoal}.` : "";
155
122
  return `${lead}${userGoal} Read the app shell first, then global styles, then component directories. After that, implement ${projectFacts.previewEntryFile} as a vertically scrollable preview surface using the repo's real design language and existing components.`;
156
123
  }
157
- async function findExistingDirectories(projectRoot, candidates) {
158
- const results = [];
159
- for (const candidate of candidates) {
160
- if (await fileExists(join(projectRoot, candidate))) {
161
- results.push(candidate);
162
- }
163
- }
164
- return results;
165
- }
166
- async function findStyleFiles(projectRoot) {
167
- const files = await glob(["**/*.{css,scss,sass,less}"], {
168
- cwd: projectRoot,
169
- nodir: true,
170
- ignore: [
171
- "**/node_modules/**",
172
- "**/.next/**",
173
- "**/dist/**",
174
- "**/build/**",
175
- "**/.capy/**",
176
- "**/coverage/**",
177
- ],
178
- });
179
- return files
180
- .map((file) => toPosixPath(file))
181
- .filter((file) => !file.includes("/preview/"))
182
- .sort();
183
- }
184
124
  function uniqueCompact(values) {
185
125
  return Array.from(new Set(values.filter(Boolean)));
186
126
  }
@@ -0,0 +1,3 @@
1
+ import type { DesignSystemArtifact, DesignSystemBuildInput } from "./types.js";
2
+ export declare function buildDesignSystemArtifact(projectRoot: string, input?: DesignSystemBuildInput): Promise<DesignSystemArtifact>;
3
+ export declare function writeDesignSystemArtifact(projectRoot: string, input?: DesignSystemBuildInput): Promise<DesignSystemArtifact>;
@@ -0,0 +1,176 @@
1
+ import { glob } from "glob";
2
+ import { basename, join } from "path";
3
+ import { buildPreviewBrief } from "./brief.js";
4
+ import { readText, toPosixPath, writeText } from "./files.js";
5
+ import { detectFramework } from "./framework.js";
6
+ import { buildProjectFacts } from "./project.js";
7
+ const DEFAULT_ARTIFACT_PATH = ".capy/design-system.json";
8
+ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
9
+ const framework = await detectFramework(projectRoot);
10
+ const projectFacts = await buildProjectFacts(projectRoot, framework);
11
+ const previewBrief = await buildPreviewBrief(projectRoot, {
12
+ task: input.mode === "update" ? "update_preview" : "build_preview",
13
+ changedFiles: input.changedFiles,
14
+ userGoal: input.userGoal,
15
+ });
16
+ const cssVariables = await collectCssVariables(projectRoot, projectFacts.likelyStyleFiles);
17
+ const components = await collectComponents(projectRoot, projectFacts);
18
+ const artifactPath = input.artifactPath ?? DEFAULT_ARTIFACT_PATH;
19
+ const sourceFiles = Array.from(new Set([
20
+ ...projectFacts.likelyStyleFiles,
21
+ ...components.map((component) => component.path),
22
+ ])).sort();
23
+ return {
24
+ meta: {
25
+ generatedAt: new Date().toISOString(),
26
+ mode: input.mode ?? "build",
27
+ artifactPath,
28
+ framework: framework.kind,
29
+ routingStyle: framework.routingStyle,
30
+ previewRoute: framework.previewRoute,
31
+ previewEntryFile: framework.previewEntryFile,
32
+ packageManager: framework.packageManager,
33
+ changedFiles: input.changedFiles ?? [],
34
+ sourceFiles,
35
+ },
36
+ directories: {
37
+ componentDirs: projectFacts.likelyComponentDirs,
38
+ pageDirs: projectFacts.likelyPageDirs,
39
+ styleFiles: projectFacts.likelyStyleFiles,
40
+ uiDirs: projectFacts.likelyUiDirs,
41
+ },
42
+ tokens: {
43
+ cssVariables,
44
+ styleFiles: projectFacts.likelyStyleFiles,
45
+ },
46
+ components,
47
+ preview: {
48
+ route: projectFacts.previewRoute,
49
+ entryFile: projectFacts.previewEntryFile,
50
+ sections: previewBrief.deliverableSpec.sections,
51
+ updateStrategy: previewBrief.updateStrategy,
52
+ },
53
+ guidance: {
54
+ summary: buildSummary(projectFacts, framework, components, cssVariables),
55
+ warnings: previewBrief.warnings,
56
+ instructions: [
57
+ "Read this file before making UI changes or updating /preview.",
58
+ "Prefer the listed components, tokens, and preview sections over inventing new UI structure.",
59
+ input.mode === "update"
60
+ ? "When updating, inspect changedFiles first and only refresh affected sections unless foundations changed."
61
+ : "If this artifact looks incomplete, inspect the listed sourceFiles before expanding it.",
62
+ ],
63
+ },
64
+ };
65
+ }
66
+ export async function writeDesignSystemArtifact(projectRoot, input = {}) {
67
+ const artifact = await buildDesignSystemArtifact(projectRoot, input);
68
+ const artifactFile = join(projectRoot, artifact.meta.artifactPath);
69
+ await writeText(artifactFile, `${JSON.stringify(artifact, null, 2)}\n`);
70
+ return artifact;
71
+ }
72
+ async function collectCssVariables(projectRoot, styleFiles) {
73
+ const variables = [];
74
+ for (const relativePath of styleFiles) {
75
+ const fileContents = await readText(join(projectRoot, relativePath));
76
+ if (!fileContents)
77
+ continue;
78
+ const regex = /--([A-Za-z0-9-_]+)\s*:\s*([^;}{]+);/g;
79
+ let match;
80
+ while ((match = regex.exec(fileContents)) !== null) {
81
+ variables.push({
82
+ name: `--${match[1]}`,
83
+ value: match[2].trim(),
84
+ category: classifyCssVariable(match[1]),
85
+ file: relativePath,
86
+ line: lineNumberAt(fileContents, match.index),
87
+ });
88
+ }
89
+ }
90
+ return variables;
91
+ }
92
+ async function collectComponents(projectRoot, projectFacts) {
93
+ if (projectFacts.likelyComponentDirs.length === 0) {
94
+ return [];
95
+ }
96
+ const patterns = projectFacts.likelyComponentDirs.map((dir) => `${dir}/**/*.{tsx,jsx,ts,js}`);
97
+ const files = await glob(patterns, {
98
+ cwd: projectRoot,
99
+ nodir: true,
100
+ ignore: [
101
+ "**/*.test.*",
102
+ "**/*.spec.*",
103
+ "**/*.stories.*",
104
+ "**/*.story.*",
105
+ "**/node_modules/**",
106
+ "**/.next/**",
107
+ "**/dist/**",
108
+ "**/build/**",
109
+ "**/coverage/**",
110
+ "**/preview/**",
111
+ ],
112
+ });
113
+ const componentRecords = await Promise.all(files
114
+ .map((file) => toPosixPath(file))
115
+ .sort()
116
+ .map(async (relativePath) => {
117
+ const contents = await readText(join(projectRoot, relativePath));
118
+ const exports = contents ? extractExports(contents) : [];
119
+ const fallbackName = basename(relativePath).replace(/\.[^.]+$/, "");
120
+ return {
121
+ name: exports[0] ?? fallbackName,
122
+ path: relativePath,
123
+ exports,
124
+ kind: classifyComponentKind(relativePath),
125
+ };
126
+ }));
127
+ return componentRecords;
128
+ }
129
+ function buildSummary(projectFacts, framework, components, cssVariables) {
130
+ return [
131
+ `Framework: ${framework.label}.`,
132
+ `Preview target: ${projectFacts.previewEntryFile}.`,
133
+ `Found ${components.length} component candidates and ${cssVariables.length} CSS variables.`,
134
+ "Use this artifact as the canonical machine-readable UI map before editing the app or /preview.",
135
+ ].join(" ");
136
+ }
137
+ function extractExports(contents) {
138
+ const exports = new Set();
139
+ const patterns = [
140
+ /export\s+function\s+([A-Z][A-Za-z0-9_]*)/g,
141
+ /export\s+const\s+([A-Z][A-Za-z0-9_]*)/g,
142
+ /export\s+class\s+([A-Z][A-Za-z0-9_]*)/g,
143
+ /export\s+default\s+function\s+([A-Z][A-Za-z0-9_]*)/g,
144
+ ];
145
+ for (const pattern of patterns) {
146
+ let match;
147
+ while ((match = pattern.exec(contents)) !== null) {
148
+ exports.add(match[1]);
149
+ }
150
+ }
151
+ return Array.from(exports);
152
+ }
153
+ function classifyComponentKind(relativePath) {
154
+ if (relativePath.includes("/features/"))
155
+ return "feature";
156
+ if (relativePath.includes("/ui/"))
157
+ return "primitive";
158
+ if (relativePath.includes("/components/"))
159
+ return "component";
160
+ return "unknown";
161
+ }
162
+ function classifyCssVariable(name) {
163
+ if (/(color|bg|text|border|surface|accent|primary|secondary|foreground)/i.test(name)) {
164
+ return "color";
165
+ }
166
+ if (/(font|type|text|leading|tracking|heading|body)/i.test(name)) {
167
+ return "typography";
168
+ }
169
+ if (/(space|spacing|gap|radius|shadow|size|width|height)/i.test(name)) {
170
+ return "layout";
171
+ }
172
+ return "other";
173
+ }
174
+ function lineNumberAt(contents, index) {
175
+ return contents.slice(0, index).split("\n").length;
176
+ }
package/dist/files.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare function fileExists(path: string): Promise<boolean>;
2
2
  export declare function readText(path: string): Promise<string | null>;
3
+ export declare function writeText(path: string, contents: string): Promise<void>;
3
4
  export declare function toPosixPath(path: string): string;
package/dist/files.js CHANGED
@@ -1,4 +1,5 @@
1
- import { readFile, stat } from "fs/promises";
1
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
2
+ import { dirname } from "path";
2
3
  export async function fileExists(path) {
3
4
  try {
4
5
  await stat(path);
@@ -16,6 +17,10 @@ export async function readText(path) {
16
17
  return null;
17
18
  }
18
19
  }
20
+ export async function writeText(path, contents) {
21
+ await mkdir(dirname(path), { recursive: true });
22
+ await writeFile(path, contents, "utf8");
23
+ }
19
24
  export function toPosixPath(path) {
20
25
  return path.replace(/\\/g, "/");
21
26
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { buildPreviewBrief } from "./brief.js";
2
+ export { buildDesignSystemArtifact, writeDesignSystemArtifact } from "./design-system.js";
2
3
  export { detectFramework } from "./framework.js";
3
4
  export { createServer, main } from "./server.js";
4
- export type { DeliverableSpec, FrameworkInfo, FrameworkKind, InspectionStep, PreviewBrief, ProjectFacts, RoutingStyle, } from "./types.js";
5
+ export type { ComponentRecord, CssVariableRecord, DesignSystemArtifact, DesignSystemBuildInput, DeliverableSpec, FrameworkInfo, FrameworkKind, InspectionStep, PreviewBrief, ProjectFacts, RoutingStyle, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { buildPreviewBrief } from "./brief.js";
2
+ export { buildDesignSystemArtifact, writeDesignSystemArtifact } from "./design-system.js";
2
3
  export { detectFramework } from "./framework.js";
3
4
  export { createServer, main } from "./server.js";
@@ -0,0 +1,2 @@
1
+ import type { FrameworkInfo, ProjectFacts } from "./types.js";
2
+ export declare function buildProjectFacts(projectRoot: string, framework: FrameworkInfo): Promise<ProjectFacts>;
@@ -0,0 +1,61 @@
1
+ import { glob } from "glob";
2
+ import { join } from "path";
3
+ import { fileExists, toPosixPath } from "./files.js";
4
+ export async function buildProjectFacts(projectRoot, framework) {
5
+ const componentDirs = await findExistingDirectories(projectRoot, [
6
+ "src/components",
7
+ "src/ui",
8
+ "src/features",
9
+ "components",
10
+ "ui",
11
+ "features",
12
+ ]);
13
+ const pageDirs = await findExistingDirectories(projectRoot, [
14
+ "src/app",
15
+ "app",
16
+ "src/pages",
17
+ "pages",
18
+ "src/routes",
19
+ "routes",
20
+ ]);
21
+ const styleFiles = await findStyleFiles(projectRoot);
22
+ const likelyUiDirs = Array.from(new Set([...componentDirs, ...pageDirs.filter((dir) => !dir.endsWith("/preview"))]));
23
+ return {
24
+ framework: framework.kind,
25
+ routingStyle: framework.routingStyle,
26
+ previewRoute: framework.previewRoute,
27
+ previewEntryFile: framework.previewEntryFile,
28
+ packageManager: framework.packageManager,
29
+ likelyComponentDirs: componentDirs,
30
+ likelyStyleFiles: styleFiles,
31
+ likelyPageDirs: pageDirs,
32
+ likelyUiDirs,
33
+ };
34
+ }
35
+ async function findExistingDirectories(projectRoot, candidates) {
36
+ const results = [];
37
+ for (const candidate of candidates) {
38
+ if (await fileExists(join(projectRoot, candidate))) {
39
+ results.push(candidate);
40
+ }
41
+ }
42
+ return results;
43
+ }
44
+ async function findStyleFiles(projectRoot) {
45
+ const files = await glob(["**/*.{css,scss,sass,less}"], {
46
+ cwd: projectRoot,
47
+ nodir: true,
48
+ ignore: [
49
+ "**/node_modules/**",
50
+ "**/.next/**",
51
+ "**/dist/**",
52
+ "**/build/**",
53
+ "**/.capy/**",
54
+ "**/coverage/**",
55
+ ],
56
+ });
57
+ return files
58
+ .map((file) => toPosixPath(file))
59
+ .filter((file) => !file.includes("/preview/"))
60
+ .sort();
61
+ }
package/dist/server.js CHANGED
@@ -3,10 +3,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { buildPreviewBrief } from "./brief.js";
6
+ import { writeDesignSystemArtifact } from "./design-system.js";
6
7
  export function createServer(projectRoot = process.cwd()) {
7
8
  const server = new McpServer({
8
9
  name: "capy",
9
- version: "0.2.0",
10
+ version: "1.0.2",
10
11
  });
11
12
  server.registerTool("get_preview_brief", {
12
13
  title: "Get Preview Brief",
@@ -117,6 +118,83 @@ export function createServer(projectRoot = process.cwd()) {
117
118
  structuredContent,
118
119
  };
119
120
  });
121
+ server.registerTool("get_design_system", {
122
+ title: "Get Design System",
123
+ description: "Builds and writes a stable machine-readable design-system artifact for the current repo. Use this when the agent needs a durable JSON source of truth for future UI work.",
124
+ inputSchema: {
125
+ artifactPath: z
126
+ .string()
127
+ .default(".capy/design-system.json")
128
+ .describe("Where to write the design-system JSON artifact relative to the repo root."),
129
+ mode: z
130
+ .enum(["build", "update"])
131
+ .default("build")
132
+ .describe("Whether this is the first artifact pass or an incremental refresh."),
133
+ changedFiles: z
134
+ .array(z.string())
135
+ .optional()
136
+ .describe("Files changed since the previous pass, when known."),
137
+ userGoal: z
138
+ .string()
139
+ .optional()
140
+ .describe("Optional end-user request to keep the artifact guidance aligned with the task."),
141
+ },
142
+ outputSchema: {
143
+ artifact_path: z.string(),
144
+ framework: z.enum([
145
+ "next-app-router",
146
+ "next-pages-router",
147
+ "react-router",
148
+ "react-no-router",
149
+ "unknown",
150
+ ]),
151
+ routing_style: z.enum([
152
+ "app-router",
153
+ "pages-router",
154
+ "react-router",
155
+ "none",
156
+ "unknown",
157
+ ]),
158
+ preview_route: z.string(),
159
+ preview_entry_file: z.string(),
160
+ component_count: z.number(),
161
+ css_variable_count: z.number(),
162
+ source_files: z.array(z.string()),
163
+ warnings: z.array(z.string()),
164
+ summary: z.string(),
165
+ },
166
+ }, async ({ artifactPath = ".capy/design-system.json", mode = "build", changedFiles, userGoal }) => {
167
+ const artifact = await writeDesignSystemArtifact(projectRoot, {
168
+ artifactPath,
169
+ mode,
170
+ changedFiles,
171
+ userGoal,
172
+ });
173
+ const structuredContent = {
174
+ artifact_path: artifact.meta.artifactPath,
175
+ framework: artifact.meta.framework,
176
+ routing_style: artifact.meta.routingStyle,
177
+ preview_route: artifact.meta.previewRoute,
178
+ preview_entry_file: artifact.meta.previewEntryFile,
179
+ component_count: artifact.components.length,
180
+ css_variable_count: artifact.tokens.cssVariables.length,
181
+ source_files: artifact.meta.sourceFiles,
182
+ warnings: artifact.guidance.warnings,
183
+ summary: artifact.guidance.summary,
184
+ };
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: JSON.stringify({
190
+ ...structuredContent,
191
+ design_system: artifact,
192
+ }, null, 2),
193
+ },
194
+ ],
195
+ structuredContent,
196
+ };
197
+ });
120
198
  return server;
121
199
  }
122
200
  export async function main() {
package/dist/types.d.ts CHANGED
@@ -45,3 +45,58 @@ export interface PreviewBrief {
45
45
  warnings: string[];
46
46
  instructions: string;
47
47
  }
48
+ export interface DesignSystemBuildInput {
49
+ artifactPath?: string;
50
+ changedFiles?: string[];
51
+ mode?: "build" | "update";
52
+ userGoal?: string;
53
+ }
54
+ export interface CssVariableRecord {
55
+ name: string;
56
+ value: string;
57
+ category: "color" | "typography" | "layout" | "other";
58
+ file: string;
59
+ line: number;
60
+ }
61
+ export interface ComponentRecord {
62
+ name: string;
63
+ path: string;
64
+ exports: string[];
65
+ kind: "primitive" | "component" | "feature" | "unknown";
66
+ }
67
+ export interface DesignSystemArtifact {
68
+ meta: {
69
+ generatedAt: string;
70
+ mode: "build" | "update";
71
+ artifactPath: string;
72
+ framework: FrameworkKind;
73
+ routingStyle: RoutingStyle;
74
+ previewRoute: string;
75
+ previewEntryFile: string;
76
+ packageManager: FrameworkInfo["packageManager"];
77
+ changedFiles: string[];
78
+ sourceFiles: string[];
79
+ };
80
+ directories: {
81
+ componentDirs: string[];
82
+ pageDirs: string[];
83
+ styleFiles: string[];
84
+ uiDirs: string[];
85
+ };
86
+ tokens: {
87
+ cssVariables: CssVariableRecord[];
88
+ styleFiles: string[];
89
+ };
90
+ components: ComponentRecord[];
91
+ preview: {
92
+ route: string;
93
+ entryFile: string;
94
+ sections: string[];
95
+ updateStrategy: string[];
96
+ };
97
+ guidance: {
98
+ summary: string;
99
+ warnings: string[];
100
+ instructions: string[];
101
+ };
102
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "capy-mcp",
3
- "version": "1.0.1",
4
- "description": "MCP server that inspects a repo and returns a structured /preview brief for AI coding agents",
3
+ "version": "1.0.2",
4
+ "description": "MCP server that inspects a repo, returns a structured /preview brief, and writes a design-system JSON artifact for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",