@gemini-designer/mcp-server 0.1.2 → 0.1.29

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 (129) hide show
  1. package/dist/components/catalog.d.ts.map +1 -1
  2. package/dist/components/catalog.js +10 -4
  3. package/dist/components/catalog.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +11 -6
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/context/builder.d.ts.map +1 -1
  8. package/dist/context/builder.js.map +1 -1
  9. package/dist/context/filter.d.ts.map +1 -1
  10. package/dist/context/filter.js +5 -1
  11. package/dist/context/filter.js.map +1 -1
  12. package/dist/context/grounding.d.ts.map +1 -1
  13. package/dist/context/grounding.js +7 -3
  14. package/dist/context/grounding.js.map +1 -1
  15. package/dist/context/guards.d.ts.map +1 -1
  16. package/dist/context/guards.js +53 -0
  17. package/dist/context/guards.js.map +1 -1
  18. package/dist/context/repo-hints.js.map +1 -1
  19. package/dist/context/styling-detector.d.ts +24 -0
  20. package/dist/context/styling-detector.d.ts.map +1 -0
  21. package/dist/context/styling-detector.js +337 -0
  22. package/dist/context/styling-detector.js.map +1 -0
  23. package/dist/design/principles.js.map +1 -1
  24. package/dist/generation/gemini-client.d.ts.map +1 -1
  25. package/dist/generation/gemini-client.js.map +1 -1
  26. package/dist/generation/litellm-client.d.ts.map +1 -1
  27. package/dist/generation/litellm-client.js +14 -7
  28. package/dist/generation/litellm-client.js.map +1 -1
  29. package/dist/generation/remote-client.d.ts +10 -5
  30. package/dist/generation/remote-client.d.ts.map +1 -1
  31. package/dist/generation/remote-client.js +13 -2
  32. package/dist/generation/remote-client.js.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/output/file-writer.d.ts.map +1 -1
  35. package/dist/output/file-writer.js +4 -4
  36. package/dist/output/file-writer.js.map +1 -1
  37. package/dist/output/formatter.d.ts.map +1 -1
  38. package/dist/output/formatter.js +5 -2
  39. package/dist/output/formatter.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +2 -1
  42. package/dist/server.js.map +1 -1
  43. package/dist/stack/detect.d.ts.map +1 -1
  44. package/dist/stack/detect.js +42 -9
  45. package/dist/stack/detect.js.map +1 -1
  46. package/dist/tokens/sync.d.ts.map +1 -1
  47. package/dist/tokens/sync.js +22 -5
  48. package/dist/tokens/sync.js.map +1 -1
  49. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
  50. package/dist/tools/analyze-screenshot-ui.js +5 -5
  51. package/dist/tools/analyze-screenshot-ui.js.map +1 -1
  52. package/dist/tools/analyze-tokens.d.ts.map +1 -1
  53. package/dist/tools/analyze-tokens.js +3 -1
  54. package/dist/tools/analyze-tokens.js.map +1 -1
  55. package/dist/tools/catalog-components.d.ts.map +1 -1
  56. package/dist/tools/catalog-components.js +1 -4
  57. package/dist/tools/catalog-components.js.map +1 -1
  58. package/dist/tools/create-ui.d.ts +3 -0
  59. package/dist/tools/create-ui.d.ts.map +1 -1
  60. package/dist/tools/create-ui.js +203 -75
  61. package/dist/tools/create-ui.js.map +1 -1
  62. package/dist/tools/detect-ui-stack.js.map +1 -1
  63. package/dist/tools/generate-component-variants.d.ts.map +1 -1
  64. package/dist/tools/generate-component-variants.js +15 -4
  65. package/dist/tools/generate-component-variants.js.map +1 -1
  66. package/dist/tools/generate-vibes.d.ts.map +1 -1
  67. package/dist/tools/generate-vibes.js +7 -3
  68. package/dist/tools/generate-vibes.js.map +1 -1
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/modify-ui.d.ts.map +1 -1
  71. package/dist/tools/modify-ui.js +7 -2
  72. package/dist/tools/modify-ui.js.map +1 -1
  73. package/dist/tools/scaffold-project.d.ts.map +1 -1
  74. package/dist/tools/scaffold-project.js +3 -1
  75. package/dist/tools/scaffold-project.js.map +1 -1
  76. package/dist/tools/snippet-ui.d.ts +3 -1
  77. package/dist/tools/snippet-ui.d.ts.map +1 -1
  78. package/dist/tools/snippet-ui.js +219 -88
  79. package/dist/tools/snippet-ui.js.map +1 -1
  80. package/dist/tools/sync-design-tokens.d.ts.map +1 -1
  81. package/dist/tools/sync-design-tokens.js +26 -11
  82. package/dist/tools/sync-design-tokens.js.map +1 -1
  83. package/dist/utils/walk.d.ts.map +1 -1
  84. package/dist/utils/walk.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +5 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +55 -55
  90. package/src/__tests__/builder.test.ts +19 -19
  91. package/src/__tests__/config.test.ts +63 -31
  92. package/src/__tests__/filter.test.ts +98 -92
  93. package/src/__tests__/remote-client.test.ts +179 -0
  94. package/src/components/catalog.ts +170 -166
  95. package/src/config/index.ts +185 -177
  96. package/src/context/builder.ts +157 -157
  97. package/src/context/filter.ts +110 -104
  98. package/src/context/grounding.ts +143 -129
  99. package/src/context/guards.ts +97 -38
  100. package/src/context/repo-hints.ts +24 -24
  101. package/src/context/styling-detector.ts +460 -0
  102. package/src/design/principles.ts +14 -14
  103. package/src/generation/gemini-client.ts +53 -56
  104. package/src/generation/litellm-client.ts +102 -86
  105. package/src/generation/remote-client.ts +100 -77
  106. package/src/index.ts +16 -16
  107. package/src/output/file-writer.ts +123 -123
  108. package/src/output/formatter.ts +139 -132
  109. package/src/server.ts +12 -11
  110. package/src/stack/detect.ts +226 -175
  111. package/src/tokens/sync.ts +189 -155
  112. package/src/tools/analyze-screenshot-ui.ts +89 -88
  113. package/src/tools/analyze-tokens.ts +80 -78
  114. package/src/tools/catalog-components.ts +68 -68
  115. package/src/tools/create-ui.ts +295 -142
  116. package/src/tools/detect-ui-stack.ts +36 -36
  117. package/src/tools/generate-component-variants.ts +155 -135
  118. package/src/tools/generate-vibes.ts +121 -117
  119. package/src/tools/index.ts +14 -14
  120. package/src/tools/modify-ui.ts +170 -165
  121. package/src/tools/scaffold-project.ts +68 -66
  122. package/src/tools/snippet-ui.ts +323 -172
  123. package/src/tools/sync-design-tokens.ts +217 -195
  124. package/src/utils/walk.ts +47 -45
  125. package/src/version.ts +6 -0
  126. package/tsconfig.json +23 -33
  127. package/vitest.config.ts +10 -10
  128. package/.prettierrc +0 -9
  129. package/eslint.config.js +0 -37
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Scaffolds new UI components or views.
5
5
  * Generates complete, responsive, accessible frontend code.
