@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
@@ -23,23 +23,31 @@ import { writeFile } from '../output/file-writer.js';
23
23
  import { DESIGN_PRINCIPLES_COMPACT } from '../design/principles.js';
24
24
 
25
25
  const inputSchema = {
26
- componentFile: z.string().describe('Path to an existing component file (e.g., src/components/Button.tsx).'),
27
- variants: z
28
- .object({
29
- sizes: z.array(z.string()).optional(),
30
- intents: z.array(z.string()).optional(),
31
- states: z.array(z.string()).optional(),
32
- themes: z.array(z.string()).optional(),
33
- custom: z.array(z.string()).optional().describe('Any additional variant requirements.'),
34
- })
35
- .optional()
36
- .describe('Variant dimensions to generate.'),
37
- instruction: z.string().optional().describe('Extra guidance about behavior, styling, or API.'),
38
- storybook: z.boolean().default(true).describe('Generate a Storybook stories file if true.'),
39
- storyFile: z.string().optional().describe('Optional path for the stories file (defaults next to component).'),
40
- context: z.array(z.string()).optional().describe('Related files (tokens, theme, utilities).'),
41
- applyChanges: z.boolean().default(false).describe('If true, write files to disk.'),
42
- backup: z.boolean().default(true).describe('Create .bak backups before overwriting existing files.'),
26
+ componentFile: z
27
+ .string()
28
+ .describe('Path to an existing component file (e.g., src/components/Button.tsx).'),
29
+ variants: z
30
+ .object({
31
+ sizes: z.array(z.string()).optional(),
32
+ intents: z.array(z.string()).optional(),
33
+ states: z.array(z.string()).optional(),
34
+ themes: z.array(z.string()).optional(),
35
+ custom: z.array(z.string()).optional().describe('Any additional variant requirements.'),
36
+ })
37
+ .optional()
38
+ .describe('Variant dimensions to generate.'),
39
+ instruction: z.string().optional().describe('Extra guidance about behavior, styling, or API.'),
40
+ storybook: z.boolean().default(true).describe('Generate a Storybook stories file if true.'),
41
+ storyFile: z
42
+ .string()
43
+ .optional()
44
+ .describe('Optional path for the stories file (defaults next to component).'),
45
+ context: z.array(z.string()).optional().describe('Related files (tokens, theme, utilities).'),
46
+ applyChanges: z.boolean().default(false).describe('If true, write files to disk.'),
47
+ backup: z
48
+ .boolean()
49
+ .default(true)
50
+ .describe('Create .bak backups before overwriting existing files.'),
43
51
  };
44
52
 
45
53
  const SYSTEM_PROMPT = `You are a senior frontend engineer creating DISTINCTIVE component variants.
@@ -68,67 +76,76 @@ API guidance:
68
76
  - Apply distinctive typography and color for each variant.`;
69
77
 
70
78
  function cleanJson(response: string): string {
71
- let out = response.trim();
72
- const fenced = out.match(/```(?:json)?\n([\s\S]*?)```/);
73
- if (fenced) out = fenced[1].trim();
74
- return out;
79
+ let out = response.trim();
80
+ const fenced = out.match(/```(?:json)?\n([\s\S]*?)```/);
81
+ if (fenced) out = fenced[1].trim();
82
+ return out;
75
83
  }
76
84
 
77
85
  function defaultStoryPath(componentFile: string): string {
78
- const dir = path.dirname(componentFile);
79
- const base = path.basename(componentFile).replace(/\.(tsx|jsx|ts|js)$/, '');
80
- return path.join(dir, `${base}.stories.tsx`);
86
+ const dir = path.dirname(componentFile);
87
+ const base = path.basename(componentFile).replace(/\.(tsx|jsx|ts|js)$/, '');
88
+ return path.join(dir, `${base}.stories.tsx`);
81
89
  }
82
90
 
