@monoharada/wcf-mcp 0.1.2 → 0.2.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 +348 -8
- package/bin.mjs +19 -2
- package/core.mjs +1125 -80
- package/data/design-tokens.json +1708 -2
- package/data/guidelines-index.json +589 -3
- package/data/llms-full.txt +5291 -0
- package/examples/plugins/custom-validation-plugin.mjs +70 -0
- package/package.json +4 -2
- package/server.mjs +183 -5
- package/validator.mjs +459 -0
- package/wcf-mcp.config.example.json +24 -0
package/core.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and helper functions live in exactly one place.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +17,8 @@ export const CANONICAL_PREFIX = 'dads';
|
|
|
17
17
|
export const MAX_PREFIX_LENGTH = 64;
|
|
18
18
|
export const STRUCTURED_CONTENT_DISABLE_FLAG = 'WCF_MCP_DISABLE_STRUCTURED_CONTENT';
|
|
19
19
|
export const MAX_TOOL_RESULT_BYTES = 100 * 1024;
|
|
20
|
+
export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1).';
|
|
21
|
+
export const PLUGIN_CONTRACT_VERSION = '1.0.0';
|
|
20
22
|
|
|
21
23
|
export const CATEGORY_MAP = {
|
|
22
24
|
'dads-input-text': 'Form',
|
|
@@ -85,6 +87,32 @@ export const CATEGORY_MAP = {
|
|
|
85
87
|
const TOKEN_MISUSE_ALLOWED_TYPES = Object.freeze(new Set(['color', 'spacing']));
|
|
86
88
|
const STRUCTURED_CONTENT_DISABLE_TRUE_VALUES = Object.freeze(new Set(['1', 'true', 'yes', 'on']));
|
|
87
89
|
const WCAG_LEVELS = Object.freeze(new Set(['A', 'AA', 'AAA', 'all']));
|
|
90
|
+
const TOKEN_THEMES = Object.freeze(new Set(['light', 'dark', 'all']));
|
|
91
|
+
const GUIDELINE_TOPICS = Object.freeze(['accessibility', 'css', 'patterns', 'all']);
|
|
92
|
+
const GUIDELINE_TOPIC_SET = Object.freeze(new Set(GUIDELINE_TOPICS));
|
|
93
|
+
const PLUGIN_DATA_SOURCE_KEYS = Object.freeze(new Set([
|
|
94
|
+
'custom-elements.json',
|
|
95
|
+
'install-registry.json',
|
|
96
|
+
'pattern-registry.json',
|
|
97
|
+
'design-tokens.json',
|
|
98
|
+
'guidelines-index.json',
|
|
99
|
+
]));
|
|
100
|
+
const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
|
|
101
|
+
'get_design_system_overview',
|
|
102
|
+
'list_components',
|
|
103
|
+
'search_icons',
|
|
104
|
+
'get_component_api',
|
|
105
|
+
'generate_usage_snippet',
|
|
106
|
+
'get_install_recipe',
|
|
107
|
+
'validate_markup',
|
|
108
|
+
'list_patterns',
|
|
109
|
+
'get_pattern_recipe',
|
|
110
|
+
'generate_pattern_snippet',
|
|
111
|
+
'get_design_tokens',
|
|
112
|
+
'get_design_token_detail',
|
|
113
|
+
'get_accessibility_docs',
|
|
114
|
+
'search_guidelines',
|
|
115
|
+
]));
|
|
88
116
|
const A11Y_CATEGORY_LEVEL_MAP = Object.freeze({
|
|
89
117
|
semantics: 'A',
|
|
90
118
|
keyboard: 'A',
|
|
@@ -99,6 +127,40 @@ const NPX_TEMPLATE = Object.freeze({
|
|
|
99
127
|
command: 'npx',
|
|
100
128
|
args: ['@monoharada/wcf-mcp'],
|
|
101
129
|
});
|
|
130
|
+
export const FIGMA_TO_WCF_PROMPT = 'figma_to_wcf';
|
|
131
|
+
export const WCF_RESOURCE_URIS = Object.freeze({
|
|
132
|
+
components: 'wcf://components',
|
|
133
|
+
tokens: 'wcf://tokens',
|
|
134
|
+
guidelinesTemplate: 'wcf://guidelines/{topic}',
|
|
135
|
+
llmsFull: 'wcf://llms-full',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Unidirectional synonym table: key → expands to include these terms (DIG-09)
|
|
139
|
+
// Searching "keyboard" also matches "focus", "tab" etc. but NOT reverse.
|
|
140
|
+
const SYNONYM_TABLE = new Map([
|
|
141
|
+
['aria-live', ['role=alert', 'aria-describedby', 'live region', 'error text']],
|
|
142
|
+
['keyboard', ['focus', 'tab', 'tabindex', 'key event', 'focus trap']],
|
|
143
|
+
['contrast', ['color', 'wcag', 'color contrast']],
|
|
144
|
+
['spacing', ['margin', 'padding', 'gap', 'spacing token']],
|
|
145
|
+
['skip-navigation', ['skip-link', 'landmark', 'skip nav']],
|
|
146
|
+
['heading', ['heading hierarchy', 'h1', 'heading level']],
|
|
147
|
+
['form', ['input', 'validation', 'required', 'label']],
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
export function expandQueryWithSynonyms(query) {
|
|
151
|
+
const q = String(query ?? '').toLowerCase().trim();
|
|
152
|
+
if (!q) return [q];
|
|
153
|
+
const terms = [q];
|
|
154
|
+
for (const [key, synonyms] of SYNONYM_TABLE) {
|
|
155
|
+
if (q.includes(key)) {
|
|
156
|
+
for (const syn of synonyms) {
|
|
157
|
+
if (!terms.includes(syn)) terms.push(syn);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return terms;
|
|
162
|
+
}
|
|
163
|
+
|
|
102
164
|
export const IDE_SETUP_TEMPLATES = Object.freeze([
|
|
103
165
|
{
|
|
104
166
|
ide: 'Claude Desktop',
|
|
@@ -127,6 +189,24 @@ export const IDE_SETUP_TEMPLATES = Object.freeze([
|
|
|
127
189
|
},
|
|
128
190
|
},
|
|
129
191
|
},
|
|
192
|
+
{
|
|
193
|
+
ide: 'VS Code (GitHub Copilot)',
|
|
194
|
+
configPath: '.vscode/mcp.json',
|
|
195
|
+
snippet: {
|
|
196
|
+
mcpServers: {
|
|
197
|
+
wcf: NPX_TEMPLATE,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
ide: 'Windsurf',
|
|
203
|
+
configPath: '.windsurf/mcp_config.json',
|
|
204
|
+
snippet: {
|
|
205
|
+
mcpServers: {
|
|
206
|
+
wcf: NPX_TEMPLATE,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
130
210
|
]);
|
|
131
211
|
|
|
132
212
|
export function isStructuredContentDisabled(env = process.env) {
|
|
@@ -204,6 +284,406 @@ export function buildTokenSuggestionMap(designTokensData) {
|
|
|
204
284
|
return out;
|
|
205
285
|
}
|
|
206
286
|
|
|
287
|
+
export function normalizeTokenIdentifier(value) {
|
|
288
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
289
|
+
if (!raw) return '';
|
|
290
|
+
const cssVariable = normalizeCssVariable(raw);
|
|
291
|
+
if (cssVariable) return cssVariable;
|
|
292
|
+
if (raw.startsWith('--')) return raw;
|
|
293
|
+
return `--${raw.replace(/^[-]+/, '')}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function resolveTokenTheme(theme) {
|
|
297
|
+
const requested = String(theme ?? 'light').trim().toLowerCase() || 'light';
|
|
298
|
+
if (!TOKEN_THEMES.has(requested)) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
errorCode: 'INVALID_THEME',
|
|
302
|
+
message: `Unsupported theme: ${requested}. Allowed values are light, dark, all.`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (requested !== 'light') {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
errorCode: 'INVALID_THEME',
|
|
309
|
+
message: `Theme "${requested}" is not available yet. Use theme="light" (NG-06).`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
requested,
|
|
315
|
+
resolved: 'light',
|
|
316
|
+
available: ['light'],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function extractReferencedTokenNames(value) {
|
|
321
|
+
if (typeof value !== 'string') return [];
|
|
322
|
+
const refs = [];
|
|
323
|
+
const re = /var\(\s*(--[^,\s)]+)\s*(?:,\s*[^)]+)?\)/g;
|
|
324
|
+
let match;
|
|
325
|
+
while ((match = re.exec(value))) {
|
|
326
|
+
const tokenName = normalizeTokenIdentifier(match[1]);
|
|
327
|
+
if (tokenName) refs.push(tokenName);
|
|
328
|
+
}
|
|
329
|
+
return [...new Set(refs)];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function buildTokenRelationshipIndex(designTokensData) {
|
|
333
|
+
const byToken = {};
|
|
334
|
+
const tokens = Array.isArray(designTokensData?.tokens) ? designTokensData.tokens : [];
|
|
335
|
+
const fromData = designTokensData?.relationships?.byToken;
|
|
336
|
+
if (fromData && typeof fromData === 'object') {
|
|
337
|
+
for (const [rawName, rawRel] of Object.entries(fromData)) {
|
|
338
|
+
const name = normalizeTokenIdentifier(rawName);
|
|
339
|
+
if (!name) continue;
|
|
340
|
+
const refs = Array.isArray(rawRel?.references)
|
|
341
|
+
? rawRel.references.map((r) => normalizeTokenIdentifier(r)).filter(Boolean)
|
|
342
|
+
: [];
|
|
343
|
+
const referencedBy = Array.isArray(rawRel?.referencedBy)
|
|
344
|
+
? rawRel.referencedBy.map((r) => normalizeTokenIdentifier(r)).filter(Boolean)
|
|
345
|
+
: [];
|
|
346
|
+
byToken[name] = {
|
|
347
|
+
references: [...new Set(refs)].sort(),
|
|
348
|
+
referencedBy: [...new Set(referencedBy)].sort(),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (Object.keys(byToken).length > 0) {
|
|
354
|
+
return { byToken };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const token of tokens) {
|
|
358
|
+
const name = normalizeTokenIdentifier(token?.name);
|
|
359
|
+
if (!name) continue;
|
|
360
|
+
if (!byToken[name]) byToken[name] = { references: [], referencedBy: [] };
|
|
361
|
+
const refs = extractReferencedTokenNames(token?.value);
|
|
362
|
+
byToken[name].references = refs;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const [sourceName, relation] of Object.entries(byToken)) {
|
|
366
|
+
for (const refName of relation.references) {
|
|
367
|
+
if (!byToken[refName]) byToken[refName] = { references: [], referencedBy: [] };
|
|
368
|
+
byToken[refName].referencedBy.push(sourceName);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const relation of Object.values(byToken)) {
|
|
373
|
+
relation.references = [...new Set(relation.references)].sort();
|
|
374
|
+
relation.referencedBy = [...new Set(relation.referencedBy)].sort();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { byToken };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function toTokenSummary(token) {
|
|
381
|
+
return {
|
|
382
|
+
name: String(token?.name ?? ''),
|
|
383
|
+
value: String(token?.value ?? ''),
|
|
384
|
+
type: String(token?.type ?? ''),
|
|
385
|
+
category: String(token?.category ?? ''),
|
|
386
|
+
cssVariable: String(token?.cssVariable ?? ''),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function suggestTokenNames(targetName, tokens, maxSuggestions = 5) {
|
|
391
|
+
const target = normalizeTokenIdentifier(targetName);
|
|
392
|
+
if (!target) return [];
|
|
393
|
+
const allNames = [...new Set(tokens
|
|
394
|
+
.map((token) => normalizeTokenIdentifier(token?.name))
|
|
395
|
+
.filter(Boolean))];
|
|
396
|
+
|
|
397
|
+
const startsWith = allNames.filter((name) => name.startsWith(target));
|
|
398
|
+
if (startsWith.length >= maxSuggestions) return startsWith.slice(0, maxSuggestions);
|
|
399
|
+
|
|
400
|
+
const includes = allNames.filter((name) => name.includes(target) && !startsWith.includes(name));
|
|
401
|
+
const ranked = allNames
|
|
402
|
+
.filter((name) => !startsWith.includes(name) && !includes.includes(name))
|
|
403
|
+
.map((name) => ({ name, distance: levenshteinDistance(target, name) }))
|
|
404
|
+
.sort((left, right) => left.distance - right.distance || left.name.localeCompare(right.name))
|
|
405
|
+
.map((entry) => entry.name);
|
|
406
|
+
|
|
407
|
+
return [...startsWith, ...includes, ...ranked].slice(0, maxSuggestions);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function buildUsageExamples(token) {
|
|
411
|
+
const cssVar = String(token?.cssVariable ?? '');
|
|
412
|
+
const type = String(token?.type ?? '').toLowerCase();
|
|
413
|
+
if (!cssVar) return [];
|
|
414
|
+
if (type === 'color') {
|
|
415
|
+
return [
|
|
416
|
+
`.example { color: ${cssVar}; }`,
|
|
417
|
+
`.example { background-color: ${cssVar}; }`,
|
|
418
|
+
];
|
|
419
|
+
}
|
|
420
|
+
if (type === 'spacing') {
|
|
421
|
+
return [
|
|
422
|
+
`.example { padding: ${cssVar}; }`,
|
|
423
|
+
`.example { gap: ${cssVar}; }`,
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
if (type === 'typography') {
|
|
427
|
+
return [
|
|
428
|
+
`.example { font-size: ${cssVar}; }`,
|
|
429
|
+
`.example { line-height: ${cssVar}; }`,
|
|
430
|
+
];
|
|
431
|
+
}
|
|
432
|
+
if (type === 'radius') {
|
|
433
|
+
return [`.example { border-radius: ${cssVar}; }`];
|
|
434
|
+
}
|
|
435
|
+
if (type === 'shadow') {
|
|
436
|
+
return [`.example { box-shadow: ${cssVar}; }`];
|
|
437
|
+
}
|
|
438
|
+
return [`.example { --token-value: ${cssVar}; }`];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildTokenErrorPayload(code, message, extra = {}) {
|
|
442
|
+
return {
|
|
443
|
+
isError: true,
|
|
444
|
+
payload: {
|
|
445
|
+
error: { code, message },
|
|
446
|
+
...extra,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function buildDesignTokenDetailPayload(designTokensData, name, theme) {
|
|
452
|
+
if (!Array.isArray(designTokensData?.tokens)) {
|
|
453
|
+
return buildTokenErrorPayload(
|
|
454
|
+
'DESIGN_TOKENS_DATA_UNAVAILABLE',
|
|
455
|
+
'Design tokens data not available. Run: npm run mcp:extract-tokens',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const themeInfo = resolveTokenTheme(theme);
|
|
460
|
+
if (!themeInfo.ok) {
|
|
461
|
+
return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const normalizedName = normalizeTokenIdentifier(name);
|
|
465
|
+
if (!normalizedName) {
|
|
466
|
+
return buildTokenErrorPayload('INVALID_TOKEN_INPUT', 'Token name is required.');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const tokens = designTokensData.tokens;
|
|
470
|
+
const token = tokens.find((item) => normalizeTokenIdentifier(item?.name) === normalizedName);
|
|
471
|
+
if (!token) {
|
|
472
|
+
return buildTokenErrorPayload(
|
|
473
|
+
'TOKEN_NOT_FOUND',
|
|
474
|
+
`Token not found: ${normalizedName}`,
|
|
475
|
+
{ suggestions: suggestTokenNames(normalizedName, tokens) },
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const relationshipIndex = buildTokenRelationshipIndex(designTokensData);
|
|
480
|
+
const relation = relationshipIndex.byToken[normalizedName] ?? { references: [], referencedBy: [] };
|
|
481
|
+
const tokenByName = new Map(tokens
|
|
482
|
+
.map((item) => [normalizeTokenIdentifier(item?.name), item])
|
|
483
|
+
.filter(([tokenName]) => tokenName));
|
|
484
|
+
const references = relation.references
|
|
485
|
+
.map((tokenName) => tokenByName.get(tokenName))
|
|
486
|
+
.filter(Boolean)
|
|
487
|
+
.map(toTokenSummary);
|
|
488
|
+
const referencedBy = relation.referencedBy
|
|
489
|
+
.map((tokenName) => tokenByName.get(tokenName))
|
|
490
|
+
.filter(Boolean)
|
|
491
|
+
.map(toTokenSummary);
|
|
492
|
+
const relatedTokens = referencedBy
|
|
493
|
+
.filter((item) => String(item.category).toLowerCase() === 'semantic')
|
|
494
|
+
.map((item) => item.name);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
isError: false,
|
|
498
|
+
payload: {
|
|
499
|
+
token: {
|
|
500
|
+
...toTokenSummary(token),
|
|
501
|
+
group: token?.group ?? null,
|
|
502
|
+
},
|
|
503
|
+
references,
|
|
504
|
+
referencedBy,
|
|
505
|
+
relatedTokens,
|
|
506
|
+
usageExamples: buildUsageExamples(token),
|
|
507
|
+
theme: {
|
|
508
|
+
requested: themeInfo.requested,
|
|
509
|
+
resolved: themeInfo.resolved,
|
|
510
|
+
available: themeInfo.available,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function buildDesignTokensPayload(designTokensData, { type, category, query, theme } = {}) {
|
|
517
|
+
if (!designTokensData) {
|
|
518
|
+
return buildTokenErrorPayload(
|
|
519
|
+
'DESIGN_TOKENS_DATA_UNAVAILABLE',
|
|
520
|
+
'Design tokens data not available. Run: npm run mcp:extract-tokens',
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const themeInfo = resolveTokenTheme(theme);
|
|
525
|
+
if (!themeInfo.ok) {
|
|
526
|
+
return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let tokens = Array.isArray(designTokensData.tokens) ? designTokensData.tokens : [];
|
|
530
|
+
if (type) tokens = tokens.filter((t) => t.type === type);
|
|
531
|
+
if (category) tokens = tokens.filter((t) => t.category === category);
|
|
532
|
+
if (query) {
|
|
533
|
+
const q = String(query).toLowerCase();
|
|
534
|
+
tokens = tokens.filter((t) => String(t.name ?? '').toLowerCase().includes(q));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
isError: false,
|
|
539
|
+
payload: {
|
|
540
|
+
total: tokens.length,
|
|
541
|
+
tokens,
|
|
542
|
+
summary: designTokensData.summary,
|
|
543
|
+
theme: {
|
|
544
|
+
requested: themeInfo.requested,
|
|
545
|
+
resolved: themeInfo.resolved,
|
|
546
|
+
available: themeInfo.available,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function isPlainObject(value) {
|
|
553
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function toPluginErrorMessage(name, reason) {
|
|
557
|
+
return `Invalid plugin (${name}): ${reason}`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
|
|
562
|
+
* @typedef {{
|
|
563
|
+
* fileName: string,
|
|
564
|
+
* path: string,
|
|
565
|
+
* }} WcfMcpDataSourceConfig
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
|
|
570
|
+
* @typedef {{
|
|
571
|
+
* name: string,
|
|
572
|
+
* description?: string,
|
|
573
|
+
* inputSchema?: Record<string, unknown>,
|
|
574
|
+
* handler?: (args: Record<string, unknown>, context: { plugin: { name: string, version: string }, helpers: { loadJsonData: Function } }) => unknown,
|
|
575
|
+
* staticPayload?: unknown,
|
|
576
|
+
* }} WcfMcpPluginTool
|
|
577
|
+
*/
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
|
|
581
|
+
* @typedef {{
|
|
582
|
+
* name: string,
|
|
583
|
+
* version: string,
|
|
584
|
+
* tools?: WcfMcpPluginTool[],
|
|
585
|
+
* dataSources?: WcfMcpDataSourceConfig[],
|
|
586
|
+
* }} WcfMcpPlugin
|
|
587
|
+
*/
|
|
588
|
+
|
|
589
|
+
function normalizePluginDataSources(pluginName, dataSources) {
|
|
590
|
+
if (!Array.isArray(dataSources)) return [];
|
|
591
|
+
const out = [];
|
|
592
|
+
for (const entry of dataSources) {
|
|
593
|
+
if (!isPlainObject(entry)) {
|
|
594
|
+
throw new Error(toPluginErrorMessage(pluginName, 'dataSources entries must be objects'));
|
|
595
|
+
}
|
|
596
|
+
const fileName = String(entry.fileName ?? '').trim();
|
|
597
|
+
const sourcePath = String(entry.path ?? '').trim();
|
|
598
|
+
if (!fileName || !sourcePath) {
|
|
599
|
+
throw new Error(toPluginErrorMessage(pluginName, 'dataSources entries require fileName and path'));
|
|
600
|
+
}
|
|
601
|
+
if (!PLUGIN_DATA_SOURCE_KEYS.has(fileName)) {
|
|
602
|
+
throw new Error(toPluginErrorMessage(pluginName, `unsupported data source key: ${fileName}`));
|
|
603
|
+
}
|
|
604
|
+
out.push({ fileName, path: sourcePath });
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function normalizePluginTools(pluginName, tools) {
|
|
610
|
+
if (!Array.isArray(tools)) return [];
|
|
611
|
+
const out = [];
|
|
612
|
+
for (const rawTool of tools) {
|
|
613
|
+
if (!isPlainObject(rawTool)) {
|
|
614
|
+
throw new Error(toPluginErrorMessage(pluginName, 'tools entries must be objects'));
|
|
615
|
+
}
|
|
616
|
+
const name = String(rawTool.name ?? '').trim();
|
|
617
|
+
if (!name) throw new Error(toPluginErrorMessage(pluginName, 'tool.name is required'));
|
|
618
|
+
const hasHandler = typeof rawTool.handler === 'function';
|
|
619
|
+
const hasStaticPayload = Object.prototype.hasOwnProperty.call(rawTool, 'staticPayload');
|
|
620
|
+
if (!hasHandler && !hasStaticPayload) {
|
|
621
|
+
throw new Error(toPluginErrorMessage(pluginName, `tool "${name}" needs handler or staticPayload`));
|
|
622
|
+
}
|
|
623
|
+
// When both are specified, handler takes priority (contract v1: handler-wins)
|
|
624
|
+
// staticPayload is ignored silently.
|
|
625
|
+
const description = String(rawTool.description ?? '').trim() ||
|
|
626
|
+
`Plugin tool provided by ${pluginName}. ${PLUGIN_TOOL_NOTICE}`;
|
|
627
|
+
const inputSchema = isPlainObject(rawTool.inputSchema) ? rawTool.inputSchema : {};
|
|
628
|
+
out.push({
|
|
629
|
+
name,
|
|
630
|
+
description,
|
|
631
|
+
inputSchema,
|
|
632
|
+
handler: hasHandler ? rawTool.handler : undefined,
|
|
633
|
+
staticPayload: hasStaticPayload ? rawTool.staticPayload : undefined,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function normalizePlugins(plugins = []) {
|
|
640
|
+
if (!Array.isArray(plugins)) throw new Error('Invalid plugin configuration: plugins must be an array');
|
|
641
|
+
const normalized = [];
|
|
642
|
+
const seenPluginNames = new Set();
|
|
643
|
+
const seenToolNames = new Set(BUILTIN_TOOL_NAMES);
|
|
644
|
+
|
|
645
|
+
for (const rawPlugin of plugins) {
|
|
646
|
+
if (!isPlainObject(rawPlugin)) throw new Error('Invalid plugin configuration: each plugin must be an object');
|
|
647
|
+
const name = String(rawPlugin.name ?? '').trim();
|
|
648
|
+
const version = String(rawPlugin.version ?? '').trim();
|
|
649
|
+
if (!name || !version) throw new Error('Invalid plugin configuration: plugin.name and plugin.version are required');
|
|
650
|
+
if (seenPluginNames.has(name)) throw new Error(`Duplicate plugin name: ${name}`);
|
|
651
|
+
seenPluginNames.add(name);
|
|
652
|
+
|
|
653
|
+
const tools = normalizePluginTools(name, rawPlugin.tools);
|
|
654
|
+
for (const tool of tools) {
|
|
655
|
+
if (seenToolNames.has(tool.name)) {
|
|
656
|
+
throw new Error(toPluginErrorMessage(name, `tool name collision: ${tool.name}`));
|
|
657
|
+
}
|
|
658
|
+
seenToolNames.add(tool.name);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const dataSources = normalizePluginDataSources(name, rawPlugin.dataSources);
|
|
662
|
+
normalized.push({ name, version, tools, dataSources });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return normalized;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function buildPluginDataSourceMap(plugins = []) {
|
|
669
|
+
const out = new Map();
|
|
670
|
+
for (const plugin of plugins) {
|
|
671
|
+
const pluginName = String(plugin?.name ?? 'unknown-plugin');
|
|
672
|
+
const dataSources = Array.isArray(plugin?.dataSources) ? plugin.dataSources : [];
|
|
673
|
+
for (const source of dataSources) {
|
|
674
|
+
const fileName = String(source?.fileName ?? '').trim();
|
|
675
|
+
const sourcePath = String(source?.path ?? '').trim();
|
|
676
|
+
if (!fileName || !sourcePath) continue;
|
|
677
|
+
if (out.has(fileName)) {
|
|
678
|
+
const prev = out.get(fileName);
|
|
679
|
+
throw new Error(`Duplicate data source override for ${fileName} (${prev.pluginName}, ${pluginName})`);
|
|
680
|
+
}
|
|
681
|
+
out.set(fileName, { path: sourcePath, pluginName });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
|
|
207
687
|
// ---------------------------------------------------------------------------
|
|
208
688
|
// Helpers (exported for testing)
|
|
209
689
|
// ---------------------------------------------------------------------------
|
|
@@ -480,18 +960,29 @@ export function findDeclByComponentId(indexes, componentIdRaw) {
|
|
|
480
960
|
return undefined;
|
|
481
961
|
}
|
|
482
962
|
|
|
483
|
-
|
|
963
|
+
/**
|
|
964
|
+
* Generic helper: remap tag-keyed Map to a different prefix.
|
|
965
|
+
* Used by validate_markup to build prefix-aware CEM/enum/slot maps.
|
|
966
|
+
*/
|
|
967
|
+
export function applyPrefixToTagMap(map, prefix) {
|
|
484
968
|
const p = normalizePrefix(prefix);
|
|
485
|
-
if (p === CANONICAL_PREFIX) return
|
|
969
|
+
if (p === CANONICAL_PREFIX) return map;
|
|
486
970
|
|
|
487
971
|
const out = new Map();
|
|
488
|
-
for (const [tag,
|
|
489
|
-
|
|
490
|
-
out.set(nextTag, meta);
|
|
972
|
+
for (const [tag, value] of map.entries()) {
|
|
973
|
+
out.set(withPrefix(tag, p), value);
|
|
491
974
|
}
|
|
492
975
|
return out;
|
|
493
976
|
}
|
|
494
977
|
|
|
978
|
+
function mergeWithPrefixed(canonicalMap, prefix) {
|
|
979
|
+
const prefixed = applyPrefixToTagMap(canonicalMap, prefix);
|
|
980
|
+
if (prefixed === canonicalMap) return canonicalMap;
|
|
981
|
+
const combined = new Map(canonicalMap);
|
|
982
|
+
for (const [k, v] of prefixed.entries()) combined.set(k, v);
|
|
983
|
+
return combined;
|
|
984
|
+
}
|
|
985
|
+
|
|
495
986
|
export function applyPrefixToHtml(html, prefix) {
|
|
496
987
|
const p = normalizePrefix(prefix);
|
|
497
988
|
if (p === CANONICAL_PREFIX) return String(html ?? '');
|
|
@@ -534,7 +1025,8 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
|
534
1025
|
export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix } = {}) {
|
|
535
1026
|
const p = normalizePrefix(prefix);
|
|
536
1027
|
const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
537
|
-
const
|
|
1028
|
+
const limitExplicit = Number.isInteger(limit);
|
|
1029
|
+
const pageSize = limitExplicit ? Math.max(1, Math.min(limit, 200)) : 20;
|
|
538
1030
|
const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
539
1031
|
|
|
540
1032
|
let items = indexes.decls.map(({ decl, tagName, modulePath }) => ({
|
|
@@ -565,13 +1057,20 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
|
|
|
565
1057
|
const total = items.length;
|
|
566
1058
|
const paged = items.slice(pageOffset, pageOffset + pageSize);
|
|
567
1059
|
|
|
568
|
-
|
|
1060
|
+
const result = {
|
|
569
1061
|
total,
|
|
570
1062
|
limit: pageSize,
|
|
571
1063
|
offset: pageOffset,
|
|
572
1064
|
hasMore: pageOffset + paged.length < total,
|
|
573
1065
|
items: paged,
|
|
574
1066
|
};
|
|
1067
|
+
|
|
1068
|
+
// DIG-19: Add migration notice when limit is not explicitly provided
|
|
1069
|
+
if (!limitExplicit && total > pageSize) {
|
|
1070
|
+
result._notice = 'Default pagination changed to 20 items. Set limit:200 for all results.';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return result;
|
|
575
1074
|
}
|
|
576
1075
|
|
|
577
1076
|
export function parseIconNamesFromDescription(description) {
|
|
@@ -897,6 +1396,123 @@ export function queryAccessibilityIndex(
|
|
|
897
1396
|
};
|
|
898
1397
|
}
|
|
899
1398
|
|
|
1399
|
+
function buildComponentsResourcePayload(indexes) {
|
|
1400
|
+
const page = buildComponentSummaries(indexes, { limit: 200 });
|
|
1401
|
+
const componentsByCategory = {};
|
|
1402
|
+
for (const item of page.items) {
|
|
1403
|
+
const category = String(item?.category ?? 'Other');
|
|
1404
|
+
componentsByCategory[category] = (componentsByCategory[category] ?? 0) + 1;
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
total: page.total,
|
|
1408
|
+
componentsByCategory,
|
|
1409
|
+
components: page.items,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function buildTokensResourcePayload(designTokensData) {
|
|
1414
|
+
if (!Array.isArray(designTokensData?.tokens)) {
|
|
1415
|
+
return {
|
|
1416
|
+
isError: true,
|
|
1417
|
+
error: {
|
|
1418
|
+
code: 'DESIGN_TOKENS_DATA_UNAVAILABLE',
|
|
1419
|
+
message: 'Design tokens data not available. Run: npm run mcp:extract-tokens',
|
|
1420
|
+
},
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const tokens = designTokensData.tokens;
|
|
1425
|
+
const tokenTypes = [...new Set(tokens
|
|
1426
|
+
.map((token) => String(token?.type ?? '').trim())
|
|
1427
|
+
.filter(Boolean))].sort();
|
|
1428
|
+
const tokenCategories = [...new Set(tokens
|
|
1429
|
+
.map((token) => String(token?.category ?? '').trim())
|
|
1430
|
+
.filter(Boolean))].sort();
|
|
1431
|
+
|
|
1432
|
+
return {
|
|
1433
|
+
isError: false,
|
|
1434
|
+
payload: {
|
|
1435
|
+
total: tokens.length,
|
|
1436
|
+
summary: designTokensData.summary ?? {},
|
|
1437
|
+
themes: designTokensData.themes ?? { default: 'light', available: ['light'] },
|
|
1438
|
+
tokenTypes,
|
|
1439
|
+
tokenCategories,
|
|
1440
|
+
sample: tokens.slice(0, 20).map(toTokenSummary),
|
|
1441
|
+
},
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function buildGuidelinesResourcePayload(guidelinesIndexData, rawTopic) {
|
|
1446
|
+
const topic = String(rawTopic ?? '').trim().toLowerCase();
|
|
1447
|
+
if (!GUIDELINE_TOPIC_SET.has(topic)) {
|
|
1448
|
+
return {
|
|
1449
|
+
isError: true,
|
|
1450
|
+
error: {
|
|
1451
|
+
code: 'INVALID_GUIDELINE_TOPIC',
|
|
1452
|
+
message: `Unsupported topic: ${topic}. Allowed values are ${GUIDELINE_TOPICS.join(', ')}.`,
|
|
1453
|
+
},
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (!Array.isArray(guidelinesIndexData?.documents)) {
|
|
1458
|
+
return {
|
|
1459
|
+
isError: true,
|
|
1460
|
+
error: {
|
|
1461
|
+
code: 'GUIDELINES_INDEX_UNAVAILABLE',
|
|
1462
|
+
message: 'Guidelines index not available. Run: npm run mcp:index-guidelines',
|
|
1463
|
+
},
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const documents = guidelinesIndexData.documents
|
|
1468
|
+
.filter((doc) => topic === 'all' || String(doc?.topic ?? '').toLowerCase() === topic)
|
|
1469
|
+
.map((doc) => {
|
|
1470
|
+
const sections = Array.isArray(doc?.sections) ? doc.sections : [];
|
|
1471
|
+
return {
|
|
1472
|
+
id: String(doc?.id ?? ''),
|
|
1473
|
+
title: String(doc?.title ?? ''),
|
|
1474
|
+
topic: String(doc?.topic ?? ''),
|
|
1475
|
+
sectionCount: sections.length,
|
|
1476
|
+
sections: sections.map((section) => ({
|
|
1477
|
+
heading: String(section?.heading ?? ''),
|
|
1478
|
+
startLine: Number.isInteger(section?.startLine) ? section.startLine : undefined,
|
|
1479
|
+
})),
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
return {
|
|
1484
|
+
isError: false,
|
|
1485
|
+
payload: {
|
|
1486
|
+
topic,
|
|
1487
|
+
totalDocuments: documents.length,
|
|
1488
|
+
topicCounts: guidelinesIndexData.topicCounts ?? {},
|
|
1489
|
+
documents,
|
|
1490
|
+
},
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function buildFigmaToWcfPromptText({ figmaUrl, userIntent }) {
|
|
1495
|
+
const url = String(figmaUrl ?? '').trim();
|
|
1496
|
+
const intent = String(userIntent ?? '').trim();
|
|
1497
|
+
|
|
1498
|
+
return [
|
|
1499
|
+
`Figma URL: ${url}`,
|
|
1500
|
+
intent ? `Implementation goal: ${intent}` : 'Implementation goal: (not specified)',
|
|
1501
|
+
'',
|
|
1502
|
+
'Use the workflow below in this exact order:',
|
|
1503
|
+
'1. get_design_system_overview',
|
|
1504
|
+
'2. get_design_tokens',
|
|
1505
|
+
'3. get_component_api',
|
|
1506
|
+
'4. generate_usage_snippet (or get_pattern_recipe)',
|
|
1507
|
+
'5. validate_markup',
|
|
1508
|
+
'',
|
|
1509
|
+
'Output requirements:',
|
|
1510
|
+
'- Split the UI into sections before writing code.',
|
|
1511
|
+
'- For each section, name concrete components and token variables.',
|
|
1512
|
+
'- Provide final validation notes and required fixes.',
|
|
1513
|
+
].join('\n');
|
|
1514
|
+
}
|
|
1515
|
+
|
|
900
1516
|
function resolveDeclByComponent(indexes, component, prefix) {
|
|
901
1517
|
const byTagOrClass =
|
|
902
1518
|
pickDecl(indexes, { tagName: component, prefix }) ??
|
|
@@ -909,7 +1525,50 @@ function resolveDeclByComponent(indexes, component, prefix) {
|
|
|
909
1525
|
};
|
|
910
1526
|
}
|
|
911
1527
|
|
|
912
|
-
|
|
1528
|
+
const byComponentId = findDeclByComponentId(indexes, component);
|
|
1529
|
+
if (byComponentId) return byComponentId;
|
|
1530
|
+
|
|
1531
|
+
// Auto-prefix: try with canonical prefix if bare name was given (DIG-15)
|
|
1532
|
+
const comp = typeof component === 'string' ? component.trim().toLowerCase() : '';
|
|
1533
|
+
const p = normalizePrefix(prefix);
|
|
1534
|
+
if (comp && !comp.startsWith(p)) {
|
|
1535
|
+
const prefixed = `${p}-${comp}`;
|
|
1536
|
+
const byPrefixed = pickDecl(indexes, { tagName: prefixed, prefix: p });
|
|
1537
|
+
if (byPrefixed) {
|
|
1538
|
+
const canonicalTag = typeof byPrefixed.tagName === 'string' ? byPrefixed.tagName.toLowerCase() : undefined;
|
|
1539
|
+
return {
|
|
1540
|
+
decl: byPrefixed,
|
|
1541
|
+
modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return undefined;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function buildComponentNotFoundError(component, indexes, prefix) {
|
|
1550
|
+
const comp = typeof component === 'string' ? component.trim() : '';
|
|
1551
|
+
const p = normalizePrefix(prefix);
|
|
1552
|
+
const suggestions = [];
|
|
1553
|
+
|
|
1554
|
+
// Try suggesting with prefix
|
|
1555
|
+
if (comp && !comp.toLowerCase().startsWith(p)) {
|
|
1556
|
+
const prefixed = `${p}-${comp.toLowerCase()}`;
|
|
1557
|
+
if (indexes.byTag.has(prefixed)) {
|
|
1558
|
+
suggestions.push(prefixed);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Levenshtein-based suggestion
|
|
1563
|
+
const suggested = suggestUnknownElementTagName(comp.includes('-') ? comp : `${p}-${comp}`, indexes.byTag);
|
|
1564
|
+
if (suggested && !suggestions.includes(suggested)) {
|
|
1565
|
+
suggestions.push(suggested);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const msg = suggestions.length > 0
|
|
1569
|
+
? `Component not found: ${comp}. Did you mean: ${suggestions.join(', ')}?`
|
|
1570
|
+
: `Component not found: ${comp}`;
|
|
1571
|
+
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
913
1572
|
}
|
|
914
1573
|
|
|
915
1574
|
// ---------------------------------------------------------------------------
|
|
@@ -918,45 +1577,210 @@ function resolveDeclByComponent(indexes, component, prefix) {
|
|
|
918
1577
|
//
|
|
919
1578
|
// loadJsonData(fileName: string) → Promise<object>
|
|
920
1579
|
// loadValidator() → Promise<{ collectCemCustomElements, validateTextAgainstCem }>
|
|
1580
|
+
// options?: {
|
|
1581
|
+
// plugins?: WcfMcpPlugin[],
|
|
1582
|
+
// loadJsonDataFromPath?: (path: string, fileName: string, pluginName?: string) => Promise<object>
|
|
1583
|
+
// loadTextData?: (fileName: string) => Promise<string>
|
|
1584
|
+
// }
|
|
921
1585
|
// ---------------------------------------------------------------------------
|
|
922
1586
|
|
|
923
|
-
export async function createMcpServer(loadJsonData, loadValidator) {
|
|
924
|
-
const
|
|
1587
|
+
export async function createMcpServer(loadJsonData, loadValidator, options = {}) {
|
|
1588
|
+
const plugins = normalizePlugins(options?.plugins ?? []);
|
|
1589
|
+
const pluginDataSourceMap = buildPluginDataSourceMap(plugins);
|
|
1590
|
+
const loadJsonDataFromPath = typeof options?.loadJsonDataFromPath === 'function'
|
|
1591
|
+
? options.loadJsonDataFromPath
|
|
1592
|
+
: null;
|
|
1593
|
+
const loadTextData = typeof options?.loadTextData === 'function'
|
|
1594
|
+
? options.loadTextData
|
|
1595
|
+
: null;
|
|
1596
|
+
|
|
1597
|
+
const loadJson = async (fileName) => {
|
|
1598
|
+
const override = pluginDataSourceMap.get(fileName);
|
|
1599
|
+
if (!override) return loadJsonData(fileName);
|
|
1600
|
+
if (!loadJsonDataFromPath) {
|
|
1601
|
+
throw new Error(`Plugin data source override for ${fileName} requires loadJsonDataFromPath`);
|
|
1602
|
+
}
|
|
1603
|
+
return loadJsonDataFromPath(override.path, fileName, override.pluginName);
|
|
1604
|
+
};
|
|
1605
|
+
const loadText = async (fileName) => {
|
|
1606
|
+
if (!loadTextData) throw new Error(`Text data loader not configured for ${fileName}`);
|
|
1607
|
+
return loadTextData(fileName);
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
const manifest = await loadJson('custom-elements.json');
|
|
925
1611
|
const indexes = buildIndexes(manifest);
|
|
926
1612
|
const {
|
|
927
1613
|
collectCemCustomElements,
|
|
928
1614
|
validateTextAgainstCem,
|
|
929
1615
|
detectTokenMisuseInInlineStyles = () => [],
|
|
930
1616
|
detectAccessibilityMisuseInMarkup = () => [],
|
|
1617
|
+
buildEnumAttributeMap = () => new Map(),
|
|
1618
|
+
detectEnumValueMisuse = () => [],
|
|
1619
|
+
buildSlotNameMap = () => new Map(),
|
|
1620
|
+
detectInvalidSlotName = () => [],
|
|
1621
|
+
detectMissingRequiredAttributes = () => [],
|
|
1622
|
+
detectOrphanedChildComponents = () => [],
|
|
1623
|
+
detectEmptyInteractiveElement = () => [],
|
|
931
1624
|
} = await loadValidator();
|
|
932
1625
|
const canonicalCemIndex = collectCemCustomElements(manifest);
|
|
933
|
-
const
|
|
934
|
-
const
|
|
1626
|
+
const canonicalEnumMap = buildEnumAttributeMap(manifest);
|
|
1627
|
+
const canonicalSlotMap = buildSlotNameMap(manifest);
|
|
1628
|
+
const installRegistry = await loadJson('install-registry.json');
|
|
1629
|
+
const patternRegistry = await loadJson('pattern-registry.json');
|
|
935
1630
|
const { patterns } = loadPatternRegistryShape(patternRegistry);
|
|
936
1631
|
const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
|
|
937
1632
|
|
|
938
1633
|
// Load optional data files (design tokens, guidelines index)
|
|
939
1634
|
let designTokensData = null;
|
|
940
1635
|
try {
|
|
941
|
-
designTokensData = await
|
|
1636
|
+
designTokensData = await loadJson('design-tokens.json');
|
|
942
1637
|
} catch {
|
|
943
1638
|
// design-tokens.json may not exist yet
|
|
944
1639
|
}
|
|
945
1640
|
|
|
946
1641
|
let guidelinesIndexData = null;
|
|
947
1642
|
try {
|
|
948
|
-
guidelinesIndexData = await
|
|
1643
|
+
guidelinesIndexData = await loadJson('guidelines-index.json');
|
|
949
1644
|
} catch {
|
|
950
1645
|
// guidelines-index.json may not exist yet
|
|
951
1646
|
}
|
|
1647
|
+
let llmsFullText = null;
|
|
1648
|
+
try {
|
|
1649
|
+
llmsFullText = await loadText('llms-full.txt');
|
|
1650
|
+
} catch {
|
|
1651
|
+
// llms-full.txt may not exist in local setup
|
|
1652
|
+
}
|
|
952
1653
|
|
|
953
1654
|
const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
|
|
954
1655
|
|
|
955
1656
|
const server = new McpServer({
|
|
956
1657
|
name: 'web-components-factory-design-system',
|
|
957
|
-
version: '0.
|
|
1658
|
+
version: '0.2.0',
|
|
958
1659
|
});
|
|
959
1660
|
|
|
1661
|
+
server.registerPrompt(
|
|
1662
|
+
FIGMA_TO_WCF_PROMPT,
|
|
1663
|
+
{
|
|
1664
|
+
title: 'Figma To WCF',
|
|
1665
|
+
description:
|
|
1666
|
+
'Guided prompt for converting a Figma URL into WCF implementation steps with a strict tool order.',
|
|
1667
|
+
argsSchema: {
|
|
1668
|
+
figmaUrl: z.string().trim().url().describe('Figma URL (design or board link)'),
|
|
1669
|
+
userIntent: z.string().optional().describe('Optional implementation intent / screen purpose'),
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
async ({ figmaUrl, userIntent }) => ({
|
|
1673
|
+
messages: [{
|
|
1674
|
+
role: 'user',
|
|
1675
|
+
content: {
|
|
1676
|
+
type: 'text',
|
|
1677
|
+
text: buildFigmaToWcfPromptText({ figmaUrl, userIntent }),
|
|
1678
|
+
},
|
|
1679
|
+
}],
|
|
1680
|
+
}),
|
|
1681
|
+
);
|
|
1682
|
+
|
|
1683
|
+
server.registerResource(
|
|
1684
|
+
'wcf_components',
|
|
1685
|
+
WCF_RESOURCE_URIS.components,
|
|
1686
|
+
{
|
|
1687
|
+
title: 'WCF Component Catalog',
|
|
1688
|
+
description: 'Component catalog snapshot with categories and API entry points.',
|
|
1689
|
+
mimeType: 'application/json',
|
|
1690
|
+
},
|
|
1691
|
+
async () => {
|
|
1692
|
+
const payload = buildComponentsResourcePayload(indexes);
|
|
1693
|
+
return {
|
|
1694
|
+
contents: [{
|
|
1695
|
+
uri: WCF_RESOURCE_URIS.components,
|
|
1696
|
+
mimeType: 'application/json',
|
|
1697
|
+
text: JSON.stringify(payload, null, 2),
|
|
1698
|
+
}],
|
|
1699
|
+
};
|
|
1700
|
+
},
|
|
1701
|
+
);
|
|
1702
|
+
|
|
1703
|
+
server.registerResource(
|
|
1704
|
+
'wcf_tokens',
|
|
1705
|
+
WCF_RESOURCE_URIS.tokens,
|
|
1706
|
+
{
|
|
1707
|
+
title: 'WCF Design Tokens',
|
|
1708
|
+
description: 'Token summary resource for colors, spacing, typography, radius, and shadows.',
|
|
1709
|
+
mimeType: 'application/json',
|
|
1710
|
+
},
|
|
1711
|
+
async () => {
|
|
1712
|
+
const result = buildTokensResourcePayload(designTokensData);
|
|
1713
|
+
const payload = result.isError ? { error: result.error } : result.payload;
|
|
1714
|
+
return {
|
|
1715
|
+
contents: [{
|
|
1716
|
+
uri: WCF_RESOURCE_URIS.tokens,
|
|
1717
|
+
mimeType: 'application/json',
|
|
1718
|
+
text: JSON.stringify(payload, null, 2),
|
|
1719
|
+
}],
|
|
1720
|
+
};
|
|
1721
|
+
},
|
|
1722
|
+
);
|
|
1723
|
+
|
|
1724
|
+
server.registerResource(
|
|
1725
|
+
'wcf_guidelines',
|
|
1726
|
+
new ResourceTemplate(WCF_RESOURCE_URIS.guidelinesTemplate, {
|
|
1727
|
+
list: async () => ({
|
|
1728
|
+
resources: GUIDELINE_TOPICS.map((topic) => ({
|
|
1729
|
+
uri: `wcf://guidelines/${topic}`,
|
|
1730
|
+
name: `wcf guidelines (${topic})`,
|
|
1731
|
+
description: `Guideline summary for topic=${topic}`,
|
|
1732
|
+
})),
|
|
1733
|
+
}),
|
|
1734
|
+
complete: {
|
|
1735
|
+
topic: async (value) => {
|
|
1736
|
+
const query = String(value ?? '').trim().toLowerCase();
|
|
1737
|
+
return GUIDELINE_TOPICS.filter((topic) => topic.startsWith(query));
|
|
1738
|
+
},
|
|
1739
|
+
},
|
|
1740
|
+
}),
|
|
1741
|
+
{
|
|
1742
|
+
title: 'WCF Guidelines',
|
|
1743
|
+
description: 'Topic-scoped guideline resource (accessibility|css|patterns|all).',
|
|
1744
|
+
mimeType: 'application/json',
|
|
1745
|
+
},
|
|
1746
|
+
async (_uri, variables) => {
|
|
1747
|
+
const topic = String(variables?.topic ?? '').trim().toLowerCase();
|
|
1748
|
+
const result = buildGuidelinesResourcePayload(guidelinesIndexData, topic);
|
|
1749
|
+
if (result.isError) {
|
|
1750
|
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
1751
|
+
}
|
|
1752
|
+
return {
|
|
1753
|
+
contents: [{
|
|
1754
|
+
uri: `wcf://guidelines/${topic}`,
|
|
1755
|
+
mimeType: 'application/json',
|
|
1756
|
+
text: JSON.stringify(result.payload, null, 2),
|
|
1757
|
+
}],
|
|
1758
|
+
};
|
|
1759
|
+
},
|
|
1760
|
+
);
|
|
1761
|
+
|
|
1762
|
+
server.registerResource(
|
|
1763
|
+
'wcf_llms_full',
|
|
1764
|
+
WCF_RESOURCE_URIS.llmsFull,
|
|
1765
|
+
{
|
|
1766
|
+
title: 'WCF llms-full',
|
|
1767
|
+
description: 'LLM reference corpus for WCF usage, generated from repository docs.',
|
|
1768
|
+
mimeType: 'text/plain',
|
|
1769
|
+
},
|
|
1770
|
+
async () => {
|
|
1771
|
+
if (typeof llmsFullText !== 'string' || llmsFullText.length === 0) {
|
|
1772
|
+
throw new Error('LLMS_FULL_UNAVAILABLE: llms-full.txt is not available.');
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
contents: [{
|
|
1776
|
+
uri: WCF_RESOURCE_URIS.llmsFull,
|
|
1777
|
+
mimeType: 'text/plain',
|
|
1778
|
+
text: llmsFullText,
|
|
1779
|
+
}],
|
|
1780
|
+
};
|
|
1781
|
+
},
|
|
1782
|
+
);
|
|
1783
|
+
|
|
960
1784
|
// -----------------------------------------------------------------------
|
|
961
1785
|
// Tool: get_design_system_overview
|
|
962
1786
|
// -----------------------------------------------------------------------
|
|
@@ -981,13 +1805,44 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
981
1805
|
|
|
982
1806
|
const overview = {
|
|
983
1807
|
name: 'DADS Web Components (wcf)',
|
|
984
|
-
version: '0.
|
|
1808
|
+
version: '0.2.0',
|
|
985
1809
|
prefix: CANONICAL_PREFIX,
|
|
986
1810
|
totalComponents: indexes.decls.length,
|
|
987
1811
|
componentsByCategory: categoryCount,
|
|
988
1812
|
totalPatterns: patternList.length,
|
|
989
1813
|
patterns: patternList,
|
|
1814
|
+
setupInfo: {
|
|
1815
|
+
npmPackage: 'web-components-factory',
|
|
1816
|
+
installCommand: 'npm install web-components-factory',
|
|
1817
|
+
vendorRuntimePath: 'vendor-runtime/',
|
|
1818
|
+
htmlBoilerplate: '<script type="module" src="vendor-runtime/src/autoload.js"></script>',
|
|
1819
|
+
noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
|
|
1820
|
+
},
|
|
990
1821
|
ideSetupTemplates: IDE_SETUP_TEMPLATES,
|
|
1822
|
+
availablePrompts: [
|
|
1823
|
+
{
|
|
1824
|
+
name: FIGMA_TO_WCF_PROMPT,
|
|
1825
|
+
purpose: 'Figma-to-WCF conversion workflow prompt',
|
|
1826
|
+
},
|
|
1827
|
+
],
|
|
1828
|
+
availableResources: [
|
|
1829
|
+
{
|
|
1830
|
+
uri: WCF_RESOURCE_URIS.components,
|
|
1831
|
+
purpose: 'Component catalog snapshot',
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
uri: WCF_RESOURCE_URIS.tokens,
|
|
1835
|
+
purpose: 'Token summary snapshot',
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
uri: WCF_RESOURCE_URIS.guidelinesTemplate,
|
|
1839
|
+
purpose: 'Topic-based guideline summaries',
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
uri: WCF_RESOURCE_URIS.llmsFull,
|
|
1843
|
+
purpose: 'Full LLM reference text for WCF',
|
|
1844
|
+
},
|
|
1845
|
+
],
|
|
991
1846
|
availableTools: [
|
|
992
1847
|
{ name: 'get_design_system_overview', purpose: 'This overview (start here)' },
|
|
993
1848
|
{ name: 'list_components', purpose: 'Browse components with progressive disclosure and filters' },
|
|
@@ -1000,23 +1855,51 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1000
1855
|
{ name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
|
|
1001
1856
|
{ name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
|
|
1002
1857
|
{ name: 'get_design_tokens', purpose: 'Query design tokens (colors, spacing, typography, radius, shadows)' },
|
|
1858
|
+
{ name: 'get_design_token_detail', purpose: 'Get details, relationships, and usage examples for one token' },
|
|
1003
1859
|
{ name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
|
|
1004
1860
|
{ name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
|
|
1005
1861
|
],
|
|
1006
1862
|
recommendedWorkflow: [
|
|
1007
1863
|
'1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
|
|
1008
|
-
'2.
|
|
1009
|
-
'3.
|
|
1010
|
-
'4.
|
|
1011
|
-
'5.
|
|
1012
|
-
'6.
|
|
1013
|
-
'7.
|
|
1014
|
-
'8.
|
|
1015
|
-
'9.
|
|
1016
|
-
'10.
|
|
1864
|
+
'2. figma_to_wcf (optional) → bootstrap the Figma-to-WCF tool sequence',
|
|
1865
|
+
'3. wcf://components and wcf://tokens resources → preload catalog/token context',
|
|
1866
|
+
'4. search_guidelines → find relevant guidelines',
|
|
1867
|
+
'5. get_design_tokens → get correct token values',
|
|
1868
|
+
'6. get_design_token_detail → inspect one token with references/referencedBy and usage examples',
|
|
1869
|
+
'7. get_accessibility_docs → fetch component-level accessibility checklist',
|
|
1870
|
+
'8. list_components (category/query + pagination) → shortlist components',
|
|
1871
|
+
'9. search_icons (optional) → find icon names quickly',
|
|
1872
|
+
'10. get_component_api → check attributes, slots, events, CSS parts',
|
|
1873
|
+
'11. generate_usage_snippet or get_pattern_recipe → get code',
|
|
1874
|
+
'12. validate_markup → verify your HTML and use suggestions to self-correct',
|
|
1875
|
+
'13. get_install_recipe → get import/install instructions',
|
|
1017
1876
|
],
|
|
1877
|
+
experimental: {
|
|
1878
|
+
plugins: {
|
|
1879
|
+
enabled: plugins.length > 0,
|
|
1880
|
+
note: PLUGIN_TOOL_NOTICE,
|
|
1881
|
+
pluginCount: plugins.length,
|
|
1882
|
+
pluginToolCount: plugins.reduce((sum, plugin) => sum + (plugin.tools?.length ?? 0), 0),
|
|
1883
|
+
plugins: plugins.map((plugin) => ({
|
|
1884
|
+
name: plugin.name,
|
|
1885
|
+
version: plugin.version,
|
|
1886
|
+
toolCount: plugin.tools?.length ?? 0,
|
|
1887
|
+
dataSourceOverrides: plugin.dataSources?.map((source) => source.fileName) ?? [],
|
|
1888
|
+
})),
|
|
1889
|
+
},
|
|
1890
|
+
},
|
|
1018
1891
|
};
|
|
1019
1892
|
|
|
1893
|
+
for (const plugin of plugins) {
|
|
1894
|
+
const tools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
1895
|
+
for (const tool of tools) {
|
|
1896
|
+
overview.availableTools.push({
|
|
1897
|
+
name: tool.name,
|
|
1898
|
+
purpose: `${tool.description} (plugin: ${plugin.name})`,
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1020
1903
|
return {
|
|
1021
1904
|
content: [{ type: 'text', text: JSON.stringify(overview, null, 2) }],
|
|
1022
1905
|
};
|
|
@@ -1030,22 +1913,30 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1030
1913
|
'list_components',
|
|
1031
1914
|
{
|
|
1032
1915
|
description:
|
|
1033
|
-
'List custom elements in the design system. When: exploring available components, searching by keyword, or paging through results. Returns: array of {tagName, className, description, category}. After: use get_component_api for details on a specific component.',
|
|
1916
|
+
'List custom elements in the design system. When: exploring available components, searching by keyword, or paging through results. Returns: {items, total, limit, offset, hasMore} where items is array of {tagName, className, description, category}. After: use get_component_api for details on a specific component.',
|
|
1034
1917
|
inputSchema: {
|
|
1035
1918
|
category: z
|
|
1036
1919
|
.enum(['Form', 'Actions', 'Navigation', 'Content', 'Display', 'Layout', 'Other'])
|
|
1037
1920
|
.optional()
|
|
1038
1921
|
.describe('Filter by component category'),
|
|
1039
1922
|
query: z.string().optional().describe('Search by tagName/className/description/category/modulePath'),
|
|
1040
|
-
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (
|
|
1923
|
+
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
|
|
1041
1924
|
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
1042
1925
|
prefix: z.string().optional(),
|
|
1043
1926
|
},
|
|
1044
1927
|
},
|
|
1045
1928
|
async ({ category, query, limit, offset, prefix }) => {
|
|
1046
|
-
const
|
|
1929
|
+
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix });
|
|
1930
|
+
const payload = {
|
|
1931
|
+
items: page.items,
|
|
1932
|
+
total: page.total,
|
|
1933
|
+
limit: page.limit,
|
|
1934
|
+
offset: page.offset,
|
|
1935
|
+
hasMore: page.hasMore,
|
|
1936
|
+
};
|
|
1937
|
+
if (page._notice) payload._notice = page._notice;
|
|
1047
1938
|
return {
|
|
1048
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1939
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1049
1940
|
};
|
|
1050
1941
|
},
|
|
1051
1942
|
);
|
|
@@ -1082,27 +1973,34 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1082
1973
|
description:
|
|
1083
1974
|
'Get the full API surface of a single component (attributes, slots, events, CSS parts, CSS custom properties). When: you need detailed specs for a component. Returns: complete component specification. After: use generate_usage_snippet for a code example.',
|
|
1084
1975
|
inputSchema: {
|
|
1085
|
-
tagName: z.string().optional(),
|
|
1086
|
-
className: z.string().optional(),
|
|
1976
|
+
tagName: z.string().optional().describe('Tag name (e.g., "dads-button")'),
|
|
1977
|
+
className: z.string().optional().describe('Class name (e.g., "DadsButton")'),
|
|
1978
|
+
component: z.string().optional().describe('Any identifier: tagName, className, or bare name (e.g., "button")'),
|
|
1087
1979
|
prefix: z.string().optional(),
|
|
1088
1980
|
},
|
|
1089
1981
|
},
|
|
1090
|
-
async ({ tagName, className, prefix }) => {
|
|
1091
|
-
const
|
|
1982
|
+
async ({ tagName, className, component, prefix }) => {
|
|
1983
|
+
const p = normalizePrefix(prefix);
|
|
1984
|
+
let decl;
|
|
1985
|
+
let modulePath;
|
|
1986
|
+
|
|
1987
|
+
if (component) {
|
|
1988
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
1989
|
+
decl = resolved?.decl;
|
|
1990
|
+
modulePath = resolved?.modulePath;
|
|
1991
|
+
} else {
|
|
1992
|
+
decl = pickDecl(indexes, { tagName, className, prefix: p });
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1092
1995
|
if (!decl) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
{
|
|
1096
|
-
type: 'text',
|
|
1097
|
-
text: `Component not found (tagName=${String(tagName ?? '')}, className=${String(className ?? '')})`,
|
|
1098
|
-
},
|
|
1099
|
-
],
|
|
1100
|
-
isError: true,
|
|
1101
|
-
};
|
|
1996
|
+
const identifier = component || tagName || className || '';
|
|
1997
|
+
return buildComponentNotFoundError(identifier, indexes, p);
|
|
1102
1998
|
}
|
|
1103
1999
|
|
|
1104
2000
|
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
1105
|
-
|
|
2001
|
+
if (!modulePath) {
|
|
2002
|
+
modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
2003
|
+
}
|
|
1106
2004
|
const api = serializeApi(decl, modulePath, prefix);
|
|
1107
2005
|
const relatedComponents = getRelatedComponentsForTag({
|
|
1108
2006
|
canonicalTagName: canonicalTag,
|
|
@@ -1136,19 +2034,16 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1136
2034
|
},
|
|
1137
2035
|
},
|
|
1138
2036
|
async ({ component, prefix }) => {
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
2037
|
+
const p = normalizePrefix(prefix);
|
|
2038
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
2039
|
+
const decl = resolved?.decl;
|
|
1142
2040
|
|
|
1143
2041
|
if (!decl) {
|
|
1144
|
-
return
|
|
1145
|
-
content: [{ type: 'text', text: `Component not found: ${component}` }],
|
|
1146
|
-
isError: true,
|
|
1147
|
-
};
|
|
2042
|
+
return buildComponentNotFoundError(component, indexes, p);
|
|
1148
2043
|
}
|
|
1149
2044
|
|
|
1150
2045
|
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
1151
|
-
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
2046
|
+
const modulePath = resolved?.modulePath ?? (canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined);
|
|
1152
2047
|
const api = serializeApi(decl, modulePath, prefix);
|
|
1153
2048
|
const snippet = generateSnippet(api, prefix);
|
|
1154
2049
|
|
|
@@ -1206,6 +2101,11 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1206
2101
|
const deps = Array.isArray(install.deps) ? install.deps : [];
|
|
1207
2102
|
const tags = Array.isArray(install.tags) ? install.tags : [];
|
|
1208
2103
|
|
|
2104
|
+
// Resolve transitive dependencies via BFS
|
|
2105
|
+
const transitiveDeps = componentId
|
|
2106
|
+
? resolveComponentClosure({ installRegistry }, [componentId]).filter((id) => id !== componentId)
|
|
2107
|
+
: [];
|
|
2108
|
+
|
|
1209
2109
|
const tagNames =
|
|
1210
2110
|
tags.length > 0 ? tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : [api.tagName];
|
|
1211
2111
|
|
|
@@ -1228,6 +2128,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1228
2128
|
componentId,
|
|
1229
2129
|
tagNames,
|
|
1230
2130
|
deps,
|
|
2131
|
+
transitiveDeps,
|
|
1231
2132
|
define,
|
|
1232
2133
|
defineHint,
|
|
1233
2134
|
source: install.source,
|
|
@@ -1250,7 +2151,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1250
2151
|
'validate_markup',
|
|
1251
2152
|
{
|
|
1252
2153
|
description:
|
|
1253
|
-
'Validate HTML against the design system Custom Elements Manifest. When: checking generated or written HTML for correctness. Returns: diagnostics array with errors (unknown elements), warnings (unknown attributes/token misuse/accessibility misuse), and optional suggestion text for quick recovery. Use after generating HTML to catch mistakes.',
|
|
2154
|
+
'Validate HTML against the design system Custom Elements Manifest. When: checking generated or written HTML for correctness. Returns: diagnostics array with errors (unknown elements/invalid enum values/invalid slot names/missing required attributes), warnings (unknown attributes/token misuse/accessibility misuse/orphaned children/empty interactive elements), and optional suggestion text for quick recovery. Use after generating HTML to catch mistakes.',
|
|
1254
2155
|
inputSchema: {
|
|
1255
2156
|
html: z.string(),
|
|
1256
2157
|
prefix: z.string().optional(),
|
|
@@ -1259,11 +2160,12 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1259
2160
|
async ({ html, prefix }) => {
|
|
1260
2161
|
const p = normalizePrefix(prefix);
|
|
1261
2162
|
let cemIndex = canonicalCemIndex;
|
|
2163
|
+
let enumMap = canonicalEnumMap;
|
|
2164
|
+
let slotMap = canonicalSlotMap;
|
|
1262
2165
|
if (p !== CANONICAL_PREFIX) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
cemIndex = combined;
|
|
2166
|
+
cemIndex = mergeWithPrefixed(canonicalCemIndex, p);
|
|
2167
|
+
enumMap = mergeWithPrefixed(canonicalEnumMap, p);
|
|
2168
|
+
slotMap = mergeWithPrefixed(canonicalSlotMap, p);
|
|
1267
2169
|
}
|
|
1268
2170
|
|
|
1269
2171
|
const cemDiagnostics = validateTextAgainstCem({
|
|
@@ -1276,6 +2178,13 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1276
2178
|
},
|
|
1277
2179
|
});
|
|
1278
2180
|
|
|
2181
|
+
const enumDiagnostics = detectEnumValueMisuse({
|
|
2182
|
+
filePath: '<markup>',
|
|
2183
|
+
text: html,
|
|
2184
|
+
enumMap,
|
|
2185
|
+
severity: 'error',
|
|
2186
|
+
});
|
|
2187
|
+
|
|
1279
2188
|
const tokenMisuseDiagnostics = detectTokenMisuseInInlineStyles({
|
|
1280
2189
|
filePath: '<markup>',
|
|
1281
2190
|
text: html,
|
|
@@ -1289,7 +2198,35 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1289
2198
|
severity: 'warning',
|
|
1290
2199
|
});
|
|
1291
2200
|
|
|
1292
|
-
const
|
|
2201
|
+
const slotDiagnostics = detectInvalidSlotName({
|
|
2202
|
+
filePath: '<markup>',
|
|
2203
|
+
text: html,
|
|
2204
|
+
slotMap,
|
|
2205
|
+
severity: 'error',
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
const requiredAttrDiagnostics = detectMissingRequiredAttributes({
|
|
2209
|
+
filePath: '<markup>',
|
|
2210
|
+
text: html,
|
|
2211
|
+
prefix: p,
|
|
2212
|
+
severity: 'error',
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
const orphanDiagnostics = detectOrphanedChildComponents({
|
|
2216
|
+
filePath: '<markup>',
|
|
2217
|
+
text: html,
|
|
2218
|
+
prefix: p,
|
|
2219
|
+
severity: 'warning',
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
const emptyInteractiveDiagnostics = detectEmptyInteractiveElement({
|
|
2223
|
+
filePath: '<markup>',
|
|
2224
|
+
text: html,
|
|
2225
|
+
prefix: p,
|
|
2226
|
+
severity: 'warning',
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
const diagnostics = [...cemDiagnostics, ...enumDiagnostics, ...slotDiagnostics, ...requiredAttrDiagnostics, ...orphanDiagnostics, ...emptyInteractiveDiagnostics, ...tokenMisuseDiagnostics, ...accessibilityDiagnostics].map((d) => {
|
|
1293
2230
|
const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex });
|
|
1294
2231
|
return {
|
|
1295
2232
|
file: d.file,
|
|
@@ -1454,35 +2391,48 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1454
2391
|
.describe('Filter by token category'),
|
|
1455
2392
|
query: z.string().optional()
|
|
1456
2393
|
.describe('Search token names (partial match)'),
|
|
2394
|
+
theme: z.enum(['light', 'dark', 'all']).optional()
|
|
2395
|
+
.describe('Theme filter (currently light only; dark/all return an error due to NG-06)'),
|
|
1457
2396
|
},
|
|
1458
2397
|
},
|
|
1459
|
-
async ({ type, category, query }) => {
|
|
1460
|
-
|
|
2398
|
+
async ({ type, category, query, theme }) => {
|
|
2399
|
+
const { isError, payload } = buildDesignTokensPayload(designTokensData, { type, category, query, theme });
|
|
2400
|
+
if (isError) {
|
|
1461
2401
|
return {
|
|
1462
|
-
content: [{ type: 'text', text:
|
|
2402
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1463
2403
|
isError: true,
|
|
1464
2404
|
};
|
|
1465
2405
|
}
|
|
2406
|
+
return buildJsonToolResponse(payload);
|
|
2407
|
+
},
|
|
2408
|
+
);
|
|
1466
2409
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2410
|
+
// -----------------------------------------------------------------------
|
|
2411
|
+
// Tool: get_design_token_detail
|
|
2412
|
+
// -----------------------------------------------------------------------
|
|
2413
|
+
server.registerTool(
|
|
2414
|
+
'get_design_token_detail',
|
|
2415
|
+
{
|
|
2416
|
+
description:
|
|
2417
|
+
'Get details for one design token. ' +
|
|
2418
|
+
'When: you already found a token and need its references, referencedBy, and usage examples. ' +
|
|
2419
|
+
'Returns: token detail object with relationships and example CSS snippets. ' +
|
|
2420
|
+
'After: apply the cssVariable in your implementation or validate related semantic aliases.',
|
|
2421
|
+
inputSchema: {
|
|
2422
|
+
name: z.string()
|
|
2423
|
+
.describe('Token name or css variable (e.g. --color-primary or var(--color-primary))'),
|
|
2424
|
+
theme: z.enum(['light', 'dark', 'all']).optional()
|
|
2425
|
+
.describe('Theme selector (currently only light is supported due to NG-06)'),
|
|
2426
|
+
},
|
|
2427
|
+
},
|
|
2428
|
+
async ({ name, theme }) => {
|
|
2429
|
+
const { isError, payload } = buildDesignTokenDetailPayload(designTokensData, name, theme);
|
|
2430
|
+
if (isError) {
|
|
2431
|
+
return {
|
|
2432
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
2433
|
+
isError: true,
|
|
2434
|
+
};
|
|
1478
2435
|
}
|
|
1479
|
-
|
|
1480
|
-
const payload = {
|
|
1481
|
-
total: tokens.length,
|
|
1482
|
-
tokens,
|
|
1483
|
-
summary: designTokensData.summary,
|
|
1484
|
-
};
|
|
1485
|
-
|
|
1486
2436
|
return buildJsonToolResponse(payload);
|
|
1487
2437
|
},
|
|
1488
2438
|
);
|
|
@@ -1582,6 +2532,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1582
2532
|
const max = maxResults ?? 5;
|
|
1583
2533
|
const documents = Array.isArray(guidelinesIndexData.documents) ? guidelinesIndexData.documents : [];
|
|
1584
2534
|
const q = query.toLowerCase();
|
|
2535
|
+
const expandedTerms = expandQueryWithSynonyms(q);
|
|
1585
2536
|
|
|
1586
2537
|
// Score and rank sections
|
|
1587
2538
|
const results = [];
|
|
@@ -1595,6 +2546,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1595
2546
|
const heading = String(section.heading ?? '').toLowerCase();
|
|
1596
2547
|
const keywords = Array.isArray(section.keywords) ? section.keywords : [];
|
|
1597
2548
|
const snippet = String(section.snippet ?? '').toLowerCase();
|
|
2549
|
+
const body = String(section.body ?? '').toLowerCase();
|
|
1598
2550
|
|
|
1599
2551
|
// Heading match: weight 3
|
|
1600
2552
|
if (heading.includes(q)) score += 3;
|
|
@@ -1610,6 +2562,27 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1610
2562
|
// Snippet match: weight 1
|
|
1611
2563
|
if (snippet.includes(q)) score += 1;
|
|
1612
2564
|
|
|
2565
|
+
// Body text match: weight 1
|
|
2566
|
+
if (body && body.includes(q)) score += 1;
|
|
2567
|
+
|
|
2568
|
+
// Synonym expansion match: weight 1 (only for expanded terms, not the original)
|
|
2569
|
+
if (score === 0 && expandedTerms.length > 1) {
|
|
2570
|
+
for (let i = 1; i < expandedTerms.length; i++) {
|
|
2571
|
+
const syn = expandedTerms[i];
|
|
2572
|
+
if (heading.includes(syn) || snippet.includes(syn) || body.includes(syn)) {
|
|
2573
|
+
score += 1;
|
|
2574
|
+
break;
|
|
2575
|
+
}
|
|
2576
|
+
for (const kw of keywords) {
|
|
2577
|
+
if (String(kw).toLowerCase().includes(syn)) {
|
|
2578
|
+
score += 1;
|
|
2579
|
+
break;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
if (score > 0) break;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
1613
2586
|
if (score > 0) {
|
|
1614
2587
|
results.push({
|
|
1615
2588
|
score,
|
|
@@ -1635,9 +2608,81 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1635
2608
|
results: topResults,
|
|
1636
2609
|
};
|
|
1637
2610
|
|
|
2611
|
+
// Zero-result fallback: suggest alternative queries and tools
|
|
2612
|
+
if (results.length === 0) {
|
|
2613
|
+
const synonymExpansions = expandedTerms.filter((t) => t !== q);
|
|
2614
|
+
payload.suggestions = {
|
|
2615
|
+
alternativeQueries: synonymExpansions.length > 0 ? synonymExpansions : [],
|
|
2616
|
+
alternativeTools: [
|
|
2617
|
+
{ tool: 'get_accessibility_docs', hint: 'For component-specific a11y checks' },
|
|
2618
|
+
{ tool: 'get_component_api', hint: 'For component API details' },
|
|
2619
|
+
],
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
|
|
1638
2623
|
return buildJsonToolResponse(payload);
|
|
1639
2624
|
},
|
|
1640
2625
|
);
|
|
1641
2626
|
|
|
1642
|
-
|
|
2627
|
+
for (const plugin of plugins) {
|
|
2628
|
+
const pluginTools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
2629
|
+
for (const tool of pluginTools) {
|
|
2630
|
+
server.registerTool(
|
|
2631
|
+
tool.name,
|
|
2632
|
+
{
|
|
2633
|
+
description: tool.description,
|
|
2634
|
+
inputSchema: tool.inputSchema ?? {},
|
|
2635
|
+
},
|
|
2636
|
+
async (args) => {
|
|
2637
|
+
try {
|
|
2638
|
+
if (typeof tool.handler === 'function') {
|
|
2639
|
+
const result = await tool.handler(args, {
|
|
2640
|
+
plugin: { name: plugin.name, version: plugin.version },
|
|
2641
|
+
helpers: {
|
|
2642
|
+
loadJsonData: loadJson,
|
|
2643
|
+
buildJsonToolResponse,
|
|
2644
|
+
normalizePrefix,
|
|
2645
|
+
withPrefix,
|
|
2646
|
+
toCanonicalTagName,
|
|
2647
|
+
},
|
|
2648
|
+
});
|
|
2649
|
+
if (isPlainObject(result) && Array.isArray(result.content)) {
|
|
2650
|
+
return result;
|
|
2651
|
+
}
|
|
2652
|
+
return buildJsonToolResponse(result ?? {});
|
|
2653
|
+
}
|
|
2654
|
+
return buildJsonToolResponse(tool.staticPayload ?? {});
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2657
|
+
return {
|
|
2658
|
+
content: [{
|
|
2659
|
+
type: 'text',
|
|
2660
|
+
text: JSON.stringify({
|
|
2661
|
+
error: {
|
|
2662
|
+
code: 'PLUGIN_TOOL_RUNTIME_ERROR',
|
|
2663
|
+
message: `Plugin tool failed (${tool.name}): ${message}`,
|
|
2664
|
+
plugin: plugin.name,
|
|
2665
|
+
},
|
|
2666
|
+
}, null, 2),
|
|
2667
|
+
}],
|
|
2668
|
+
isError: true,
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
return {
|
|
2677
|
+
server,
|
|
2678
|
+
pluginRuntime: {
|
|
2679
|
+
pluginCount: plugins.length,
|
|
2680
|
+
pluginToolCount: plugins.reduce((sum, plugin) => sum + (plugin.tools?.length ?? 0), 0),
|
|
2681
|
+
dataSourceOverrides: [...pluginDataSourceMap.entries()].map(([fileName, item]) => ({
|
|
2682
|
+
fileName,
|
|
2683
|
+
path: item.path,
|
|
2684
|
+
pluginName: item.pluginName,
|
|
2685
|
+
})),
|
|
2686
|
+
},
|
|
2687
|
+
};
|
|
1643
2688
|
}
|