6
+ *
7
+ * Automatically detects project styling approach (Tailwind, CSS Modules, styled-components, etc.)
8
+ * and generates code matching the project conventions.
6
9
  */
7
10
 
8
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -14,60 +17,129 @@ import { generateWithGemini } from '../generation/gemini-client.js';
14
17
  import { writeFile } from '../output/file-writer.js';
15
18
  import { formatCode } from '../output/formatter.js';
16
19
  import { buildRepoHints } from '../context/repo-hints.js';
17
-
18
20
  import { DESIGN_PRINCIPLES } from '../design/principles.js';
21
+ import {
22
+ detectStylingApproach,
23
+ getStylingInstructions,
24
+ StylingApproach,
25
+ StylingInfo,
26
+ } from '../context/styling-detector.js';
27
+
28
+ interface GeneratedFile {
29
+ path: string;
30
+ content: string;
31
+ type: 'component' | 'styles' | 'types' | 'index';
32
+ }
19
33
 
20
34
  const inputSchema = {
21
- description: z.string().describe('Description of the UI to create (component or view)'),
22
- framework: z.enum(['react', 'vue', 'svelte', 'html']).default('react'),
23
- vibe: z.string().optional().describe('Design tokens/vibe from generate_vibes'),
24
- contextFiles: z.array(z.string()).optional().describe('Paths to relevant UI files for context'),
25
- targetPath: z.string().describe('Target file path to generate (e.g. src/components/Button.tsx)'),
26
- includeStyles: z.boolean().default(true).describe('Include styling in the generated code'),
27
- writeToFile: z.boolean().default(false).describe('Write the generated code to targetPath'),
28
- backup: z.boolean().default(true).describe('Create .bak backup if overwriting an existing file'),
29
- format: z.boolean().default(true).describe('Format the code with Prettier before returning/writing'),
35
+ description: z.string().describe('Description of the UI to create (component or view)'),
36
+ framework: z.enum(['react', 'vue', 'svelte', 'html']).default('react'),
37
+ vibe: z.string().optional().describe('Design tokens/vibe from generate_vibes'),
38
+ contextFiles: z.array(z.string()).optional().describe('Paths to relevant UI files for context'),
39
+ targetPath: z.string().describe('Target file path for component (e.g. src/components/Hero.tsx)'),
40
+ projectRoot: z
41
+ .string()
42
+ .optional()
43
+ .describe('Project root for auto-detecting styling approach (defaults to cwd)'),
44
+ stylingApproach: z
45
+ .enum([
46
+ 'auto',
47
+ 'tailwind',
48
+ 'css-modules',
49
+ 'styled-components',
50
+ 'emotion',
51
+ 'scss',
52
+ 'vanilla-extract',
53
+ 'panda-css',
54
+ 'uno-css',
55
+ 'stylex',
56
+ 'vanilla-css',
57
+ ])
58
+ .default('auto')
59
+ .describe('Styling approach to use (auto-detects from project if "auto")'),
60
+ writeToFile: z
61
+ .boolean()
62
+ .default(false)
63
+ .describe('Write generated files to disk (default: return JSON only)'),
64
+ backup: z.boolean().default(true).describe('Create .bak backup if overwriting existing files'),
65
+ format: z.boolean().default(true).describe('Format code with Prettier before returning/writing'),
30
66
  };
