@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.
- package/.prettierrc +9 -0
- package/dist/components/catalog.d.ts +24 -0
- package/dist/components/catalog.d.ts.map +1 -0
- package/dist/components/catalog.js +186 -0
- package/dist/components/catalog.js.map +1 -0
- package/dist/config/index.d.ts +60 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +199 -0
- package/dist/config/index.js.map +1 -0
- package/dist/context/builder.d.ts +32 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +194 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/context/filter.d.ts +28 -0
- package/dist/context/filter.d.ts.map +1 -0
- package/dist/context/filter.js +136 -0
- package/dist/context/filter.js.map +1 -0
- package/dist/context/grounding.d.ts +27 -0
- package/dist/context/grounding.d.ts.map +1 -0
- package/dist/context/grounding.js +162 -0
- package/dist/context/grounding.js.map +1 -0
- package/dist/context/guards.d.ts +31 -0
- package/dist/context/guards.d.ts.map +1 -0
- package/dist/context/guards.js +76 -0
- package/dist/context/guards.js.map +1 -0
- package/dist/context/repo-hints.d.ts +12 -0
- package/dist/context/repo-hints.d.ts.map +1 -0
- package/dist/context/repo-hints.js +40 -0
- package/dist/context/repo-hints.js.map +1 -0
- package/dist/generation/gemini-client.d.ts +27 -0
- package/dist/generation/gemini-client.d.ts.map +1 -0
- package/dist/generation/gemini-client.js +64 -0
- package/dist/generation/gemini-client.js.map +1 -0
- package/dist/generation/litellm-client.d.ts +16 -0
- package/dist/generation/litellm-client.d.ts.map +1 -0
- package/dist/generation/litellm-client.js +98 -0
- package/dist/generation/litellm-client.js.map +1 -0
- package/dist/generation/remote-client.d.ts +20 -0
- package/dist/generation/remote-client.d.ts.map +1 -0
- package/dist/generation/remote-client.js +69 -0
- package/dist/generation/remote-client.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/output/file-writer.d.ts +39 -0
- package/dist/output/file-writer.d.ts.map +1 -0
- package/dist/output/file-writer.js +153 -0
- package/dist/output/file-writer.js.map +1 -0
- package/dist/output/formatter.d.ts +26 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +156 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +22 -0
- package/dist/server.js.map +1 -0
- package/dist/stack/detect.d.ts +49 -0
- package/dist/stack/detect.d.ts.map +1 -0
- package/dist/stack/detect.js +157 -0
- package/dist/stack/detect.js.map +1 -0
- package/dist/tokens/sync.d.ts +32 -0
- package/dist/tokens/sync.d.ts.map +1 -0
- package/dist/tokens/sync.js +188 -0
- package/dist/tokens/sync.js.map +1 -0
- package/dist/tools/analyze-screenshot-ui.d.ts +18 -0
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -0
- package/dist/tools/analyze-screenshot-ui.js +133 -0
- package/dist/tools/analyze-screenshot-ui.js.map +1 -0
- package/dist/tools/analyze-tokens.d.ts +10 -0
- package/dist/tools/analyze-tokens.d.ts.map +1 -0
- package/dist/tools/analyze-tokens.js +107 -0
- package/dist/tools/analyze-tokens.js.map +1 -0
- package/dist/tools/catalog-components.d.ts +14 -0
- package/dist/tools/catalog-components.d.ts.map +1 -0
- package/dist/tools/catalog-components.js +85 -0
- package/dist/tools/catalog-components.js.map +1 -0
- package/dist/tools/create-ui.d.ts +10 -0
- package/dist/tools/create-ui.d.ts.map +1 -0
- package/dist/tools/create-ui.js +167 -0
- package/dist/tools/create-ui.js.map +1 -0
- package/dist/tools/detect-ui-stack.d.ts +15 -0
- package/dist/tools/detect-ui-stack.d.ts.map +1 -0
- package/dist/tools/detect-ui-stack.js +52 -0
- package/dist/tools/detect-ui-stack.js.map +1 -0
- package/dist/tools/generate-component-variants.d.ts +15 -0
- package/dist/tools/generate-component-variants.d.ts.map +1 -0
- package/dist/tools/generate-component-variants.js +199 -0
- package/dist/tools/generate-component-variants.js.map +1 -0
- package/dist/tools/generate-vibes.d.ts +10 -0
- package/dist/tools/generate-vibes.d.ts.map +1 -0
- package/dist/tools/generate-vibes.js +145 -0
- package/dist/tools/generate-vibes.js.map +1 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/modify-ui.d.ts +11 -0
- package/dist/tools/modify-ui.d.ts.map +1 -0
- package/dist/tools/modify-ui.js +207 -0
- package/dist/tools/modify-ui.js.map +1 -0
- package/dist/tools/scaffold-project.d.ts +10 -0
- package/dist/tools/scaffold-project.d.ts.map +1 -0
- package/dist/tools/scaffold-project.js +122 -0
- package/dist/tools/scaffold-project.js.map +1 -0
- package/dist/tools/snippet-ui.d.ts +11 -0
- package/dist/tools/snippet-ui.d.ts.map +1 -0
- package/dist/tools/snippet-ui.js +194 -0
- package/dist/tools/snippet-ui.js.map +1 -0
- package/dist/tools/sync-design-tokens.d.ts +14 -0
- package/dist/tools/sync-design-tokens.d.ts.map +1 -0
- package/dist/tools/sync-design-tokens.js +233 -0
- package/dist/tools/sync-design-tokens.js.map +1 -0
- package/dist/utils/walk.d.ts +15 -0
- package/dist/utils/walk.d.ts.map +1 -0
- package/dist/utils/walk.js +63 -0
- package/dist/utils/walk.js.map +1 -0
- package/eslint.config.js +37 -0
- package/package.json +56 -0
- package/src/__tests__/builder.test.ts +31 -0
- package/src/__tests__/config.test.ts +52 -0
- package/src/__tests__/filter.test.ts +109 -0
- package/src/components/catalog.ts +214 -0
- package/src/config/index.ts +237 -0
- package/src/context/builder.ts +233 -0
- package/src/context/filter.ts +164 -0
- package/src/context/grounding.ts +191 -0
- package/src/context/guards.ts +94 -0
- package/src/context/repo-hints.ts +43 -0
- package/src/generation/gemini-client.ts +94 -0
- package/src/generation/litellm-client.ts +121 -0
- package/src/generation/remote-client.ts +103 -0
- package/src/index.ts +36 -0
- package/src/output/file-writer.ts +181 -0
- package/src/output/formatter.ts +186 -0
- package/src/server.ts +28 -0
- package/src/stack/detect.ts +204 -0
- package/src/tokens/sync.ts +212 -0
- package/src/tools/analyze-screenshot-ui.ts +150 -0
- package/src/tools/analyze-tokens.ts +123 -0
- package/src/tools/catalog-components.ts +99 -0
- package/src/tools/create-ui.ts +194 -0
- package/src/tools/detect-ui-stack.ts +64 -0
- package/src/tools/generate-component-variants.ts +218 -0
- package/src/tools/generate-vibes.ts +177 -0
- package/src/tools/index.ts +42 -0
- package/src/tools/modify-ui.ts +230 -0
- package/src/tools/scaffold-project.ts +138 -0
- package/src/tools/snippet-ui.ts +222 -0
- package/src/tools/sync-design-tokens.ts +256 -0
- package/src/utils/walk.ts +75 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack Detection
|
|
3
|
+
*
|
|
4
|
+
* Deterministically detect the frontend stack of a project:
|
|
5
|
+
* - framework (next/react/vue/svelte)
|
|
6
|
+
* - styling approach (tailwind/css-modules/emotion/styled-components)
|
|
7
|
+
* - common component & icon libraries
|
|
8
|
+
* - presence of storybook, eslint, prettier
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
export interface StackDetectionResult {
|
|
15
|
+
root: string;
|
|
16
|
+
framework: {
|
|
17
|
+
name: 'nextjs' | 'react' | 'vue' | 'svelte' | 'unknown';
|
|
18
|
+
version?: string;
|
|
19
|
+
router?: 'app' | 'pages' | 'unknown';
|
|
20
|
+
};
|
|
21
|
+
language: {
|
|
22
|
+
typescript: boolean;
|
|
23
|
+
};
|
|
24
|
+
styling: {
|
|
25
|
+
primary: 'tailwind' | 'css-modules' | 'emotion' | 'styled-components' | 'sass' | 'vanilla' | 'unknown';
|
|
26
|
+
detected: string[];
|
|
27
|
+
};
|
|
28
|
+
uiLibraries: string[];
|
|
29
|
+
iconLibraries: string[];
|
|
30
|
+
tooling: {
|
|
31
|
+
storybook: boolean;
|
|
32
|
+
eslint: boolean;
|
|
33
|
+
prettier: boolean;
|
|
34
|
+
vitest: boolean;
|
|
35
|
+
jest: boolean;
|
|
36
|
+
};
|
|
37
|
+
conventions: {
|
|
38
|
+
srcDir: boolean;
|
|
39
|
+
hasAppDir: boolean;
|
|
40
|
+
hasPagesDir: boolean;
|
|
41
|
+
tsconfigPaths?: Record<string, string[]>;
|
|
42
|
+
};
|
|
43
|
+
files: {
|
|
44
|
+
packageJson?: string;
|
|
45
|
+
tsconfig?: string;
|
|
46
|
+
tailwindConfig?: string;
|
|
47
|
+
componentsJson?: string;
|
|
48
|
+
storybookDir?: string;
|
|
49
|
+
};
|
|
50
|
+
warnings: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJsonIfExists(filePath: string): any | null {
|
|
54
|
+
if (!fs.existsSync(filePath)) return null;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exists(root: string, ...parts: string[]): string | null {
|
|
63
|
+
const p = path.join(root, ...parts);
|
|
64
|
+
return fs.existsSync(p) ? p : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function depVersion(pkg: any, name: string): string | undefined {
|
|
68
|
+
const d = pkg?.dependencies?.[name] || pkg?.devDependencies?.[name] || pkg?.peerDependencies?.[name];
|
|
69
|
+
return typeof d === 'string' ? d : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function detectUiStack(rootDir: string): StackDetectionResult {
|
|
73
|
+
const warnings: string[] = [];
|
|
74
|
+
|
|
75
|
+
const pkgPath = exists(rootDir, 'package.json');
|
|
76
|
+
const pkg = pkgPath ? readJsonIfExists(pkgPath) : null;
|
|
77
|
+
if (!pkg) warnings.push('package.json not found or invalid JSON');
|
|
78
|
+
|
|
79
|
+
const hasTs = Boolean(
|
|
80
|
+
exists(rootDir, 'tsconfig.json') ||
|
|
81
|
+
exists(rootDir, 'tsconfig.base.json') ||
|
|
82
|
+
exists(rootDir, 'tsconfig.app.json') ||
|
|
83
|
+
depVersion(pkg, 'typescript')
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Framework detection
|
|
87
|
+
let framework: StackDetectionResult['framework'] = { name: 'unknown', router: 'unknown' };
|
|
88
|
+
|
|
89
|
+
if (depVersion(pkg, 'next')) {
|
|
90
|
+
framework = { name: 'nextjs', version: depVersion(pkg, 'next'), router: 'unknown' };
|
|
91
|
+
} else if (depVersion(pkg, 'react')) {
|
|
92
|
+
framework = { name: 'react', version: depVersion(pkg, 'react'), router: 'unknown' };
|
|
93
|
+
} else if (depVersion(pkg, 'vue')) {
|
|
94
|
+
framework = { name: 'vue', version: depVersion(pkg, 'vue'), router: 'unknown' };
|
|
95
|
+
} else if (depVersion(pkg, 'svelte')) {
|
|
96
|
+
framework = { name: 'svelte', version: depVersion(pkg, 'svelte'), router: 'unknown' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const srcDir = Boolean(exists(rootDir, 'src'));
|
|
100
|
+
const hasAppDir = Boolean(exists(rootDir, 'app') || exists(rootDir, 'src', 'app'));
|
|
101
|
+
const hasPagesDir = Boolean(exists(rootDir, 'pages') || exists(rootDir, 'src', 'pages'));
|
|
102
|
+
|
|
103
|
+
if (framework.name === 'nextjs') {
|
|
104
|
+
framework.router = hasAppDir ? 'app' : hasPagesDir ? 'pages' : 'unknown';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Styling detection
|
|
108
|
+
const detectedStyling: string[] = [];
|
|
109
|
+
if (depVersion(pkg, 'tailwindcss') || exists(rootDir, 'tailwind.config.js') || exists(rootDir, 'tailwind.config.ts')) {
|
|
110
|
+
detectedStyling.push('tailwind');
|
|
111
|
+
}
|
|
112
|
+
if (depVersion(pkg, 'sass') || depVersion(pkg, 'node-sass')) detectedStyling.push('sass');
|
|
113
|
+
if (depVersion(pkg, 'styled-components')) detectedStyling.push('styled-components');
|
|
114
|
+
if (depVersion(pkg, '@emotion/react') || depVersion(pkg, '@emotion/styled')) detectedStyling.push('emotion');
|
|
115
|
+
|
|
116
|
+
// CSS Modules: heuristic – Next and CRA often use .module.css. We can't scan whole tree here; mark as possible.
|
|
117
|
+
if (framework.name === 'nextjs') detectedStyling.push('css-modules');
|
|
118
|
+
|
|
119
|
+
let primary: StackDetectionResult['styling']['primary'] = 'unknown';
|
|
120
|
+
if (detectedStyling.includes('tailwind')) primary = 'tailwind';
|
|
121
|
+
else if (detectedStyling.includes('emotion')) primary = 'emotion';
|
|
122
|
+
else if (detectedStyling.includes('styled-components')) primary = 'styled-components';
|
|
123
|
+
else if (detectedStyling.includes('sass')) primary = 'sass';
|
|
124
|
+
else if (framework.name !== 'unknown') primary = 'vanilla';
|
|
125
|
+
|
|
126
|
+
// UI libraries (coarse)
|
|
127
|
+
const uiCandidates = [
|
|
128
|
+
'@mui/material',
|
|
129
|
+
'antd',
|
|
130
|
+
'@chakra-ui/react',
|
|
131
|
+
'@radix-ui/react-dialog',
|
|
132
|
+
'@headlessui/react',
|
|
133
|
+
'shadcn-ui',
|
|
134
|
+
'lucide-react',
|
|
135
|
+
'@mantine/core',
|
|
136
|
+
'@nextui-org/react',
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const uiLibraries = uiCandidates.filter((n) => Boolean(depVersion(pkg, n)));
|
|
140
|
+
|
|
141
|
+
// Icon libraries
|
|
142
|
+
const iconCandidates = ['lucide-react', 'react-icons', '@heroicons/react', '@tabler/icons-react', '@phosphor-icons/react'];
|
|
143
|
+
const iconLibraries = iconCandidates.filter((n) => Boolean(depVersion(pkg, n)));
|
|
144
|
+
|
|
145
|
+
// Storybook / linting / formatting / tests
|
|
146
|
+
const hasStorybook = Boolean(exists(rootDir, '.storybook')) || Boolean(depVersion(pkg, 'storybook')) || Boolean(depVersion(pkg, '@storybook/react'));
|
|
147
|
+
const hasEslint = Boolean(depVersion(pkg, 'eslint')) || Boolean(exists(rootDir, '.eslintrc')) || Boolean(exists(rootDir, '.eslintrc.js'));
|
|
148
|
+
const hasPrettier = Boolean(depVersion(pkg, 'prettier')) || Boolean(exists(rootDir, '.prettierrc'));
|
|
149
|
+
const hasVitest = Boolean(depVersion(pkg, 'vitest'));
|
|
150
|
+
const hasJest = Boolean(depVersion(pkg, 'jest'));
|
|
151
|
+
|
|
152
|
+
// Shadcn detection
|
|
153
|
+
const componentsJson = exists(rootDir, 'components.json');
|
|
154
|
+
if (componentsJson) {
|
|
155
|
+
if (!uiLibraries.includes('shadcn-ui')) uiLibraries.push('shadcn-ui');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// tsconfig paths
|
|
159
|
+
const tsconfigPath = exists(rootDir, 'tsconfig.json') || exists(rootDir, 'tsconfig.base.json');
|
|
160
|
+
let tsconfigPaths: Record<string, string[]> | undefined;
|
|
161
|
+
if (tsconfigPath) {
|
|
162
|
+
const tsconfig = readJsonIfExists(tsconfigPath);
|
|
163
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
164
|
+
if (paths && typeof paths === 'object') {
|
|
165
|
+
tsconfigPaths = paths;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tailwindConfig =
|
|
170
|
+
exists(rootDir, 'tailwind.config.ts') ||
|
|
171
|
+
exists(rootDir, 'tailwind.config.js') ||
|
|
172
|
+
exists(rootDir, 'tailwind.config.cjs') ||
|
|
173
|
+
exists(rootDir, 'tailwind.config.mjs');
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
root: rootDir,
|
|
177
|
+
framework,
|
|
178
|
+
language: { typescript: hasTs },
|
|
179
|
+
styling: { primary, detected: Array.from(new Set(detectedStyling)) },
|
|
180
|
+
uiLibraries: Array.from(new Set(uiLibraries)),
|
|
181
|
+
iconLibraries: Array.from(new Set(iconLibraries)),
|
|
182
|
+
tooling: {
|
|
183
|
+
storybook: hasStorybook,
|
|
184
|
+
eslint: hasEslint,
|
|
185
|
+
prettier: hasPrettier,
|
|
186
|
+
vitest: hasVitest,
|
|
187
|
+
jest: hasJest,
|
|
188
|
+
},
|
|
189
|
+
conventions: {
|
|
190
|
+
srcDir,
|
|
191
|
+
hasAppDir,
|
|
192
|
+
hasPagesDir,
|
|
193
|
+
tsconfigPaths,
|
|
194
|
+
},
|
|
195
|
+
files: {
|
|
196
|
+
packageJson: pkgPath || undefined,
|
|
197
|
+
tsconfig: tsconfigPath || undefined,
|
|
198
|
+
tailwindConfig: tailwindConfig || undefined,
|
|
199
|
+
componentsJson: componentsJson || undefined,
|
|
200
|
+
storybookDir: exists(rootDir, '.storybook') || undefined,
|
|
201
|
+
},
|
|
202
|
+
warnings,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Sync Utilities
|
|
3
|
+
*
|
|
4
|
+
* Normalizes tokens into a small internal representation and can output:
|
|
5
|
+
* - CSS custom properties
|
|
6
|
+
* - Tailwind theme extension
|
|
7
|
+
* - Tokens Studio JSON
|
|
8
|
+
* - Style Dictionary JSON
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
|
|
13
|
+
export type TokenType = 'color' | 'dimension' | 'fontFamily' | 'fontWeight' | 'number' | 'string' | 'shadow' | 'duration' | 'easing';
|
|
14
|
+
|
|
15
|
+
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
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TokenParseResult {
|
|
27
|
+
tokens: Token[];
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
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
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isDimensionValue(v: string): boolean {
|
|
45
|
+
const s = v.trim().toLowerCase();
|
|
46
|
+
return /^-?\d+(\.\d+)?(px|rem|em|vh|vw|%|ch|ex)$/.test(s);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isDurationValue(v: string): boolean {
|
|
50
|
+
const s = v.trim().toLowerCase();
|
|
51
|
+
return /^\d+(\.\d+)?(ms|s)$/.test(s);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
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';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
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(/\.$/, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
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
|
+
}
|
|
111
|
+
|
|
112
|
+
if (tokens.length === 0) {
|
|
113
|
+
warnings.push('No CSS custom properties found (expected patterns like --token-name: value;)');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { tokens, warnings };
|
|
117
|
+
}
|
|
118
|
+
|
|
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>;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
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');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
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
|
+
}
|
|
180
|
+
|
|
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
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return `/** Auto-generated by sync_design_tokens */\nexport default {\n theme: {\n extend: ${JSON.stringify(extend, null, 2)}\n }\n}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
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);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
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);
|
|
212
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze_screenshot_ui Tool
|
|
3
|
+
*
|
|
4
|
+
* Multimodal screenshot analysis:
|
|
5
|
+
* - layout breakdown
|
|
6
|
+
* - component suggestions
|
|
7
|
+
* - token guesses
|
|
8
|
+
* - accessibility notes
|
|
9
|
+
*
|
|
10
|
+
* This tool can run in:
|
|
11
|
+
* - local gemini mode (Gemini SDK multimodal)
|
|
12
|
+
* - remote mode (gateway supports userParts)
|
|
13
|
+
* - local litellm mode (OpenAI-compatible image_url content)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { Config } from '../config/index.js';
|
|
21
|
+
import { assertReadablePath } from '../context/guards.js';
|
|
22
|
+
import { sanitizeContent } from '../context/filter.js';
|
|
23
|
+
import { buildContext } from '../context/builder.js';
|
|
24
|
+
import { generateWithGemini } from '../generation/gemini-client.js';
|
|
25
|
+
|
|
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.'),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const SYSTEM_PROMPT = `You are a senior UI engineer and design system expert.
|
|
43
|
+
|
|
44
|
+
You will analyze a UI screenshot and return a structured breakdown suitable for code generation.
|
|
45
|
+
|
|
46
|
+
Output requirements:
|
|
47
|
+
- If outputFormat=json: output ONE valid JSON object only. No prose. No markdown. No code fences.
|
|
48
|
+
- If outputFormat=markdown: output concise markdown with headings and bullet points.
|
|
49
|
+
|
|
50
|
+
When outputFormat=json, include:
|
|
51
|
+
{
|
|
52
|
+
"layout": {"type": "page|modal|panel|component", "grid": "...", "sections": [...]},
|
|
53
|
+
"components": [{"name": "...", "role": "...", "suggestedImplementation": "..."}],
|
|
54
|
+
"tokensGuess": {"colors": [...], "typography": [...], "spacing": [...], "radii": [...], "shadows": [...]},
|
|
55
|
+
"a11y": {"landmarks": [...], "labels": [...], "keyboard": [...], "contrastRisks": [...]},
|
|
56
|
+
"notes": [...]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Be practical: prefer reusable primitives (Button, Input, Card, Table) and suggest variants/states.
|
|
60
|
+
Do not invent backend logic.`;
|
|
61
|
+
|
|
62
|
+
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';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
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.
|
|
120
|
+
|
|
121
|
+
outputFormat: ${outputFormat}
|
|
122
|
+
frameworkHint: ${framework}
|
|
123
|
+
instruction: ${instruction || '(full breakdown)'}
|
|
124
|
+
|
|
125
|
+
${contextContent ? `RELATED FILES (sanitized):\n${contextContent}` : ''}
|
|
126
|
+
|
|
127
|
+
Return in the requested format.`;
|
|
128
|
+
|
|
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
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze_tokens Tool
|
|
3
|
+
*
|
|
4
|
+
* Extracts and understands existing design systems.
|
|
5
|
+
* Returns structured design tokens from CSS/SCSS files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import { Config } from '../config/index.js';
|
|
12
|
+
import { assertReadablePath } from '../context/guards.js';
|
|
13
|
+
import { sanitizeContent } from '../context/filter.js';
|
|
14
|
+
import { generateWithGemini } from '../generation/gemini-client.js';
|
|
15
|
+
|
|
16
|
+
const inputSchema = {
|
|
17
|
+
paths: z.array(z.string()).describe('Paths to CSS, SCSS, or design token files to analyze'),
|
|
18
|
+
format: z
|
|
19
|
+
.enum(['json', 'css-vars', 'tailwind', 'summary'])
|
|
20
|
+
.default('json')
|
|
21
|
+
.describe('Output format for the extracted tokens'),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SYSTEM_PROMPT = `You are a design system analyst. Your task is to extract and structure design tokens from CSS/SCSS files.
|
|
25
|
+
|
|
26
|
+
Analyze the provided files and extract:
|
|
27
|
+
1. Colors - with semantic names and usage hints
|
|
28
|
+
2. Typography - font families, sizes, weights, line heights
|
|
29
|
+
3. Spacing - margin/padding values, identify a scale (e.g. 4px base)
|
|
30
|
+
4. Breakpoints - media query values
|
|
31
|
+
5. Shadows - box-shadow values
|
|
32
|
+
6. Border radii - radius values
|
|
33
|
+
7. Transitions - duration and easing values
|
|
34
|
+
8. Z-index - layering values
|
|
35
|
+
|
|
36
|
+
Output rules:
|
|
37
|
+
- If format=json: output ONE valid JSON object only. No prose. No markdown. No code fences.
|
|
38
|
+
- If format=css-vars: output ONLY CSS custom properties (no markdown).
|
|
39
|
+
- If format=tailwind: output ONLY a tailwind theme extension (JS object or full tailwind.config.js), no markdown.
|
|
40
|
+
- If format=summary: output concise bullet points.
|
|
41
|
+
|
|
42
|
+
When naming tokens, prefer semantic names (primary, surface, text, border) rather than raw hex values.`;
|
|
43
|
+
|
|
44
|
+
export function registerAnalyzeTokens(server: McpServer, config: Config): void {
|
|
45
|
+
server.registerTool(
|
|
46
|
+
'analyze_tokens',
|
|
47
|
+
{
|
|
48
|
+
title: 'Analyze Design Tokens',
|
|
49
|
+
description:
|
|
50
|
+
'Extract and structure design tokens from existing CSS/SCSS files. Use before create_ui to understand existing design systems.',
|
|
51
|
+
inputSchema,
|
|
52
|
+
},
|
|
53
|
+
async (args) => {
|
|
54
|
+
const paths = args.paths as string[];
|
|
55
|
+
const format = (args.format as 'json' | 'css-vars' | 'tailwind' | 'summary') || 'json';
|
|
56
|
+
|
|
57
|
+
// Read all specified files (with safety checks)
|
|
58
|
+
const fileContents: string[] = [];
|
|
59
|
+
const errors: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const filePath of paths) {
|
|
62
|
+
try {
|
|
63
|
+
const safePath = assertReadablePath(filePath, config);
|
|
64
|
+
const raw = fs.readFileSync(safePath, 'utf-8');
|
|
65
|
+
const content = sanitizeContent(raw);
|
|
66
|
+
fileContents.push(`/* File: ${filePath} */\n${content}`);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : 'Could not read';
|
|
69
|
+
errors.push(`${filePath}: ${message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (fileContents.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text' as const,
|
|
78
|
+
text: `Error: Could not read any files.\n${errors.join('\n')}`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
isError: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (errors.length > 0 && config.debug) {
|
|
86
|
+
console.error('[analyze_tokens] Warnings:\n' + errors.join('\n'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const combinedContent = fileContents.join('\n\n');
|
|
90
|
+
|
|
91
|
+
const userPrompt = `Analyze these CSS/SCSS files and extract design tokens.
|
|
92
|
+
Format: ${format}
|
|
93
|
+
|
|
94
|
+
FILES:
|
|
95
|
+
${combinedContent}`;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const response = await generateWithGemini(config, SYSTEM_PROMPT, userPrompt, { toolName: 'analyze_tokens' });
|
|
99
|
+
|
|
100
|
+
// IMPORTANT: Return raw model output ONLY (no wrappers) so JSON / CSS output stays valid.
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: 'text' as const,
|
|
105
|
+
text: response.trim(),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text' as const,
|
|
115
|
+
text: `Error analyzing tokens: ${message}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
}
|