@gemini-designer/mcp-server 0.1.0

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.
Files changed (153) hide show
  1. package/.prettierrc +9 -0
  2. package/dist/components/catalog.d.ts +24 -0
  3. package/dist/components/catalog.d.ts.map +1 -0
  4. package/dist/components/catalog.js +186 -0
  5. package/dist/components/catalog.js.map +1 -0
  6. package/dist/config/index.d.ts +60 -0
  7. package/dist/config/index.d.ts.map +1 -0
  8. package/dist/config/index.js +199 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/context/builder.d.ts +32 -0
  11. package/dist/context/builder.d.ts.map +1 -0
  12. package/dist/context/builder.js +194 -0
  13. package/dist/context/builder.js.map +1 -0
  14. package/dist/context/filter.d.ts +28 -0
  15. package/dist/context/filter.d.ts.map +1 -0
  16. package/dist/context/filter.js +136 -0
  17. package/dist/context/filter.js.map +1 -0
  18. package/dist/context/grounding.d.ts +27 -0
  19. package/dist/context/grounding.d.ts.map +1 -0
  20. package/dist/context/grounding.js +162 -0
  21. package/dist/context/grounding.js.map +1 -0
  22. package/dist/context/guards.d.ts +31 -0
  23. package/dist/context/guards.d.ts.map +1 -0
  24. package/dist/context/guards.js +76 -0
  25. package/dist/context/guards.js.map +1 -0
  26. package/dist/context/repo-hints.d.ts +12 -0
  27. package/dist/context/repo-hints.d.ts.map +1 -0
  28. package/dist/context/repo-hints.js +40 -0
  29. package/dist/context/repo-hints.js.map +1 -0
  30. package/dist/generation/gemini-client.d.ts +27 -0
  31. package/dist/generation/gemini-client.d.ts.map +1 -0
  32. package/dist/generation/gemini-client.js +64 -0
  33. package/dist/generation/gemini-client.js.map +1 -0
  34. package/dist/generation/litellm-client.d.ts +16 -0
  35. package/dist/generation/litellm-client.d.ts.map +1 -0
  36. package/dist/generation/litellm-client.js +98 -0
  37. package/dist/generation/litellm-client.js.map +1 -0
  38. package/dist/generation/remote-client.d.ts +20 -0
  39. package/dist/generation/remote-client.d.ts.map +1 -0
  40. package/dist/generation/remote-client.js +69 -0
  41. package/dist/generation/remote-client.js.map +1 -0
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +30 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/output/file-writer.d.ts +39 -0
  47. package/dist/output/file-writer.d.ts.map +1 -0
  48. package/dist/output/file-writer.js +153 -0
  49. package/dist/output/file-writer.js.map +1 -0
  50. package/dist/output/formatter.d.ts +26 -0
  51. package/dist/output/formatter.d.ts.map +1 -0
  52. package/dist/output/formatter.js +156 -0
  53. package/dist/output/formatter.js.map +1 -0
  54. package/dist/server.d.ts +9 -0
  55. package/dist/server.d.ts.map +1 -0
  56. package/dist/server.js +22 -0
  57. package/dist/server.js.map +1 -0
  58. package/dist/stack/detect.d.ts +49 -0
  59. package/dist/stack/detect.d.ts.map +1 -0
  60. package/dist/stack/detect.js +157 -0
  61. package/dist/stack/detect.js.map +1 -0
  62. package/dist/tokens/sync.d.ts +32 -0
  63. package/dist/tokens/sync.d.ts.map +1 -0
  64. package/dist/tokens/sync.js +188 -0
  65. package/dist/tokens/sync.js.map +1 -0
  66. package/dist/tools/analyze-screenshot-ui.d.ts +18 -0
  67. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -0
  68. package/dist/tools/analyze-screenshot-ui.js +133 -0
  69. package/dist/tools/analyze-screenshot-ui.js.map +1 -0
  70. package/dist/tools/analyze-tokens.d.ts +10 -0
  71. package/dist/tools/analyze-tokens.d.ts.map +1 -0
  72. package/dist/tools/analyze-tokens.js +107 -0
  73. package/dist/tools/analyze-tokens.js.map +1 -0
  74. package/dist/tools/catalog-components.d.ts +14 -0
  75. package/dist/tools/catalog-components.d.ts.map +1 -0
  76. package/dist/tools/catalog-components.js +85 -0
  77. package/dist/tools/catalog-components.js.map +1 -0
  78. package/dist/tools/create-ui.d.ts +10 -0
  79. package/dist/tools/create-ui.d.ts.map +1 -0
  80. package/dist/tools/create-ui.js +167 -0
  81. package/dist/tools/create-ui.js.map +1 -0
  82. package/dist/tools/detect-ui-stack.d.ts +15 -0
  83. package/dist/tools/detect-ui-stack.d.ts.map +1 -0
  84. package/dist/tools/detect-ui-stack.js +52 -0
  85. package/dist/tools/detect-ui-stack.js.map +1 -0
  86. package/dist/tools/generate-component-variants.d.ts +15 -0
  87. package/dist/tools/generate-component-variants.d.ts.map +1 -0
  88. package/dist/tools/generate-component-variants.js +199 -0
  89. package/dist/tools/generate-component-variants.js.map +1 -0
  90. package/dist/tools/generate-vibes.d.ts +10 -0
  91. package/dist/tools/generate-vibes.d.ts.map +1 -0
  92. package/dist/tools/generate-vibes.js +145 -0
  93. package/dist/tools/generate-vibes.js.map +1 -0
  94. package/dist/tools/index.d.ts +12 -0
  95. package/dist/tools/index.d.ts.map +1 -0
  96. package/dist/tools/index.js +36 -0
  97. package/dist/tools/index.js.map +1 -0
  98. package/dist/tools/modify-ui.d.ts +11 -0
  99. package/dist/tools/modify-ui.d.ts.map +1 -0
  100. package/dist/tools/modify-ui.js +207 -0
  101. package/dist/tools/modify-ui.js.map +1 -0
  102. package/dist/tools/scaffold-project.d.ts +10 -0
  103. package/dist/tools/scaffold-project.d.ts.map +1 -0
  104. package/dist/tools/scaffold-project.js +122 -0
  105. package/dist/tools/scaffold-project.js.map +1 -0
  106. package/dist/tools/snippet-ui.d.ts +11 -0
  107. package/dist/tools/snippet-ui.d.ts.map +1 -0
  108. package/dist/tools/snippet-ui.js +194 -0
  109. package/dist/tools/snippet-ui.js.map +1 -0
  110. package/dist/tools/sync-design-tokens.d.ts +14 -0
  111. package/dist/tools/sync-design-tokens.d.ts.map +1 -0
  112. package/dist/tools/sync-design-tokens.js +233 -0
  113. package/dist/tools/sync-design-tokens.js.map +1 -0
  114. package/dist/utils/walk.d.ts +15 -0
  115. package/dist/utils/walk.d.ts.map +1 -0
  116. package/dist/utils/walk.js +63 -0
  117. package/dist/utils/walk.js.map +1 -0
  118. package/eslint.config.js +37 -0
  119. package/package.json +56 -0
  120. package/src/__tests__/builder.test.ts +31 -0
  121. package/src/__tests__/config.test.ts +52 -0
  122. package/src/__tests__/filter.test.ts +109 -0
  123. package/src/components/catalog.ts +214 -0
  124. package/src/config/index.ts +237 -0
  125. package/src/context/builder.ts +233 -0
  126. package/src/context/filter.ts +164 -0
  127. package/src/context/grounding.ts +191 -0
  128. package/src/context/guards.ts +94 -0
  129. package/src/context/repo-hints.ts +43 -0
  130. package/src/generation/gemini-client.ts +94 -0
  131. package/src/generation/litellm-client.ts +121 -0
  132. package/src/generation/remote-client.ts +103 -0
  133. package/src/index.ts +36 -0
  134. package/src/output/file-writer.ts +181 -0
  135. package/src/output/formatter.ts +186 -0
  136. package/src/server.ts +28 -0
  137. package/src/stack/detect.ts +204 -0
  138. package/src/tokens/sync.ts +212 -0
  139. package/src/tools/analyze-screenshot-ui.ts +150 -0
  140. package/src/tools/analyze-tokens.ts +123 -0
  141. package/src/tools/catalog-components.ts +99 -0
  142. package/src/tools/create-ui.ts +194 -0
  143. package/src/tools/detect-ui-stack.ts +64 -0
  144. package/src/tools/generate-component-variants.ts +218 -0
  145. package/src/tools/generate-vibes.ts +177 -0
  146. package/src/tools/index.ts +42 -0
  147. package/src/tools/modify-ui.ts +230 -0
  148. package/src/tools/scaffold-project.ts +138 -0
  149. package/src/tools/snippet-ui.ts +222 -0
  150. package/src/tools/sync-design-tokens.ts +256 -0
  151. package/src/utils/walk.ts +75 -0
  152. package/tsconfig.json +34 -0
  153. package/vitest.config.ts +15 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * catalog_components Tool