31
67
 
32
- const SYSTEM_PROMPT = `You are a senior frontend engineer and designer who creates DISTINCTIVE, memorable UI.
68
+ function buildSystemPrompt(stylingInfo: StylingInfo): string {
69
+ return `You are a senior frontend engineer and designer who creates DISTINCTIVE, memorable UI.
33
70
 
34
71
  Your task: generate production-ready UI code that avoids generic AI aesthetics.
35
72
 
36
73
  ${DESIGN_PRINCIPLES}
37
74
 
38
- ## Code Requirements
75
+ ${getStylingInstructions(stylingInfo)}
39
76
 
40
- Hard rules (must follow):
41
- - Output ONLY the final code. No explanations. No markdown. No code fences.
42
- - Use the specified framework idiomatically.
43
- - NO new dependencies unless explicitly requested.
44
- - Accessibility: WCAG AA default (semantic HTML, labels, aria-*, keyboard nav, :focus-visible)
45
- - Responsive by default (mobile-first)
46
- - If vibe/tokens provided, use them (prefer CSS variables). Don't hard-code random colors.
77
+ ## Output Structure (CRITICAL)
47
78
 
48
- ## Quality Bar
79
+ You MUST return a valid JSON object with this exact structure:
80
+ {
81
+ "files": [
82
+ {
83
+ "path": "<filePath>",
84
+ "content": "<file content>",
85
+ "type": "component" | "styles" | "types"
86
+ }
87
+ ],
88
+ "stylingApproach": "${stylingInfo.approach}"
89
+ }
90
+
91
+ ## File Requirements Based on Styling Approach
92
+
93
+ ${
94
+ stylingInfo.approach === 'tailwind' ||
95
+ stylingInfo.approach === 'panda-css' ||
96
+ stylingInfo.approach === 'uno-css' ||
97
+ stylingInfo.approach === 'styled-components' ||
98
+ stylingInfo.approach === 'emotion'
99
+ ? `For ${stylingInfo.approach.toUpperCase()}: Return ONLY the component file. Styles are embedded.`
100
+ : `For ${stylingInfo.approach.toUpperCase()}: Return component file AND separate styles file.`
101
+ }
102
+
103
+ Component file rules:
49
104
  - TypeScript types where applicable
