gemini-design-mcp 3.5.1 → 3.6.1

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/build/index.js CHANGED
@@ -58,7 +58,9 @@ Present options to user, they select one, then pass it here via designSystem.vib
58
58
  📤 OUTPUT
59
59
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60
60
 
61
- Returns a COMPLETE file ready to save.`, createFrontendSchema, createFrontend);
61
+ Returns a COMPLETE file ready to save.
62
+
63
+ 💡 TIP: Use writeFile: true to write directly to disk (faster, lighter context).`, createFrontendSchema, createFrontend);
62
64
  // =============================================================================
63
65
  // TOOL 2: MODIFY_FRONTEND
64
66
  // =============================================================================
@@ -100,7 +102,9 @@ import { X } from "y";
100
102
  <exact existing code>
101
103
 
102
104
  // REPLACE WITH:
103
- <new redesigned code>`, modifyFrontendSchema, modifyFrontend);
105
+ <new redesigned code>
106
+
107
+ 💡 TIP: Use writeFile: true to apply the modification directly to disk.`, modifyFrontendSchema, modifyFrontend);
104
108
  // =============================================================================
105
109
  // TOOL 3: SNIPPET_FRONTEND
106
110
  // =============================================================================
@@ -210,7 +214,9 @@ import { Search } from "lucide-react";
210
214
  You (Claude) are responsible for:
211
215
  - Adding the logic (useState, handlers) BEFORE calling this tool
212
216
  - Merging new imports
213
- - Inserting the snippet at the correct location`, snippetFrontendSchema, snippetFrontend);
217
+ - Inserting the snippet at the correct location
218
+
219
+ 💡 TIP: Use writeFile: true + insertAtLine or insertAfterPattern to insert directly.`, snippetFrontendSchema, snippetFrontend);
214
220
  // =============================================================================
215
221
  // TOOL 4: GENERATE_VIBES