3
+ *
4
+ * Builds a machine-readable catalog of exported components in the codebase.
5
+ *
6
+ * Why this matters:
7
+ * - Lets create_ui/snippet_ui/modify_ui match existing patterns
8
+ * - Enables component-library detection & reuse instead of regenerating duplicates
9
+ * - Grounding data for agents (props, file paths, exports)
10
+ */
11
+
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import { z } from 'zod';
14
+ import * as path from 'node:path';
15
+ import { Config } from '../config/index.js';
16
+ import { assertReadableDir } from '../context/guards.js';
17
+ import { walkFiles, toPosixPath } from '../utils/walk.js';
18
+ import { buildComponentCatalog } from '../components/catalog.js';
19
+
20
+ const inputSchema = {
21
+ roots: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe('Directories to scan (defaults to ["src"]).'),
25
+ extensions: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe('File extensions to include (defaults to [".tsx", ".jsx"]).'),
29
+ maxFiles: z
30
+ .number()
31
+ .int()
32
+ .min(1)
33
+ .max(50_000)
34
+ .default(2000)
35
+ .describe('Safety limit to avoid scanning huge repos.'),
36
+ };
37
+
38
+ export function registerCatalogComponents(server: McpServer, config: Config): void {
39
+ server.registerTool(
40
+ 'catalog_components',
41
+ {
42
+ title: 'Catalog Components',
43
+ description: 'Scan the repo and return a JSON catalog of exported components (no LLM).',
44
+ inputSchema,
45
+ },
46
+ async (args) => {
47
+ const roots = (args.roots as string[] | undefined) || ['src'];
48
+ const extensions = (args.extensions as string[] | undefined) || ['.tsx', '.jsx'];
49
+ const maxFiles = (args.maxFiles as number | undefined) || 2000;
50
+
51
+ const safeRoots: string[] = [];
52
+ for (const r of roots) {
53
+ try {
54
+ safeRoots.push(assertReadableDir(r, config));
55
+ } catch (error) {
56
+ const message = error instanceof Error ? error.message : 'Invalid root';
57
+ return {
58
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
59
+ isError: true,
60
+ };
61
+ }
62
+ }
63
+
64
+ const allFiles: string[] = [];
65
+ for (const r of safeRoots) {
66
+ const found = walkFiles(r, { includeExtensions: extensions, maxFiles });
67
+ allFiles.push(...found);
68
+ }
69
+
70
+ // Use cwd as catalog root so paths are stable for agents
71
+ const root = path.resolve(process.cwd());
72
+
73
+ const catalog = await buildComponentCatalog(root, allFiles.map((f) => path.resolve(f)));
74
+
75
+ // Ensure POSIX paths
76
+ catalog.components = catalog.components.map((c) => ({ ...c, file: toPosixPath(c.file) }));
77
+ catalog.root = toPosixPath(catalog.root);
78
+
79
+ // Small summary
80
+ const summary = {
81
+ totalFiles: catalog.filesScanned,
82
+ totalComponents: catalog.components.length,
83
+ byExportType: {
84
+ named: catalog.components.filter((c) => c.exportType === 'named').length,
85
+ default: catalog.components.filter((c) => c.exportType === 'default').length,
86
+ },
87
+ };
88
+
89
+ return {
90
+ content: [
91
+ {
92
+ type: 'text' as const,
93
+ text: JSON.stringify({ ...catalog, summary }, null, 2),
94
+ },
95
+ ],
96
+ };
97
+ }
98
+ );
99
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * create_ui Tool
3
+ *
4
+ * Scaffolds new UI components or views.
5
+ * Generates complete, responsive, accessible frontend code.
6
+ */
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { z } from 'zod';
10
+ import { Config } from '../config/index.js';
11
+ import { buildContext } from '../context/builder.js';
12
+ import { assertWritablePath } from '../context/guards.js';
13
+ import { generateWithGemini } from '../generation/gemini-client.js';
14
+ import { writeFile } from '../output/file-writer.js';
15
+ import { formatCode } from '../output/formatter.js';
16
+ import { buildRepoHints } from '../context/repo-hints.js';
17
+
18
+ const inputSchema = {
19
+ description: z.string().describe('Description of the UI to create (component or view)'),
20
+ framework: z.enum(['react', 'vue', 'svelte', 'html']).default('react'),
21
+ vibe: z.string().optional().describe('Design tokens/vibe from generate_vibes'),
22
+ contextFiles: z.array(z.string()).optional().describe('Paths to relevant UI files for context'),
23
+ targetPath: z.string().describe('Target file path to generate (e.g. src/components/Button.tsx)'),
24
+ includeStyles: z.boolean().default(true).describe('Include styling in the generated code'),
25
+ writeToFile: z.boolean().default(false).describe('Write the generated code to targetPath'),
26
+ backup: z.boolean().default(true).describe('Create .bak backup if overwriting an existing file'),
27
+ format: z.boolean().default(true).describe('Format the code with Prettier before returning/writing'),
28
+ };
29
+
30
+ const SYSTEM_PROMPT = `You are a senior frontend engineer and UI designer.
31
+
32
+ Your task: generate a COMPLETE, production-ready UI implementation.
33
+
34
+ Hard rules (must follow):
35
+ - Output ONLY the final code. No explanations. No markdown. No code fences.
36
+ - Use the specified framework and keep the code idiomatic for that ecosystem.
37
+ - Do NOT introduce new dependencies unless the user explicitly asked to.
38
+ - Accessibility: meet WCAG AA as a default.
39
+ - Semantic HTML, proper labels, aria-* where appropriate
40
+ - Keyboard navigation for all interactive elements
41
+ - Visible focus states (use :focus-visible patterns if relevant)
42
+ - Responsive by default (mobile-first).
43
+ - If design tokens / vibe are provided, use them (prefer CSS variables) and do not hard-code random colors.
44
+ - Keep logic minimal and UI-focused; avoid fake backend calls.
45
+
46
+ Quality bar:
47
+ - TypeScript types where applicable
48
+ - No unused imports/vars
49
+ - Clean component structure and clear naming`;
50
+
51
+ function buildUserPrompt(args: {
52
+ description: string;
53
+ framework: string;
54
+ vibe?: string;
55
+ context?: string;
56
+ targetPath: string;
57
+ includeStyles: boolean;
58
+ repoHints?: string;
59
+ }): string {
60
+ return `Create UI code.
61
+
62
+ ${args.repoHints || ''}
63
+
64
+ Framework: ${args.framework}
65
+ Target file: ${args.targetPath}
66
+
67
+ Description:
68
+ ${args.description}
69
+
70
+ Design direction / tokens (if provided):
71
+ ${args.vibe || '(none)'}
72
+
73
+ Project UI context (if provided):
74
+ ${args.context || '(none)'}
75
+
76
+ Styling:
77
+ - includeStyles = ${args.includeStyles}
78
+ - If Tailwind classes appear in context, prefer Tailwind.
79
+ - Otherwise use simple CSS variables + className patterns, minimal inline styles.
80
+
81
+ Return ONLY the code for the target file.`;
82
+ }
83
+
84
+ /**
85
+ * Extract code from a model response that might contain fences.
86
+ * (We still tell the model not to use fences, but this makes the tool resilient.)
87
+ */
88
+ function extractCode(response: string): string {
89
+ const codeBlockMatch = response.match(/```[a-zA-Z]*\n([\s\S]*?)```/);
90
+ if (codeBlockMatch) {
91
+ return codeBlockMatch[1].trim();
92
+ }
93
+ return response.trim();
94
+ }
95
+
96
+ export function registerCreateUI(server: McpServer, config: Config) {
97
+ server.registerTool(
98
+ 'create_ui',
99
+ {
100
+ title: 'Create UI Component/View',
101
+ description: 'Generate a complete UI component or view from description and design vibe',
102
+ inputSchema,
103
+ },
104
+ async (args) => {
105
+ try {
106
+ const description = args.description as string;
107
+ const framework = (args.framework as string) || config.defaultFramework;
108
+ const vibe = args.vibe as string | undefined;
109
+ const contextFiles = (args.contextFiles as string[]) || [];
110
+ const targetPath = args.targetPath as string;
111
+ const includeStyles = args.includeStyles as boolean;
112
+ const writeToFile = args.writeToFile as boolean;
113
+ const backup = args.backup as boolean;
114
+ const format = args.format as boolean;
115
+
116
+ // Build context from files (token-optimized)
117
+ let context = '';
118
+ if (contextFiles.length > 0) {
119
+ context = await buildContext(contextFiles, config);
120
+ }
121
+
122
+ const userPrompt = buildUserPrompt({
123
+ description,
124
+ framework,
125
+ vibe,
126
+ context,
127
+ targetPath,
128
+ includeStyles,
129
+ repoHints: buildRepoHints(config),
130
+ });
131
+
132
+ if (config.debug) {
133
+ console.error(`[create_ui] Generating UI for: ${description}`);
134
+ console.error(`[create_ui] Framework: ${framework}`);
135
+ console.error(`[create_ui] Context files: ${contextFiles.length}`);
136
+ }
137
+
138
+ // Generate code with Gemini
139
+ const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, { toolName: 'create_ui' });
140
+ let code = extractCode(response);
141
+
142
+ // Format if requested
143
+ if (format) {
144
+ try {
145
+ code = await formatCode(code, { filePath: targetPath });
146
+ } catch {
147
+ // ignore formatting errors
148
+ }
149
+ }
150
+
151
+ // Write to file if requested
152
+ if (writeToFile) {
153
+ const safePath = assertWritablePath(targetPath, config);
154
+ const result = await writeFile(safePath, code, { backup, format });
155
+
156
+ if (!result.success) {
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text',
161
+ text: `Error writing file: ${result.error || 'Unknown error'}`,
162
+ },
163
+ ],
164
+ isError: true,
165
+ };
166
+ }
167
+
168
+ const msg = [
169
+ `✅ UI written successfully.`,
170
+ `File: ${result.filePath}`,
171
+ result.backupPath ? `Backup: ${result.backupPath}` : undefined,
172
+ ]
173
+ .filter(Boolean)
174
+ .join('\n');
175
+
176
+ return {
177
+ content: [{ type: 'text', text: msg }],
178
+ };
179
+ }
180
+
181
+ // Return code only (no wrappers) for best compatibility with agents
182
+ return {
183
+ content: [{ type: 'text', text: code }],
184
+ };
185
+ } catch (error) {
186
+ const message = error instanceof Error ? error.message : 'Unknown error';
187
+ return {
188
+ content: [{ type: 'text', text: `Error: ${message}` }],
189
+ isError: true,
190
+ };
191
+ }
192
+ }
193
+ );
194
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * detect_ui_stack Tool
3
+ *
4
+ * Deterministically inspects the workspace to detect the UI stack:
5
+ * framework, styling approach, component libraries, storybook, etc.
6
+ *
7
+ * This tool is intentionally non-LLM to:
8
+ * - reduce token usage
9
+ * - increase reliability
10
+ * - enable downstream tools to condition prompts/outputs on real repo constraints
11
+ */
12
+
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { z } from 'zod';
15
+ import * as path from 'node:path';
16
+ import { Config } from '../config/index.js';
17
+ import { assertReadableDir } from '../context/guards.js';
18
+ import { detectUiStack } from '../stack/detect.js';
19
+
20
+ const inputSchema = {
21
+ root: z
22
+ .string()
23
+ .optional()
24
+ .describe('Project root directory (defaults to current working directory)')
25
+ .default(process.cwd()),
26
+ };
27
+
28
+ export function registerDetectUIStack(server: McpServer, config: Config): void {
29
+ server.registerTool(
30
+ 'detect_ui_stack',
31
+ {
32
+ title: 'Detect UI Stack',
33
+ description:
34
+ 'Detect framework/styling/component libraries from the repo (no LLM). Useful to ground other tools.',
35
+ inputSchema,
36
+ },
37
+ async (args) => {
38
+ const root = (args.root as string) || process.cwd();
39
+
40
+ let safeRoot: string;
41
+ try {
42
+ safeRoot = assertReadableDir(root, config);
43
+ } catch (error) {
44
+ const message = error instanceof Error ? error.message : 'Invalid root directory';
45
+ return {
46
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
47
+ isError: true,
48
+ };
49
+ }
50
+
51
+ const result = detectUiStack(safeRoot);
52
+
53
+ // Normalize paths for agent readability
54
+ const normalized = {
55
+ ...result,
56
+ root: path.resolve(result.root),
57
+ };
58
+
59
+ return {
60
+ content: [{ type: 'text' as const, text: JSON.stringify(normalized, null, 2) }],
61
+ };
62
+ }
63
+ );
64
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * generate_component_variants Tool
3
+ *
4
+ * Given an existing component, generate variants (sizes, intents, states) and
5
+ * optional Storybook stories.
6
+ *
7
+ * Output contract:
8
+ * - When applyChanges=false: outputs ONE JSON object only:
9
+ * {"files": [{"path": string, "content": string}]}
10
+ * - When applyChanges=true: writes files and returns a short summary string.
11
+ */
12
+
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { z } from 'zod';
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { Config } from '../config/index.js';
18
+ import { buildContext } from '../context/builder.js';
19
+ import { assertReadablePath, assertWritablePath } from '../context/guards.js';
20
+ import { buildRepoGrounding } from '../context/grounding.js';
21
+ import { generateWithGemini } from '../generation/gemini-client.js';
22
+ import { writeFile } from '../output/file-writer.js';
23
+
24
+ const inputSchema = {
25
+ componentFile: z.string().describe('Path to an existing component file (e.g., src/components/Button.tsx).'),
26
+ variants: z
27
+ .object({
28
+ sizes: z.array(z.string()).optional(),
29
+ intents: z.array(z.string()).optional(),
30
+ states: z.array(z.string()).optional(),
31
+ themes: z.array(z.string()).optional(),
32
+ custom: z.array(z.string()).optional().describe('Any additional variant requirements.'),
33
+ })
34
+ .optional()
35
+ .describe('Variant dimensions to generate.'),
36
+ instruction: z.string().optional().describe('Extra guidance about behavior, styling, or API.'),
37
+ storybook: z.boolean().default(true).describe('Generate a Storybook stories file if true.'),
38
+ storyFile: z.string().optional().describe('Optional path for the stories file (defaults next to component).'),
39
+ context: z.array(z.string()).optional().describe('Related files (tokens, theme, utilities).'),
40
+ applyChanges: z.boolean().default(false).describe('If true, write files to disk.'),
41
+ backup: z.boolean().default(true).describe('Create .bak backups before overwriting existing files.'),
42
+ };
43
+
44
+ const SYSTEM_PROMPT = `You are a senior frontend engineer specializing in reusable component APIs.
45
+
46
+ Your task:
47
+ - Update an existing component to support the requested variants.
48
+ - Preserve existing behavior and imports unless required.
49
+ - Keep code style consistent with the file.
50
+ - Ensure accessibility: keyboard/focus, aria-labels, disabled states.
51
+
52
+ Repo alignment (CRITICAL):
53
+ - Prefer reusing existing utilities/components already present in the repo.
54
+ - You will receive an auto-detected stack summary + component catalog; do not invent duplicates.
55
+
56
+ Output contract (CRITICAL):
57
+ - If storybook=true, output JSON with an array of files: {"files": [{"path": string, "content": string}]}
58
+ - If storybook=false, output JSON with exactly one file (the component file).
59
+ - Output MUST be ONE valid JSON object only. No prose. No markdown. No code fences.
60
+
61
+ API guidance:
62
+ - Prefer adding/expanding a typed prop such as "size" | "variant" | "intent" and mapping to classes.
63
+ - Avoid breaking changes: keep default props aligned to existing visuals.
64
+ - Do not introduce new dependencies unless explicitly requested.`;
65
+
66
+ function cleanJson(response: string): string {
67
+ let out = response.trim();
68
+ const fenced = out.match(/```(?:json)?\n([\s\S]*?)```/);
69
+ if (fenced) out = fenced[1].trim();
70
+ return out;
71
+ }
72
+
73
+ function defaultStoryPath(componentFile: string): string {
74
+ const dir = path.dirname(componentFile);
75
+ const base = path.basename(componentFile).replace(/\.(tsx|jsx|ts|js)$/, '');
76
+ return path.join(dir, `${base}.stories.tsx`);
77
+ }
78
+
79
+ export function registerGenerateComponentVariants(server: McpServer, config: Config): void {
80
+ server.registerTool(
81
+ 'generate_component_variants',
82
+ {
83
+ title: 'Generate Component Variants',
84
+ description:
85
+ 'Enhance an existing component with variants (sizes/intents/states) and optionally generate Storybook stories.',
86
+ inputSchema,
87
+ },
88
+ async (args) => {
89
+ const componentFile = args.componentFile as string;
90
+ const variants = (args.variants as any) || {};
91
+ const instruction = (args.instruction as string | undefined) || '';
92
+ const storybook = args.storybook !== false;
93
+ const storyFile = (args.storyFile as string | undefined) || defaultStoryPath(componentFile);
94
+ const contextPaths = args.context as string[] | undefined;
95
+ const applyChanges = args.applyChanges === true;
96
+ const backup = args.backup !== false;
97
+
98
+ let safeComponent: string;
99
+ try {
100
+ safeComponent = assertReadablePath(componentFile, config);
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : 'Invalid componentFile';
103
+ return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
104
+ }
105
+
106
+ let componentContent: string;
107
+ try {
108
+ componentContent = fs.readFileSync(safeComponent, 'utf-8');
109
+ } catch {
110
+ return {
111
+ content: [{ type: 'text' as const, text: `Error: Could not read ${componentFile}` }],
112
+ isError: true,
113
+ };
114
+ }
115
+
116
+ let contextContent = '';
117
+ if (contextPaths && contextPaths.length > 0) {
118
+ contextContent = await buildContext(contextPaths, config);
119
+ }
120
+
121
+ // Deterministic repo grounding (stack + reusable components)
122
+ const grounding = await buildRepoGrounding(config, {
123
+ focusFileAbs: safeComponent,
124
+ instruction,
125
+ });
126
+
127
+ const userPrompt = `Enhance the component to support variants.
128
+
129
+ ${grounding}
130
+
131
+ COMPONENT FILE PATH:
132
+ ${componentFile}
133
+
134
+ VARIANTS (JSON):
135
+ ${JSON.stringify(variants, null, 2)}
136
+
137
+ STORYBOOK:
138
+ ${storybook}
139
+
140
+ STORY FILE PATH (if storybook=true):
141
+ ${storyFile}
142
+
143
+ EXTRA INSTRUCTION:
144
+ ${instruction || '(none)'}
145
+
146
+ CURRENT COMPONENT FILE CONTENT:
147
+ ${componentContent}
148
+
149
+ ${contextContent ? `RELATED FILES FOR CONTEXT:\n${contextContent}` : ''}
150
+
151
+ Remember: Output JSON only: {"files": [{"path": string, "content": string}]}`;
152
+
153
+ try {
154
+ const resp = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, {
155
+ toolName: 'generate_component_variants',
156
+ });
157
+ const cleaned = cleanJson(resp);
158
+ const parsed = JSON.parse(cleaned) as { files?: Array<{ path: string; content: string }> };
159
+
160
+ if (!parsed || !Array.isArray(parsed.files) || parsed.files.length === 0) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: 'text' as const,
165
+ text: 'Error: Model output did not match expected JSON format: {"files": [...]}',
166
+ },
167
+ ],
168
+ isError: true,
169
+ };
170
+ }
171
+
172
+ if (!applyChanges) {
173
+ return { content: [{ type: 'text' as const, text: JSON.stringify(parsed, null, 2) }] };
174
+ }
175
+
176
+ const writes: Array<{ file: string; backup?: string }> = [];
177
+ for (const f of parsed.files) {
178
+ const target = f.path;
179
+ const content = f.content;
180
+ let safeOut: string;
181
+ try {
182
+ safeOut = assertWritablePath(target, config);
183
+ } catch (error) {
184
+ const message = error instanceof Error ? error.message : 'Invalid output path';
185
+ return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
186
+ }
187
+
188
+ const result = await writeFile(safeOut, content, { format: true, backup });
189
+ if (!result.success) {
190
+ return {
191
+ content: [
192
+ {
193
+ type: 'text' as const,
194
+ text: `Error: Failed writing ${target}: ${result.error || 'Unknown error'}`,
195
+ },
196
+ ],
197
+ isError: true,
198
+ };
199
+ }
200
+ writes.push({ file: result.filePath, backup: result.backupPath });
201
+ }
202
+
203
+ const summary = [
204
+ '✅ Generated component variants and wrote files:',
205
+ ...writes.map((w) => `- ${w.file}${w.backup ? ` (backup: ${w.backup})` : ''}`),
206
+ ].join('\n');
207
+
208
+ return { content: [{ type: 'text' as const, text: summary }] };
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : 'Unknown error';
211
+ return {
212
+ content: [{ type: 'text' as const, text: `Error generating variants: ${message}` }],
213
+ isError: true,
214
+ };
215
+ }
216
+ }
217
+ );
218
+ }