50
- - No unused imports/vars
51
- - Clean component structure and clear naming
52
- - Animations should use CSS or framework-native solutions
53
- - Background treatments should create depth (not flat solid colors)`;
105
+ - Accessible by default (WCAG AA)
106
+ - Responsive (mobile-first)
107
+ - Use the detected styling approach correctly
108
+
109
+ NEVER output markdown, code fences, or explanations. ONLY the JSON object.`;
110
+ }
54
111
 
55
112
  function buildUserPrompt(args: {
56
- description: string;
57
- framework: string;
58
- vibe?: string;
59
- context?: string;
60
- targetPath: string;
61
- includeStyles: boolean;
62
- repoHints?: string;
113
+ description: string;
114
+ framework: string;
115
+ vibe?: string;
116
+ context?: string;
117
+ targetPath: string;
118
+ stylingInfo: StylingInfo;
119
+ repoHints?: string;
63
120
  }): string {
64
- return `Create UI code.
121
+ const needsSeparateStyleFile = ![
122
+ 'tailwind',
123
+ 'panda-css',
124
+ 'uno-css',
125
+ 'styled-components',
126
+ 'emotion',
127
+ ].includes(args.stylingInfo.approach);
128
+
129
+ let stylingInstructions = '';
130
+ if (needsSeparateStyleFile) {
131
+ const ext = args.stylingInfo.fileExtension;
132
+ const stylePath = args.targetPath.replace(/\.(tsx?|jsx?|vue|svelte)$/, ext);
133
+ stylingInstructions = `Style file: ${stylePath}\n`;
134
+ }
135
+
136
+ return `Create UI using ${args.stylingInfo.approach.toUpperCase()} styling.
65
137
 
66
138
  ${args.repoHints || ''}
67
139
 
68
140
  Framework: ${args.framework}
69
- Target file: ${args.targetPath}
70
-
141
+ Component file: ${args.targetPath}
142
+ ${stylingInstructions}
71
143
  Description:
72
144
  ${args.description}
73
145
 
@@ -77,122 +149,203 @@ ${args.vibe || '(none)'}
77
149
  Project UI context (if provided):
78
150
  ${args.context || '(none)'}
79
151
 
80
- Styling:
81
- - includeStyles = ${args.includeStyles}
82
- - If Tailwind classes appear in context, prefer Tailwind.
83
- - Otherwise use simple CSS variables + className patterns, minimal inline styles.
84
-
85
- Return ONLY the code for the target file.`;
152
+ Return a JSON object with "files" array containing the generated files.
153
+ ${
154
+ needsSeparateStyleFile
155
+ ? 'Include both component and style files.'
156
+ : 'Include only the component file (styles are embedded).'
157
+ }`;
86
158
  }
87
159
 