83
91
  export function registerGenerateComponentVariants(server: McpServer, config: Config): void {
84
- server.registerTool(
85
- 'generate_component_variants',
86
- {
87
- title: 'Generate Component Variants',
88
- description:
89
- 'Enhance an existing component with variants (sizes/intents/states) and optionally generate Storybook stories.',
90
- inputSchema,
91
- },
92
- async (args) => {
93
- const componentFile = args.componentFile as string;
94
- const variants = (args.variants as any) || {};
95
- const instruction = (args.instruction as string | undefined) || '';
96
- const storybook = args.storybook !== false;
97
- const storyFile = (args.storyFile as string | undefined) || defaultStoryPath(componentFile);
98
- const contextPaths = args.context as string[] | undefined;
99
- const applyChanges = args.applyChanges === true;
100
- const backup = args.backup !== false;
101
-
102
- let safeComponent: string;
103
- try {
104
- safeComponent = assertReadablePath(componentFile, config);
105
- } catch (error) {
106
- const message = error instanceof Error ? error.message : 'Invalid componentFile';
107
- return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
92
+ server.registerTool(
93
+ 'generate_component_variants',
94
+ {
95
+ title: 'Generate Component Variants',
96
+ description:
97
+ 'Enhance an existing component with variants (sizes/intents/states) and optionally generate Storybook stories.',
98
+ inputSchema,
99
+ },
100
+ async (args) => {
101
+ const componentFile = args.componentFile as string;
102
+ const variants =
103
+ (args.variants as
104
+ | {
105
+ sizes?: string[];
106
+ intents?: string[];
107
+ states?: string[];
108
+ themes?: string[];
109
+ custom?: string[];
108
110
  }
109
-
110
- let componentContent: string;
111
- try {
112
- componentContent = fs.readFileSync(safeComponent, 'utf-8');
113
- } catch {
114
- return {
115
- content: [{ type: 'text' as const, text: `Error: Could not read ${componentFile}` }],
116
- isError: true,
117
- };
118
- }
119
-
120
- let contextContent = '';
121
- if (contextPaths && contextPaths.length > 0) {
122
- contextContent = await buildContext(contextPaths, config);
123
- }
124
-
125
- // Deterministic repo grounding (stack + reusable components)
126
- const grounding = await buildRepoGrounding(config, {
127
- focusFileAbs: safeComponent,
128
- instruction,
129
- });
130
-
131
- const userPrompt = `Enhance the component to support variants.
111
+ | undefined) || {};
112
+ const instruction = (args.instruction as string | undefined) || '';
113
+ const storybook = args.storybook !== false;
114
+ const storyFile = (args.storyFile as string | undefined) || defaultStoryPath(componentFile);
115
+ const contextPaths = args.context as string[] | undefined;
116
+ const applyChanges = args.applyChanges === true;
117
+ const backup = args.backup !== false;
118
+
119
+ let safeComponent: string;
120
+ try {
121
+ safeComponent = assertReadablePath(componentFile, config);
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : 'Invalid componentFile';
124
+ return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
125
+ }
126
+
127
+ let componentContent: string;
128
+ try {
129
+ componentContent = fs.readFileSync(safeComponent, 'utf-8');
130
+ } catch {
131
+ return {
132
+ content: [{ type: 'text' as const, text: `Error: Could not read ${componentFile}` }],
133
+ isError: true,
134
+ };
135
+ }
136
+
137
+ let contextContent = '';
138
+ if (contextPaths && contextPaths.length > 0) {
139
+ contextContent = await buildContext(contextPaths, config);
140
+ }
141
+
142
+ // Deterministic repo grounding (stack + reusable components)
143
+ const grounding = await buildRepoGrounding(config, {
144
+ focusFileAbs: safeComponent,
145
+ instruction,
146
+ });
147
+
148
+ const userPrompt = `Enhance the component to support variants.
132
149
 
133
150
  ${grounding}
134
151
 
@@ -154,69 +171,72 @@ ${contextContent ? `RELATED FILES FOR CONTEXT:\n${contextContent}` : ''}
154
171
 
155
172
  Remember: Output JSON only: {"files": [{"path": string, "content": string}]}`;
156
173
 
157
- try {
158
- const resp = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, {
159
- toolName: 'generate_component_variants',
160
- });
161
- const cleaned = cleanJson(resp);
162
- const parsed = JSON.parse(cleaned) as { files?: Array<{ path: string; content: string }> };
163
-
164
- if (!parsed || !Array.isArray(parsed.files) || parsed.files.length === 0) {
165
- return {
166
- content: [
167
- {
168
- type: 'text' as const,
169
- text: 'Error: Model output did not match expected JSON format: {"files": [...]}',
170
- },
171
- ],
172
- isError: true,
173
- };
174
- }
175
-
176
- if (!applyChanges) {
177
- return { content: [{ type: 'text' as const, text: JSON.stringify(parsed, null, 2) }] };
178
- }
179
-
180
- const writes: Array<{ file: string; backup?: string }> = [];
181
- for (const f of parsed.files) {
182
- const target = f.path;
183
- const content = f.content;
184
- let safeOut: string;
185
- try {
186
- safeOut = assertWritablePath(target, config);
187
- } catch (error) {
188
- const message = error instanceof Error ? error.message : 'Invalid output path';
189
- return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
190
- }
191
-
192
- const result = await writeFile(safeOut, content, { format: true, backup });
193
- if (!result.success) {
194
- return {
195
- content: [
196
- {
197
- type: 'text' as const,
198
- text: `Error: Failed writing ${target}: ${result.error || 'Unknown error'}`,
199
- },
200
- ],
201
- isError: true,
202
- };
203
- }
204
- writes.push({ file: result.filePath, backup: result.backupPath });
205
- }
206
-
207
- const summary = [
208
- '✅ Generated component variants and wrote files:',
209
- ...writes.map((w) => `- ${w.file}${w.backup ? ` (backup: ${w.backup})` : ''}`),
210
- ].join('\n');
211
-
212
- return { content: [{ type: 'text' as const, text: summary }] };
213
- } catch (error) {
214
- const message = error instanceof Error ? error.message : 'Unknown error';
215
- return {
216
- content: [{ type: 'text' as const, text: `Error generating variants: ${message}` }],
217
- isError: true,
218
- };
219
- }
174
+ try {
175
+ const resp = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, {
176
+ toolName: 'generate_component_variants',
177
+ });
178
+ const cleaned = cleanJson(resp);
179
+ const parsed = JSON.parse(cleaned) as { files?: Array<{ path: string; content: string }> };
180
+
181
+ if (!parsed || !Array.isArray(parsed.files) || parsed.files.length === 0) {
182
+ return {
183
+ content: [
184
+ {
185
+ type: 'text' as const,
186
+ text: 'Error: Model output did not match expected JSON format: {"files": [...]}',
187
+ },
188
+ ],
189
+ isError: true,
190
+ };
191
+ }
192
+
193
+ if (!applyChanges) {
194
+ return { content: [{ type: 'text' as const, text: JSON.stringify(parsed, null, 2) }] };
220
195
  }
221
- );
196
+
197
+ const writes: Array<{ file: string; backup?: string }> = [];
198
+ for (const f of parsed.files) {
199
+ const target = f.path;
200
+ const content = f.content;
201
+ let safeOut: string;
202
+ try {
203
+ safeOut = assertWritablePath(target, config);
204
+ } catch (error) {
205
+ const message = error instanceof Error ? error.message : 'Invalid output path';
206
+ return {
207
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
208
+ isError: true,
209
+ };
210
+ }
211
+
212
+ const result = await writeFile(safeOut, content, { format: true, backup });
213
+ if (!result.success) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: 'text' as const,
218
+ text: `Error: Failed writing ${target}: ${result.error || 'Unknown error'}`,
219
+ },
220
+ ],
221
+ isError: true,
222
+ };
223
+ }
224
+ writes.push({ file: result.filePath, backup: result.backupPath });
225
+ }
226
+
227
+ const summary = [
228
+ '✅ Generated component variants and wrote files:',
229
+ ...writes.map((w) => `- ${w.file}${w.backup ? ` (backup: ${w.backup})` : ''}`),
230
+ ].join('\n');
231
+
232
+ return { content: [{ type: 'text' as const, text: summary }] };
233
+ } catch (error) {
234
+ const message = error instanceof Error ? error.message : 'Unknown error';
235
+ return {
236
+ content: [{ type: 'text' as const, text: `Error generating variants: ${message}` }],
237
+ isError: true,
238
+ };
239
+ }
240
+ }
241
+ );
222
242
  }
@@ -14,39 +14,41 @@ import { generateWithGemini } from '../generation/gemini-client.js';
14
14
  import { DESIGN_PRINCIPLES_COMPACT, BANNED_FONTS, SUGGESTED_FONTS } from '../design/principles.js';
15
15
 
16
16
  const inputSchema = {
17
- projectContext: z
18
- .string()
19
- .optional()
20
- .describe('Brief description of the project (e.g., "Modern SaaS dashboard for analytics")'),
21
- existingTokens: z
22
- .string()
23
- .optional()
24
- .describe('Path to existing design tokens or CSS file to analyze (optional)'),
25
- vibeCount: z
26
- .number()
27
- .min(1)
28
- .max(5)
29
- .default(3)
30
- .describe('Number of vibe options to generate (1-5)'),
31
- preferences: z
32
- .object({
33
- colorTemp: z.enum(['warm', 'cool', 'neutral']).optional(),
34
- density: z.enum(['compact', 'comfortable', 'spacious']).optional(),
35
- style: z.enum([
36
- 'minimal',
37
- 'playful',
38
- 'corporate',
39
- 'brutalist',
40
- 'glassmorphism',
41
- 'editorial',
42
- 'retro-futuristic',
43
- 'luxury',
44
- 'organic',
45
- 'art-deco',
46
- ]).optional(),
47
- })
48
- .optional()
49
- .describe('Optional style preferences to guide generation'),
17
+ projectContext: z
18
+ .string()
19
+ .optional()
20
+ .describe('Brief description of the project (e.g., "Modern SaaS dashboard for analytics")'),
21
+ existingTokens: z
22
+ .string()
23
+ .optional()
24
+ .describe('Path to existing design tokens or CSS file to analyze (optional)'),
25
+ vibeCount: z
26
+ .number()
27
+ .min(1)
28
+ .max(5)
29
+ .default(3)
30
+ .describe('Number of vibe options to generate (1-5)'),
31
+ preferences: z
32
+ .object({
33
+ colorTemp: z.enum(['warm', 'cool', 'neutral']).optional(),
34
+ density: z.enum(['compact', 'comfortable', 'spacious']).optional(),
35
+ style: z
36
+ .enum([
37
+ 'minimal',
38
+ 'playful',
39
+ 'corporate',
40
+ 'brutalist',
41
+ 'glassmorphism',
42
+ 'editorial',
43
+ 'retro-futuristic',
44
+ 'luxury',
45
+ 'organic',
46
+ 'art-deco',
47
+ ])
48
+ .optional(),
49
+ })
50
+ .optional()
51
+ .describe('Optional style preferences to guide generation'),
50
52
  };
51
53
 
52
54
  const SYSTEM_PROMPT = `You are an expert UI/UX designer who creates DISTINCTIVE, memorable design systems.
@@ -106,98 +108,100 @@ SUGGESTED FONTS:
106
108
  }`;
107
109
 
108
110
  export function registerGenerateVibes(server: McpServer, config: Config): void {
109
- server.registerTool(
110
- 'generate_vibes',
111
- {
112
- title: 'Generate Design Vibes',
113
- description:
114
- 'Generate design direction options (vibes) with color palettes, typography, and spacing. Call this before create_ui to establish design consistency.',
115
- inputSchema,
116
- },
117
- async (args) => {
118
- const projectContext = args.projectContext as string | undefined;
119
- const vibeCount = args.vibeCount as number | undefined;
120
- const preferences = args.preferences as
121
- | {
122
- colorTemp?: 'warm' | 'cool' | 'neutral';
123
- density?: 'compact' | 'comfortable' | 'spacious';
124
- style?: 'minimal' | 'playful' | 'corporate' | 'brutalist' | 'glassmorphism';
125
- }
126
- | undefined;
127
-
128
- const existingTokensPath = args.existingTokens as string | undefined;
129
-
130
- let existingTokensContext = '';
131
- if (existingTokensPath) {
132
- // Use the same context pipeline (path allowlist + sensitive file filtering + truncation)
133
- existingTokensContext = await buildContext([existingTokensPath], config);
134
- }
135
-
136
- let userPrompt = buildPrompt({
137
- projectContext,
138
- vibeCount,
139
- preferences,
140
- });
141
-
142
- if (existingTokensContext) {
143
- userPrompt += `\n\nExisting design tokens / styles to respect:\n${existingTokensContext}`;
144
- }
145
-
146
- try {
147
- const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, { toolName: 'generate_vibes' });
148
-
149
- return {
150
- content: [
151
- {
152
- type: 'text' as const,
153
- text: response.trim(),
154
- },
155
- ],
156
- };
157
- } catch (error) {
158
- const message = error instanceof Error ? error.message : 'Unknown error';
159
- return {
160
- content: [
161
- {
162
- type: 'text' as const,
163
- text: `Error generating vibes: ${message}`,
164
- },
165
- ],
166
- isError: true,
167
- };
168
- }
169
- }
170
- );
111
+ server.registerTool(
112
+ 'generate_vibes',
113
+ {
114
+ title: 'Generate Design Vibes',
115
+ description:
116
+ 'Generate design direction options (vibes) with color palettes, typography, and spacing. Call this before create_ui to establish design consistency.',
117
+ inputSchema,
118
+ },
119
+ async (args) => {
120
+ const projectContext = args.projectContext as string | undefined;
121
+ const vibeCount = args.vibeCount as number | undefined;
122
+ const preferences = args.preferences as
123
+ | {
124
+ colorTemp?: 'warm' | 'cool' | 'neutral';
125
+ density?: 'compact' | 'comfortable' | 'spacious';
126
+ style?: 'minimal' | 'playful' | 'corporate' | 'brutalist' | 'glassmorphism';
127
+ }
128
+ | undefined;
129
+
130
+ const existingTokensPath = args.existingTokens as string | undefined;
131
+
132
+ let existingTokensContext = '';
133
+ if (existingTokensPath) {
134
+ // Use the same context pipeline (path allowlist + sensitive file filtering + truncation)
135
+ existingTokensContext = await buildContext([existingTokensPath], config);
136
+ }
137
+
138
+ let userPrompt = buildPrompt({
139
+ projectContext,
140
+ vibeCount,
141
+ preferences,
142
+ });
143
+
144
+ if (existingTokensContext) {
145
+ userPrompt += `\n\nExisting design tokens / styles to respect:\n${existingTokensContext}`;
146
+ }
147
+
148
+ try {
149
+ const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, {
150
+ toolName: 'generate_vibes',
151
+ });
152
+
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text' as const,
157
+ text: response.trim(),
158
+ },
159
+ ],
160
+ };
161
+ } catch (error) {
162
+ const message = error instanceof Error ? error.message : 'Unknown error';
163
+ return {
164
+ content: [
165
+ {
166
+ type: 'text' as const,
167
+ text: `Error generating vibes: ${message}`,
168
+ },
169
+ ],
170
+ isError: true,
171
+ };
172
+ }
173
+ }
174
+ );
171
175
  }
172
176
 
173
177
  function buildPrompt(input: {
174
- projectContext?: string;
175
- vibeCount?: number;
176
- preferences?: {
177
- colorTemp?: 'warm' | 'cool' | 'neutral';
178
- density?: 'compact' | 'comfortable' | 'spacious';
179
- style?: 'minimal' | 'playful' | 'corporate' | 'brutalist' | 'glassmorphism';
180
- };
178
+ projectContext?: string;
179
+ vibeCount?: number;
180
+ preferences?: {
181
+ colorTemp?: 'warm' | 'cool' | 'neutral';
182
+ density?: 'compact' | 'comfortable' | 'spacious';
183
+ style?: 'minimal' | 'playful' | 'corporate' | 'brutalist' | 'glassmorphism';
184
+ };
181
185
  }): string {
182
- const vibeCount = input.vibeCount ?? 3;
183
- let prompt = `Generate ${vibeCount} distinct design vibes`;
186
+ const vibeCount = input.vibeCount ?? 3;
187
+ let prompt = `Generate ${vibeCount} distinct design vibes`;
184
188
 
185
- if (input.projectContext) {
186
- prompt += ` for this project: "${input.projectContext}"`;
187
- }
189
+ if (input.projectContext) {
190
+ prompt += ` for this project: "${input.projectContext}"`;
191
+ }
188
192
 
189
- if (input.preferences) {
190
- const prefs = [];
191
- if (input.preferences.colorTemp) prefs.push(`${input.preferences.colorTemp} color temperature`);
192
- if (input.preferences.density) prefs.push(`${input.preferences.density} spacing density`);
193
- if (input.preferences.style) prefs.push(`${input.preferences.style} style`);
193
+ if (input.preferences) {
194
+ const prefs = [];
195
+ if (input.preferences.colorTemp) prefs.push(`${input.preferences.colorTemp} color temperature`);
196
+ if (input.preferences.density) prefs.push(`${input.preferences.density} spacing density`);
197
+ if (input.preferences.style) prefs.push(`${input.preferences.style} style`);
194
198
 
195
- if (prefs.length > 0) {
196
- prompt += `. Preferences: ${prefs.join(', ')}`;
197
- }
199
+ if (prefs.length > 0) {
200
+ prompt += `. Preferences: ${prefs.join(', ')}`;
198
201
  }
202
+ }
199
203
 
200
- prompt += '. Return valid JSON only.';
204
+ prompt += '. Return valid JSON only.';
201
205
 
202
- return prompt;
206
+ return prompt;
203
207
  }
@@ -22,21 +22,21 @@ import { registerAnalyzeScreenshotUI } from './analyze-screenshot-ui.js';
22
22
  * Register all MCP tools
23
23
  */
24
24
  export function registerTools(server: McpServer, config: Config): void {
25
- console.error('[tools] Registering MCP tools...');
25
+ console.error('[tools] Registering MCP tools...');
26
26
 
27
- registerGenerateVibes(server, config);
28
- registerCreateUI(server, config);
29
- registerModifyUI(server, config);
30
- registerSnippetUI(server, config);
31
- registerScaffoldProject(server, config);
32
- registerAnalyzeTokens(server, config);
27
+ registerGenerateVibes(server, config);
28
+ registerCreateUI(server, config);
29
+ registerModifyUI(server, config);
30
+ registerSnippetUI(server, config);
31
+ registerScaffoldProject(server, config);
32
+ registerAnalyzeTokens(server, config);
33
33
 
34
- // New grounding + power tools
35
- registerDetectUIStack(server, config);
36
- registerSyncDesignTokens(server, config);
37
- registerCatalogComponents(server, config);
38
- registerGenerateComponentVariants(server, config);
39
- registerAnalyzeScreenshotUI(server, config);
34
+ // New grounding + power tools
35
+ registerDetectUIStack(server, config);
36
+ registerSyncDesignTokens(server, config);
37
+ registerCatalogComponents(server, config);
38
+ registerGenerateComponentVariants(server, config);
39
+ registerAnalyzeScreenshotUI(server, config);
40
40
 
41
- console.error('[tools] All tools registered');
41
+ console.error('[tools] All tools registered');
42
42
  }