216
222
  // =============================================================================
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Write a file, creating parent directories if they don't exist
3
+ */
4
+ export declare function writeFileWithDirs(filePath: string, content: string): void;
5
+ /**
6
+ * Read a file if it exists, return null otherwise
7
+ */
8
+ export declare function readFileIfExists(filePath: string): string | null;
9
+ /**
10
+ * Get file size in human readable format
11
+ */
12
+ export declare function getFileSize(content: string): string;
13
+ /**
14
+ * Parse FIND/REPLACE format from Gemini output
15
+ * Returns null if parsing fails
16
+ */
17
+ export declare function parseFindReplace(geminiOutput: string): {
18
+ imports?: string;
19
+ find: string;
20
+ replace: string;
21
+ } | null;
22
+ /**
23
+ * Parse SNIPPET format from Gemini output
24
+ * Returns null if parsing fails
25
+ */
26
+ export declare function parseSnippet(geminiOutput: string): {
27
+ imports?: string;
28
+ snippet: string;
29
+ } | null;
30
+ /**
31
+ * Apply find/replace to file content
32
+ */
33
+ export declare function applyFindReplace(fileContent: string, find: string, replace: string): {
34
+ success: boolean;
35
+ newContent: string;
36
+ error?: string;
37
+ };
38
+ /**
39
+ * Merge new imports into existing file content
40
+ */
41
+ export declare function mergeImports(fileContent: string, newImports: string): string;
42
+ /**
43
+ * Insert snippet after a specific line number (1-indexed)
44
+ */
45
+ export declare function insertAfterLine(fileContent: string, snippet: string, lineNumber: number): string;
46
+ /**
47
+ * Insert snippet after a pattern match
48
+ */
49
+ export declare function insertAfterPattern(fileContent: string, snippet: string, pattern: string): {
50
+ success: boolean;
51
+ newContent: string;
52
+ error?: string;
53
+ };
@@ -0,0 +1,145 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ /**
4
+ * Write a file, creating parent directories if they don't exist
5
+ */
6
+ export function writeFileWithDirs(filePath, content) {
7
+ const dir = path.dirname(filePath);
8
+ fs.mkdirSync(dir, { recursive: true });
9
+ fs.writeFileSync(filePath, content, "utf-8");
10
+ }
11
+ /**
12
+ * Read a file if it exists, return null otherwise
13
+ */
14
+ export function readFileIfExists(filePath) {
15
+ if (fs.existsSync(filePath)) {
16
+ return fs.readFileSync(filePath, "utf-8");
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Get file size in human readable format
22
+ */
23
+ export function getFileSize(content) {
24
+ const bytes = Buffer.byteLength(content, "utf-8");
25
+ if (bytes < 1024)
26
+ return `${bytes} B`;
27
+ return `${(bytes / 1024).toFixed(1)} KB`;
28
+ }
29
+ /**
30
+ * Parse FIND/REPLACE format from Gemini output
31
+ * Returns null if parsing fails
32
+ */
33
+ export function parseFindReplace(geminiOutput) {
34
+ try {
35
+ let imports;
36
+ // Extract imports if present
37
+ const importsMatch = geminiOutput.match(/\/\/ NEW IMPORTS NEEDED:\s*([\s\S]*?)(?=\/\/ FIND THIS CODE:)/);
38
+ if (importsMatch) {
39
+ imports = importsMatch[1].trim();
40
+ }
41
+ // Extract find section
42
+ const findMatch = geminiOutput.match(/\/\/ FIND THIS CODE:\s*([\s\S]*?)(?=\/\/ REPLACE WITH:)/);
43
+ if (!findMatch)
44
+ return null;
45
+ // Extract replace section
46
+ const replaceMatch = geminiOutput.match(/\/\/ REPLACE WITH:\s*([\s\S]*?)$/);
47
+ if (!replaceMatch)
48
+ return null;
49
+ return {
50
+ imports: imports || undefined,
51
+ find: findMatch[1].trim(),
52
+ replace: replaceMatch[1].trim(),
53
+ };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Parse SNIPPET format from Gemini output
61
+ * Returns null if parsing fails
62
+ */
63
+ export function parseSnippet(geminiOutput) {
64
+ try {
65
+ let imports;
66
+ // Extract imports if present
67
+ const importsMatch = geminiOutput.match(/\/\/ NEW IMPORTS NEEDED:\s*([\s\S]*?)(?=\/\/ SNIPPET:)/);
68
+ if (importsMatch) {
69
+ imports = importsMatch[1].trim();
70
+ }
71
+ // Extract snippet
72
+ const snippetMatch = geminiOutput.match(/\/\/ SNIPPET:\s*([\s\S]*?)$/);
73
+ if (!snippetMatch) {
74
+ // If no SNIPPET marker, assume the whole thing (minus imports) is the snippet
75
+ return { imports, snippet: geminiOutput.trim() };
76
+ }
77
+ return {
78
+ imports: imports || undefined,
79
+ snippet: snippetMatch[1].trim(),
80
+ };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Apply find/replace to file content
88
+ */
89
+ export function applyFindReplace(fileContent, find, replace) {
90
+ if (!fileContent.includes(find)) {
91
+ return {
92
+ success: false,
93
+ newContent: fileContent,
94
+ error: "Could not find the target code in file",
95
+ };
96
+ }
97
+ const newContent = fileContent.replace(find, replace);
98
+ return { success: true, newContent };
99
+ }
100
+ /**
101
+ * Merge new imports into existing file content
102
+ */
103
+ export function mergeImports(fileContent, newImports) {
104
+ const lines = fileContent.split("\n");
105
+ // Find the last import line
106
+ let lastImportIndex = -1;
107
+ for (let i = 0; i < lines.length; i++) {
108
+ if (lines[i].startsWith("import ") || lines[i].startsWith("import{")) {
109
+ lastImportIndex = i;
110
+ }
111
+ }
112
+ if (lastImportIndex === -1) {
113
+ // No imports found, add at the beginning
114
+ return newImports + "\n" + fileContent;
115
+ }
116
+ // Insert after last import
117
+ lines.splice(lastImportIndex + 1, 0, newImports);
118
+ return lines.join("\n");
119
+ }
120
+ /**
121
+ * Insert snippet after a specific line number (1-indexed)
122
+ */
123
+ export function insertAfterLine(fileContent, snippet, lineNumber) {
124
+ const lines = fileContent.split("\n");
125
+ lines.splice(lineNumber, 0, snippet);
126
+ return lines.join("\n");
127
+ }
128
+ /**
129
+ * Insert snippet after a pattern match
130
+ */
131
+ export function insertAfterPattern(fileContent, snippet, pattern) {
132
+ const index = fileContent.indexOf(pattern);
133
+ if (index === -1) {
134
+ return {
135
+ success: false,
136
+ newContent: fileContent,
137
+ error: `Pattern not found: ${pattern}`,
138
+ };
139
+ }
140
+ const endOfPattern = index + pattern.length;
141
+ const newContent = fileContent.slice(0, endOfPattern) +
142
+ "\n" + snippet +
143
+ fileContent.slice(endOfPattern);
144
+ return { success: true, newContent };
145
+ }
@@ -36,6 +36,7 @@ export declare const createFrontendSchema: {
36
36
  keywords: string[];
37
37
  };
38
38
  }>>;
39
+ writeFile: z.ZodOptional<z.ZodBoolean>;
39
40
  };
40
41
  export declare function createFrontend(params: {
41
42
  request: string;
@@ -50,6 +51,7 @@ export declare function createFrontend(params: {
50
51
  keywords: string[];
51
52
  };
52
53
  };
54
+ writeFile?: boolean;
53
55
  }): Promise<{
54
56
  content: {
55
57
  type: "text";
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { generateWithGemini } from "../lib/gemini.js";
3
3
  import { CREATE_FRONTEND_PROMPT } from "../prompts/system.js";
4
+ import { writeFileWithDirs, getFileSize } from "../lib/filesystem.js";
4
5
  export const createFrontendSchema = {
5
6
  request: z.string().describe("What to create: describe the page, component, or section. " +
6
7
  "Be specific about functionality and content. " +
@@ -21,24 +22,53 @@ export const createFrontendSchema = {
21
22
  }).describe("The selected design vibe"),
22
23
  }).optional().describe("Design system with selected vibe. REQUIRED for new projects without existing design. " +
23
24
  "Call generate_vibes first, user selects, then pass the selection here."),
25
+ writeFile: z.boolean().optional().describe("If true, write the file directly to disk instead of returning the code. " +
26
+ "Returns a confirmation message instead of the full code, keeping context lightweight."),
24
27
  };
25
28
  export async function createFrontend(params) {
26
- const { request, filePath, techStack, context, designSystem } = params;
29
+ const { request, filePath, techStack, context, designSystem, writeFile } = params;
27
30
  // Build design system instructions if provided
28
31
  let designSystemInstructions = '';
29
32
  if (designSystem?.vibe) {
30
33
  const { vibe } = designSystem;
31
34
  const scale = vibe.scale || "balanced";
35
+ // Conceptual scale descriptions - let Gemini decide exact values
36
+ const scaleDescriptions = {
37
+ refined: `**SCALE: REFINED**
38
+ This is about ELEMENT SIZE, not density. Refined means:
39
+ - Smaller, more elegant typography — avoid oversized headlines
40
+ - Compact, contained buttons — not chunky or oversized
41
+ - Cards and containers with constrained widths — not full-width sprawling layouts
42
+ - Tighter, purposeful spacing between elements
43
+ - Smaller icons that complement rather than dominate
44
+
45
+ Reference: Linear.app, Raycast, Notion — these apps feel sophisticated because their elements are proportionally small and refined, never billboard-sized or bloated.
46
+
47
+ The interface should feel like a premium Swiss watch: precise, elegant, where every element is intentionally sized smaller than the default.`,
48
+ balanced: `**SCALE: BALANCED**
49
+ Standard, conventional sizing. Use typical defaults — nothing particularly large or small. A middle-ground that works for most general-purpose applications.`,
50
+ zoomed: `**SCALE: ZOOMED**
51
+ This is about ELEMENT SIZE, not density. Zoomed means:
52
+ - Large, prominent typography — bold headlines that command attention
53
+ - Big, chunky buttons — easy to see and click/tap
54
+ - Wide cards and containers — full-width or near full-width layouts
55
+ - Generous spacing and padding throughout
56
+ - Large icons that stand out
57
+
58
+ Reference: Kids apps, accessibility-focused interfaces, bold marketing sites — elements are intentionally oversized for impact and ease of use.`
59
+ };
32
60
  designSystemInstructions = `
33
61
  MANDATORY DESIGN SYSTEM (User Selected Vibe):
34
62
 
35
63
  **Design Vibe: "${vibe.name}"**
36
64
  ${vibe.description}
37
65
 
38
- **Scale: ${scale}**
66
+ ${scaleDescriptions[scale] || scaleDescriptions.balanced}
67
+
39
68
  Keywords: ${vibe.keywords.join(', ')}
40
69
 
41
70
  Interpret this vibe creatively — choose colors, typography, and styling that embody this atmosphere.
71
+ The scale above is MANDATORY and defines how large or small UI elements should be.
42
72
  `;
43
73
  }
44
74
  // Build context instructions (ignore empty strings and "null" strings)
@@ -64,6 +94,18 @@ FILE PATH: ${filePath}
64
94
 
65
95
  Remember: Return a COMPLETE file ready to save at ${filePath}`.trim();
66
96
  const result = await generateWithGemini(systemPrompt, request, undefined, "high");
97
+ // Write file directly if requested
98
+ if (writeFile) {
99
+ writeFileWithDirs(filePath, result);
100
+ const size = getFileSize(result);
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: `✅ Created: ${filePath} (${size})`,
105
+ }],
106
+ };
107
+ }
108
+ // Default: return the code
67
109
  return {
68
110
  content: [{ type: "text", text: result }],
69
111
  };
@@ -4,12 +4,14 @@ export declare const modifyFrontendSchema: {
4
4
  targetCode: z.ZodString;
5
5
  filePath: z.ZodString;
6
6
  context: z.ZodOptional<z.ZodString>;
7
+ writeFile: z.ZodOptional<z.ZodBoolean>;
7
8
  };
8
9
  export declare function modifyFrontend(params: {
9
10
  modification: string;
10
11
  targetCode: string;
11
12
  filePath: string;
12
13
  context?: string;
14
+ writeFile?: boolean;
13
15
  }): Promise<{
14
16
  content: {
15
17
  type: "text";
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { generateWithGemini } from "../lib/gemini.js";
3
3
  import { MODIFY_FRONTEND_PROMPT } from "../prompts/system.js";
4
+ import { readFileIfExists, writeFileWithDirs, parseFindReplace, applyFindReplace, mergeImports, } from "../lib/filesystem.js";
4
5
  export const modifyFrontendSchema = {
5
6
  modification: z.string().describe("The SINGLE design modification to make. Be specific. " +
6
7
  "Examples: " +
@@ -14,9 +15,12 @@ export const modifyFrontendSchema = {
14
15
  "Example: 'src/components/Sidebar.tsx'"),
15
16
  context: z.string().optional().describe("Additional context about the file's design system. " +
16
17
  "Example: 'Uses Tailwind, dark theme with zinc colors, rounded-xl borders'"),
18
+ writeFile: z.boolean().optional().describe("If true, apply the modification directly to the file on disk. " +
19
+ "Reads the file, applies find/replace, and writes back. " +
20
+ "Returns a confirmation instead of the find/replace instructions."),
17
21
  };
18
22
  export async function modifyFrontend(params) {
19
- const { modification, targetCode, filePath, context } = params;
23
+ const { modification, targetCode, filePath, context, writeFile } = params;
20
24
  // Build context instructions
21
25
  let contextInstructions = '';
22
26
  if (context) {
@@ -38,6 +42,52 @@ MODIFICATION REQUESTED: ${modification}
38
42
 
39
43
  Remember: Return ONLY the find/replace block. ONE modification, surgical precision.`.trim();
40
44
  const result = await generateWithGemini(systemPrompt, modification, undefined, "minimal");
45
+ // Apply modification directly if requested
46
+ if (writeFile) {
47
+ const fileContent = readFileIfExists(filePath);
48
+ if (!fileContent) {
49
+ return {
50
+ content: [{
51
+ type: "text",
52
+ text: `❌ Error: File not found: ${filePath}`,
53
+ }],
54
+ };
55
+ }
56
+ const parsed = parseFindReplace(result);
57
+ if (!parsed) {
58
+ // Fallback: return raw result if parsing fails
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: `⚠️ Could not parse Gemini output. Raw result:\n\n${result}`,
63
+ }],
64
+ };
65
+ }
66
+ // Apply the find/replace
67
+ const { success, newContent, error } = applyFindReplace(fileContent, parsed.find, parsed.replace);
68
+ if (!success) {
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: `❌ Error: ${error}\n\nExpected to find:\n${parsed.find.slice(0, 200)}...`,
73
+ }],
74
+ };
75
+ }
76
+ // Merge imports if any
77
+ let finalContent = newContent;
78
+ if (parsed.imports) {
79
+ finalContent = mergeImports(newContent, parsed.imports);
80
+ }
81
+ // Write the file
82
+ writeFileWithDirs(filePath, finalContent);
83
+ return {
84
+ content: [{
85
+ type: "text",
86
+ text: `✅ Modified: ${filePath}${parsed.imports ? `\n Added imports` : ""}`,
87
+ }],
88
+ };
89
+ }
90
+ // Default: return the find/replace instructions
41
91
  return {
42
92
  content: [{ type: "text", text: result }],
43
93
  };