88
160
  /**
89
- * Extract code from a model response that might contain fences.
90
- * (We still tell the model not to use fences, but this makes the tool resilient.)
161
+ * Extract JSON from model response (handles code fences)
91
162
  */
92
- function extractCode(response: string): string {
93
- const codeBlockMatch = response.match(/```[a-zA-Z]*\n([\s\S]*?)```/);
94
- if (codeBlockMatch) {
95
- return codeBlockMatch[1].trim();
96
- }
97
- return response.trim();
163
+ function extractJson(response: string): string {
164
+ // Try to find JSON in code fences first
165
+ const jsonBlockMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
166
+ if (jsonBlockMatch) {
167
+ return jsonBlockMatch[1].trim();
168
+ }
169
+
170
+ // Try to find raw JSON object
171
+ const jsonMatch = response.match(/\{[\s\S]*"files"[\s\S]*\}/);
172
+ if (jsonMatch) {
173
+ return jsonMatch[0];
174
+ }
175
+
176
+ return response.trim();
98
177
  }
99
178
 
100
179
  export function registerCreateUI(server: McpServer, config: Config) {
101
- server.registerTool(
102
- 'create_ui',
103
- {
104
- title: 'Create UI Component/View',
105
- description: 'Generate a complete UI component or view from description and design vibe',
106
- inputSchema,
107
- },
108
- async (args) => {
180
+ server.registerTool(
181
+ 'create_ui',
182
+ {
183
+ title: 'Create UI Component/View',
184
+ description:
185
+ 'Generate a complete UI component. Auto-detects project styling (Tailwind, CSS Modules, styled-components, etc.) and outputs matching code structure.',
186
+ inputSchema,
187
+ },
188
+ async (args) => {
189
+ try {
190
+ const description = args.description as string;
191
+ const framework = (args.framework as string) || config.defaultFramework;
192
+ const vibe = args.vibe as string | undefined;
193
+ const contextFiles = (args.contextFiles as string[]) || [];
194
+ const targetPath = args.targetPath as string;
195
+ const projectRoot = (args.projectRoot as string) || process.cwd();
196
+ const requestedApproach = (args.stylingApproach as string) || 'auto';
197
+ const shouldWrite = args.writeToFile === true;
198
+ const backup = args.backup !== false;
199
+ const format = args.format !== false;
200
+
201
+ // Detect or use specified styling approach
202
+ let stylingInfo: StylingInfo;
203
+ if (requestedApproach === 'auto') {
204
+ stylingInfo = detectStylingApproach(projectRoot);
205
+ if (config.debug) {
206
+ console.error(
207
+ `[create_ui] Auto-detected styling: ${stylingInfo.approach} (${stylingInfo.confidence})`
208
+ );
209
+ console.error(`[create_ui] Detected from: ${stylingInfo.detectedFrom}`);
210
+ }
211
+ } else {
212
+ stylingInfo = {
213
+ approach: requestedApproach as StylingApproach,
214
+ confidence: 'high',
215
+ detectedFrom: 'user specified',
216
+ fileExtension: getFileExtension(requestedApproach as StylingApproach),
217
+ importStatement: '',
218
+ usage: '',
219
+ };
220
+ }
221
+
222
+ // Build context from files (token-optimized)
223
+ let context = '';
224
+ if (contextFiles.length > 0) {
225
+ context = await buildContext(contextFiles, config);
226
+ }
227
+
228
+ const systemPrompt = buildSystemPrompt(stylingInfo);
229
+ const userPrompt = buildUserPrompt({
230
+ description,
231
+ framework,
232
+ vibe,
233
+ context,
234
+ targetPath,
235
+ stylingInfo,
236
+ repoHints: buildRepoHints(config),
237
+ });
238
+
239
+ if (config.debug) {
240
+ console.error(`[create_ui] Generating UI for: ${description}`);
241
+ console.error(`[create_ui] Framework: ${framework}, Styling: ${stylingInfo.approach}`);
242
+ }
243
+
244
+ // Generate code with Gemini
245
+ const response = await generateWithGemini(config, systemPrompt, userPrompt, {
246
+ toolName: 'create_ui',
247
+ });
248
+ const jsonStr = extractJson(response);
249
+
250
+ let parsed: { files?: GeneratedFile[]; stylingApproach?: string };
251
+ try {
252
+ parsed = JSON.parse(jsonStr);
253
+ } catch {
254
+ return {
255
+ content: [
256
+ {
257
+ type: 'text' as const,
258
+ text: `Error: Model did not return valid JSON. Raw response:\n${response}`,
259
+ },
260
+ ],
261
+ isError: true,
262
+ };
263
+ }
264
+
265
+ if (!parsed.files || !Array.isArray(parsed.files) || parsed.files.length === 0) {
266
+ return {
267
+ content: [{ type: 'text' as const, text: 'Error: Model output missing "files" array' }],
268
+ isError: true,
269
+ };
270
+ }
271
+
272
+ // Format files if requested
273
+ if (format) {
274
+ for (const file of parsed.files) {
109
275
  try {
110
- const description = args.description as string;
111
- const framework = (args.framework as string) || config.defaultFramework;
112
- const vibe = args.vibe as string | undefined;
113
- const contextFiles = (args.contextFiles as string[]) || [];
114
- const targetPath = args.targetPath as string;
115
- const includeStyles = args.includeStyles as boolean;
116
- const writeToFile = args.writeToFile as boolean;
117
- const backup = args.backup as boolean;
118
- const format = args.format as boolean;
119
-
120
- // Build context from files (token-optimized)
121
- let context = '';
122
- if (contextFiles.length > 0) {
123
- context = await buildContext(contextFiles, config);
124
- }
125
-
126
- const userPrompt = buildUserPrompt({
127
- description,
128
- framework,
129
- vibe,
130
- context,
131
- targetPath,
132
- includeStyles,
133
- repoHints: buildRepoHints(config),
134
- });
135
-
136
- if (config.debug) {
137
- console.error(`[create_ui] Generating UI for: ${description}`);
138
- console.error(`[create_ui] Framework: ${framework}`);
139
- console.error(`[create_ui] Context files: ${contextFiles.length}`);
140
- }
141
-
142
- // Generate code with Gemini
143
- const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, { toolName: 'create_ui' });
144
- let code = extractCode(response);
145
-
146
- // Format if requested
147
- if (format) {
148
- try {
149
- code = await formatCode(code, { filePath: targetPath });
150
- } catch {
151
- // ignore formatting errors
152
- }
153
- }
154
-
155
- // Write to file if requested
156
- if (writeToFile) {
157
- const safePath = assertWritablePath(targetPath, config);
158
- const result = await writeFile(safePath, code, { backup, format });
159
-
160
- if (!result.success) {
161
- return {
162
- content: [
163
- {
164
- type: 'text',
165
- text: `Error writing file: ${result.error || 'Unknown error'}`,
166
- },
167
- ],
168
- isError: true,
169
- };
170
- }
171
-
172
- const msg = [
173
- `✅ UI written successfully.`,
174
- `File: ${result.filePath}`,
175
- result.backupPath ? `Backup: ${result.backupPath}` : undefined,
176
- ]
177
- .filter(Boolean)
178
- .join('\n');
179
-
180
- return {
181
- content: [{ type: 'text', text: msg }],
182
- };
183
- }
184
-
185
- // Return code only (no wrappers) for best compatibility with agents
186
- return {
187
- content: [{ type: 'text', text: code }],
188
- };
189
- } catch (error) {
190
- const message = error instanceof Error ? error.message : 'Unknown error';
191
- return {
192
- content: [{ type: 'text', text: `Error: ${message}` }],
193
- isError: true,
194
- };
276
+ file.content = await formatCode(file.content, { filePath: file.path });
277
+ } catch {
278
+ // Ignore formatting errors
279
+ }
280
+ }
281
+ }
282
+
283
+ // Add metadata about styling approach used
284
+ const result = {
285
+ stylingApproach: stylingInfo.approach,
286
+ detectedFrom: stylingInfo.detectedFrom,
287
+ confidence: stylingInfo.confidence,
288
+ files: parsed.files,
289
+ };
290
+
291
+ // Write to files if requested
292
+ if (shouldWrite) {
293
+ const writes: Array<{ file: string; backup?: string }> = [];
294
+
295
+ for (const file of parsed.files) {
296
+ const safePath = assertWritablePath(file.path, config);
297
+ const writeResult = await writeFile(safePath, file.content, { backup, format: false });
298
+
299
+ if (!writeResult.success) {
300
+ return {
301
+ content: [
302
+ {
303
+ type: 'text' as const,
304
+ text: `Error writing ${file.path}: ${writeResult.error}`,
305
+ },
306
+ ],
307
+ isError: true,
308
+ };
195
309
  }
310
+ writes.push({ file: writeResult.filePath, backup: writeResult.backupPath });
311
+ }
312
+
313
+ const msg = [
314
+ `✅ UI files written (${stylingInfo.approach} styling):`,
315
+ ...writes.map((w) => `- ${w.file}${w.backup ? ` (backup: ${w.backup})` : ''}`),
316
+ ].join('\n');
317
+
318
+ return { content: [{ type: 'text' as const, text: msg }] };
196
319
  }
197
- );
320
+
321
+ // Return JSON with files and metadata
322
+ return {
323
+ content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
324
+ };
325
+ } catch (error) {
326
+ const message = error instanceof Error ? error.message : 'Unknown error';
327
+ return {
328
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
329
+ isError: true,
330
+ };
331
+ }
332
+ }
333
+ );
334
+ }
335
+
336
+ function getFileExtension(approach: StylingApproach): string {
337
+ const extensions: Record<StylingApproach, string> = {
338
+ tailwind: '',
339
+ 'css-modules': '.module.css',
340
+ 'styled-components': '.tsx',
341
+ emotion: '.tsx',
342
+ scss: '.module.scss',
343
+ 'vanilla-extract': '.css.ts',
344
+ 'panda-css': '',
345
+ 'uno-css': '',
346
+ stylex: '.stylex.ts',
347
+ 'css-in-js': '.tsx',
348
+ 'vanilla-css': '.css',
349
+ };
350
+ return extensions[approach] || '.css';
198
351
  }
@@ -18,47 +18,47 @@ import { assertReadableDir } from '../context/guards.js';
18
18
  import { detectUiStack } from '../stack/detect.js';
19
19
 
20
20
  const inputSchema = {
21
- root: z
22
- .string()
23
- .optional()
24
- .describe('Project root directory (defaults to current working directory)')
25
- .default(process.cwd()),
21
+ root: z
22
+ .string()
23
+ .optional()
24
+ .describe('Project root directory (defaults to current working directory)')
25
+ .default(process.cwd()),
26
26
  };
27
27
 
28
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();
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
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
- }
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
50
 
51
- const result = detectUiStack(safeRoot);
51
+ const result = detectUiStack(safeRoot);
52
52
 
53
- // Normalize paths for agent readability
54
- const normalized = {
55
- ...result,
56
- root: path.resolve(result.root),
57
- };
53
+ // Normalize paths for agent readability
54
+ const normalized = {
55
+ ...result,
56
+ root: path.resolve(result.root),
57
+ };
58
58
 
59
- return {
60
- content: [{ type: 'text' as const, text: JSON.stringify(normalized, null, 2) }],
61
- };
62
- }
63
- );
59
+ return {
60
+ content: [{ type: 'text' as const, text: JSON.stringify(normalized, null, 2) }],
61
+ };
62
+ }
63
+ );
64
64
  }