design-learn-server 0.1.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/README.md +123 -0
- package/package.json +29 -0
- package/src/cli.js +152 -0
- package/src/mcp/index.js +556 -0
- package/src/pipeline/index.js +335 -0
- package/src/playwrightSupport.js +65 -0
- package/src/preview/index.js +204 -0
- package/src/server.js +1385 -0
- package/src/stdio.js +464 -0
- package/src/storage/fileStore.js +45 -0
- package/src/storage/index.js +983 -0
- package/src/storage/paths.js +113 -0
- package/src/storage/sqliteStore.js +114 -0
- package/src/uipro/bm25.js +121 -0
- package/src/uipro/config.js +264 -0
- package/src/uipro/csv.js +90 -0
- package/src/uipro/data/charts.csv +26 -0
- package/src/uipro/data/colors.csv +97 -0
- package/src/uipro/data/icons.csv +101 -0
- package/src/uipro/data/landing.csv +31 -0
- package/src/uipro/data/products.csv +97 -0
- package/src/uipro/data/prompts.csv +24 -0
- package/src/uipro/data/stacks/flutter.csv +53 -0
- package/src/uipro/data/stacks/html-tailwind.csv +56 -0
- package/src/uipro/data/stacks/nextjs.csv +53 -0
- package/src/uipro/data/stacks/nuxt-ui.csv +51 -0
- package/src/uipro/data/stacks/nuxtjs.csv +59 -0
- package/src/uipro/data/stacks/react-native.csv +52 -0
- package/src/uipro/data/stacks/react.csv +54 -0
- package/src/uipro/data/stacks/shadcn.csv +61 -0
- package/src/uipro/data/stacks/svelte.csv +54 -0
- package/src/uipro/data/stacks/swiftui.csv +51 -0
- package/src/uipro/data/stacks/vue.csv +50 -0
- package/src/uipro/data/styles.csv +59 -0
- package/src/uipro/data/typography.csv +58 -0
- package/src/uipro/data/ux-guidelines.csv +100 -0
- package/src/uipro/index.js +581 -0
package/src/stdio.js
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
6
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
7
|
+
const { z } = require('zod');
|
|
8
|
+
|
|
9
|
+
const { createStorage } = require('./storage');
|
|
10
|
+
const { createUipro } = require('./uipro');
|
|
11
|
+
|
|
12
|
+
const serverRoot = path.resolve(__dirname, '..');
|
|
13
|
+
const autoInstallPlaywright = process.env.DESIGN_LEARN_AUTO_INSTALL_PLAYWRIGHT !== '0';
|
|
14
|
+
|
|
15
|
+
function isPlaywrightInstalled() {
|
|
16
|
+
try {
|
|
17
|
+
require.resolve('playwright', { paths: [serverRoot] });
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installPlaywright() {
|
|
25
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const child = spawn(npmCmd, ['install', 'playwright'], {
|
|
28
|
+
cwd: serverRoot,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
child.stdout?.on('data', (chunk) => process.stderr.write(chunk));
|
|
32
|
+
child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
|
|
33
|
+
child.on('error', reject);
|
|
34
|
+
child.on('exit', (code) => {
|
|
35
|
+
if (code === 0) resolve();
|
|
36
|
+
else reject(new Error(`npm_install_failed:${code ?? 'unknown'}`));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function ensurePlaywright() {
|
|
42
|
+
if (!autoInstallPlaywright) return;
|
|
43
|
+
if (isPlaywrightInstalled()) return;
|
|
44
|
+
console.error('[design-learn-mcp] Playwright not found, installing...');
|
|
45
|
+
try {
|
|
46
|
+
await installPlaywright();
|
|
47
|
+
console.error('[design-learn-mcp] Playwright installed.');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('[design-learn-mcp] Playwright install failed:', error?.message || error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 数据目录优先级:环境变量 > 用户目录下的 .design-learn
|
|
54
|
+
// 这样 npx 用户也能正常使用,数据统一存放在 ~/.design-learn/data
|
|
55
|
+
const os = require('os');
|
|
56
|
+
const defaultDataDir = path.join(os.homedir(), '.design-learn', 'data');
|
|
57
|
+
const dataDir = process.env.DESIGN_LEARN_DATA_DIR || process.env.DATA_DIR || defaultDataDir;
|
|
58
|
+
const storage = createStorage({ dataDir });
|
|
59
|
+
const uipro = createUipro({ dataDir });
|
|
60
|
+
|
|
61
|
+
function matchDesigns(query) {
|
|
62
|
+
const needle = query.toLowerCase();
|
|
63
|
+
const designs = storage.listDesigns();
|
|
64
|
+
return designs.filter((design) => {
|
|
65
|
+
const tags = Array.isArray(design.metadata?.tags) ? design.metadata.tags.join(' ') : '';
|
|
66
|
+
const haystack = [design.name, design.url, design.description, design.category, tags]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(' ')
|
|
69
|
+
.toLowerCase();
|
|
70
|
+
return haystack.includes(needle);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const server = new McpServer(
|
|
75
|
+
{
|
|
76
|
+
name: process.env.MCP_SERVER_NAME || 'design-learn',
|
|
77
|
+
version: process.env.MCP_SERVER_VERSION || '0.1.0',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
capabilities: {
|
|
81
|
+
tools: {},
|
|
82
|
+
resources: {},
|
|
83
|
+
prompts: {},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Tools
|
|
89
|
+
server.tool(
|
|
90
|
+
'list_designs',
|
|
91
|
+
'List stored design resources',
|
|
92
|
+
{ limit: z.number().min(1).max(100).optional() },
|
|
93
|
+
async ({ limit }) => {
|
|
94
|
+
const designs = storage.listDesigns();
|
|
95
|
+
const data = typeof limit === 'number' ? designs.slice(0, limit) : designs;
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
98
|
+
structuredContent: { designs: data },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
server.tool(
|
|
104
|
+
'search_designs',
|
|
105
|
+
'Search designs by keyword, tags, or URL.',
|
|
106
|
+
{
|
|
107
|
+
query: z.string(),
|
|
108
|
+
limit: z.number().min(1).max(100).optional(),
|
|
109
|
+
},
|
|
110
|
+
async ({ query, limit }) => {
|
|
111
|
+
const matches = matchDesigns(query);
|
|
112
|
+
const data = typeof limit === 'number' ? matches.slice(0, limit) : matches;
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
115
|
+
structuredContent: { designs: data },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
server.tool(
|
|
121
|
+
'search_library',
|
|
122
|
+
'One-shot smart search across local templates (captured designs) + built-in UIPro guidelines. Use this when the user asks for a style/pattern/component/UX guideline or "a template like X". Domain is auto-detected unless specified; provide stack only when the user requests a specific tech stack (e.g. html-tailwind/react).',
|
|
123
|
+
{
|
|
124
|
+
query: z.string().min(1),
|
|
125
|
+
sources: z.array(z.enum(['designs', 'uipro', 'uipro_stack'])).optional(),
|
|
126
|
+
domain: z
|
|
127
|
+
.union([
|
|
128
|
+
z.literal('auto'),
|
|
129
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
130
|
+
])
|
|
131
|
+
.optional(),
|
|
132
|
+
stack: z.string().min(1).optional(),
|
|
133
|
+
limit: z.number().min(1).max(50).optional(),
|
|
134
|
+
designLimit: z.number().min(1).max(100).optional(),
|
|
135
|
+
},
|
|
136
|
+
async ({ query, sources, domain, stack, limit, designLimit }) => {
|
|
137
|
+
const effectiveSources = Array.isArray(sources) && sources.length > 0 ? sources : ['designs', 'uipro'];
|
|
138
|
+
const designs =
|
|
139
|
+
effectiveSources.includes('designs')
|
|
140
|
+
? (() => {
|
|
141
|
+
const matches = matchDesigns(query);
|
|
142
|
+
const max = typeof designLimit === 'number' ? designLimit : typeof limit === 'number' ? limit : undefined;
|
|
143
|
+
return typeof max === 'number' ? matches.slice(0, max) : matches;
|
|
144
|
+
})()
|
|
145
|
+
: [];
|
|
146
|
+
|
|
147
|
+
const resolvedDomain = domain === 'auto' ? undefined : domain;
|
|
148
|
+
const maxUipro = typeof limit === 'number' ? limit : 10;
|
|
149
|
+
|
|
150
|
+
let uiproResult = null;
|
|
151
|
+
if (effectiveSources.includes('uipro')) {
|
|
152
|
+
try {
|
|
153
|
+
uiproResult = uipro.search({ query, domain: resolvedDomain, limit: maxUipro });
|
|
154
|
+
} catch {
|
|
155
|
+
uiproResult = {
|
|
156
|
+
error: 'uipro_data_unavailable',
|
|
157
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let uiproStackResult = null;
|
|
163
|
+
if (effectiveSources.includes('uipro_stack') && typeof stack === 'string' && stack.trim()) {
|
|
164
|
+
try {
|
|
165
|
+
uiproStackResult = uipro.searchStack({ query, stack: stack.trim(), limit: maxUipro });
|
|
166
|
+
} catch {
|
|
167
|
+
uiproStackResult = {
|
|
168
|
+
error: 'uipro_data_unavailable',
|
|
169
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const items = [];
|
|
175
|
+
for (const d of designs) {
|
|
176
|
+
items.push({
|
|
177
|
+
source: 'design',
|
|
178
|
+
id: d.id,
|
|
179
|
+
name: d.name,
|
|
180
|
+
url: d.url,
|
|
181
|
+
updatedAt: d.updatedAt || d.createdAt || null,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (uiproResult && !uiproResult.error && Array.isArray(uiproResult.results)) {
|
|
185
|
+
uiproResult.results.forEach((row) => items.push({ source: 'uipro', domain: uiproResult.domain, row }));
|
|
186
|
+
}
|
|
187
|
+
if (uiproStackResult && !uiproStackResult.error && Array.isArray(uiproStackResult.results)) {
|
|
188
|
+
uiproStackResult.results.forEach((row) => items.push({ source: 'uipro_stack', stack: uiproStackResult.stack, row }));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const data = { query, sources: effectiveSources, designs, uipro: uiproResult, uiproStack: uiproStackResult, items };
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
194
|
+
structuredContent: data,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
server.tool(
|
|
200
|
+
'get_styleguide',
|
|
201
|
+
'Get styleguide markdown by design ID (latest version).',
|
|
202
|
+
{ designId: z.string() },
|
|
203
|
+
async ({ designId }) => {
|
|
204
|
+
const versions = storage.listVersions(designId);
|
|
205
|
+
if (!versions || versions.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: `No versions found for design: ${designId}` }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const latest = versions[0];
|
|
211
|
+
const version = await storage.getVersion(latest.id);
|
|
212
|
+
if (!version) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: 'text', text: `Version not found: ${latest.id}` }],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const markdown = version.styleguideMarkdown || '';
|
|
218
|
+
if (!markdown) {
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text', text: `Styleguide is empty for design: ${designId}` }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: 'text', text: markdown }],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
server.tool('list_uipro_domains', 'List available domains from the built-in UI/UX Pro Max dataset.', {}, async () => {
|
|
230
|
+
const data = uipro.domains;
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
233
|
+
structuredContent: { domains: data },
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
server.tool('list_uipro_stacks', 'List available stacks from the built-in UI/UX Pro Max dataset.', {}, async () => {
|
|
238
|
+
const data = uipro.stacks;
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
241
|
+
structuredContent: { stacks: data },
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
server.tool(
|
|
246
|
+
'search_uipro',
|
|
247
|
+
'Search UI/UX Pro Max dataset (BM25) by query and optional domain.',
|
|
248
|
+
{
|
|
249
|
+
query: z.string(),
|
|
250
|
+
domain: z
|
|
251
|
+
.union([
|
|
252
|
+
z.literal('auto'),
|
|
253
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
254
|
+
])
|
|
255
|
+
.optional(),
|
|
256
|
+
limit: z.number().min(1).max(20).optional(),
|
|
257
|
+
},
|
|
258
|
+
async ({ query, domain, limit }) => {
|
|
259
|
+
let data;
|
|
260
|
+
try {
|
|
261
|
+
data = uipro.search({ query, domain: domain === 'auto' ? undefined : domain, limit });
|
|
262
|
+
} catch {
|
|
263
|
+
data = {
|
|
264
|
+
error: 'uipro_data_unavailable',
|
|
265
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
270
|
+
structuredContent: data,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
server.tool(
|
|
276
|
+
'search_uipro_stack',
|
|
277
|
+
'Search stack-specific UI/UX Pro Max guidelines (BM25).',
|
|
278
|
+
{
|
|
279
|
+
query: z.string(),
|
|
280
|
+
stack: z.string().min(1),
|
|
281
|
+
limit: z.number().min(1).max(20).optional(),
|
|
282
|
+
},
|
|
283
|
+
async ({ query, stack, limit }) => {
|
|
284
|
+
let data;
|
|
285
|
+
try {
|
|
286
|
+
data = uipro.searchStack({ query, stack, limit });
|
|
287
|
+
} catch {
|
|
288
|
+
data = {
|
|
289
|
+
error: 'uipro_data_unavailable',
|
|
290
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
295
|
+
structuredContent: data,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
server.tool(
|
|
301
|
+
'browse_uipro',
|
|
302
|
+
'Browse UIPro entries without a query (useful to explore what can be searched).',
|
|
303
|
+
{
|
|
304
|
+
domain: z
|
|
305
|
+
.union([
|
|
306
|
+
z.literal('auto'),
|
|
307
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
308
|
+
])
|
|
309
|
+
.optional(),
|
|
310
|
+
limit: z.number().min(1).max(50).optional(),
|
|
311
|
+
offset: z.number().min(0).max(100000).optional(),
|
|
312
|
+
},
|
|
313
|
+
async ({ domain, limit, offset }) => {
|
|
314
|
+
let data;
|
|
315
|
+
try {
|
|
316
|
+
data = uipro.browse({ domain: domain === 'auto' ? undefined : domain, limit, offset });
|
|
317
|
+
} catch {
|
|
318
|
+
data = {
|
|
319
|
+
error: 'uipro_data_unavailable',
|
|
320
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
325
|
+
structuredContent: data,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
server.tool(
|
|
331
|
+
'suggest_uipro',
|
|
332
|
+
'Suggest common keywords for a UIPro domain to help you pick what to search.',
|
|
333
|
+
{
|
|
334
|
+
domain: z
|
|
335
|
+
.union([
|
|
336
|
+
z.literal('auto'),
|
|
337
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
338
|
+
])
|
|
339
|
+
.optional(),
|
|
340
|
+
limit: z.number().min(1).max(50).optional(),
|
|
341
|
+
},
|
|
342
|
+
async ({ domain, limit }) => {
|
|
343
|
+
let data;
|
|
344
|
+
try {
|
|
345
|
+
data = uipro.suggest({ domain: domain === 'auto' ? undefined : domain, limit });
|
|
346
|
+
} catch {
|
|
347
|
+
data = {
|
|
348
|
+
error: 'uipro_data_unavailable',
|
|
349
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
354
|
+
structuredContent: data,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
server.tool(
|
|
360
|
+
'browse_uipro_stack',
|
|
361
|
+
'Browse stack-specific UIPro guidelines without a query.',
|
|
362
|
+
{
|
|
363
|
+
stack: z.string().min(1),
|
|
364
|
+
limit: z.number().min(1).max(50).optional(),
|
|
365
|
+
offset: z.number().min(0).max(100000).optional(),
|
|
366
|
+
},
|
|
367
|
+
async ({ stack, limit, offset }) => {
|
|
368
|
+
let data;
|
|
369
|
+
try {
|
|
370
|
+
data = uipro.browseStack({ stack, limit, offset });
|
|
371
|
+
} catch {
|
|
372
|
+
data = {
|
|
373
|
+
error: 'uipro_data_unavailable',
|
|
374
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
379
|
+
structuredContent: data,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
server.tool(
|
|
385
|
+
'suggest_uipro_stack',
|
|
386
|
+
'Suggest common keywords for a UIPro stack (html-tailwind/react/...).',
|
|
387
|
+
{
|
|
388
|
+
stack: z.string().min(1),
|
|
389
|
+
limit: z.number().min(1).max(50).optional(),
|
|
390
|
+
},
|
|
391
|
+
async ({ stack, limit }) => {
|
|
392
|
+
let data;
|
|
393
|
+
try {
|
|
394
|
+
data = uipro.suggestStack({ stack, limit });
|
|
395
|
+
} catch {
|
|
396
|
+
data = {
|
|
397
|
+
error: 'uipro_data_unavailable',
|
|
398
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
403
|
+
structuredContent: data,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Resources
|
|
409
|
+
server.resource(
|
|
410
|
+
'server-info',
|
|
411
|
+
'design-learn://info',
|
|
412
|
+
async (uri) => ({
|
|
413
|
+
contents: [
|
|
414
|
+
{
|
|
415
|
+
uri: uri.href,
|
|
416
|
+
text: JSON.stringify({
|
|
417
|
+
name: 'design-learn',
|
|
418
|
+
version: '0.1.0',
|
|
419
|
+
dataDir,
|
|
420
|
+
timestamp: new Date().toISOString(),
|
|
421
|
+
}),
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
})
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Prompts
|
|
428
|
+
server.prompt(
|
|
429
|
+
'analyze_design',
|
|
430
|
+
'Summarize design metadata for review',
|
|
431
|
+
{ designId: z.string() },
|
|
432
|
+
({ designId }) => ({
|
|
433
|
+
messages: [
|
|
434
|
+
{
|
|
435
|
+
role: 'user',
|
|
436
|
+
content: {
|
|
437
|
+
type: 'text',
|
|
438
|
+
text: `Analyze design metadata for ID: ${designId}. Summarize key traits and risks.`,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
async function main() {
|
|
446
|
+
await ensurePlaywright();
|
|
447
|
+
const startHttpServer = process.env.DESIGN_LEARN_STDIO_START_HTTP_SERVER !== '0';
|
|
448
|
+
if (startHttpServer) {
|
|
449
|
+
// 同时启动 HTTP 服务(给 Chrome/VSCode 插件用)
|
|
450
|
+
const httpServer = spawn('node', [path.join(__dirname, 'server.js')], {
|
|
451
|
+
stdio: 'ignore',
|
|
452
|
+
detached: true,
|
|
453
|
+
env: { ...process.env, DESIGN_LEARN_DATA_DIR: dataDir },
|
|
454
|
+
});
|
|
455
|
+
httpServer.unref();
|
|
456
|
+
}
|
|
457
|
+
const transport = new StdioServerTransport();
|
|
458
|
+
await server.connect(transport);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
main().catch((error) => {
|
|
462
|
+
console.error('[design-learn-mcp] stdio error:', error);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
async function ensureDir(dirPath) {
|
|
5
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function writeJson(filePath, data) {
|
|
9
|
+
await ensureDir(path.dirname(filePath));
|
|
10
|
+
const payload = JSON.stringify(data, null, 2);
|
|
11
|
+
await fs.writeFile(filePath, payload, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readJson(filePath) {
|
|
15
|
+
const payload = await fs.readFile(filePath, 'utf-8');
|
|
16
|
+
return JSON.parse(payload);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function writeText(filePath, content) {
|
|
20
|
+
await ensureDir(path.dirname(filePath));
|
|
21
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readText(filePath) {
|
|
25
|
+
return fs.readFile(filePath, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function removePath(targetPath) {
|
|
29
|
+
try {
|
|
30
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
ensureDir,
|
|
40
|
+
writeJson,
|
|
41
|
+
readJson,
|
|
42
|
+
writeText,
|
|
43
|
+
readText,
|
|
44
|
+
removePath,
|
|
45
|
+
};
|