@@ -5,6 +5,9 @@ export declare const snippetFrontendSchema: {
5
5
  techStack: z.ZodString;
6
6
  insertionContext: z.ZodString;
7
7
  context: z.ZodOptional<z.ZodString>;
8
+ writeFile: z.ZodOptional<z.ZodBoolean>;
9
+ insertAtLine: z.ZodOptional<z.ZodNumber>;
10
+ insertAfterPattern: z.ZodOptional<z.ZodString>;
8
11
  };
9
12
  export declare function snippetFrontend(params: {
10
13
  request: string;
@@ -12,6 +15,9 @@ export declare function snippetFrontend(params: {
12
15
  techStack: string;
13
16
  insertionContext: string;
14
17
  context?: string;
18
+ writeFile?: boolean;
19
+ insertAtLine?: number;
20
+ insertAfterPattern?: string;
15
21
  }): Promise<{
16
22
  content: {
17
23
  type: "text";
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { generateWithGemini } from "../lib/gemini.js";
3
3
  import { SNIPPET_FRONTEND_PROMPT } from "../prompts/system.js";
4
+ import { readFileIfExists, writeFileWithDirs, parseSnippet, mergeImports, insertAfterLine, insertAfterPattern, } from "../lib/filesystem.js";
4
5
  export const snippetFrontendSchema = {
5
6
  request: z.string().describe("What code snippet to generate. Be specific about what you need. " +
6
7
  "Examples: " +
@@ -19,9 +20,17 @@ export const snippetFrontendSchema = {
19
20
  context: z.string().optional().describe("Additional project files for design/pattern reference. " +
20
21
  "Pass components, styles, or utilities that the snippet should match. " +
21
22
  "Optional but recommended for consistency."),
23
+ writeFile: z.boolean().optional().describe("If true, insert the snippet directly into the file on disk. " +
24
+ "Requires insertAtLine OR insertAfterPattern to know where to insert."),
25
+ insertAtLine: z.number().optional().describe("Line number (1-indexed) where to insert the snippet. " +
26
+ "The snippet will be inserted AFTER this line. " +
27
+ "Required if writeFile is true and insertAfterPattern is not provided."),
28
+ insertAfterPattern: z.string().optional().describe("A string pattern to find in the file. The snippet will be inserted after this pattern. " +
29
+ "Example: 'return (' or '<main>' or '</header>'. " +
30
+ "Required if writeFile is true and insertAtLine is not provided."),
22
31
  };
23
32
  export async function snippetFrontend(params) {
24
- const { request, targetFile, techStack, insertionContext, context } = params;
33
+ const { request, targetFile, techStack, insertionContext, context, writeFile, insertAtLine: lineNumber, insertAfterPattern: pattern, } = params;
25
34
  // Build context instructions
26
35
  let contextInstructions = '';
27
36
  if (context) {
@@ -40,6 +49,69 @@ ${insertionContext}
40
49
 
41
50
  Generate a snippet that will integrate smoothly at this location.`.trim();
42
51
  const result = await generateWithGemini(systemPrompt, request, undefined, "minimal");
52
+ // Insert snippet directly if requested
53
+ if (writeFile) {
54
+ if (!lineNumber && !pattern) {
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: `❌ Error: writeFile requires either insertAtLine or insertAfterPattern`,
59
+ }],
60
+ };
61
+ }
62
+ const fileContent = readFileIfExists(targetFile);
63
+ if (!fileContent) {
64
+ return {
65
+ content: [{
66
+ type: "text",
67
+ text: `❌ Error: File not found: ${targetFile}`,
68
+ }],
69
+ };
70
+ }
71
+ const parsed = parseSnippet(result);
72
+ if (!parsed) {
73
+ return {
74
+ content: [{
75
+ type: "text",
76
+ text: `⚠️ Could not parse snippet. Raw result:\n\n${result}`,
77
+ }],
78
+ };
79
+ }
80
+ let newContent;
81
+ // Insert using line number or pattern
82
+ if (lineNumber) {
83
+ newContent = insertAfterLine(fileContent, parsed.snippet, lineNumber);
84
+ }
85
+ else if (pattern) {
86
+ const insertResult = insertAfterPattern(fileContent, parsed.snippet, pattern);
87
+ if (!insertResult.success) {
88
+ return {
89
+ content: [{
90
+ type: "text",
91
+ text: `❌ Error: ${insertResult.error}`,
92
+ }],
93
+ };
94
+ }
95
+ newContent = insertResult.newContent;
96
+ }
97
+ else {
98
+ // Should never reach here due to earlier check
99
+ newContent = fileContent;
100
+ }
101
+ // Merge imports if any
102
+ if (parsed.imports) {
103
+ newContent = mergeImports(newContent, parsed.imports);
104
+ }
105
+ // Write the file
106
+ writeFileWithDirs(targetFile, newContent);
107
+ return {
108
+ content: [{
109
+ type: "text",
110
+ text: `✅ Inserted snippet in: ${targetFile}${parsed.imports ? `\n Added imports` : ""}${lineNumber ? `\n After line ${lineNumber}` : ""}${pattern ? `\n After pattern: ${pattern}` : ""}`,
111
+ }],
112
+ };
113
+ }
114
+ // Default: return the snippet
43
115
  return {
44
116
  content: [{ type: "text", text: result }],
45
117
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-design-mcp",
3
- "version": "3.5.1",
3
+ "version": "3.6.1",
4
4
  "description": "MCP server that uses Gemini 3 Pro for frontend/design code generation",
5
5
  "main": "build/index.js",
6
6
  "bin": {