capy-mcp 1.0.2 → 1.0.4

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 two MCP tools:
9
+ The server currently registers three MCP tools:
10
10
 
11
11
  - `get_preview_brief`
12
+ - `update_preview`
12
13
  - `get_design_system`
13
14
 
14
15
  The tool accepts:
@@ -29,6 +30,8 @@ The tool returns structured content with:
29
30
 
30
31
  `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
32
 
33
+ `update_preview` writes `.capy/preview-state.json`, diffs the current repo against the prior snapshot, refreshes `.capy/design-system.json`, and returns an incremental `/preview` update brief. This works even if the repo is not using git.
34
+
32
35
  ## Install
33
36
 
34
37
  ```bash
@@ -62,8 +65,11 @@ Example stdio MCP config:
62
65
  import {
63
66
  buildDesignSystemArtifact,
64
67
  buildPreviewBrief,
68
+ buildPreviewStateSnapshot,
65
69
  detectFramework,
70
+ runPreviewUpdate,
66
71
  writeDesignSystemArtifact,
72
+ writePreviewStateSnapshot,
67
73
  } from "capy-mcp";
68
74
 
69
75
  const framework = await detectFramework(process.cwd());
@@ -72,6 +78,9 @@ const brief = await buildPreviewBrief(process.cwd(), {
72
78
  });
73
79
  const designSystem = await buildDesignSystemArtifact(process.cwd());
74
80
  await writeDesignSystemArtifact(process.cwd());
81
+ const previewState = await buildPreviewStateSnapshot(process.cwd());
82
+ await writePreviewStateSnapshot(process.cwd(), ".capy/preview-state.json", previewState);
83
+ const update = await runPreviewUpdate(process.cwd());
75
84
  ```
76
85
 
77
86
  ## Local development
package/dist/brief.js CHANGED
@@ -3,6 +3,7 @@ import { buildProjectFacts } from "./project.js";
3
3
  const SECTION_ORDER = [
4
4
  "Foundations",
5
5
  "Colors",
6
+ "Icons",
6
7
  "Typography",
7
8
  "Spacing",
8
9
  "Inputs",
@@ -25,12 +26,18 @@ export async function buildPreviewBrief(projectRoot, input) {
25
26
  goal: input.task === "update_preview"
26
27
  ? "Update the existing /preview surface incrementally based on the files or areas that changed."
27
28
  : "Create a local /preview route that helps humans and agents inspect the app's real UI system quickly.",
28
- layout: "vertical-scroll",
29
+ layout: "bidirectional-scroll",
29
30
  allowHorizontalRows: true,
30
31
  previewRoute: projectFacts.previewRoute,
31
32
  previewEntryFile: projectFacts.previewEntryFile,
32
33
  sections: SECTION_ORDER,
33
34
  useExistingComponentsFirst: true,
35
+ interactionFeatures: [
36
+ "Allow both vertical and horizontal scrolling when the preview surface benefits from a canvas-like layout.",
37
+ "Display icons used in the app in a dedicated section whenever they can be discovered from the repo.",
38
+ "Render color swatches in a consistent format with 6-character hex labels.",
39
+ "Use a pointer cursor and click-to-copy behavior for color hex values.",
40
+ ],
34
41
  },
35
42
  updateStrategy: buildUpdateStrategy(input.changedFiles),
36
43
  warnings,
@@ -80,10 +87,12 @@ function buildInspectionPlan(projectFacts) {
80
87
  function buildConstraints(framework) {
81
88
  const constraints = [
82
89
  "Do not invent colors, spacing, typography, or component APIs before inspecting the repo files listed in inspection_plan.",
83
- "Build a vertically scrollable preview page.",
90
+ "Build a preview page that can support both vertical and horizontal scanning where useful.",
84
91
  "Use horizontal specimen rows only when they make scanning easier.",
85
92
  "Prefer existing components over creating preview-only components.",
86
93
  "Keep the page neat, easy to scan, and aligned with the app's current design language.",
94
+ "Include an icon inventory when the repo exposes app icons clearly enough to catalogue them.",
95
+ "Show colors in a uniform swatch format with normalized 6-character hex labels and click-to-copy affordance.",
87
96
  ];
88
97
  if (framework.needsConfirmation && framework.confirmationMessage) {
89
98
  constraints.push(framework.confirmationMessage);
@@ -119,7 +128,7 @@ function buildInstructions(projectFacts, input) {
119
128
  ? "Update the existing /preview route incrementally."
120
129
  : "Create the /preview route from scratch.";
121
130
  const userGoal = input.userGoal ? ` User goal: ${input.userGoal}.` : "";
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.`;
131
+ return `${lead}${userGoal} Read the app shell first, then global styles, then component directories. After that, implement ${projectFacts.previewEntryFile} as a clean preview surface that supports both vertical and horizontal scanning when useful, includes a dedicated icon section when icons can be discovered, and renders colors as consistent swatches with 6-character hex labels plus click-to-copy behavior using a pointer cursor.`;
123
132
  }
