@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
@@ -10,203 +10,237 @@
10
10
 
11
11
  import * as path from 'node:path';
12
12
 
13
- export type TokenType = 'color' | 'dimension' | 'fontFamily' | 'fontWeight' | 'number' | 'string' | 'shadow' | 'duration' | 'easing';
13
+ export type TokenType =
14
+ | 'color'
15
+ | 'dimension'
16
+ | 'fontFamily'
17
+ | 'fontWeight'
18
+ | 'number'
19
+ | 'string'
20
+ | 'shadow'
21
+ | 'duration'
22
+ | 'easing';
14
23
 
15
24
  export interface Token {
16
- name: string; // normalized token name, e.g. color.primary
17
- value: string;
18
- type: TokenType;
19
- description?: string;
20
- source?: {
21
- file?: string;
22
- originalName?: string;
23
- };
25
+ name: string; // normalized token name, e.g. color.primary
26
+ value: string;
27
+ type: TokenType;
28
+ description?: string;
29
+ source?: {
30
+ file?: string;
31
+ originalName?: string;
32
+ };
24
33
  }
25
34
 
26
35
  export interface TokenParseResult {
27
- tokens: Token[];
28
- warnings: string[];
36
+ tokens: Token[];
37
+ warnings: string[];
29
38
  }
30
39
 
