@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,177 @@
1
+ /**
2
+ * generate_vibes Tool
3
+ *
4
+ * Generates design direction options (vibes) before creating UI.
5
+ * Returns color palettes, typography, spacing, and sample CSS variables.
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 { generateWithGemini } from '../generation/gemini-client.js';
13
+
14
+ const inputSchema = {
15
+ projectContext: z
16
+ .string()
17
+ .optional()
18
+ .describe('Brief description of the project (e.g., "Modern SaaS dashboard for analytics")'),
19
+ existingTokens: z
20
+ .string()
21
+ .optional()
22
+ .describe('Path to existing design tokens or CSS file to analyze (optional)'),
23
+ vibeCount: z
24
+ .number()
25
+ .min(1)
26
+ .max(5)
27
+ .default(3)
28
+ .describe('Number of vibe options to generate (1-5)'),
29
+ preferences: z
30
+ .object({
31
+ colorTemp: z.enum(['warm', 'cool', 'neutral']).optional(),
32
+ density: z.enum(['compact', 'comfortable', 'spacious']).optional(),
33
+ style: z.enum(['minimal', 'playful', 'corporate', 'brutalist', 'glassmorphism']).optional(),
34
+ })
35
+ .optional()
36
+ .describe('Optional style preferences to guide generation'),
37
+ };
38
+
39
+ const SYSTEM_PROMPT = `You are an expert UI/UX designer specializing in modern web design systems.
40
+
41
+ Your task is to generate design "vibes"—cohesive design directions that include colors, typography, and spacing.
42
+
43
+ Requirements:
44
+ - Return ONE valid JSON object with a "vibes" array (no markdown, no code fences, no extra commentary).
45
+ - Use semantic color roles (primary, secondary, accent, background, surface, text, textMuted, border, danger, success).
46
+ - Prefer accessible color combinations (aim for WCAG AA contrast for text on background/surface).
47
+ - Provide a sensible type scale and spacing scale (consistent multiplier).
48
+ - Provide a ready-to-paste CSS variable block (light theme). If you can, include a dark theme block too.
49
+
50
+ Output structure:
51
+ {
52
+ "vibes": [
53
+ {
54
+ "name": "Vibe Name",
55
+ "description": "Brief aesthetic description",
56
+ "colors": {
57
+ "primary": "#hex",
58
+ "secondary": "#hex",
59
+ "accent": "#hex",
60
+ "background": "#hex",
61
+ "surface": "#hex",
62
+ "text": "#hex",
63
+ "textMuted": "#hex",
64
+ "border": "#hex",
65
+ "danger": "#hex",
66
+ "success": "#hex"
67
+ },
68
+ "typography": {
69
+ "fontFamily": "font stack",
70
+ "headingFamily": "font stack or same",
71
+ "scale": [12, 14, 16, 18, 20, 24, 30, 36, 48, 60]
72
+ },
73
+ "spacing": {
74
+ "unit": 4,
75
+ "scale": [0, 4, 8, 12, 16, 24, 32, 48, 64, 96]
76
+ },
77
+ "cssVariables": "/* light */\\n:root { --color-primary: #...; }\\n\\n/* dark */\\n[data-theme='dark'] { --color-primary: #...; }"
78
+ }
79
+ ]
80
+ }`;
81
+
82
+ export function registerGenerateVibes(server: McpServer, config: Config): void {
83
+ server.registerTool(
84
+ 'generate_vibes',
85
+ {
86
+ title: 'Generate Design Vibes',
87
+ description:
88
+ 'Generate design direction options (vibes) with color palettes, typography, and spacing. Call this before create_ui to establish design consistency.',
89
+ inputSchema,
90
+ },
91
+ async (args) => {
92
+ const projectContext = args.projectContext as string | undefined;
93
+ const vibeCount = args.vibeCount as number | undefined;
94
+ const preferences = args.preferences as
95
+ | {
96
+ colorTemp?: 'warm' | 'cool' | 'neutral';
97
+ density?: 'compact' | 'comfortable' | 'spacious';
98
+ style?: 'minimal' | 'playful' | 'corporate' | 'brutalist' | 'glassmorphism';
99
+ }
100
+ | undefined;
101
+
102
+ const existingTokensPath = args.existingTokens as string | undefined;
103
+
104
+ let existingTokensContext = '';
105
+ if (existingTokensPath) {
106
+ // Use the same context pipeline (path allowlist + sensitive file filtering + truncation)
107
+ existingTokensContext = await buildContext([existingTokensPath], config);
108
+ }
109
+
110
+ let userPrompt = buildPrompt({
111
+ projectContext,
112
+ vibeCount,
113
+ preferences,
114
+ });
115
+
116
+ if (existingTokensContext) {
117
+ userPrompt += `\n\nExisting design tokens / styles to respect:\n${existingTokensContext}`;
118
+ }
119
+
120
+ try {
121
+ const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, { toolName: 'generate_vibes' });
122
+
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text' as const,
127
+ text: response.trim(),
128
+ },
129
+ ],
130
+ };
131
+ } catch (error) {
132
+ const message = error instanceof Error ? error.message : 'Unknown error';
133
+ return {
134
+ content: [
135
+ {
136
+ type: 'text' as const,
137
+ text: `Error generating vibes: ${message}`,
138
+ },
139
+ ],
140
+ isError: true,
141
+ };
142
+ }
143
+ }
144
+ );
145
+ }
146
+
147
+ function buildPrompt(input: {
148
+ projectContext?: string;
149
+ vibeCount?: number;
150
+ preferences?: {
151
+ colorTemp?: 'warm' | 'cool' | 'neutral';
152
+ density?: 'compact' | 'comfortable' | 'spacious';
153
+ style?: 'minimal' | 'playful' | 'corporate' | 'brutalist' | 'glassmorphism';
154
+ };
155
+ }): string {
156
+ const vibeCount = input.vibeCount ?? 3;
157
+ let prompt = `Generate ${vibeCount} distinct design vibes`;
158
+
159
+ if (input.projectContext) {
160
+ prompt += ` for this project: "${input.projectContext}"`;
161
+ }
162
+
163
+ if (input.preferences) {
164
+ const prefs = [];
165
+ if (input.preferences.colorTemp) prefs.push(`${input.preferences.colorTemp} color temperature`);
166
+ if (input.preferences.density) prefs.push(`${input.preferences.density} spacing density`);
167
+ if (input.preferences.style) prefs.push(`${input.preferences.style} style`);
168
+
169
+ if (prefs.length > 0) {
170
+ prompt += `. Preferences: ${prefs.join(', ')}`;
171
+ }
172
+ }
173
+
174
+ prompt += '. Return valid JSON only.';
175
+
176
+ return prompt;
177
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tool Registry
3
+ *
4
+ * Registers all MCP tools with the server.
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { Config } from '../config/index.js';
9
+ import { registerGenerateVibes } from './generate-vibes.js';
10
+ import { registerCreateUI } from './create-ui.js';
11
+ import { registerModifyUI } from './modify-ui.js';
12
+ import { registerSnippetUI } from './snippet-ui.js';
13
+ import { registerScaffoldProject } from './scaffold-project.js';
14
+ import { registerAnalyzeTokens } from './analyze-tokens.js';
15
+ import { registerDetectUIStack } from './detect-ui-stack.js';
16
+ import { registerSyncDesignTokens } from './sync-design-tokens.js';
17
+ import { registerCatalogComponents } from './catalog-components.js';
18
+ import { registerGenerateComponentVariants } from './generate-component-variants.js';
19
+ import { registerAnalyzeScreenshotUI } from './analyze-screenshot-ui.js';
20
+
21
+ /**
22
+ * Register all MCP tools
23
+ */
24
+ export function registerTools(server: McpServer, config: Config): void {
25
+ console.error('[tools] Registering MCP tools...');
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);
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);
40
+
41
+ console.error('[tools] All tools registered');
42
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * modify_ui Tool
3
+ *
4
+ * Surgical edits to existing components with minimal breakage.
5
+ * Preserves logic and only modifies UI-related code.
6
+ * Can optionally apply changes directly if requested.
7
+ */
8
+
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { z } from 'zod';
11
+ import * as fs from 'node:fs';
12
+ import { Config } from '../config/index.js';
13
+ import { buildContext } from '../context/builder.js';
14
+ import { assertReadablePath, assertWritablePath } from '../context/guards.js';
15
+ import { buildRepoGrounding } from '../context/grounding.js';
16
+ import { generateWithGemini } from '../generation/gemini-client.js';
17
+ import { writeFile } from '../output/file-writer.js';
18
+
19
+ const inputSchema = {
20
+ targetFile: z.string().describe('Path to the file to modify'),
21
+ instruction: z
22
+ .string()
23
+ .describe('What to change (e.g., "Make the header sticky and add a subtle shadow")'),
24
+ context: z
25
+ .array(z.string())
26
+ .optional()
27
+ .describe('Related files for awareness (design tokens, parent components)'),
28
+ preserveLogic: z
29
+ .boolean()
30
+ .default(true)
31
+ .describe('Never touch non-UI code like API calls, state logic, etc.'),
32
+ returnDiff: z.boolean().default(false).describe('Return unified diff instead of full file'),
33
+ applyChanges: z
34
+ .boolean()
35
+ .default(false)
36
+ .describe('If true, apply the modifications directly to the file (only when returnDiff=false)'),
37
+ backup: z.boolean().default(true).describe('If true (default), create a .bak backup before modifying'),
38
+ };
39
+
40
+ function getSystemPrompt(returnDiff: boolean): string {
41
+ const base = `You are a senior frontend engineer performing surgical code modifications.
42
+
43
+ CRITICAL RULES (must follow):
44
+ 1. ONLY modify what is explicitly requested.
45
+ 2. PRESERVE all existing functionality, imports, and logic.
46
+ 3. If preserveLogic is true, NEVER touch:
47
+ - API calls, fetch, axios
48
+ - State management (useState, Redux, Vuex, stores)
49
+ - Event handlers that aren't purely UI
50
+ - Business logic / data transforms
51
+ 4. Maintain the existing code style and formatting.
52
+ 5. Keep existing comments unless they conflict with the requested change.
53
+ 6. Accessibility: do not regress accessibility; keep/repair aria, labels, focus handling.
54
+ 7. Prefer reusing existing components/utilities from the repo (you will receive an auto component catalog).
55
+ 8. Output MUST NOT contain explanations, markdown, or code fences.`;
56
+
57
+ if (returnDiff) {
58
+ return `${base}\n\nOutput a UNIFIED DIFF only.`;
59
+ }
60
+
61
+ return `${base}\n\nOutput the COMPLETE modified file only (raw code).`;
62
+ }
63
+
64
+ /**
65
+ * Extract code from a model response that might contain fences.
66
+ */
67
+ function cleanResponse(response: string): string {
68
+ let out = response.trim();
69
+ const codeBlockMatch = out.match(/```(?:\w+)?\n([\s\S]*?)```/);
70
+ if (codeBlockMatch) {
71
+ out = codeBlockMatch[1].trim();
72
+ }
73
+ return out;
74
+ }
75
+
76
+ export function registerModifyUI(server: McpServer, config: Config): void {
77
+ server.registerTool(
78
+ 'modify_ui',
79
+ {
80
+ title: 'Modify UI Component',
81
+ description:
82
+ 'Make surgical edits to existing UI components. Preserves logic and only modifies UI-related code. Set applyChanges=true to write directly.',
83
+ inputSchema,
84
+ },
85
+ async (args) => {
86
+ const targetFile = args.targetFile as string;
87
+ const instruction = args.instruction as string;
88
+ const contextPaths = args.context as string[] | undefined;
89
+ const preserveLogic = args.preserveLogic !== false;
90
+ const returnDiff = args.returnDiff === true;
91
+ const shouldApply = args.applyChanges === true;
92
+ const shouldBackup = args.backup !== false;
93
+
94
+ if (shouldApply && returnDiff) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: 'text' as const,
99
+ text: 'Error: applyChanges=true is not supported when returnDiff=true. Set returnDiff=false to apply changes.',
100
+ },
101
+ ],
102
+ isError: true,
103
+ };
104
+ }
105
+
106
+ // Resolve and validate the target path
107
+ let safeReadPath: string;
108
+ try {
109
+ safeReadPath = assertReadablePath(targetFile, config);
110
+ } catch (error) {
111
+ const message = error instanceof Error ? error.message : 'Invalid targetFile';
112
+ return {
113
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
114
+ isError: true,
115
+ };
116
+ }
117
+
118
+ // Read the target file
119
+ let fileContent: string;
120
+ try {
121
+ fileContent = fs.readFileSync(safeReadPath, 'utf-8');
122
+ } catch {
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text' as const,
127
+ text: `Error: Could not read file at ${targetFile}`,
128
+ },
129
+ ],
130
+ isError: true,
131
+ };
132
+ }
133
+
134
+ // Build context from related files
135
+ let contextContent = '';
136
+ if (contextPaths && contextPaths.length > 0) {
137
+ contextContent = await buildContext(contextPaths, config);
138
+ }
139
+
140
+ // Auto-inject deterministic project grounding (stack + reusable components)
141
+ const grounding = await buildRepoGrounding(config, {
142
+ focusFileAbs: safeReadPath,
143
+ instruction,
144
+ });
145
+
146
+ const systemPrompt = getSystemPrompt(returnDiff);
147
+
148
+ const userPrompt = `Modify this file according to the instruction.
149
+
150
+ ${grounding}
151
+
152
+ INSTRUCTION:
153
+ ${instruction}
154
+
155
+ PRESERVE LOGIC:
156
+ ${preserveLogic}
157
+
158
+ CURRENT FILE (${targetFile}):
159
+ ${fileContent}
160
+
161
+ ${contextContent ? `\n\nRELATED FILES FOR CONTEXT:\n${contextContent}\n` : ''}
162
+
163
+ ${returnDiff ? 'Return a unified diff only.' : 'Return the full modified file only.'}`;
164
+
165
+ try {
166
+ const response = await generateWithGemini(config, systemPrompt, userPrompt, { toolName: 'modify_ui' });
167
+ const cleaned = cleanResponse(response);
168
+
169
+ // Apply changes if requested (and not returning diff)
170
+ if (shouldApply) {
171
+ let safeWritePath: string;
172
+ try {
173
+ safeWritePath = assertWritablePath(targetFile, config);
174
+ } catch (error) {
175
+ const message = error instanceof Error ? error.message : 'Invalid targetFile';
176
+ return {
177
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
178
+ isError: true,
179
+ };
180
+ }
181
+
182
+ const result = await writeFile(safeWritePath, cleaned, {
183
+ format: true,
184
+ backup: shouldBackup,
185
+ });
186
+
187
+ if (!result.success) {
188
+ return {
189
+ content: [
190
+ {
191
+ type: 'text' as const,
192
+ text: `❌ Failed to apply changes: ${result.error || 'Unknown error'}`,
193
+ },
194
+ ],
195
+ isError: true,
196
+ };
197
+ }
198
+
199
+ const msg = [
200
+ `✅ Applied changes successfully.`,
201
+ `File: ${result.filePath}`,
202
+ result.backupPath ? `Backup: ${result.backupPath}` : undefined,
203
+ ]
204
+ .filter(Boolean)
205
+ .join('\n');
206
+
207
+ return {
208
+ content: [{ type: 'text' as const, text: msg }],
209
+ };
210
+ }
211
+
212
+ // Return diff or full file ONLY (no wrappers) for best agent compatibility
213
+ return {
214
+ content: [{ type: 'text' as const, text: cleaned }],
215
+ };
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : 'Unknown error';
218
+ return {
219
+ content: [
220
+ {
221
+ type: 'text' as const,
222
+ text: `Error modifying UI: ${message}`,
223
+ },
224
+ ],
225
+ isError: true,
226
+ };
227
+ }
228
+ }
229
+ );
230
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * scaffold_project Tool
3
+ *
4
+ * Creates a complete UI plan for a new project.
5
+ * Outputs component hierarchy, page layouts, and design tokens.
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 { generateWithGemini } from '../generation/gemini-client.js';
12
+
13
+ const inputSchema = {
14
+ projectType: z
15
+ .enum(['landing', 'dashboard', 'ecommerce', 'saas', 'blog', 'portfolio', 'docs', 'custom'])
16
+ .describe('Type of project to scaffold'),
17
+ name: z.string().describe('Project name'),
18
+ pages: z
19
+ .array(z.string())
20
+ .describe('List of pages to plan (e.g., ["Home", "Pricing", "About", "Contact"])'),
21
+ vibe: z.string().optional().describe('Selected design vibe from generate_vibes'),
22
+ features: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe('Special features (e.g., ["dark mode", "i18n", "auth", "animations"])'),
26
+ framework: z
27
+ .enum(['vanilla', 'react', 'vue', 'svelte', 'nextjs'])
28
+ .optional()
29
+ .describe('Target framework'),
30
+ };
31
+
32
+ const SYSTEM_PROMPT = `You are a senior frontend architect creating a comprehensive UI plan for a new project.
33
+
34
+ Return ONE valid JSON object only. No markdown. No code fences. No extra commentary.
35
+
36
+ Output structure (example):
37
+ {
38
+ "projectName": "name",
39
+ "projectType": "saas",
40
+ "framework": "nextjs",
41
+ "structure": {
42
+ "components": {
43
+ "common": ["Button", "Input", "Card"],
44
+ "layout": ["Header", "Footer", "Sidebar"],
45
+ "features": { "auth": ["LoginForm", "SignupForm"] }
46
+ },
47
+ "pages": {
48
+ "Home": {
49
+ "path": "/",
50
+ "layout": "MarketingLayout",
51
+ "components": ["Hero", "Features", "CTA"],
52
+ "description": "Page purpose"
53
+ }
54
+ }
55
+ },
56
+ "designTokens": {
57
+ "colors": {},
58
+ "typography": {},
59
+ "spacing": {},
60
+ "breakpoints": {}
61
+ },
62
+ "fileStructure": [
63
+ "src/components/common/Button.tsx",
64
+ "src/components/layout/Header.tsx",
65
+ "src/pages/Home.tsx"
66
+ ],
67
+ "dependencies": [],
68
+ "setupInstructions": []
69
+ }
70
+
71
+ Focus on:
72
+ - Reusable component identification
73
+ - Consistent naming conventions
74
+ - Logical file organization
75
+ - Clear responsibilities (avoid bloated god-components)
76
+ - Accessibility defaults (WCAG AA) for interactive components`;
77
+
78
+ export function registerScaffoldProject(server: McpServer, config: Config): void {
79
+ server.registerTool(
80
+ 'scaffold_project',
81
+ {
82
+ title: 'Scaffold Project',
83
+ description:
84
+ 'Create a complete UI architecture plan for a new project including component hierarchy, pages, and design tokens.',
85
+ inputSchema,
86
+ },
87
+ async (args) => {
88
+ const projectType = args.projectType as string;
89
+ const name = args.name as string;
90
+ const pages = args.pages as string[];
91
+ const vibe = args.vibe as string | undefined;
92
+ const features = args.features as string[] | undefined;
93
+ const framework =
94
+ (args.framework as 'vanilla' | 'react' | 'vue' | 'svelte' | 'nextjs') ||
95
+ config.defaultFramework;
96
+
97
+ let userPrompt = `Create a UI scaffold for a ${projectType} project called "${name}".
98
+
99
+ Framework: ${framework}
100
+ Pages: ${pages.join(', ')}`;
101
+
102
+ if (features && features.length > 0) {
103
+ userPrompt += `\nFeatures: ${features.join(', ')}`;
104
+ }
105
+
106
+ if (vibe) {
107
+ userPrompt += `\n\nDesign vibe/tokens:\n${vibe}`;
108
+ }
109
+
110
+ userPrompt += '\n\nReturn valid JSON only.';
111
+
112
+ try {
113
+ const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, { toolName: 'scaffold_project' });
114
+
115
+ // IMPORTANT: Return raw JSON ONLY (no wrappers) so agents can parse it reliably.
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text' as const,
120
+ text: response.trim(),
121
+ },
122
+ ],
123
+ };
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : 'Unknown error';
126
+ return {
127
+ content: [
128
+ {
129
+ type: 'text' as const,
130
+ text: `Error scaffolding project: ${message}`,
131
+ },
132
+ ],
133
+ isError: true,
134
+ };
135
+ }
136
+ }
137
+ );
138
+ }