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/mcp/index.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
const { randomUUID } = require('crypto');
|
|
2
|
+
const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
3
|
+
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
4
|
+
const { isInitializeRequest } = require('@modelcontextprotocol/sdk/types.js');
|
|
5
|
+
const { z } = require('zod');
|
|
6
|
+
|
|
7
|
+
const { createStorage } = require('../storage');
|
|
8
|
+
const { createUipro } = require('../uipro');
|
|
9
|
+
|
|
10
|
+
const tools = {
|
|
11
|
+
list_designs: {
|
|
12
|
+
title: 'List Designs',
|
|
13
|
+
description: 'List stored design resources.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
limit: z.number().min(1).max(100).optional(),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
search_designs: {
|
|
19
|
+
title: 'Search Designs',
|
|
20
|
+
description: 'Search designs by keyword, tags, or URL.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
query: z.string(),
|
|
23
|
+
limit: z.number().min(1).max(100).optional(),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
search_library: {
|
|
27
|
+
title: 'Search Library',
|
|
28
|
+
description:
|
|
29
|
+
'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).',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
query: z.string().min(1),
|
|
32
|
+
sources: z.array(z.enum(['designs', 'uipro', 'uipro_stack'])).optional(),
|
|
33
|
+
domain: z
|
|
34
|
+
.union([
|
|
35
|
+
z.literal('auto'),
|
|
36
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
37
|
+
])
|
|
38
|
+
.optional(),
|
|
39
|
+
stack: z.string().min(1).optional(),
|
|
40
|
+
limit: z.number().min(1).max(50).optional(),
|
|
41
|
+
designLimit: z.number().min(1).max(100).optional(),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
get_styleguide: {
|
|
45
|
+
title: 'Get Styleguide',
|
|
46
|
+
description: 'Fetch styleguide markdown by design ID (latest version).',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
designId: z.string(),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
list_uipro_domains: {
|
|
52
|
+
title: 'List UI/UX Pro Domains',
|
|
53
|
+
description: 'List available domains from the built-in UI/UX Pro Max dataset.',
|
|
54
|
+
inputSchema: {},
|
|
55
|
+
},
|
|
56
|
+
list_uipro_stacks: {
|
|
57
|
+
title: 'List UI/UX Pro Stacks',
|
|
58
|
+
description: 'List available stacks from the built-in UI/UX Pro Max dataset.',
|
|
59
|
+
inputSchema: {},
|
|
60
|
+
},
|
|
61
|
+
search_uipro: {
|
|
62
|
+
title: 'Search UI/UX Pro',
|
|
63
|
+
description: 'Search UI/UX Pro Max dataset (BM25) by query and optional domain.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
query: z.string(),
|
|
66
|
+
domain: z
|
|
67
|
+
.union([
|
|
68
|
+
z.literal('auto'),
|
|
69
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
70
|
+
])
|
|
71
|
+
.optional(),
|
|
72
|
+
limit: z.number().min(1).max(20).optional(),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
search_uipro_stack: {
|
|
76
|
+
title: 'Search UI/UX Pro Stack',
|
|
77
|
+
description: 'Search stack-specific UI/UX Pro Max guidelines (BM25).',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
query: z.string(),
|
|
80
|
+
stack: z.string().min(1),
|
|
81
|
+
limit: z.number().min(1).max(20).optional(),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
browse_uipro: {
|
|
85
|
+
title: 'Browse UI/UX Pro',
|
|
86
|
+
description: 'Browse UIPro entries without a query (useful to explore what can be searched).',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
domain: z
|
|
89
|
+
.union([
|
|
90
|
+
z.literal('auto'),
|
|
91
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
92
|
+
])
|
|
93
|
+
.optional(),
|
|
94
|
+
limit: z.number().min(1).max(50).optional(),
|
|
95
|
+
offset: z.number().min(0).max(100000).optional(),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
suggest_uipro: {
|
|
99
|
+
title: 'Suggest UI/UX Pro Keywords',
|
|
100
|
+
description: 'Suggest common keywords for a UIPro domain to help you pick what to search.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
domain: z
|
|
103
|
+
.union([
|
|
104
|
+
z.literal('auto'),
|
|
105
|
+
z.enum(['style', 'prompt', 'color', 'chart', 'landing', 'product', 'ux', 'typography', 'icons']),
|
|
106
|
+
])
|
|
107
|
+
.optional(),
|
|
108
|
+
limit: z.number().min(1).max(50).optional(),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
browse_uipro_stack: {
|
|
112
|
+
title: 'Browse UI/UX Pro Stack',
|
|
113
|
+
description: 'Browse stack-specific UIPro guidelines without a query.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
stack: z.string().min(1),
|
|
116
|
+
limit: z.number().min(1).max(50).optional(),
|
|
117
|
+
offset: z.number().min(0).max(100000).optional(),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
suggest_uipro_stack: {
|
|
121
|
+
title: 'Suggest UI/UX Pro Stack Keywords',
|
|
122
|
+
description: 'Suggest common keywords for a UIPro stack (html-tailwind/react/...).',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
stack: z.string().min(1),
|
|
125
|
+
limit: z.number().min(1).max(50).optional(),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const prompts = {
|
|
131
|
+
analyze_design: {
|
|
132
|
+
title: 'Analyze Design',
|
|
133
|
+
description: 'Summarize design metadata for review.',
|
|
134
|
+
argsSchema: {
|
|
135
|
+
designId: z.string(),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function createToolHandlers(storage, uipro) {
|
|
141
|
+
function matchDesigns(query) {
|
|
142
|
+
const needle = query.toLowerCase();
|
|
143
|
+
const designs = storage.listDesigns();
|
|
144
|
+
return designs.filter((design) => {
|
|
145
|
+
const tags = Array.isArray(design.metadata?.tags) ? design.metadata.tags.join(' ') : '';
|
|
146
|
+
const haystack = [design.name, design.url, design.description, design.category, tags]
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.join(' ')
|
|
149
|
+
.toLowerCase();
|
|
150
|
+
return haystack.includes(needle);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
list_designs: async ({ limit }) => {
|
|
156
|
+
const designs = storage.listDesigns();
|
|
157
|
+
const data = typeof limit === 'number' ? designs.slice(0, limit) : designs;
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
160
|
+
structuredContent: { designs: data },
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
search_designs: async ({ query, limit }) => {
|
|
164
|
+
const matches = matchDesigns(query);
|
|
165
|
+
const data = typeof limit === 'number' ? matches.slice(0, limit) : matches;
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
168
|
+
structuredContent: { designs: data },
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
search_library: async ({ query, sources, domain, stack, limit, designLimit }) => {
|
|
172
|
+
const effectiveSources = Array.isArray(sources) && sources.length > 0 ? sources : ['designs', 'uipro'];
|
|
173
|
+
const designs =
|
|
174
|
+
effectiveSources.includes('designs')
|
|
175
|
+
? (() => {
|
|
176
|
+
const matches = matchDesigns(query);
|
|
177
|
+
const max = typeof designLimit === 'number' ? designLimit : typeof limit === 'number' ? limit : undefined;
|
|
178
|
+
return typeof max === 'number' ? matches.slice(0, max) : matches;
|
|
179
|
+
})()
|
|
180
|
+
: [];
|
|
181
|
+
|
|
182
|
+
const resolvedDomain = domain === 'auto' ? undefined : domain;
|
|
183
|
+
const maxUipro = typeof limit === 'number' ? limit : 10;
|
|
184
|
+
|
|
185
|
+
let uiproResult = null;
|
|
186
|
+
if (effectiveSources.includes('uipro')) {
|
|
187
|
+
try {
|
|
188
|
+
uiproResult = uipro.search({ query, domain: resolvedDomain, limit: maxUipro });
|
|
189
|
+
} catch {
|
|
190
|
+
uiproResult = {
|
|
191
|
+
error: 'uipro_data_unavailable',
|
|
192
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let uiproStackResult = null;
|
|
198
|
+
if (effectiveSources.includes('uipro_stack') && typeof stack === 'string' && stack.trim()) {
|
|
199
|
+
try {
|
|
200
|
+
uiproStackResult = uipro.searchStack({ query, stack: stack.trim(), limit: maxUipro });
|
|
201
|
+
} catch {
|
|
202
|
+
uiproStackResult = {
|
|
203
|
+
error: 'uipro_data_unavailable',
|
|
204
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const items = [];
|
|
210
|
+
for (const d of designs) {
|
|
211
|
+
items.push({
|
|
212
|
+
source: 'design',
|
|
213
|
+
id: d.id,
|
|
214
|
+
name: d.name,
|
|
215
|
+
url: d.url,
|
|
216
|
+
updatedAt: d.updatedAt || d.createdAt || null,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (uiproResult && !uiproResult.error && Array.isArray(uiproResult.results)) {
|
|
220
|
+
uiproResult.results.forEach((row) => items.push({ source: 'uipro', domain: uiproResult.domain, row }));
|
|
221
|
+
}
|
|
222
|
+
if (uiproStackResult && !uiproStackResult.error && Array.isArray(uiproStackResult.results)) {
|
|
223
|
+
uiproStackResult.results.forEach((row) => items.push({ source: 'uipro_stack', stack: uiproStackResult.stack, row }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = {
|
|
227
|
+
query,
|
|
228
|
+
sources: effectiveSources,
|
|
229
|
+
designs,
|
|
230
|
+
uipro: uiproResult,
|
|
231
|
+
uiproStack: uiproStackResult,
|
|
232
|
+
items,
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
236
|
+
structuredContent: data,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
get_styleguide: async ({ designId }) => {
|
|
240
|
+
const versions = storage.listVersions(designId);
|
|
241
|
+
if (!versions || versions.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: 'text', text: `No versions found for design: ${designId}` }],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const latest = versions[0];
|
|
247
|
+
const version = await storage.getVersion(latest.id);
|
|
248
|
+
if (!version) {
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: 'text', text: `Version not found: ${latest.id}` }],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const markdown = version.styleguideMarkdown || '';
|
|
254
|
+
if (!markdown) {
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: 'text', text: `Styleguide is empty for design: ${designId}` }],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
content: [{ type: 'text', text: markdown }],
|
|
261
|
+
structuredContent: { designId, versionId: latest.id, markdown },
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
list_uipro_domains: async () => {
|
|
265
|
+
const data = uipro.domains;
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
268
|
+
structuredContent: { domains: data },
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
list_uipro_stacks: async () => {
|
|
272
|
+
const data = uipro.stacks;
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
275
|
+
structuredContent: { stacks: data },
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
search_uipro: async ({ query, domain, limit }) => {
|
|
279
|
+
let data;
|
|
280
|
+
try {
|
|
281
|
+
data = uipro.search({ query, domain: domain === 'auto' ? undefined : domain, limit });
|
|
282
|
+
} catch {
|
|
283
|
+
data = {
|
|
284
|
+
error: 'uipro_data_unavailable',
|
|
285
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
290
|
+
structuredContent: data,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
search_uipro_stack: async ({ query, stack, limit }) => {
|
|
294
|
+
let data;
|
|
295
|
+
try {
|
|
296
|
+
data = uipro.searchStack({ query, stack, limit });
|
|
297
|
+
} catch {
|
|
298
|
+
data = {
|
|
299
|
+
error: 'uipro_data_unavailable',
|
|
300
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
305
|
+
structuredContent: data,
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
browse_uipro: async ({ domain, limit, offset }) => {
|
|
309
|
+
let data;
|
|
310
|
+
try {
|
|
311
|
+
data = uipro.browse({ domain: domain === 'auto' ? undefined : domain, limit, offset });
|
|
312
|
+
} catch {
|
|
313
|
+
data = {
|
|
314
|
+
error: 'uipro_data_unavailable',
|
|
315
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
320
|
+
structuredContent: data,
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
suggest_uipro: async ({ domain, limit }) => {
|
|
324
|
+
let data;
|
|
325
|
+
try {
|
|
326
|
+
data = uipro.suggest({ domain: domain === 'auto' ? undefined : domain, limit });
|
|
327
|
+
} catch {
|
|
328
|
+
data = {
|
|
329
|
+
error: 'uipro_data_unavailable',
|
|
330
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
335
|
+
structuredContent: data,
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
browse_uipro_stack: async ({ stack, limit, offset }) => {
|
|
339
|
+
let data;
|
|
340
|
+
try {
|
|
341
|
+
data = uipro.browseStack({ stack, limit, offset });
|
|
342
|
+
} catch {
|
|
343
|
+
data = {
|
|
344
|
+
error: 'uipro_data_unavailable',
|
|
345
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
350
|
+
structuredContent: data,
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
suggest_uipro_stack: async ({ stack, limit }) => {
|
|
354
|
+
let data;
|
|
355
|
+
try {
|
|
356
|
+
data = uipro.suggestStack({ stack, limit });
|
|
357
|
+
} catch {
|
|
358
|
+
data = {
|
|
359
|
+
error: 'uipro_data_unavailable',
|
|
360
|
+
hint: 'Check DESIGN_LEARN_UIPRO_DATA_DIR or built-in dataset integrity.',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
365
|
+
structuredContent: data,
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function createMcpServer({ name, version, storage, uipro }) {
|
|
372
|
+
const server = new McpServer(
|
|
373
|
+
{
|
|
374
|
+
name,
|
|
375
|
+
version,
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
capabilities: {
|
|
379
|
+
tools: Object.keys(tools),
|
|
380
|
+
resources: ['server-info', 'design'],
|
|
381
|
+
prompts: Object.keys(prompts),
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const handlers = createToolHandlers(storage, uipro);
|
|
387
|
+
Object.entries(tools).forEach(([toolName, schema]) => {
|
|
388
|
+
server.registerTool(toolName, schema, handlers[toolName]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
server.registerResource(
|
|
392
|
+
'server-info',
|
|
393
|
+
'design-learn://info',
|
|
394
|
+
{
|
|
395
|
+
title: 'Design-Learn Server Info',
|
|
396
|
+
description: 'Basic server metadata.',
|
|
397
|
+
mimeType: 'application/json',
|
|
398
|
+
},
|
|
399
|
+
async (uri) => ({
|
|
400
|
+
contents: [
|
|
401
|
+
{
|
|
402
|
+
uri: uri.href,
|
|
403
|
+
text: JSON.stringify({ name, version, timestamp: new Date().toISOString() }),
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
server.registerResource(
|
|
410
|
+
'design',
|
|
411
|
+
new ResourceTemplate('design://{designId}', { list: undefined }),
|
|
412
|
+
{
|
|
413
|
+
title: 'Design Metadata',
|
|
414
|
+
description: 'Design metadata stored in the local database.',
|
|
415
|
+
mimeType: 'application/json',
|
|
416
|
+
},
|
|
417
|
+
async (uri, { designId }) => {
|
|
418
|
+
const design = await storage.getDesign(designId);
|
|
419
|
+
return {
|
|
420
|
+
contents: [
|
|
421
|
+
{
|
|
422
|
+
uri: uri.href,
|
|
423
|
+
text: JSON.stringify(design || { error: 'not_found', designId }, null, 2),
|
|
424
|
+
mimeType: 'application/json',
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
Object.entries(prompts).forEach(([promptName, schema]) => {
|
|
432
|
+
server.registerPrompt(promptName, schema, ({ 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
|
+
return server;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function createMcpHandler(options = {}) {
|
|
449
|
+
const storage = options.storage || createStorage({ dataDir: options.dataDir });
|
|
450
|
+
const ownsStorage = !options.storage;
|
|
451
|
+
const serverName = options.serverName || 'design-learn';
|
|
452
|
+
const serverVersion = options.serverVersion || '0.1.0';
|
|
453
|
+
const authToken = options.authToken || null;
|
|
454
|
+
const uipro = options.uipro || createUipro({ dataDir: storage.dataDir });
|
|
455
|
+
const server = createMcpServer({ name: serverName, version: serverVersion, storage, uipro });
|
|
456
|
+
const transports = new Map();
|
|
457
|
+
|
|
458
|
+
function verifyAuth(req, res) {
|
|
459
|
+
if (!authToken) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const header = req.headers.authorization || '';
|
|
464
|
+
if (header === `Bearer ${authToken}`) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
res.writeHead(401, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
469
|
+
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function handleMcpPost(req, res, body) {
|
|
474
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
475
|
+
let transport;
|
|
476
|
+
|
|
477
|
+
if (sessionId && transports.has(sessionId)) {
|
|
478
|
+
transport = transports.get(sessionId);
|
|
479
|
+
} else if (!sessionId && isInitializeRequest(body)) {
|
|
480
|
+
transport = new StreamableHTTPServerTransport({
|
|
481
|
+
sessionIdGenerator: () => randomUUID(),
|
|
482
|
+
onsessioninitialized: (id) => {
|
|
483
|
+
transports.set(id, transport);
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
transport.onclose = () => {
|
|
487
|
+
if (transport.sessionId) {
|
|
488
|
+
transports.delete(transport.sessionId);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
await server.connect(transport);
|
|
492
|
+
} else {
|
|
493
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
494
|
+
res.end(
|
|
495
|
+
JSON.stringify({
|
|
496
|
+
jsonrpc: '2.0',
|
|
497
|
+
error: { code: -32000, message: 'Invalid or missing session ID' },
|
|
498
|
+
id: null,
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
await transport.handleRequest(req, res, body);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function handleMcpStream(req, res) {
|
|
508
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
509
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
510
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
511
|
+
res.end('Invalid or missing session ID');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const transport = transports.get(sessionId);
|
|
516
|
+
await transport.handleRequest(req, res);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function handleRequest(req, res, body) {
|
|
520
|
+
if (!verifyAuth(req, res)) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (req.method === 'POST') {
|
|
525
|
+
await handleMcpPost(req, res, body);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (req.method === 'GET' || req.method === 'DELETE') {
|
|
530
|
+
await handleMcpStream(req, res);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
535
|
+
res.end(JSON.stringify({ error: 'method_not_allowed' }));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function close() {
|
|
539
|
+
for (const transport of transports.values()) {
|
|
540
|
+
await transport.close();
|
|
541
|
+
}
|
|
542
|
+
transports.clear();
|
|
543
|
+
if (ownsStorage) {
|
|
544
|
+
storage.close();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
handleRequest,
|
|
550
|
+
close,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = {
|
|
555
|
+
createMcpHandler,
|
|
556
|
+
};
|