@gemini-designer/mcp-server 0.1.2 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/catalog.d.ts.map +1 -1
- package/dist/components/catalog.js +10 -4
- package/dist/components/catalog.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -6
- package/dist/config/index.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/filter.d.ts.map +1 -1
- package/dist/context/filter.js +5 -1
- package/dist/context/filter.js.map +1 -1
- package/dist/context/grounding.d.ts.map +1 -1
- package/dist/context/grounding.js +7 -3
- package/dist/context/grounding.js.map +1 -1
- package/dist/context/guards.d.ts.map +1 -1
- package/dist/context/guards.js +53 -0
- package/dist/context/guards.js.map +1 -1
- package/dist/context/repo-hints.js.map +1 -1
- package/dist/context/styling-detector.d.ts +24 -0
- package/dist/context/styling-detector.d.ts.map +1 -0
- package/dist/context/styling-detector.js +337 -0
- package/dist/context/styling-detector.js.map +1 -0
- package/dist/design/principles.js.map +1 -1
- package/dist/generation/gemini-client.d.ts.map +1 -1
- package/dist/generation/gemini-client.js.map +1 -1
- package/dist/generation/litellm-client.d.ts.map +1 -1
- package/dist/generation/litellm-client.js +14 -7
- package/dist/generation/litellm-client.js.map +1 -1
- package/dist/generation/remote-client.d.ts +10 -5
- package/dist/generation/remote-client.d.ts.map +1 -1
- package/dist/generation/remote-client.js +13 -2
- package/dist/generation/remote-client.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output/file-writer.d.ts.map +1 -1
- package/dist/output/file-writer.js +4 -4
- package/dist/output/file-writer.js.map +1 -1
- package/dist/output/formatter.d.ts.map +1 -1
- package/dist/output/formatter.js +5 -2
- package/dist/output/formatter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/stack/detect.d.ts.map +1 -1
- package/dist/stack/detect.js +42 -9
- package/dist/stack/detect.js.map +1 -1
- package/dist/tokens/sync.d.ts.map +1 -1
- package/dist/tokens/sync.js +22 -5
- package/dist/tokens/sync.js.map +1 -1
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
- package/dist/tools/analyze-screenshot-ui.js +5 -5
- package/dist/tools/analyze-screenshot-ui.js.map +1 -1
- package/dist/tools/analyze-tokens.d.ts.map +1 -1
- package/dist/tools/analyze-tokens.js +3 -1
- package/dist/tools/analyze-tokens.js.map +1 -1
- package/dist/tools/catalog-components.d.ts.map +1 -1
- package/dist/tools/catalog-components.js +1 -4
- package/dist/tools/catalog-components.js.map +1 -1
- package/dist/tools/create-ui.d.ts +3 -0
- package/dist/tools/create-ui.d.ts.map +1 -1
- package/dist/tools/create-ui.js +203 -75
- package/dist/tools/create-ui.js.map +1 -1
- package/dist/tools/detect-ui-stack.js.map +1 -1
- package/dist/tools/generate-component-variants.d.ts.map +1 -1
- package/dist/tools/generate-component-variants.js +15 -4
- package/dist/tools/generate-component-variants.js.map +1 -1
- package/dist/tools/generate-vibes.d.ts.map +1 -1
- package/dist/tools/generate-vibes.js +7 -3
- package/dist/tools/generate-vibes.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/modify-ui.d.ts.map +1 -1
- package/dist/tools/modify-ui.js +7 -2
- package/dist/tools/modify-ui.js.map +1 -1
- package/dist/tools/scaffold-project.d.ts.map +1 -1
- package/dist/tools/scaffold-project.js +3 -1
- package/dist/tools/scaffold-project.js.map +1 -1
- package/dist/tools/snippet-ui.d.ts +3 -1
- package/dist/tools/snippet-ui.d.ts.map +1 -1
- package/dist/tools/snippet-ui.js +219 -88
- package/dist/tools/snippet-ui.js.map +1 -1
- package/dist/tools/sync-design-tokens.d.ts.map +1 -1
- package/dist/tools/sync-design-tokens.js +26 -11
- package/dist/tools/sync-design-tokens.js.map +1 -1
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -55
- package/src/__tests__/builder.test.ts +19 -19
- package/src/__tests__/config.test.ts +63 -31
- package/src/__tests__/filter.test.ts +98 -92
- package/src/__tests__/remote-client.test.ts +179 -0
- package/src/components/catalog.ts +170 -166
- package/src/config/index.ts +185 -177
- package/src/context/builder.ts +157 -157
- package/src/context/filter.ts +110 -104
- package/src/context/grounding.ts +143 -129
- package/src/context/guards.ts +97 -38
- package/src/context/repo-hints.ts +24 -24
- package/src/context/styling-detector.ts +460 -0
- package/src/design/principles.ts +14 -14
- package/src/generation/gemini-client.ts +53 -56
- package/src/generation/litellm-client.ts +102 -86
- package/src/generation/remote-client.ts +100 -77
- package/src/index.ts +16 -16
- package/src/output/file-writer.ts +123 -123
- package/src/output/formatter.ts +139 -132
- package/src/server.ts +12 -11
- package/src/stack/detect.ts +226 -175
- package/src/tokens/sync.ts +189 -155
- package/src/tools/analyze-screenshot-ui.ts +89 -88
- package/src/tools/analyze-tokens.ts +80 -78
- package/src/tools/catalog-components.ts +68 -68
- package/src/tools/create-ui.ts +295 -142
- package/src/tools/detect-ui-stack.ts +36 -36
- package/src/tools/generate-component-variants.ts +155 -135
- package/src/tools/generate-vibes.ts +121 -117
- package/src/tools/index.ts +14 -14
- package/src/tools/modify-ui.ts +170 -165
- package/src/tools/scaffold-project.ts +68 -66
- package/src/tools/snippet-ui.ts +323 -172
- package/src/tools/sync-design-tokens.ts +217 -195
- package/src/utils/walk.ts +47 -45
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -33
- package/vitest.config.ts +10 -10
- package/.prettierrc +0 -9
- package/eslint.config.js +0 -37
package/src/tokens/sync.ts
CHANGED
|
@@ -10,203 +10,237 @@
|
|
|
10
10
|
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
|
|
13
|
-
export type TokenType =
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
36
|
+
tokens: Token[];
|
|
37
|
+
warnings: string[];
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
function isColorValue(v: string): boolean {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
113
|
-
|
|
150
|
+
// Block dangerous keys
|
|
151
|
+
if (dangerousKeys.has(key)) {
|
|
152
|
+
return;
|
|
114
153
|
}
|
|
115
154
|
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
if (i === pathParts.length - 1) {
|
|
156
|
+
cur[key] = value;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
118
159
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
}
|