@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.
- package/dist/components/catalog.d.ts.map +1 -1
- package/dist/components/catalog.js +10 -4
- package/dist/components/catalog.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -6
- package/dist/config/index.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/filter.d.ts.map +1 -1
- package/dist/context/filter.js +5 -1
- package/dist/context/filter.js.map +1 -1
- package/dist/context/grounding.d.ts.map +1 -1
- package/dist/context/grounding.js +7 -3
- package/dist/context/grounding.js.map +1 -1
- package/dist/context/guards.d.ts.map +1 -1
- package/dist/context/guards.js +53 -0
- package/dist/context/guards.js.map +1 -1
- package/dist/context/repo-hints.js.map +1 -1
- package/dist/context/styling-detector.d.ts +24 -0
- package/dist/context/styling-detector.d.ts.map +1 -0
- package/dist/context/styling-detector.js +337 -0
- package/dist/context/styling-detector.js.map +1 -0
- package/dist/design/principles.js.map +1 -1
- package/dist/generation/gemini-client.d.ts.map +1 -1
- package/dist/generation/gemini-client.js.map +1 -1
- package/dist/generation/litellm-client.d.ts.map +1 -1
- package/dist/generation/litellm-client.js +14 -7
- package/dist/generation/litellm-client.js.map +1 -1
- package/dist/generation/remote-client.d.ts +10 -5
- package/dist/generation/remote-client.d.ts.map +1 -1
- package/dist/generation/remote-client.js +13 -2
- package/dist/generation/remote-client.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output/file-writer.d.ts.map +1 -1
- package/dist/output/file-writer.js +4 -4
- package/dist/output/file-writer.js.map +1 -1
- package/dist/output/formatter.d.ts.map +1 -1
- package/dist/output/formatter.js +5 -2
- package/dist/output/formatter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/stack/detect.d.ts.map +1 -1
- package/dist/stack/detect.js +42 -9
- package/dist/stack/detect.js.map +1 -1
- package/dist/tokens/sync.d.ts.map +1 -1
- package/dist/tokens/sync.js +22 -5
- package/dist/tokens/sync.js.map +1 -1
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
- package/dist/tools/analyze-screenshot-ui.js +5 -5
- package/dist/tools/analyze-screenshot-ui.js.map +1 -1
- package/dist/tools/analyze-tokens.d.ts.map +1 -1
- package/dist/tools/analyze-tokens.js +3 -1
- package/dist/tools/analyze-tokens.js.map +1 -1
- package/dist/tools/catalog-components.d.ts.map +1 -1
- package/dist/tools/catalog-components.js +1 -4
- package/dist/tools/catalog-components.js.map +1 -1
- package/dist/tools/create-ui.d.ts +3 -0
- package/dist/tools/create-ui.d.ts.map +1 -1
- package/dist/tools/create-ui.js +203 -75
- package/dist/tools/create-ui.js.map +1 -1
- package/dist/tools/detect-ui-stack.js.map +1 -1
- package/dist/tools/generate-component-variants.d.ts.map +1 -1
- package/dist/tools/generate-component-variants.js +15 -4
- package/dist/tools/generate-component-variants.js.map +1 -1
- package/dist/tools/generate-vibes.d.ts.map +1 -1
- package/dist/tools/generate-vibes.js +7 -3
- package/dist/tools/generate-vibes.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/modify-ui.d.ts.map +1 -1
- package/dist/tools/modify-ui.js +7 -2
- package/dist/tools/modify-ui.js.map +1 -1
- package/dist/tools/scaffold-project.d.ts.map +1 -1
- package/dist/tools/scaffold-project.js +3 -1
- package/dist/tools/scaffold-project.js.map +1 -1
- package/dist/tools/snippet-ui.d.ts +3 -1
- package/dist/tools/snippet-ui.d.ts.map +1 -1
- package/dist/tools/snippet-ui.js +219 -88
- package/dist/tools/snippet-ui.js.map +1 -1
- package/dist/tools/sync-design-tokens.d.ts.map +1 -1
- package/dist/tools/sync-design-tokens.js +26 -11
- package/dist/tools/sync-design-tokens.js.map +1 -1
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -55
- package/src/__tests__/builder.test.ts +19 -19
- package/src/__tests__/config.test.ts +63 -31
- package/src/__tests__/filter.test.ts +98 -92
- package/src/__tests__/remote-client.test.ts +179 -0
- package/src/components/catalog.ts +170 -166
- package/src/config/index.ts +185 -177
- package/src/context/builder.ts +157 -157
- package/src/context/filter.ts +110 -104
- package/src/context/grounding.ts +143 -129
- package/src/context/guards.ts +97 -38
- package/src/context/repo-hints.ts +24 -24
- package/src/context/styling-detector.ts +460 -0
- package/src/design/principles.ts +14 -14
- package/src/generation/gemini-client.ts +53 -56
- package/src/generation/litellm-client.ts +102 -86
- package/src/generation/remote-client.ts +100 -77
- package/src/index.ts +16 -16
- package/src/output/file-writer.ts +123 -123
- package/src/output/formatter.ts +139 -132
- package/src/server.ts +12 -11
- package/src/stack/detect.ts +226 -175
- package/src/tokens/sync.ts +189 -155
- package/src/tools/analyze-screenshot-ui.ts +89 -88
- package/src/tools/analyze-tokens.ts +80 -78
- package/src/tools/catalog-components.ts +68 -68
- package/src/tools/create-ui.ts +295 -142
- package/src/tools/detect-ui-stack.ts +36 -36
- package/src/tools/generate-component-variants.ts +155 -135
- package/src/tools/generate-vibes.ts +121 -117
- package/src/tools/index.ts +14 -14
- package/src/tools/modify-ui.ts +170 -165
- package/src/tools/scaffold-project.ts +68 -66
- package/src/tools/snippet-ui.ts +323 -172
- package/src/tools/sync-design-tokens.ts +217 -195
- package/src/utils/walk.ts +47 -45
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -33
- package/vitest.config.ts +10 -10
- package/.prettierrc +0 -9
- package/eslint.config.js +0 -37
package/src/tools/create-ui.ts
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
${getStylingInstructions(stylingInfo)}
|
|
39
76
|
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
+
const result = detectUiStack(safeRoot);
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// Normalize paths for agent readability
|
|
54
|
+
const normalized = {
|
|
55
|
+
...result,
|
|
56
|
+
root: path.resolve(result.root),
|
|
57
|
+
};
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text' as const, text: JSON.stringify(normalized, null, 2) }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
64
|
}
|