124
133
  function uniqueCompact(values) {
125
134
  return Array.from(new Set(values.filter(Boolean)));
@@ -15,25 +15,35 @@ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
15
15
  });
16
16
  const cssVariables = await collectCssVariables(projectRoot, projectFacts.likelyStyleFiles);
17
17
  const components = await collectComponents(projectRoot, projectFacts);
18
+ const readFirst = buildReadFirst(previewBrief, components);
19
+ const bridges = await buildUsageBridges(projectRoot, projectFacts.likelyStyleFiles, components);
20
+ const gaps = buildGapNotes(projectFacts, components, cssVariables);
18
21
  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
22
  return {
24
- meta: {
23
+ forAgent: {
24
+ intent: input.mode === "update"
25
+ ? "Update /preview incrementally. Read the files below first, then touch only the sections affected by the changed files unless shared foundations changed."
26
+ : "Build or refine /preview. Read the files below first, then mirror the repo's existing UI language instead of inventing a new one.",
27
+ readFirst,
28
+ facts: buildAgentFacts(projectFacts, components, cssVariables),
29
+ bridges,
30
+ gaps,
31
+ updateHints: previewBrief.updateStrategy,
32
+ },
33
+ artifact: {
25
34
  generatedAt: new Date().toISOString(),
26
35
  mode: input.mode ?? "build",
27
36
  artifactPath,
37
+ changedFiles: input.changedFiles ?? [],
38
+ },
39
+ repo: {
28
40
  framework: framework.kind,
29
41
  routingStyle: framework.routingStyle,
30
42
  previewRoute: framework.previewRoute,
31
43
  previewEntryFile: framework.previewEntryFile,
32
44
  packageManager: framework.packageManager,
33
- changedFiles: input.changedFiles ?? [],
34
- sourceFiles,
35
45
  },
36
- directories: {
46
+ scan: {
37
47
  componentDirs: projectFacts.likelyComponentDirs,
38
48
  pageDirs: projectFacts.likelyPageDirs,
39
49
  styleFiles: projectFacts.likelyStyleFiles,
@@ -41,31 +51,23 @@ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
41
51
  },
42
52
  tokens: {
43
53
  cssVariables,
44
- styleFiles: projectFacts.likelyStyleFiles,
54
+ themeSourceFiles: projectFacts.likelyStyleFiles,
55
+ },
56
+ components: {
57
+ count: components.length,
58
+ items: components,
45
59
  },
46
- components,
47
60
  preview: {
48
61
  route: projectFacts.previewRoute,
49
62
  entryFile: projectFacts.previewEntryFile,
50
63
  sections: previewBrief.deliverableSpec.sections,
51
64
  updateStrategy: previewBrief.updateStrategy,
52
65
  },
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
66
  };
65
67
  }
66
68
  export async function writeDesignSystemArtifact(projectRoot, input = {}) {
67
69
  const artifact = await buildDesignSystemArtifact(projectRoot, input);
68
- const artifactFile = join(projectRoot, artifact.meta.artifactPath);
70
+ const artifactFile = join(projectRoot, artifact.artifact.artifactPath);
69
71
  await writeText(artifactFile, `${JSON.stringify(artifact, null, 2)}\n`);
70
72
  return artifact;
71
73
  }
@@ -126,14 +128,6 @@ async function collectComponents(projectRoot, projectFacts) {
126
128
  }));
127
129
  return componentRecords;
