@monoharada/wcf-mcp 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/README.md +162 -0
- package/bin.mjs +7 -0
- package/data/custom-elements.json +63877 -0
- package/data/install-registry.json +706 -0
- package/data/pattern-registry.json +126 -0
- package/package.json +29 -0
- package/server.mjs +664 -0
- package/validator.mjs +240 -0
package/server.mjs
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { collectCemCustomElements, validateTextAgainstCem } from './validator.mjs';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
const CANONICAL_PREFIX = 'dads';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Data loading - supports both bundled (npx) and local (repo) modes
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function resolveDataPath(fileName) {
|
|
18
|
+
// 1. Bundled data inside the package
|
|
19
|
+
const bundled = path.join(__dirname, 'data', fileName);
|
|
20
|
+
// 2. Repo root (when running from source)
|
|
21
|
+
const repoRoot = path.resolve(process.cwd());
|
|
22
|
+
const repoMap = {
|
|
23
|
+
'custom-elements.json': path.join(repoRoot, 'custom-elements.json'),
|
|
24
|
+
'install-registry.json': path.join(repoRoot, 'registry/install-registry.json'),
|
|
25
|
+
'pattern-registry.json': path.join(repoRoot, 'registry/pattern-registry.json'),
|
|
26
|
+
};
|
|
27
|
+
return { bundled, repo: repoMap[fileName] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadJsonData(fileName) {
|
|
31
|
+
const { bundled, repo } = resolveDataPath(fileName);
|
|
32
|
+
// Try bundled first, then fall back to repo
|
|
33
|
+
for (const p of [bundled, repo]) {
|
|
34
|
+
if (!p) continue;
|
|
35
|
+
try {
|
|
36
|
+
const text = await fs.readFile(p, 'utf8');
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
// Try next path
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`データファイルが見つかりません: ${fileName}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Prefix helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function normalizePrefix(prefix) {
|
|
50
|
+
if (typeof prefix !== 'string' || prefix.trim() === '') return CANONICAL_PREFIX;
|
|
51
|
+
return prefix.trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function withPrefix(tagName, prefix) {
|
|
55
|
+
if (typeof tagName !== 'string') return tagName;
|
|
56
|
+
const p = normalizePrefix(prefix);
|
|
57
|
+
if (p === CANONICAL_PREFIX) return tagName;
|
|
58
|
+
const from = `${CANONICAL_PREFIX}-`;
|
|
59
|
+
if (!tagName.startsWith(from)) return tagName;
|
|
60
|
+
return `${p}-${tagName.slice(from.length)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toCanonicalTagName(tagName, prefix) {
|
|
64
|
+
if (typeof tagName !== 'string') return undefined;
|
|
65
|
+
const raw = tagName.trim().toLowerCase();
|
|
66
|
+
if (!raw) return undefined;
|
|
67
|
+
if (raw.startsWith(`${CANONICAL_PREFIX}-`)) return raw;
|
|
68
|
+
|
|
69
|
+
const p = normalizePrefix(prefix);
|
|
70
|
+
if (p !== CANONICAL_PREFIX && raw.startsWith(`${p}-`)) {
|
|
71
|
+
return `${CANONICAL_PREFIX}-${raw.slice(p.length + 1)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return raw;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// CEM helpers
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function findCustomElementDeclarations(manifest) {
|
|
82
|
+
const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
|
|
83
|
+
const decls = [];
|
|
84
|
+
|
|
85
|
+
for (const mod of modules) {
|
|
86
|
+
const modulePath = typeof mod?.path === 'string' ? mod.path : undefined;
|
|
87
|
+
const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
|
|
88
|
+
for (const decl of declarations) {
|
|
89
|
+
if (!decl || typeof decl !== 'object') continue;
|
|
90
|
+
const tagName = typeof decl.tagName === 'string' ? decl.tagName : undefined;
|
|
91
|
+
const isCustomElement = decl.customElement === true || decl.kind === 'custom-element';
|
|
92
|
+
if (!isCustomElement || !tagName) continue;
|
|
93
|
+
|
|
94
|
+
decls.push({ decl, tagName: tagName.toLowerCase(), modulePath });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return decls;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildIndexes(manifest) {
|
|
102
|
+
const decls = findCustomElementDeclarations(manifest);
|
|
103
|
+
|
|
104
|
+
const byTag = new Map();
|
|
105
|
+
const byClass = new Map();
|
|
106
|
+
const modulePathByTag = new Map();
|
|
107
|
+
|
|
108
|
+
for (const { decl, tagName, modulePath } of decls) {
|
|
109
|
+
if (!byTag.has(tagName)) byTag.set(tagName, decl);
|
|
110
|
+
if (typeof decl?.name === 'string' && !byClass.has(decl.name)) byClass.set(decl.name, decl);
|
|
111
|
+
if (!modulePathByTag.has(tagName)) modulePathByTag.set(tagName, modulePath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { byTag, byClass, modulePathByTag, decls };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pickDecl({ byTag, byClass }, { tagName, className, prefix }) {
|
|
118
|
+
if (typeof tagName === 'string' && tagName.trim() !== '') {
|
|
119
|
+
const canonical = toCanonicalTagName(tagName, prefix);
|
|
120
|
+
if (canonical && byTag.has(canonical)) return byTag.get(canonical);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof className === 'string' && className.trim() !== '' && byClass.has(className.trim())) {
|
|
124
|
+
return byClass.get(className.trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function serializeApi(decl, modulePath, prefix) {
|
|
131
|
+
const tagName = typeof decl?.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
132
|
+
const outTag = tagName ? withPrefix(tagName, prefix) : undefined;
|
|
133
|
+
|
|
134
|
+
const attributes = Array.isArray(decl?.attributes) ? decl.attributes : [];
|
|
135
|
+
const slots = Array.isArray(decl?.slots) ? decl.slots : [];
|
|
136
|
+
const events = Array.isArray(decl?.events) ? decl.events : [];
|
|
137
|
+
const cssParts = Array.isArray(decl?.cssParts) ? decl.cssParts : [];
|
|
138
|
+
const cssProperties = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
tagName: outTag,
|
|
142
|
+
className: typeof decl?.name === 'string' ? decl.name : undefined,
|
|
143
|
+
description: typeof decl?.description === 'string' ? decl.description : undefined,
|
|
144
|
+
modulePath,
|
|
145
|
+
custom: decl?.custom,
|
|
146
|
+
attributes: attributes.map((a) => ({
|
|
147
|
+
name: a?.name,
|
|
148
|
+
type: a?.type?.text,
|
|
149
|
+
description: a?.description,
|
|
150
|
+
inheritedFrom: a?.inheritedFrom,
|
|
151
|
+
deprecated: a?.deprecated,
|
|
152
|
+
})),
|
|
153
|
+
slots: slots.map((s) => ({
|
|
154
|
+
name: s?.name,
|
|
155
|
+
description: s?.description,
|
|
156
|
+
})),
|
|
157
|
+
events: events.map((e) => ({
|
|
158
|
+
name: e?.name,
|
|
159
|
+
type: e?.type?.text,
|
|
160
|
+
description: e?.description,
|
|
161
|
+
inheritedFrom: e?.inheritedFrom,
|
|
162
|
+
deprecated: e?.deprecated,
|
|
163
|
+
})),
|
|
164
|
+
cssParts: cssParts.map((p) => ({
|
|
165
|
+
name: p?.name,
|
|
166
|
+
description: p?.description,
|
|
167
|
+
})),
|
|
168
|
+
cssProperties: cssProperties.map((p) => ({
|
|
169
|
+
name: p?.name,
|
|
170
|
+
default: p?.default,
|
|
171
|
+
description: p?.description,
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function generateSnippet(api, prefix) {
|
|
177
|
+
const tag = api.tagName ?? withPrefix(String(api.className ?? 'dads-component'), prefix);
|
|
178
|
+
const attrs = Array.isArray(api.attributes) ? api.attributes : [];
|
|
179
|
+
const slots = Array.isArray(api.slots) ? api.slots : [];
|
|
180
|
+
|
|
181
|
+
const attrPriority = [
|
|
182
|
+
'label',
|
|
183
|
+
'support-text',
|
|
184
|
+
'value',
|
|
185
|
+
'name',
|
|
186
|
+
'type',
|
|
187
|
+
'variant',
|
|
188
|
+
'size',
|
|
189
|
+
'required',
|
|
190
|
+
'disabled',
|
|
191
|
+
'readonly',
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const attrByName = new Map(attrs.map((a) => [String(a?.name ?? ''), a]));
|
|
195
|
+
const lines = [];
|
|
196
|
+
|
|
197
|
+
for (const name of attrPriority) {
|
|
198
|
+
const a = attrByName.get(name);
|
|
199
|
+
if (!a) continue;
|
|
200
|
+
const t = String(a.type ?? '').toLowerCase();
|
|
201
|
+
const isBoolean = t.includes('boolean');
|
|
202
|
+
if (isBoolean) lines.push(` ${name}`);
|
|
203
|
+
else lines.push(` ${name}=""`);
|
|
204
|
+
if (lines.length >= 4) break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const open = lines.length > 0 ? `<${tag}\n${lines.join('\n')}\n>` : `<${tag}>`;
|
|
208
|
+
const slotNames = slots
|
|
209
|
+
.map((s) => String(s?.name ?? '').trim())
|
|
210
|
+
.filter((s) => s !== '');
|
|
211
|
+
const slotComment =
|
|
212
|
+
slotNames.length > 0 ? `\n <!-- slots: ${slotNames.join(', ')} -->\n` : '\n';
|
|
213
|
+
|
|
214
|
+
return `${open}${slotComment}</${tag}>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findDeclByComponentId(indexes, componentIdRaw) {
|
|
218
|
+
const componentId = typeof componentIdRaw === 'string' ? componentIdRaw.trim() : '';
|
|
219
|
+
if (!componentId) return undefined;
|
|
220
|
+
for (const { decl, modulePath } of indexes.decls) {
|
|
221
|
+
const installId = decl?.custom?.install?.id;
|
|
222
|
+
const inferredId = decl?.custom?.componentId;
|
|
223
|
+
const id = typeof installId === 'string' ? installId : typeof inferredId === 'string' ? inferredId : undefined;
|
|
224
|
+
if (id === componentId) return { decl, modulePath };
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function applyPrefixToCemIndex(cemIndex, prefix) {
|
|
230
|
+
const p = normalizePrefix(prefix);
|
|
231
|
+
if (p === CANONICAL_PREFIX) return cemIndex;
|
|
232
|
+
|
|
233
|
+
const out = new Map();
|
|
234
|
+
for (const [tag, meta] of cemIndex.entries()) {
|
|
235
|
+
const nextTag = withPrefix(tag, p);
|
|
236
|
+
out.set(nextTag, meta);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function applyPrefixToHtml(html, prefix) {
|
|
242
|
+
const p = normalizePrefix(prefix);
|
|
243
|
+
if (p === CANONICAL_PREFIX) return String(html ?? '');
|
|
244
|
+
const from = `${CANONICAL_PREFIX}-`;
|
|
245
|
+
const to = `${p}-`;
|
|
246
|
+
|
|
247
|
+
return String(html ?? '').replace(
|
|
248
|
+
new RegExp(`<\\s*(\\/?)\\s*${from}([a-z0-9-]+)(?=[\\s/>])`, 'gi'),
|
|
249
|
+
(_m, slash, rest) => `<${slash ?? ''}${to}${String(rest).toLowerCase()}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function loadPatternRegistryShape(raw) {
|
|
254
|
+
if (!raw || typeof raw !== 'object') return { patterns: {} };
|
|
255
|
+
const patterns = raw.patterns && typeof raw.patterns === 'object' ? raw.patterns : {};
|
|
256
|
+
return { patterns };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
260
|
+
const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
|
|
261
|
+
const queue = [...new Set(componentIds.map((c) => String(c ?? '').trim()).filter(Boolean))];
|
|
262
|
+
const out = new Set();
|
|
263
|
+
|
|
264
|
+
while (queue.length > 0) {
|
|
265
|
+
const id = queue.shift();
|
|
266
|
+
if (!id || out.has(id)) continue;
|
|
267
|
+
out.add(id);
|
|
268
|
+
|
|
269
|
+
const meta = components[id];
|
|
270
|
+
const deps = Array.isArray(meta?.deps) ? meta.deps : [];
|
|
271
|
+
for (const d of deps) {
|
|
272
|
+
const dep = String(d ?? '').trim();
|
|
273
|
+
if (dep && !out.has(dep)) queue.push(dep);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return [...out];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Main
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
export async function startServer() {
|
|
285
|
+
const manifest = await loadJsonData('custom-elements.json');
|
|
286
|
+
const indexes = buildIndexes(manifest);
|
|
287
|
+
const canonicalCemIndex = collectCemCustomElements(manifest);
|
|
288
|
+
const installRegistry = await loadJsonData('install-registry.json');
|
|
289
|
+
const patternRegistry = await loadJsonData('pattern-registry.json');
|
|
290
|
+
const { patterns } = loadPatternRegistryShape(patternRegistry);
|
|
291
|
+
|
|
292
|
+
const server = new McpServer({
|
|
293
|
+
name: 'web-components-factory-design-system',
|
|
294
|
+
version: '0.1.0',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// -----------------------------------------------------------------------
|
|
298
|
+
// Tool: list_components
|
|
299
|
+
// -----------------------------------------------------------------------
|
|
300
|
+
server.registerTool(
|
|
301
|
+
'list_components',
|
|
302
|
+
{
|
|
303
|
+
description: 'List custom elements in the design system (from custom-elements.json).',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
prefix: z.string().optional(),
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
async ({ prefix }) => {
|
|
309
|
+
const p = normalizePrefix(prefix);
|
|
310
|
+
const list = indexes.decls.map(({ decl, tagName, modulePath }) => ({
|
|
311
|
+
tagName: withPrefix(tagName, p),
|
|
312
|
+
className: typeof decl?.name === 'string' ? decl.name : undefined,
|
|
313
|
+
description: typeof decl?.description === 'string' ? decl.description : undefined,
|
|
314
|
+
modulePath,
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: 'text', text: JSON.stringify(list, null, 2) }],
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// -----------------------------------------------------------------------
|
|
324
|
+
// Tool: get_component_api
|
|
325
|
+
// -----------------------------------------------------------------------
|
|
326
|
+
server.registerTool(
|
|
327
|
+
'get_component_api',
|
|
328
|
+
{
|
|
329
|
+
description:
|
|
330
|
+
'Get a single component API (attributes/slots/events/cssParts/cssProperties) by tagName or className.',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
tagName: z.string().optional(),
|
|
333
|
+
className: z.string().optional(),
|
|
334
|
+
prefix: z.string().optional(),
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
async ({ tagName, className, prefix }) => {
|
|
338
|
+
const decl = pickDecl(indexes, { tagName, className, prefix });
|
|
339
|
+
if (!decl) {
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: 'text',
|
|
344
|
+
text: `Component not found (tagName=${String(tagName ?? '')}, className=${String(className ?? '')})`,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
isError: true,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
352
|
+
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
353
|
+
const api = serializeApi(decl, modulePath, prefix);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: 'text', text: JSON.stringify(api, null, 2) }],
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// -----------------------------------------------------------------------
|
|
362
|
+
// Tool: generate_usage_snippet
|
|
363
|
+
// -----------------------------------------------------------------------
|
|
364
|
+
server.registerTool(
|
|
365
|
+
'generate_usage_snippet',
|
|
366
|
+
{
|
|
367
|
+
description: 'Generate a minimal usage snippet for a component.',
|
|
368
|
+
inputSchema: {
|
|
369
|
+
component: z.string(),
|
|
370
|
+
prefix: z.string().optional(),
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
async ({ component, prefix }) => {
|
|
374
|
+
const decl =
|
|
375
|
+
pickDecl(indexes, { tagName: component, prefix }) ??
|
|
376
|
+
pickDecl(indexes, { className: component, prefix });
|
|
377
|
+
|
|
378
|
+
if (!decl) {
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: 'text', text: `Component not found: ${component}` }],
|
|
381
|
+
isError: true,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
386
|
+
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
387
|
+
const api = serializeApi(decl, modulePath, prefix);
|
|
388
|
+
const snippet = generateSnippet(api, prefix);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
content: [{ type: 'text', text: snippet }],
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// -----------------------------------------------------------------------
|
|
397
|
+
// Tool: get_install_recipe
|
|
398
|
+
// -----------------------------------------------------------------------
|
|
399
|
+
server.registerTool(
|
|
400
|
+
'get_install_recipe',
|
|
401
|
+
{
|
|
402
|
+
description:
|
|
403
|
+
'Get an install recipe (componentId/deps/define + usage snippet) from CEM custom metadata.',
|
|
404
|
+
inputSchema: {
|
|
405
|
+
component: z.string(),
|
|
406
|
+
prefix: z.string().optional(),
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
async ({ component, prefix }) => {
|
|
410
|
+
const p = normalizePrefix(prefix);
|
|
411
|
+
|
|
412
|
+
const byTagOrClass =
|
|
413
|
+
pickDecl(indexes, { tagName: component, prefix: p }) ??
|
|
414
|
+
pickDecl(indexes, { className: component, prefix: p });
|
|
415
|
+
|
|
416
|
+
const byComponentId = byTagOrClass ? undefined : findDeclByComponentId(indexes, component);
|
|
417
|
+
const decl = byTagOrClass ?? byComponentId?.decl;
|
|
418
|
+
|
|
419
|
+
if (!decl) {
|
|
420
|
+
return {
|
|
421
|
+
content: [{ type: 'text', text: `Component not found: ${component}` }],
|
|
422
|
+
isError: true,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
427
|
+
const modulePath =
|
|
428
|
+
canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : byComponentId?.modulePath;
|
|
429
|
+
const api = serializeApi(decl, modulePath, p);
|
|
430
|
+
const usageSnippet = generateSnippet(api, p);
|
|
431
|
+
|
|
432
|
+
const install = decl?.custom?.install;
|
|
433
|
+
if (!install || typeof install !== 'object') {
|
|
434
|
+
return {
|
|
435
|
+
content: [
|
|
436
|
+
{
|
|
437
|
+
type: 'text',
|
|
438
|
+
text: 'Install metadata not found in CEM.',
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
isError: true,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const componentId = String(install.id ?? '').trim() || api?.custom?.componentId;
|
|
446
|
+
const define = String(install.define ?? '').trim();
|
|
447
|
+
const deps = Array.isArray(install.deps) ? install.deps : [];
|
|
448
|
+
const tags = Array.isArray(install.tags) ? install.tags : [];
|
|
449
|
+
|
|
450
|
+
const tagNames =
|
|
451
|
+
tags.length > 0 ? tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : [api.tagName];
|
|
452
|
+
|
|
453
|
+
const defineHint = define
|
|
454
|
+
? [
|
|
455
|
+
modulePath ? `import { ${define} } from "${modulePath}";` : `import { ${define} } from "<modulePath>";`,
|
|
456
|
+
`${define}();`,
|
|
457
|
+
p !== CANONICAL_PREFIX ? `// If supported: ${define}("${p}");` : undefined,
|
|
458
|
+
]
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join('\n')
|
|
461
|
+
: undefined;
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
content: [
|
|
465
|
+
{
|
|
466
|
+
type: 'text',
|
|
467
|
+
text: JSON.stringify(
|
|
468
|
+
{
|
|
469
|
+
componentId,
|
|
470
|
+
tagNames,
|
|
471
|
+
deps,
|
|
472
|
+
define,
|
|
473
|
+
defineHint,
|
|
474
|
+
source: install.source,
|
|
475
|
+
usageSnippet,
|
|
476
|
+
installHint: componentId ? `wcf add ${componentId}` : undefined,
|
|
477
|
+
},
|
|
478
|
+
null,
|
|
479
|
+
2,
|
|
480
|
+
),
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// -----------------------------------------------------------------------
|
|
488
|
+
// Tool: validate_markup
|
|
489
|
+
// -----------------------------------------------------------------------
|
|
490
|
+
server.registerTool(
|
|
491
|
+
'validate_markup',
|
|
492
|
+
{
|
|
493
|
+
description:
|
|
494
|
+
'Validate an HTML snippet against CEM (unknownElement=error, unknownAttribute=warning).',
|
|
495
|
+
inputSchema: {
|
|
496
|
+
html: z.string(),
|
|
497
|
+
prefix: z.string().optional(),
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
async ({ html, prefix }) => {
|
|
501
|
+
const p = normalizePrefix(prefix);
|
|
502
|
+
let cemIndex = canonicalCemIndex;
|
|
503
|
+
if (p !== CANONICAL_PREFIX) {
|
|
504
|
+
const combined = new Map(canonicalCemIndex);
|
|
505
|
+
const prefixed = applyPrefixToCemIndex(canonicalCemIndex, p);
|
|
506
|
+
for (const [tag, meta] of prefixed.entries()) combined.set(tag, meta);
|
|
507
|
+
cemIndex = combined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const diagnostics = validateTextAgainstCem({
|
|
511
|
+
filePath: '<markup>',
|
|
512
|
+
text: html,
|
|
513
|
+
cem: cemIndex,
|
|
514
|
+
severity: {
|
|
515
|
+
unknownElement: 'error',
|
|
516
|
+
unknownAttribute: 'warning',
|
|
517
|
+
},
|
|
518
|
+
}).map((d) => ({
|
|
519
|
+
file: d.file,
|
|
520
|
+
range: d.range,
|
|
521
|
+
severity: d.severity,
|
|
522
|
+
code: d.code,
|
|
523
|
+
message: d.message,
|
|
524
|
+
tagName: d.tagName,
|
|
525
|
+
attrName: d.attrName,
|
|
526
|
+
hint: d.hint,
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
content: [{ type: 'text', text: JSON.stringify({ diagnostics }, null, 2) }],
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// -----------------------------------------------------------------------
|
|
536
|
+
// Tool: list_patterns
|
|
537
|
+
// -----------------------------------------------------------------------
|
|
538
|
+
server.registerTool(
|
|
539
|
+
'list_patterns',
|
|
540
|
+
{
|
|
541
|
+
description: 'List UI patterns (recipes) from the pattern registry.',
|
|
542
|
+
inputSchema: {},
|
|
543
|
+
},
|
|
544
|
+
async () => {
|
|
545
|
+
const list = Object.values(patterns).map((p) => ({
|
|
546
|
+
id: p?.id,
|
|
547
|
+
title: p?.title,
|
|
548
|
+
description: p?.description,
|
|
549
|
+
requires: p?.requires,
|
|
550
|
+
}));
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
content: [{ type: 'text', text: JSON.stringify(list, null, 2) }],
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// -----------------------------------------------------------------------
|
|
559
|
+
// Tool: get_pattern_recipe
|
|
560
|
+
// -----------------------------------------------------------------------
|
|
561
|
+
server.registerTool(
|
|
562
|
+
'get_pattern_recipe',
|
|
563
|
+
{
|
|
564
|
+
description:
|
|
565
|
+
'Get a pattern recipe (required componentIds + resolved HTML snippet). Use this to drive wcf installs + UI composition.',
|
|
566
|
+
inputSchema: {
|
|
567
|
+
patternId: z.string(),
|
|
568
|
+
prefix: z.string().optional(),
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
async ({ patternId, prefix }) => {
|
|
572
|
+
const id = String(patternId ?? '').trim();
|
|
573
|
+
const p = normalizePrefix(prefix);
|
|
574
|
+
const pat = patterns[id];
|
|
575
|
+
if (!pat) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{ type: 'text', text: `Pattern not found: ${id}` }],
|
|
578
|
+
isError: true,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const requires = Array.isArray(pat.requires) ? pat.requires : [];
|
|
583
|
+
const closure = resolveComponentClosure({ installRegistry }, requires);
|
|
584
|
+
|
|
585
|
+
const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
|
|
586
|
+
const install = Object.fromEntries(
|
|
587
|
+
closure
|
|
588
|
+
.map((cid) => [cid, components[cid]])
|
|
589
|
+
.filter(([, meta]) => meta && typeof meta === 'object')
|
|
590
|
+
.map(([cid, meta]) => [
|
|
591
|
+
cid,
|
|
592
|
+
{
|
|
593
|
+
...meta,
|
|
594
|
+
tags: Array.isArray(meta.tags) ? meta.tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : meta.tags,
|
|
595
|
+
},
|
|
596
|
+
]),
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const canonicalHtml = String(pat.html ?? '');
|
|
600
|
+
const html = applyPrefixToHtml(canonicalHtml, p);
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: 'text',
|
|
606
|
+
text: JSON.stringify(
|
|
607
|
+
{
|
|
608
|
+
pattern: {
|
|
609
|
+
id: pat.id,
|
|
610
|
+
title: pat.title,
|
|
611
|
+
description: pat.description,
|
|
612
|
+
},
|
|
613
|
+
prefix: p,
|
|
614
|
+
requires,
|
|
615
|
+
components: closure,
|
|
616
|
+
install,
|
|
617
|
+
html,
|
|
618
|
+
canonicalHtml,
|
|
619
|
+
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
620
|
+
},
|
|
621
|
+
null,
|
|
622
|
+
2,
|
|
623
|
+
),
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// -----------------------------------------------------------------------
|
|
631
|
+
// Tool: generate_pattern_snippet
|
|
632
|
+
// -----------------------------------------------------------------------
|
|
633
|
+
server.registerTool(
|
|
634
|
+
'generate_pattern_snippet',
|
|
635
|
+
{
|
|
636
|
+
description: 'Generate a pattern HTML snippet. (Same as get_pattern_recipe().html)',
|
|
637
|
+
inputSchema: {
|
|
638
|
+
patternId: z.string(),
|
|
639
|
+
prefix: z.string().optional(),
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
async ({ patternId, prefix }) => {
|
|
643
|
+
const id = String(patternId ?? '').trim();
|
|
644
|
+
const p = normalizePrefix(prefix);
|
|
645
|
+
const pat = patterns[id];
|
|
646
|
+
if (!pat) {
|
|
647
|
+
return {
|
|
648
|
+
content: [{ type: 'text', text: `Pattern not found: ${id}` }],
|
|
649
|
+
isError: true,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
content: [{ type: 'text', text: applyPrefixToHtml(String(pat.html ?? ''), p) }],
|
|
655
|
+
};
|
|
656
|
+
},
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
// -----------------------------------------------------------------------
|
|
660
|
+
// Start
|
|
661
|
+
// -----------------------------------------------------------------------
|
|
662
|
+
const transport = new StdioServerTransport();
|
|
663
|
+
await server.connect(transport);
|
|
664
|
+
}
|