31
40
  function isColorValue(v: string): boolean {
32
- const s = v.trim().toLowerCase();
33
- return (
34
- /^#([0-9a-f]{3,8})$/.test(s) ||
35
- /^rgb\(/.test(s) ||
36
- /^rgba\(/.test(s) ||
37
- /^hsl\(/.test(s) ||
38
- /^hsla\(/.test(s) ||
39
- /^oklch\(/.test(s) ||
40
- /^oklab\(/.test(s)
41
- );
41
+ const s = v.trim().toLowerCase();
42
+ return (
43
+ /^#([0-9a-f]{3,8})$/.test(s) ||
44
+ /^rgb\(/.test(s) ||
45
+ /^rgba\(/.test(s) ||
46
+ /^hsl\(/.test(s) ||
47
+ /^hsla\(/.test(s) ||
48
+ /^oklch\(/.test(s) ||
49
+ /^oklab\(/.test(s)
50
+ );
42
51
  }
43
52
 
44
53
  function isDimensionValue(v: string): boolean {
45
- const s = v.trim().toLowerCase();
46
- return /^-?\d+(\.\d+)?(px|rem|em|vh|vw|%|ch|ex)$/.test(s);
54
+ const s = v.trim().toLowerCase();
55
+ return /^-?\d+(\.\d+)?(px|rem|em|vh|vw|%|ch|ex)$/.test(s);
47
56
  }
48
57
 
49
58
  function isDurationValue(v: string): boolean {
50
- const s = v.trim().toLowerCase();
51
- return /^\d+(\.\d+)?(ms|s)$/.test(s);
59
+ const s = v.trim().toLowerCase();
60
+ return /^\d+(\.\d+)?(ms|s)$/.test(s);
52
61
  }
53
62
 
54
63
  function inferTokenType(name: string, value: string): TokenType {
55
- const n = name.toLowerCase();
56
- const v = value.trim();
57
- if (isColorValue(v) || n.includes('color') || n.includes('bg') || n.includes('foreground') || n.includes('surface')) return 'color';
58
- if (isDurationValue(v) || n.includes('duration') || n.includes('transition')) return 'duration';
59
- if (n.includes('easing') || n.includes('ease')) return 'easing';
60
- if (n.includes('shadow') || v.includes('rgba') && v.includes('px')) return 'shadow';
61
- if (n.includes('font') && n.includes('family')) return 'fontFamily';
62
- if (n.includes('weight')) return 'fontWeight';
63
- if (isDimensionValue(v) || n.includes('space') || n.includes('radius') || n.includes('size') || n.includes('gap')) return 'dimension';
64
- if (/^-?\d+(\.\d+)?$/.test(v)) return 'number';
65
- return 'string';
64
+ const n = name.toLowerCase();
65
+ const v = value.trim();
66
+ if (
67
+ isColorValue(v) ||
68
+ n.includes('color') ||
69
+ n.includes('bg') ||
70
+ n.includes('foreground') ||
71
+ n.includes('surface')
72
+ )
73
+ return 'color';
74
+ if (isDurationValue(v) || n.includes('duration') || n.includes('transition')) return 'duration';
75
+ if (n.includes('easing') || n.includes('ease')) return 'easing';
76
+ if (n.includes('shadow') || (v.includes('rgba') && v.includes('px'))) return 'shadow';
77
+ if (n.includes('font') && n.includes('family')) return 'fontFamily';
78
+ if (n.includes('weight')) return 'fontWeight';
79
+ if (
80
+ isDimensionValue(v) ||
81
+ n.includes('space') ||
82
+ n.includes('radius') ||
83
+ n.includes('size') ||
84
+ n.includes('gap')
85
+ )
86
+ return 'dimension';
87
+ if (/^-?\d+(\.\d+)?$/.test(v)) return 'number';
88
+ return 'string';
66
89
  }
67
90
 
68
91
  function normalizeCssVarNameToTokenName(cssVar: string): string {
69
- // '--color-primary' -> 'color.primary'
70
- // '--radius-md' -> 'radius.md'
71
- let n = cssVar.trim();
72
- if (n.startsWith('--')) n = n.slice(2);
73
- n = n.replace(/\s+/g, '-');
74
- // Convert separators to dots for nesting
75
- return n
76
- .replace(/__/g, '.')
77
- .replace(/--/g, '.')
78
- .replace(/-/g, '.')
79
- .replace(/\.+/g, '.')
80
- .replace(/^\./, '')
81
- .replace(/\.$/, '');
92
+ // '--color-primary' -> 'color.primary'
93
+ // '--radius-md' -> 'radius.md'
94
+ let n = cssVar.trim();
95
+ if (n.startsWith('--')) n = n.slice(2);
96
+ n = n.replace(/\s+/g, '-');
97
+ // Convert separators to dots for nesting
98
+ return n
99
+ .replace(/__/g, '.')
100
+ .replace(/--/g, '.')
101
+ .replace(/-/g, '.')
102
+ .replace(/\.+/g, '.')
103
+ .replace(/^\./, '')
104
+ .replace(/\.$/, '');
82
105
  }
83
106
 
84
107
  export function parseCssVars(content: string, sourceFile?: string): TokenParseResult {
85
- const warnings: string[] = [];
86
- const tokens: Token[] = [];
87
-
88
- // Very simple parse: extract any --var: value; pairs
89
- const re = /(--[A-Za-z0-9_-]+)\s*:\s*([^;\n\r]+)\s*;/g;
90
- let match: RegExpExecArray | null;
91
- const seen = new Set<string>();
92
-
93
- while ((match = re.exec(content))) {
94
- const originalName = match[1];
95
- const value = match[2].trim();
96
- const name = normalizeCssVarNameToTokenName(originalName);
97
- if (!name) continue;
98
- if (seen.has(name)) continue;
99
- seen.add(name);
100
-
101
- tokens.push({
102
- name,
103
- value,
104
- type: inferTokenType(name, value),
105
- source: {
106
- file: sourceFile ? path.basename(sourceFile) : undefined,
107
- originalName,
108
- },
109
- });
110
- }
108
+ const warnings: string[] = [];
109
+ const tokens: Token[] = [];
110
+
111
+ // Very simple parse: extract any --var: value; pairs
112
+ const re = /(--[A-Za-z0-9_-]+)\s*:\s*([^;\n\r]+)\s*;/g;
113
+ let match: RegExpExecArray | null;
114
+ const seen = new Set<string>();
115
+
116
+ while ((match = re.exec(content))) {
117
+ const originalName = match[1];
118
+ const value = match[2].trim();
119
+ const name = normalizeCssVarNameToTokenName(originalName);
120
+ if (!name) continue;
121
+ if (seen.has(name)) continue;
122
+ seen.add(name);
123
+
124
+ tokens.push({
125
+ name,
126
+ value,
127
+ type: inferTokenType(name, value),
128
+ source: {
129
+ file: sourceFile ? path.basename(sourceFile) : undefined,
130
+ originalName,
131
+ },
132
+ });
133
+ }
134
+
135
+ if (tokens.length === 0) {
136
+ warnings.push('No CSS custom properties found (expected patterns like --token-name: value;)');
137
+ }
138
+
139
+ return { tokens, warnings };
140
+ }
141
+
142
+ function setNested(obj: Record<string, unknown>, pathParts: string[], value: unknown): void {
143
+ // Protect against prototype pollution
144
+ const dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);
145
+
146
+ let cur: Record<string, unknown> = obj;
147
+ for (let i = 0; i < pathParts.length; i++) {
148
+ const key = pathParts[i];
111
149
 
112
- if (tokens.length === 0) {
113
- warnings.push('No CSS custom properties found (expected patterns like --token-name: value;)');
150
+ // Block dangerous keys
151
+ if (dangerousKeys.has(key)) {
152
+ return;
114
153
  }
115
154
 
116
- return { tokens, warnings };
117
- }
155
+ if (i === pathParts.length - 1) {
156
+ cur[key] = value;
157
+ return;
158
+ }
118
159
 
119
- function setNested(obj: Record<string, unknown>, pathParts: string[], value: unknown): void {
120
- // Protect against prototype pollution
121
- const dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);
122
-
123
- let cur: Record<string, unknown> = obj;
124
- for (let i = 0; i < pathParts.length; i++) {
125
- const key = pathParts[i];
126
-
127
- // Block dangerous keys
128
- if (dangerousKeys.has(key)) {
129
- return;
130
- }
131
-
132
- if (i === pathParts.length - 1) {
133
- cur[key] = value;
134
- return;
135
- }
136
-
137
- if (!Object.hasOwn(cur, key) || typeof cur[key] !== 'object' || cur[key] === null) {
138
- cur[key] = Object.create(null) as Record<string, unknown>;
139
- }
140
- cur = cur[key] as Record<string, unknown>;
160
+ if (!Object.hasOwn(cur, key) || typeof cur[key] !== 'object' || cur[key] === null) {
161
+ cur[key] = Object.create(null) as Record<string, unknown>;
141
162
  }
163
+ cur = cur[key] as Record<string, unknown>;
164
+ }
142
165
  }
143
166
 
144
167
  export function tokensToCssVars(tokens: Token[], options?: { selector?: string }): string {
145
- const selector = options?.selector || ':root';
146
- const lines = [`${selector} {`];
147
- for (const t of tokens) {
148
- const cssName = `--${t.name.replace(/\./g, '-')}`;
149
- lines.push(` ${cssName}: ${t.value};`);
150
- }
151
- lines.push('}');
152
- return lines.join('\n');
168
+ const selector = options?.selector || ':root';
169
+ const lines = [`${selector} {`];
170
+ for (const t of tokens) {
171
+ const cssName = `--${t.name.replace(/\./g, '-')}`;
172
+ lines.push(` ${cssName}: ${t.value};`);
173
+ }
174
+ lines.push('}');
175
+ return lines.join('\n');
153
176
  }
154
177
 
155
178
  export function tokensToTailwindTheme(tokens: Token[]): string {
156
- const extend: any = { colors: {}, spacing: {}, borderRadius: {}, boxShadow: {}, fontFamily: {}, fontWeight: {}, transitionDuration: {} };
157
-
158
- for (const t of tokens) {
159
- const parts = t.name.split('.').filter(Boolean);
160
- if (parts.length === 0) continue;
161
- const root = parts[0];
162
- const rest = parts.slice(1);
163
-
164
- if (t.type === 'color' && (root === 'color' || root === 'colors')) {
165
- setNested(extend.colors, rest.length ? rest : ['DEFAULT'], t.value);
166
- } else if (t.type === 'dimension' && (root === 'space' || root === 'spacing')) {
167
- setNested(extend.spacing, rest.length ? rest : ['DEFAULT'], t.value);
168
- } else if (t.type === 'dimension' && (root === 'radius' || root === 'radii' || root === 'borderRadius')) {
169
- setNested(extend.borderRadius, rest.length ? rest : ['DEFAULT'], t.value);
170
- } else if (t.type === 'shadow' && (root === 'shadow' || root === 'shadows')) {
171
- setNested(extend.boxShadow, rest.length ? rest : ['DEFAULT'], t.value);
172
- } else if (t.type === 'fontFamily') {
173
- setNested(extend.fontFamily, rest.length ? rest : ['sans'], t.value);
174
- } else if (t.type === 'fontWeight') {
175
- setNested(extend.fontWeight, rest.length ? rest : ['normal'], t.value);
176
- } else if (t.type === 'duration') {
177
- setNested(extend.transitionDuration, rest.length ? rest : ['DEFAULT'], t.value);
178
- }
179
+ const extend: Record<string, Record<string, unknown>> = {
180
+ colors: {},
181
+ spacing: {},
182
+ borderRadius: {},
183
+ boxShadow: {},
184
+ fontFamily: {},
185
+ fontWeight: {},
186
+ transitionDuration: {},
187
+ };
188
+
189
+ for (const t of tokens) {
190
+ const parts = t.name.split('.').filter(Boolean);
191
+ if (parts.length === 0) continue;
192
+ const root = parts[0];
193
+ const rest = parts.slice(1);
194
+
195
+ if (t.type === 'color' && (root === 'color' || root === 'colors')) {
196
+ setNested(extend.colors, rest.length ? rest : ['DEFAULT'], t.value);
197
+ } else if (t.type === 'dimension' && (root === 'space' || root === 'spacing')) {
198
+ setNested(extend.spacing, rest.length ? rest : ['DEFAULT'], t.value);
199
+ } else if (
200
+ t.type === 'dimension' &&
201
+ (root === 'radius' || root === 'radii' || root === 'borderRadius')
202
+ ) {
203
+ setNested(extend.borderRadius, rest.length ? rest : ['DEFAULT'], t.value);
204
+ } else if (t.type === 'shadow' && (root === 'shadow' || root === 'shadows')) {
205
+ setNested(extend.boxShadow, rest.length ? rest : ['DEFAULT'], t.value);
206
+ } else if (t.type === 'fontFamily') {
207
+ setNested(extend.fontFamily, rest.length ? rest : ['sans'], t.value);
208
+ } else if (t.type === 'fontWeight') {
209
+ setNested(extend.fontWeight, rest.length ? rest : ['normal'], t.value);
210
+ } else if (t.type === 'duration') {
211
+ setNested(extend.transitionDuration, rest.length ? rest : ['DEFAULT'], t.value);
179
212
  }
213
+ }
180
214
 
181
- // Prune empty sections
182
- for (const k of Object.keys(extend)) {
183
- if (extend[k] && typeof extend[k] === 'object' && Object.keys(extend[k]).length === 0) {
184
- delete extend[k];
185
- }
215
+ // Prune empty sections
216
+ for (const k of Object.keys(extend)) {
217
+ if (extend[k] && typeof extend[k] === 'object' && Object.keys(extend[k]).length === 0) {
218
+ delete extend[k];
186
219
  }
220
+ }
187
221
 
188
- return `/** Auto-generated by sync_design_tokens */\nexport default {\n theme: {\n extend: ${JSON.stringify(extend, null, 2)}\n }\n}`;
222
+ return `/** Auto-generated by sync_design_tokens */\nexport default {\n theme: {\n extend: ${JSON.stringify(extend, null, 2)}\n }\n}`;
189
223
  }
190
224
 
191
225
  export function tokensToTokensStudio(tokens: Token[]): string {
192
- // Tokens Studio expects {"token": {"value": ..., "type": ...}}
193
- const out: any = {};
194
- for (const t of tokens) {
195
- const parts = t.name.split('.').filter(Boolean);
196
- if (parts.length === 0) continue;
197
- setNested(out, parts, { value: t.value, type: t.type, description: t.description });
198
- }
199
- return JSON.stringify(out, null, 2);
226
+ // Tokens Studio expects {"token": {"value": ..., "type": ...}}
227
+ const out: Record<string, unknown> = {};
228
+ for (const t of tokens) {
229
+ const parts = t.name.split('.').filter(Boolean);
230
+ if (parts.length === 0) continue;
231
+ setNested(out, parts, { value: t.value, type: t.type, description: t.description });
232
+ }
233
+ return JSON.stringify(out, null, 2);
200
234
  }
201
235
 
202
236
  export function tokensToStyleDictionary(tokens: Token[]): string {
203
- // Style Dictionary commonly uses nested objects with { value, type }
204
- // We'll output a format compatible with SD v3+ token JSON.
205
- const out: any = {};
206
- for (const t of tokens) {
207
- const parts = t.name.split('.').filter(Boolean);
208
- if (parts.length === 0) continue;
209
- setNested(out, parts, { value: t.value, type: t.type });
210
- }
211
- return JSON.stringify(out, null, 2);
237
+ // Style Dictionary commonly uses nested objects with { value, type }
238
+ // We'll output a format compatible with SD v3+ token JSON.
239
+ const out: Record<string, unknown> = {};
240
+ for (const t of tokens) {
241
+ const parts = t.name.split('.').filter(Boolean);
242
+ if (parts.length === 0) continue;
243
+ setNested(out, parts, { value: t.value, type: t.type });
244
+ }
245
+ return JSON.stringify(out, null, 2);
212
246
  }
@@ -24,19 +24,22 @@ import { buildContext } from '../context/builder.js';
24
24
  import { generateWithGemini } from '../generation/gemini-client.js';
25
25
 
26
26
  const inputSchema = {
27
- imagePath: z.string().optional().describe('Path to an image file (png/jpg/webp).'),
28
- imageBase64: z.string().optional().describe('Base64 image data (if not using imagePath).'),
29
- mimeType: z.string().optional().describe('Image mime type, e.g. image/png. Required when using imageBase64.'),
30
- instruction: z
31
- .string()
32
- .optional()
33
- .describe('What to extract from the screenshot. Defaults to a full UI breakdown.'),
34
- outputFormat: z.enum(['json', 'markdown']).default('json').describe('Response format.'),
35
- framework: z
36
- .enum(['react', 'nextjs', 'vue', 'svelte', 'vanilla'])
37
- .optional()
38
- .describe('Target framework hint.'),
39
- context: z.array(z.string()).optional().describe('Related design/token files for grounding.'),
27
+ imagePath: z.string().optional().describe('Path to an image file (png/jpg/webp).'),
28
+ imageBase64: z.string().optional().describe('Base64 image data (if not using imagePath).'),
29
+ mimeType: z
30
+ .string()
31
+ .optional()
32
+ .describe('Image mime type, e.g. image/png. Required when using imageBase64.'),
33
+ instruction: z
34
+ .string()
35
+ .optional()
36
+ .describe('What to extract from the screenshot. Defaults to a full UI breakdown.'),
37
+ outputFormat: z.enum(['json', 'markdown']).default('json').describe('Response format.'),
38
+ framework: z
39
+ .enum(['react', 'nextjs', 'vue', 'svelte', 'vanilla'])
40
+ .optional()
41
+ .describe('Target framework hint.'),
42
+ context: z.array(z.string()).optional().describe('Related design/token files for grounding.'),
40
43
  };
41
44
 
42
45
  const SYSTEM_PROMPT = `You are a senior UI engineer and design system expert.
@@ -60,63 +63,64 @@ Be practical: prefer reusable primitives (Button, Input, Card, Table) and sugges
60
63
  Do not invent backend logic.`;
61
64
 
62
65
  function mimeFromPath(p: string): string {
63
- const ext = path.extname(p).toLowerCase();
64
- if (ext === '.png') return 'image/png';
65
- if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
66
- if (ext === '.webp') return 'image/webp';
67
- return 'application/octet-stream';
66
+ const ext = path.extname(p).toLowerCase();
67
+ if (ext === '.png') return 'image/png';
68
+ if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
69
+ if (ext === '.webp') return 'image/webp';
70
+ return 'application/octet-stream';
68
71
  }
69
72
 
70
73
  export function registerAnalyzeScreenshotUI(server: McpServer, config: Config): void {
71
- server.registerTool(
72
- 'analyze_screenshot_ui',
73
- {
74
- title: 'Analyze UI Screenshot',
75
- description:
76
- 'Analyze a UI screenshot (layout/components/tokens/a11y). Supports multimodal prompts in local or remote mode.',
77
- inputSchema,
78
- },
79
- async (args) => {
80
- const imagePath = args.imagePath as string | undefined;
81
- const imageBase64 = args.imageBase64 as string | undefined;
82
- const mimeType = args.mimeType as string | undefined;
83
- const instruction = (args.instruction as string | undefined) || '';
84
- const outputFormat = (args.outputFormat as 'json' | 'markdown') || 'json';
85
- const framework = (args.framework as Config['defaultFramework'] | undefined) || config.defaultFramework;
86
- const contextPaths = args.context as string[] | undefined;
87
-
88
- if (!imagePath && !imageBase64) {
89
- return {
90
- content: [{ type: 'text' as const, text: 'Error: Provide imagePath or imageBase64.' }],
91
- isError: true,
92
- };
93
- }
94
-
95
- let data: string;
96
- let mime: string;
97
-
98
- try {
99
- if (imagePath) {
100
- const safe = assertReadablePath(imagePath, config);
101
- const buf = fs.readFileSync(safe);
102
- data = buf.toString('base64');
103
- mime = mimeFromPath(safe);
104
- } else {
105
- data = imageBase64 as string;
106
- mime = mimeType || 'application/octet-stream';
107
- }
108
- } catch (error) {
109
- const message = error instanceof Error ? error.message : 'Could not read image';
110
- return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
111
- }
112
-
113
- let contextContent = '';
114
- if (contextPaths && contextPaths.length > 0) {
115
- contextContent = await buildContext(contextPaths, config);
116
- contextContent = sanitizeContent(contextContent);
117
- }
118
-
119
- const userText = `Analyze this screenshot.
74
+ server.registerTool(
75
+ 'analyze_screenshot_ui',
76
+ {
77
+ title: 'Analyze UI Screenshot',
78
+ description:
79
+ 'Analyze a UI screenshot (layout/components/tokens/a11y). Supports multimodal prompts in local or remote mode.',
80
+ inputSchema,
81
+ },
82
+ async (args) => {
83
+ const imagePath = args.imagePath as string | undefined;
84
+ const imageBase64 = args.imageBase64 as string | undefined;
85
+ const mimeType = args.mimeType as string | undefined;
86
+ const instruction = (args.instruction as string | undefined) || '';
87
+ const outputFormat = (args.outputFormat as 'json' | 'markdown') || 'json';
88
+ const framework =
89
+ (args.framework as Config['defaultFramework'] | undefined) || config.defaultFramework;
90
+ const contextPaths = args.context as string[] | undefined;
91
+
92
+ if (!imagePath && !imageBase64) {
93
+ return {
94
+ content: [{ type: 'text' as const, text: 'Error: Provide imagePath or imageBase64.' }],
95
+ isError: true,
96
+ };
97
+ }
98
+
99
+ let data: string;
100
+ let mime: string;
101
+
102
+ try {
103
+ if (imagePath) {
104
+ const safe = assertReadablePath(imagePath, config);
105
+ const buf = fs.readFileSync(safe);
106
+ data = buf.toString('base64');
107
+ mime = mimeFromPath(safe);
108
+ } else {
109
+ data = imageBase64 as string;
110
+ mime = mimeType || 'application/octet-stream';
111
+ }
112
+ } catch (error) {
113
+ const message = error instanceof Error ? error.message : 'Could not read image';
114
+ return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
115
+ }
116
+
117
+ let contextContent = '';
118
+ if (contextPaths && contextPaths.length > 0) {
119
+ contextContent = await buildContext(contextPaths, config);
120
+ contextContent = sanitizeContent(contextContent);
121
+ }
122
+
123
+ const userText = `Analyze this screenshot.
120
124
 
121
125
  outputFormat: ${outputFormat}
122
126
  frameworkHint: ${framework}
@@ -126,25 +130,22 @@ ${contextContent ? `RELATED FILES (sanitized):\n${contextContent}` : ''}
126
130
 
127
131
  Return in the requested format.`;
128
132
 
129
- try {
130
- const resp = await generateWithGemini(
131
- config,
132
- SYSTEM_PROMPT,
133
- [
134
- { text: userText },
135
- { inlineData: { mimeType: mime, data } },
136
- ],
137
- { toolName: 'analyze_screenshot_ui' }
138
- );
139
-
140
- return { content: [{ type: 'text' as const, text: resp.trim() }] };
141
- } catch (error) {
142
- const message = error instanceof Error ? error.message : 'Unknown error';
143
- return {
144
- content: [{ type: 'text' as const, text: `Error analyzing screenshot: ${message}` }],
145
- isError: true,
146
- };
147
- }
148
- }
149
- );
133
+ try {
134
+ const resp = await generateWithGemini(
135
+ config,
136
+ SYSTEM_PROMPT,
137
+ [{ text: userText }, { inlineData: { mimeType: mime, data } }],
138
+ { toolName: 'analyze_screenshot_ui' }
139
+ );
140
+
141
+ return { content: [{ type: 'text' as const, text: resp.trim() }] };
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : 'Unknown error';
144
+ return {
145
+ content: [{ type: 'text' as const, text: `Error analyzing screenshot: ${message}` }],
146
+ isError: true,
147
+ };
148
+ }
149
+ }
150
+ );
150
151
  }