128
130
  }
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
131
  function extractExports(contents) {
138
132
  const exports = new Set();
139
133
  const patterns = [
@@ -174,3 +168,96 @@ function classifyCssVariable(name) {
174
168
  function lineNumberAt(contents, index) {
175
169
  return contents.slice(0, index).split("\n").length;
176
170
  }
171
+ function buildReadFirst(previewBrief, components) {
172
+ const seen = new Set();
173
+ const readFirst = [];
174
+ const firstComponentByDir = new Map();
175
+ for (const component of components) {
176
+ const slashIndex = component.path.lastIndexOf("/");
177
+ if (slashIndex === -1)
178
+ continue;
179
+ const directory = component.path.slice(0, slashIndex);
180
+ if (!firstComponentByDir.has(directory)) {
181
+ firstComponentByDir.set(directory, component.path);
182
+ }
183
+ }
184
+ for (const step of previewBrief.inspectionPlan) {
185
+ for (const target of step.targets) {
186
+ const normalizedTarget = firstComponentByDir.get(target) ?? target;
187
+ if (seen.has(normalizedTarget))
188
+ continue;
189
+ seen.add(normalizedTarget);
190
+ readFirst.push({
191
+ path: normalizedTarget,
192
+ reason: step.reason,
193
+ });
194
+ if (readFirst.length >= 8) {
195
+ return readFirst;
196
+ }
197
+ }
198
+ }
199
+ return readFirst;
200
+ }
201
+ function buildAgentFacts(projectFacts, components, cssVariables) {
202
+ return [
203
+ `Use ${projectFacts.framework} conventions.`,
204
+ `Implement /preview at ${projectFacts.previewEntryFile}.`,
205
+ "Use repo-relative paths only.",
206
+ `Found ${components.length} component candidates and ${cssVariables.length} CSS variables.`,
207
+ ];
208
+ }
209
+ function buildGapNotes(projectFacts, components, cssVariables) {
210
+ const gaps = [];
211
+ if (components.length === 0) {
212
+ gaps.push("No components were detected. Grep src/components, src/ui, src/features, components, ui, or features before assuming the app has no reusable UI.");
213
+ }
214
+ if (cssVariables.length === 0 && projectFacts.likelyStyleFiles.length > 0) {
215
+ gaps.push(`No CSS variables were detected. Read ${projectFacts.likelyStyleFiles[0]} directly and look for literal classes, theme config, or inline color tokens.`);
216
+ }
217
+ if (projectFacts.likelyStyleFiles.length === 0) {
218
+ gaps.push("No global style files were detected. Inspect app shell files and component code directly for layout and color usage.");
219
+ }
220
+ return gaps;
221
+ }
222
+ async function buildUsageBridges(projectRoot, styleFiles, components) {
223
+ const bridges = [];
224
+ for (const relativePath of styleFiles) {
225
+ const fileContents = await readText(join(projectRoot, relativePath));
226
+ if (!fileContents)
227
+ continue;
228
+ if (fileContents.includes("hsl(var(--")) {
229
+ bridges.push(`Theme tokens are bridged through hsl(var(--...)) in ${relativePath}.`);
230
+ break;
231
+ }
232
+ }
233
+ const hexByFile = await collectHexLiterals(projectRoot, components);
234
+ for (const [path, hexValues] of hexByFile.slice(0, 3)) {
235
+ bridges.push(`Literal accents also appear in ${path}: ${hexValues.join(", ")}.`);
236
+ }
237
+ return bridges;
238
+ }
239
+ async function collectHexLiterals(projectRoot, components) {
240
+ const matches = [];
241
+ for (const component of components) {
242
+ const contents = await readText(join(projectRoot, component.path));
243
+ if (!contents)
244
+ continue;
245
+ const hexMatches = Array.from(contents.matchAll(/#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/g))
246
+ .map((match) => normalizeHex(match[0]))
247
+ .filter((value, index, values) => values.indexOf(value) === index);
248
+ if (hexMatches.length > 0) {
249
+ matches.push([component.path, hexMatches.slice(0, 4)]);
250
+ }
251
+ }
252
+ return matches;
253
+ }
254
+ function normalizeHex(hex) {
255
+ const normalized = hex.replace("#", "").toUpperCase();
256
+ if (normalized.length === 3) {
257
+ return `#${normalized
258
+ .split("")
259
+ .map((char) => `${char}${char}`)
260
+ .join("")}`;
261
+ }
262
+ return `#${normalized}`;
263
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { buildPreviewBrief } from "./brief.js";
2
2
  export { buildDesignSystemArtifact, writeDesignSystemArtifact } from "./design-system.js";
3
3
  export { detectFramework } from "./framework.js";
4
4
  export { createServer, main } from "./server.js";
5
- export type { ComponentRecord, CssVariableRecord, DesignSystemArtifact, DesignSystemBuildInput, DeliverableSpec, FrameworkInfo, FrameworkKind, InspectionStep, PreviewBrief, ProjectFacts, RoutingStyle, } from "./types.js";
5
+ export { buildPreviewStateSnapshot, runPreviewUpdate, writePreviewStateSnapshot } from "./update.js";
6
+ export type { ComponentRecord, CssVariableRecord, DesignSystemArtifact, DesignSystemBuildInput, DeliverableSpec, FrameworkInfo, FrameworkKind, InspectionStep, PreviewBrief, PreviewStateSnapshot, PreviewUpdateInput, PreviewUpdateResult, ProjectFacts, RoutingStyle, } from "./types.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { buildPreviewBrief } from "./brief.js";
2
2
  export { buildDesignSystemArtifact, writeDesignSystemArtifact } from "./design-system.js";
3
3
  export { detectFramework } from "./framework.js";
4
4
  export { createServer, main } from "./server.js";
5
+ export { buildPreviewStateSnapshot, runPreviewUpdate, writePreviewStateSnapshot } from "./update.js";
package/dist/server.js CHANGED
@@ -4,10 +4,93 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { buildPreviewBrief } from "./brief.js";
6
6
  import { writeDesignSystemArtifact } from "./design-system.js";
7
+ import { runPreviewUpdate } from "./update.js";
8
+ function buildPreviewBriefSchema() {
9
+ return {
10
+ project_facts: z.object({
11
+ framework: z.enum([
12
+ "next-app-router",
13
+ "next-pages-router",
14
+ "react-router",
15
+ "react-no-router",
16
+ "unknown",
17
+ ]),
18
+ routing_style: z.enum([
19
+ "app-router",
20
+ "pages-router",
21
+ "react-router",
22
+ "none",
23
+ "unknown",
24
+ ]),
25
+ preview_route: z.string(),
26
+ preview_entry_file: z.string(),
27
+ package_manager: z.enum(["npm", "pnpm", "yarn", "bun"]),
28
+ likely_component_dirs: z.array(z.string()),
29
+ likely_style_files: z.array(z.string()),
30
+ likely_page_dirs: z.array(z.string()),
31
+ likely_ui_dirs: z.array(z.string()),
32
+ }),
33
+ inspection_plan: z.array(z.object({
34
+ step: z.number(),
35
+ action: z.string(),
36
+ targets: z.array(z.string()),
37
+ reason: z.string(),
38
+ })),
39
+ constraints: z.array(z.string()),
40
+ deliverable_spec: z.object({
41
+ goal: z.string(),
42
+ layout: z.literal("bidirectional-scroll"),
43
+ allow_horizontal_rows: z.boolean(),
44
+ preview_route: z.string(),
45
+ preview_entry_file: z.string(),
46
+ sections: z.array(z.string()),
47
+ use_existing_components_first: z.boolean(),
48
+ interaction_features: z.array(z.string()),
49
+ }),
50
+ update_strategy: z.array(z.string()),
51
+ warnings: z.array(z.string()),
52
+ instructions: z.string(),
53
+ };
54
+ }
55
+ function toStructuredPreviewBrief(brief) {
56
+ return {
57
+ project_facts: {
58
+ framework: brief.projectFacts.framework,
59
+ routing_style: brief.projectFacts.routingStyle,
60
+ preview_route: brief.projectFacts.previewRoute,
61
+ preview_entry_file: brief.projectFacts.previewEntryFile,
62
+ package_manager: brief.projectFacts.packageManager,
63
+ likely_component_dirs: brief.projectFacts.likelyComponentDirs,
64
+ likely_style_files: brief.projectFacts.likelyStyleFiles,
65
+ likely_page_dirs: brief.projectFacts.likelyPageDirs,
66
+ likely_ui_dirs: brief.projectFacts.likelyUiDirs,
67
+ },
68
+ inspection_plan: brief.inspectionPlan.map((item) => ({
69
+ step: item.step,
70
+ action: item.action,
71
+ targets: item.targets,
72
+ reason: item.reason,
73
+ })),
74
+ constraints: brief.constraints,
75
+ deliverable_spec: {
76
+ goal: brief.deliverableSpec.goal,
77
+ layout: brief.deliverableSpec.layout,
78
+ allow_horizontal_rows: brief.deliverableSpec.allowHorizontalRows,
79
+ preview_route: brief.deliverableSpec.previewRoute,
80
+ preview_entry_file: brief.deliverableSpec.previewEntryFile,
81
+ sections: brief.deliverableSpec.sections,
82
+ use_existing_components_first: brief.deliverableSpec.useExistingComponentsFirst,
83
+ interaction_features: brief.deliverableSpec.interactionFeatures,
84
+ },
85
+ update_strategy: brief.updateStrategy,
86
+ warnings: brief.warnings,
87
+ instructions: brief.instructions,
88
+ };
89
+ }
7
90
  export function createServer(projectRoot = process.cwd()) {
8
91
  const server = new McpServer({
9
92
  name: "capy",
10
- version: "1.0.2",
93
+ version: "1.0.4",
11
94
  });
12
95
  server.registerTool("get_preview_brief", {
13
96
  title: "Get Preview Brief",
@@ -26,87 +109,69 @@ export function createServer(projectRoot = process.cwd()) {
26
109
  .optional()
27
110
  .describe("The user's request, so the returned instructions can stay aligned."),
28
111
  },
29
- outputSchema: {
30
- project_facts: z.object({
31
- framework: z.enum([
32
- "next-app-router",
33
- "next-pages-router",
34
- "react-router",
35
- "react-no-router",
36
- "unknown",
37
- ]),
38
- routing_style: z.enum([
39
- "app-router",
40
- "pages-router",
41
- "react-router",
42
- "none",
43
- "unknown",
44
- ]),
45
- preview_route: z.string(),
46
- preview_entry_file: z.string(),
47
- package_manager: z.enum(["npm", "pnpm", "yarn", "bun"]),
48
- likely_component_dirs: z.array(z.string()),
49
- likely_style_files: z.array(z.string()),
50
- likely_page_dirs: z.array(z.string()),
51
- likely_ui_dirs: z.array(z.string()),
52
- }),
53
- inspection_plan: z.array(z.object({
54
- step: z.number(),
55
- action: z.string(),
56
- targets: z.array(z.string()),
57
- reason: z.string(),
58
- })),
59
- constraints: z.array(z.string()),
60
- deliverable_spec: z.object({
61
- goal: z.string(),
62
- layout: z.literal("vertical-scroll"),
63
- allow_horizontal_rows: z.boolean(),
64
- preview_route: z.string(),
65
- preview_entry_file: z.string(),
66
- sections: z.array(z.string()),
67
- use_existing_components_first: z.boolean(),
68
- }),
69
- update_strategy: z.array(z.string()),
70
- warnings: z.array(z.string()),
71
- instructions: z.string(),
72
- },
112
+ outputSchema: buildPreviewBriefSchema(),
73
113
  }, async ({ task = "build_preview", changedFiles, userGoal }) => {
74
114
  const brief = await buildPreviewBrief(projectRoot, {
75
115
  task,
76
116
  changedFiles,
77
117
  userGoal,
78
118
  });
119
+ const structuredContent = toStructuredPreviewBrief(brief);
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: JSON.stringify(structuredContent, null, 2),
125
+ },
126
+ ],
127
+ structuredContent,
128
+ };
129
+ });
130
+ server.registerTool("update_preview", {
131
+ title: "Update Preview",
132
+ description: "Diffs the current repo against Capy's last snapshot, refreshes the design-system artifact, and returns an incremental /preview update brief. Works even when the repo is not a git repository.",
133
+ inputSchema: {
134
+ snapshotPath: z
135
+ .string()
136
+ .default(".capy/preview-state.json")
137
+ .describe("Where Capy stores the previous file-hash snapshot relative to the repo root."),
138
+ designSystemPath: z
139
+ .string()
140
+ .default(".capy/design-system.json")
141
+ .describe("Where to refresh the machine-readable design-system artifact."),
142
+ changedFiles: z
143
+ .array(z.string())
144
+ .optional()
145
+ .describe("Optional manual override for changed files. If omitted, Capy diffs against its last snapshot."),
146
+ userGoal: z
147
+ .string()
148
+ .optional()
149
+ .describe("Optional user request to keep the update brief aligned with the task."),
150
+ },
151
+ outputSchema: {
152
+ snapshot_path: z.string(),
153
+ design_system_path: z.string(),
154
+ changed_files: z.array(z.string()),
155
+ baseline_created: z.boolean(),
156
+ used_manual_changed_files: z.boolean(),
157
+ warnings: z.array(z.string()),
158
+ preview_update_brief: z.object(buildPreviewBriefSchema()),
159
+ },
160
+ }, async ({ snapshotPath = ".capy/preview-state.json", designSystemPath = ".capy/design-system.json", changedFiles, userGoal, }) => {
161
+ const result = await runPreviewUpdate(projectRoot, {
162
+ snapshotPath,
163
+ designSystemPath,
164
+ changedFiles,
165
+ userGoal,
166
+ });
79
167
  const structuredContent = {
80
- project_facts: {
81
- framework: brief.projectFacts.framework,
82
- routing_style: brief.projectFacts.routingStyle,
83
- preview_route: brief.projectFacts.previewRoute,
84
- preview_entry_file: brief.projectFacts.previewEntryFile,
85
- package_manager: brief.projectFacts.packageManager,
86
- likely_component_dirs: brief.projectFacts.likelyComponentDirs,
87
- likely_style_files: brief.projectFacts.likelyStyleFiles,
88
- likely_page_dirs: brief.projectFacts.likelyPageDirs,
89
- likely_ui_dirs: brief.projectFacts.likelyUiDirs,
90
- },
91
- inspection_plan: brief.inspectionPlan.map((item) => ({
92
- step: item.step,
93
- action: item.action,
94
- targets: item.targets,
95
- reason: item.reason,
96
- })),
97
- constraints: brief.constraints,
98
- deliverable_spec: {
99
- goal: brief.deliverableSpec.goal,
100
- layout: brief.deliverableSpec.layout,
101
- allow_horizontal_rows: brief.deliverableSpec.allowHorizontalRows,
102
- preview_route: brief.deliverableSpec.previewRoute,
103
- preview_entry_file: brief.deliverableSpec.previewEntryFile,
104
- sections: brief.deliverableSpec.sections,
105
- use_existing_components_first: brief.deliverableSpec.useExistingComponentsFirst,
106
- },
107
- update_strategy: brief.updateStrategy,
108
- warnings: brief.warnings,
109
- instructions: brief.instructions,
168
+ snapshot_path: result.snapshotPath,
169
+ design_system_path: result.designSystemPath,
170
+ changed_files: result.changedFiles,
171
+ baseline_created: result.baselineCreated,
172
+ used_manual_changed_files: result.usedManualChangedFiles,
173
+ warnings: result.warnings,
174
+ preview_update_brief: toStructuredPreviewBrief(result.previewBrief),
110
175
  };
111
176
  return {
112
177
  content: [
@@ -159,7 +224,7 @@ export function createServer(projectRoot = process.cwd()) {
159
224
  preview_entry_file: z.string(),
160
225
  component_count: z.number(),
161
226
  css_variable_count: z.number(),
162
- source_files: z.array(z.string()),
227
+ read_first: z.array(z.string()),
163
228
  warnings: z.array(z.string()),
164
229
  summary: z.string(),
165
230
  },
@@ -171,16 +236,16 @@ export function createServer(projectRoot = process.cwd()) {
171
236
  userGoal,
172
237
  });
173
238
  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,
239
+ artifact_path: artifact.artifact.artifactPath,
240
+ framework: artifact.repo.framework,
241
+ routing_style: artifact.repo.routingStyle,
242
+ preview_route: artifact.repo.previewRoute,
243
+ preview_entry_file: artifact.repo.previewEntryFile,
244
+ component_count: artifact.components.count,
180
245
  css_variable_count: artifact.tokens.cssVariables.length,
181
- source_files: artifact.meta.sourceFiles,
182
- warnings: artifact.guidance.warnings,
183
- summary: artifact.guidance.summary,
246
+ read_first: artifact.forAgent.readFirst.map((item) => item.path),
247
+ warnings: artifact.forAgent.gaps,
248
+ summary: artifact.forAgent.intent,
184
249
  };
185
250
  return {
186
251
  content: [
package/dist/types.d.ts CHANGED
@@ -29,12 +29,13 @@ export interface InspectionStep {
29
29
  }
30
30
  export interface DeliverableSpec {
31
31
  goal: string;
32
- layout: "vertical-scroll";
32
+ layout: "bidirectional-scroll";
33
33
  allowHorizontalRows: boolean;
34
34
  previewRoute: string;
35
35
  previewEntryFile: string;
36
36
  sections: string[];
37
37
  useExistingComponentsFirst: boolean;
38
+ interactionFeatures: string[];
38
39
  }
39
40
  export interface PreviewBrief {
40
41
  projectFacts: ProjectFacts;
@@ -65,19 +66,31 @@ export interface ComponentRecord {
65
66
  kind: "primitive" | "component" | "feature" | "unknown";
66
67
  }
67
68
  export interface DesignSystemArtifact {
68
- meta: {
69
+ forAgent: {
70
+ intent: string;
71
+ readFirst: Array<{
72
+ path: string;
73
+ reason: string;
74
+ }>;
75
+ facts: string[];
76
+ bridges: string[];
77
+ gaps: string[];
78
+ updateHints: string[];
79
+ };
80
+ artifact: {
69
81
  generatedAt: string;
70
82
  mode: "build" | "update";
71
83
  artifactPath: string;
84
+ changedFiles: string[];
85
+ };
86
+ repo: {
72
87
  framework: FrameworkKind;
73
88
  routingStyle: RoutingStyle;
74
89
  previewRoute: string;
75
90
  previewEntryFile: string;
76
91
  packageManager: FrameworkInfo["packageManager"];
77
- changedFiles: string[];
78
- sourceFiles: string[];
79
92
  };
80
- directories: {
93
+ scan: {
81
94
  componentDirs: string[];
82
95
  pageDirs: string[];
83
96
  styleFiles: string[];
@@ -85,18 +98,36 @@ export interface DesignSystemArtifact {
85
98
  };
86
99
  tokens: {
87
100
  cssVariables: CssVariableRecord[];
88
- styleFiles: string[];
101
+ themeSourceFiles: string[];
102
+ };
103
+ components: {
104
+ count: number;
105
+ items: ComponentRecord[];
89
106
  };
90
- components: ComponentRecord[];
91
107
  preview: {
92
108
  route: string;
93
109
  entryFile: string;
94
110
  sections: string[];
95
111
  updateStrategy: string[];
96
112
  };
97
- guidance: {
98
- summary: string;
99
- warnings: string[];
100
- instructions: string[];
101
- };
113
+ }
114
+ export interface PreviewStateSnapshot {
115
+ generatedAt: string;
116
+ trackedFiles: Record<string, string>;
117
+ }
118
+ export interface PreviewUpdateInput {
119
+ snapshotPath?: string;
120
+ designSystemPath?: string;
121
+ changedFiles?: string[];
122
+ userGoal?: string;
123
+ }
124
+ export interface PreviewUpdateResult {
125
+ snapshotPath: string;
126
+ designSystemPath: string;
127
+ changedFiles: string[];
128
+ baselineCreated: boolean;
129
+ usedManualChangedFiles: boolean;
130
+ previewBrief: PreviewBrief;
131
+ designSystem: DesignSystemArtifact;
132
+ warnings: string[];
102
133
  }
@@ -0,0 +1,4 @@
1
+ import type { PreviewUpdateInput, PreviewUpdateResult, PreviewStateSnapshot } from "./types.js";
2
+ export declare function runPreviewUpdate(projectRoot: string, input?: PreviewUpdateInput): Promise<PreviewUpdateResult>;
3
+ export declare function buildPreviewStateSnapshot(projectRoot: string): Promise<PreviewStateSnapshot>;
4
+ export declare function writePreviewStateSnapshot(projectRoot: string, snapshotPath: string, snapshot: PreviewStateSnapshot): Promise<void>;
package/dist/update.js ADDED
@@ -0,0 +1,121 @@
1
+ import { createHash } from "crypto";
2
+ import { glob } from "glob";
3
+ import { join } from "path";
4
+ import { buildPreviewBrief } from "./brief.js";
5
+ import { writeDesignSystemArtifact } from "./design-system.js";
6
+ import { readText, toPosixPath, writeText } from "./files.js";
7
+ const DEFAULT_SNAPSHOT_PATH = ".capy/preview-state.json";
8
+ const DEFAULT_DESIGN_SYSTEM_PATH = ".capy/design-system.json";
9
+ export async function runPreviewUpdate(projectRoot, input = {}) {
10
+ const snapshotPath = input.snapshotPath ?? DEFAULT_SNAPSHOT_PATH;
11
+ const designSystemPath = input.designSystemPath ?? DEFAULT_DESIGN_SYSTEM_PATH;
12
+ const usedManualChangedFiles = Array.isArray(input.changedFiles);
13
+ const diffResult = usedManualChangedFiles
14
+ ? {
15
+ baselineCreated: false,
16
+ changedFiles: uniqueSorted(input.changedFiles ?? []),
17
+ snapshot: await buildPreviewStateSnapshot(projectRoot),
18
+ }
19
+ : await detectChangedFilesFromSnapshot(projectRoot, snapshotPath);
20
+ const previewBrief = await buildPreviewBrief(projectRoot, {
21
+ task: "update_preview",
22
+ changedFiles: diffResult.changedFiles,
23
+ userGoal: input.userGoal,
24
+ });
25
+ const designSystem = await writeDesignSystemArtifact(projectRoot, {
26
+ artifactPath: designSystemPath,
27
+ mode: "update",
28
+ changedFiles: diffResult.changedFiles,
29
+ userGoal: input.userGoal,
30
+ });
31
+ await writePreviewStateSnapshot(projectRoot, snapshotPath, diffResult.snapshot);
32
+ const warnings = [...previewBrief.warnings];
33
+ if (diffResult.baselineCreated) {
34
+ warnings.unshift("No prior preview-state snapshot was found. Capy created a baseline snapshot, so this run cannot infer incremental changes automatically.");
35
+ }
36
+ if (usedManualChangedFiles) {
37
+ warnings.unshift("Using caller-provided changedFiles instead of auto-detected snapshot diffs.");
38
+ }
39
+ return {
40
+ snapshotPath,
41
+ designSystemPath,
42
+ changedFiles: diffResult.changedFiles,
43
+ baselineCreated: diffResult.baselineCreated,
44
+ usedManualChangedFiles,
45
+ previewBrief,
46
+ designSystem,
47
+ warnings,
48
+ };
49
+ }
50
+ export async function buildPreviewStateSnapshot(projectRoot) {
51
+ const trackedFiles = await glob(["**/*.{ts,tsx,js,jsx,css,scss,sass,less,vue,svelte}"], {
52
+ cwd: projectRoot,
53
+ nodir: true,
54
+ ignore: [
55
+ "**/node_modules/**",
56
+ "**/.next/**",
57
+ "**/dist/**",
58
+ "**/build/**",
59
+ "**/coverage/**",
60
+ "**/.git/**",
61
+ "**/.capy/**",
62
+ ],
63
+ });
64
+ const fileHashes = {};
65
+ for (const file of trackedFiles.map((item) => toPosixPath(item)).sort()) {
66
+ const contents = await readText(join(projectRoot, file));
67
+ if (contents === null)
68
+ continue;
69
+ fileHashes[file] = createHash("sha1").update(contents).digest("hex");
70
+ }
71
+ return {
72
+ generatedAt: new Date().toISOString(),
73
+ trackedFiles: fileHashes,
74
+ };
75
+ }
76
+ export async function writePreviewStateSnapshot(projectRoot, snapshotPath, snapshot) {
77
+ await writeText(join(projectRoot, snapshotPath), `${JSON.stringify(snapshot, null, 2)}\n`);
78
+ }
79
+ async function detectChangedFilesFromSnapshot(projectRoot, snapshotPath) {
80
+ const snapshot = await buildPreviewStateSnapshot(projectRoot);
81
+ const previous = await readSnapshot(projectRoot, snapshotPath);
82
+ if (!previous) {
83
+ return {
84
+ baselineCreated: true,
85
+ changedFiles: [],
86
+ snapshot,
87
+ };
88
+ }
89
+ const changedFiles = new Set();
90
+ const previousFiles = previous.trackedFiles;
91
+ const currentFiles = snapshot.trackedFiles;
92
+ for (const [path, hash] of Object.entries(currentFiles)) {
93
+ if (previousFiles[path] !== hash) {
94
+ changedFiles.add(path);
95
+ }
96
+ }
97
+ for (const path of Object.keys(previousFiles)) {
98
+ if (!(path in currentFiles)) {
99
+ changedFiles.add(path);
100
+ }
101
+ }
102
+ return {
103
+ baselineCreated: false,
104
+ changedFiles: Array.from(changedFiles).sort(),
105
+ snapshot,
106
+ };
107
+ }
108
+ async function readSnapshot(projectRoot, snapshotPath) {
109
+ const raw = await readText(join(projectRoot, snapshotPath));
110
+ if (!raw)
111
+ return null;
112
+ try {
113
+ return JSON.parse(raw);
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ function uniqueSorted(values) {
120
+ return Array.from(new Set(values)).sort();
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capy-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
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",