@monoharada/wcf-mcp 0.1.2 → 0.5.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 +398 -8
- package/bin.mjs +19 -2
- package/core.mjs +1440 -104
- 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 +601 -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,129 @@ 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
|
+
['layout', ['grid', 'flexbox', 'layout-shell', 'responsive', 'breakpoint']],
|
|
149
|
+
['responsive', ['media query', 'breakpoint', 'viewport', 'mobile']],
|
|
150
|
+
['error', ['validation', 'aria-invalid', 'aria-describedby', 'error text']],
|
|
151
|
+
['focus', ['focus-visible', 'focus ring', 'outline', 'tabindex', 'keyboard']],
|
|
152
|
+
['token', ['design token', 'css variable', 'custom property', 'spacing token']],
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Interaction examples for form components (P-04 / #206)
|
|
156
|
+
const INTERACTION_EXAMPLES_MAP = Object.freeze({
|
|
157
|
+
'dads-input-text': [
|
|
158
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "hello";' },
|
|
159
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
160
|
+
{ scenario: 'Clear validation error', trigger: 'attribute', code: 'el.error = false; el.errorText = "";' },
|
|
161
|
+
{ scenario: 'Listen to value change', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
162
|
+
],
|
|
163
|
+
'dads-textarea': [
|
|
164
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "long text...";' },
|
|
165
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "入力できる文字数を超えています";' },
|
|
166
|
+
{ scenario: 'Listen to input event', trigger: 'event', code: "el.addEventListener('input', (e) => { console.log(e.target.value); });" },
|
|
167
|
+
],
|
|
168
|
+
'dads-select': [
|
|
169
|
+
{ scenario: 'Set selected value', trigger: 'property', code: 'el.value = "option1";' },
|
|
170
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
171
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
172
|
+
],
|
|
173
|
+
'dads-checkbox': [
|
|
174
|
+
{ scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
|
|
175
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
176
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.checked); });" },
|
|
177
|
+
],
|
|
178
|
+
'dads-radio': [
|
|
179
|
+
{ scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
|
|
180
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
181
|
+
],
|
|
182
|
+
'dads-combobox': [
|
|
183
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "selected-option";' },
|
|
184
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
185
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
186
|
+
],
|
|
187
|
+
'dads-date-picker': [
|
|
188
|
+
{ scenario: 'Set date value', trigger: 'property', code: 'el.value = "2024-01-15";' },
|
|
189
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
190
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
191
|
+
],
|
|
192
|
+
'dads-file-upload': [
|
|
193
|
+
{ scenario: 'Listen to file selection', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.files); });" },
|
|
194
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Layout behavior metadata for layout/display components (P-05 / #207)
|
|
199
|
+
const LAYOUT_BEHAVIOR_MAP = Object.freeze({
|
|
200
|
+
'dads-layout-shell': {
|
|
201
|
+
responsive: {
|
|
202
|
+
breakpoints: { desktop: '80rem', tablet: '48rem' },
|
|
203
|
+
modes: ['auto', 'desktop', 'tablet', 'mobile'],
|
|
204
|
+
defaultMode: 'auto',
|
|
205
|
+
description: 'Automatically switches between desktop/tablet/mobile layouts based on viewport width when mode="auto".',
|
|
206
|
+
},
|
|
207
|
+
overflow: {
|
|
208
|
+
strategy: 'slot-driven',
|
|
209
|
+
description: 'Slots (header, sidebar, aside, footer) are auto-hidden when empty. Sidebar collapses to rail on tablet.',
|
|
210
|
+
},
|
|
211
|
+
constraints: {
|
|
212
|
+
patterns: ['website', 'app-shell', 'master-detail', 'left-header-pane', 'three-pane', 'three-pane-shell'],
|
|
213
|
+
defaultPattern: 'app-shell',
|
|
214
|
+
mobileSidebarOptions: ['hidden', 'top', 'bottom'],
|
|
215
|
+
description: 'Choose a pattern attribute to control layout structure. Pair with mode and mobile-sidebar for full control.',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
'dads-layout-sidebar': {
|
|
219
|
+
responsive: {
|
|
220
|
+
description: 'Designed to be placed inside dads-layout-shell sidebar slot. Width is controlled by the parent shell.',
|
|
221
|
+
},
|
|
222
|
+
constraints: {
|
|
223
|
+
description: 'Simple container for sidebar content. Use inside dads-layout-shell for responsive behavior.',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
'dads-device-mock': {
|
|
227
|
+
responsive: {
|
|
228
|
+
devices: ['desktop', 'tablet', 'mobile'],
|
|
229
|
+
defaultDevice: 'mobile',
|
|
230
|
+
description: 'Renders a device frame (desktop/tablet/mobile) around slotted content. Set device attribute to switch.',
|
|
231
|
+
},
|
|
232
|
+
constraints: {
|
|
233
|
+
visibleHeight: 'Use visible-height attribute to clip the mock to a specific height (e.g. "220px").',
|
|
234
|
+
description: 'Display-only component for previewing content in device frames. Not a layout container.',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export function expandQueryWithSynonyms(query) {
|
|
240
|
+
const q = String(query ?? '').toLowerCase().trim();
|
|
241
|
+
if (!q) return [q];
|
|
242
|
+
const terms = [q];
|
|
243
|
+
for (const [key, synonyms] of SYNONYM_TABLE) {
|
|
244
|
+
if (q.includes(key)) {
|
|
245
|
+
for (const syn of synonyms) {
|
|
246
|
+
if (!terms.includes(syn)) terms.push(syn);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return terms;
|
|
251
|
+
}
|
|
252
|
+
|
|
102
253
|
export const IDE_SETUP_TEMPLATES = Object.freeze([
|
|
103
254
|
{
|
|
104
255
|
ide: 'Claude Desktop',
|
|
@@ -127,6 +278,24 @@ export const IDE_SETUP_TEMPLATES = Object.freeze([
|
|
|
127
278
|
},
|
|
128
279
|
},
|
|
129
280
|
},
|
|
281
|
+
{
|
|
282
|
+
ide: 'VS Code (GitHub Copilot)',
|
|
283
|
+
configPath: '.vscode/mcp.json',
|
|
284
|
+
snippet: {
|
|
285
|
+
mcpServers: {
|
|
286
|
+
wcf: NPX_TEMPLATE,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
ide: 'Windsurf',
|
|
292
|
+
configPath: '.windsurf/mcp_config.json',
|
|
293
|
+
snippet: {
|
|
294
|
+
mcpServers: {
|
|
295
|
+
wcf: NPX_TEMPLATE,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
130
299
|
]);
|
|
131
300
|
|
|
132
301
|
export function isStructuredContentDisabled(env = process.env) {
|
|
@@ -204,6 +373,406 @@ export function buildTokenSuggestionMap(designTokensData) {
|
|
|
204
373
|
return out;
|
|
205
374
|
}
|
|
206
375
|
|
|
376
|
+
export function normalizeTokenIdentifier(value) {
|
|
377
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
378
|
+
if (!raw) return '';
|
|
379
|
+
const cssVariable = normalizeCssVariable(raw);
|
|
380
|
+
if (cssVariable) return cssVariable;
|
|
381
|
+
if (raw.startsWith('--')) return raw;
|
|
382
|
+
return `--${raw.replace(/^[-]+/, '')}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function resolveTokenTheme(theme) {
|
|
386
|
+
const requested = String(theme ?? 'light').trim().toLowerCase() || 'light';
|
|
387
|
+
if (!TOKEN_THEMES.has(requested)) {
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
errorCode: 'INVALID_THEME',
|
|
391
|
+
message: `Unsupported theme: ${requested}. Allowed values are light, dark, all.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (requested !== 'light') {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
errorCode: 'INVALID_THEME',
|
|
398
|
+
message: `Theme "${requested}" is not available yet. Use theme="light" (NG-06).`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
ok: true,
|
|
403
|
+
requested,
|
|
404
|
+
resolved: 'light',
|
|
405
|
+
available: ['light'],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function extractReferencedTokenNames(value) {
|
|
410
|
+
if (typeof value !== 'string') return [];
|
|
411
|
+
const refs = [];
|
|
412
|
+
const re = /var\(\s*(--[^,\s)]+)\s*(?:,\s*[^)]+)?\)/g;
|
|
413
|
+
let match;
|
|
414
|
+
while ((match = re.exec(value))) {
|
|
415
|
+
const tokenName = normalizeTokenIdentifier(match[1]);
|
|
416
|
+
if (tokenName) refs.push(tokenName);
|
|
417
|
+
}
|
|
418
|
+
return [...new Set(refs)];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function buildTokenRelationshipIndex(designTokensData) {
|
|
422
|
+
const byToken = {};
|
|
423
|
+
const tokens = Array.isArray(designTokensData?.tokens) ? designTokensData.tokens : [];
|
|
424
|
+
const fromData = designTokensData?.relationships?.byToken;
|
|
425
|
+
if (fromData && typeof fromData === 'object') {
|
|
426
|
+
for (const [rawName, rawRel] of Object.entries(fromData)) {
|
|
427
|
+
const name = normalizeTokenIdentifier(rawName);
|
|
428
|
+
if (!name) continue;
|
|
429
|
+
const refs = Array.isArray(rawRel?.references)
|
|
430
|
+
? rawRel.references.map((r) => normalizeTokenIdentifier(r)).filter(Boolean)
|
|
431
|
+
: [];
|
|
432
|
+
const referencedBy = Array.isArray(rawRel?.referencedBy)
|
|
433
|
+
? rawRel.referencedBy.map((r) => normalizeTokenIdentifier(r)).filter(Boolean)
|
|
434
|
+
: [];
|
|
435
|
+
byToken[name] = {
|
|
436
|
+
references: [...new Set(refs)].sort(),
|
|
437
|
+
referencedBy: [...new Set(referencedBy)].sort(),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (Object.keys(byToken).length > 0) {
|
|
443
|
+
return { byToken };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const token of tokens) {
|
|
447
|
+
const name = normalizeTokenIdentifier(token?.name);
|
|
448
|
+
if (!name) continue;
|
|
449
|
+
if (!byToken[name]) byToken[name] = { references: [], referencedBy: [] };
|
|
450
|
+
const refs = extractReferencedTokenNames(token?.value);
|
|
451
|
+
byToken[name].references = refs;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const [sourceName, relation] of Object.entries(byToken)) {
|
|
455
|
+
for (const refName of relation.references) {
|
|
456
|
+
if (!byToken[refName]) byToken[refName] = { references: [], referencedBy: [] };
|
|
457
|
+
byToken[refName].referencedBy.push(sourceName);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const relation of Object.values(byToken)) {
|
|
462
|
+
relation.references = [...new Set(relation.references)].sort();
|
|
463
|
+
relation.referencedBy = [...new Set(relation.referencedBy)].sort();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return { byToken };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function toTokenSummary(token) {
|
|
470
|
+
return {
|
|
471
|
+
name: String(token?.name ?? ''),
|
|
472
|
+
value: String(token?.value ?? ''),
|
|
473
|
+
type: String(token?.type ?? ''),
|
|
474
|
+
category: String(token?.category ?? ''),
|
|
475
|
+
cssVariable: String(token?.cssVariable ?? ''),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function suggestTokenNames(targetName, tokens, maxSuggestions = 5) {
|
|
480
|
+
const target = normalizeTokenIdentifier(targetName);
|
|
481
|
+
if (!target) return [];
|
|
482
|
+
const allNames = [...new Set(tokens
|
|
483
|
+
.map((token) => normalizeTokenIdentifier(token?.name))
|
|
484
|
+
.filter(Boolean))];
|
|
485
|
+
|
|
486
|
+
const startsWith = allNames.filter((name) => name.startsWith(target));
|
|
487
|
+
if (startsWith.length >= maxSuggestions) return startsWith.slice(0, maxSuggestions);
|
|
488
|
+
|
|
489
|
+
const includes = allNames.filter((name) => name.includes(target) && !startsWith.includes(name));
|
|
490
|
+
const ranked = allNames
|
|
491
|
+
.filter((name) => !startsWith.includes(name) && !includes.includes(name))
|
|
492
|
+
.map((name) => ({ name, distance: levenshteinDistance(target, name) }))
|
|
493
|
+
.sort((left, right) => left.distance - right.distance || left.name.localeCompare(right.name))
|
|
494
|
+
.map((entry) => entry.name);
|
|
495
|
+
|
|
496
|
+
return [...startsWith, ...includes, ...ranked].slice(0, maxSuggestions);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function buildUsageExamples(token) {
|
|
500
|
+
const cssVar = String(token?.cssVariable ?? '');
|
|
501
|
+
const type = String(token?.type ?? '').toLowerCase();
|
|
502
|
+
if (!cssVar) return [];
|
|
503
|
+
if (type === 'color') {
|
|
504
|
+
return [
|
|
505
|
+
`.example { color: ${cssVar}; }`,
|
|
506
|
+
`.example { background-color: ${cssVar}; }`,
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
if (type === 'spacing') {
|
|
510
|
+
return [
|
|
511
|
+
`.example { padding: ${cssVar}; }`,
|
|
512
|
+
`.example { gap: ${cssVar}; }`,
|
|
513
|
+
];
|
|
514
|
+
}
|
|
515
|
+
if (type === 'typography') {
|
|
516
|
+
return [
|
|
517
|
+
`.example { font-size: ${cssVar}; }`,
|
|
518
|
+
`.example { line-height: ${cssVar}; }`,
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
if (type === 'radius') {
|
|
522
|
+
return [`.example { border-radius: ${cssVar}; }`];
|
|
523
|
+
}
|
|
524
|
+
if (type === 'shadow') {
|
|
525
|
+
return [`.example { box-shadow: ${cssVar}; }`];
|
|
526
|
+
}
|
|
527
|
+
return [`.example { --token-value: ${cssVar}; }`];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function buildTokenErrorPayload(code, message, extra = {}) {
|
|
531
|
+
return {
|
|
532
|
+
isError: true,
|
|
533
|
+
payload: {
|
|
534
|
+
error: { code, message },
|
|
535
|
+
...extra,
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function buildDesignTokenDetailPayload(designTokensData, name, theme) {
|
|
541
|
+
if (!Array.isArray(designTokensData?.tokens)) {
|
|
542
|
+
return buildTokenErrorPayload(
|
|
543
|
+
'DESIGN_TOKENS_DATA_UNAVAILABLE',
|
|
544
|
+
'Design tokens data not available. Run: npm run mcp:extract-tokens',
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const themeInfo = resolveTokenTheme(theme);
|
|
549
|
+
if (!themeInfo.ok) {
|
|
550
|
+
return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const normalizedName = normalizeTokenIdentifier(name);
|
|
554
|
+
if (!normalizedName) {
|
|
555
|
+
return buildTokenErrorPayload('INVALID_TOKEN_INPUT', 'Token name is required.');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const tokens = designTokensData.tokens;
|
|
559
|
+
const token = tokens.find((item) => normalizeTokenIdentifier(item?.name) === normalizedName);
|
|
560
|
+
if (!token) {
|
|
561
|
+
return buildTokenErrorPayload(
|
|
562
|
+
'TOKEN_NOT_FOUND',
|
|
563
|
+
`Token not found: ${normalizedName}`,
|
|
564
|
+
{ suggestions: suggestTokenNames(normalizedName, tokens) },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const relationshipIndex = buildTokenRelationshipIndex(designTokensData);
|
|
569
|
+
const relation = relationshipIndex.byToken[normalizedName] ?? { references: [], referencedBy: [] };
|
|
570
|
+
const tokenByName = new Map(tokens
|
|
571
|
+
.map((item) => [normalizeTokenIdentifier(item?.name), item])
|
|
572
|
+
.filter(([tokenName]) => tokenName));
|
|
573
|
+
const references = relation.references
|
|
574
|
+
.map((tokenName) => tokenByName.get(tokenName))
|
|
575
|
+
.filter(Boolean)
|
|
576
|
+
.map(toTokenSummary);
|
|
577
|
+
const referencedBy = relation.referencedBy
|
|
578
|
+
.map((tokenName) => tokenByName.get(tokenName))
|
|
579
|
+
.filter(Boolean)
|
|
580
|
+
.map(toTokenSummary);
|
|
581
|
+
const relatedTokens = referencedBy
|
|
582
|
+
.filter((item) => String(item.category).toLowerCase() === 'semantic')
|
|
583
|
+
.map((item) => item.name);
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
isError: false,
|
|
587
|
+
payload: {
|
|
588
|
+
token: {
|
|
589
|
+
...toTokenSummary(token),
|
|
590
|
+
group: token?.group ?? null,
|
|
591
|
+
},
|
|
592
|
+
references,
|
|
593
|
+
referencedBy,
|
|
594
|
+
relatedTokens,
|
|
595
|
+
usageExamples: buildUsageExamples(token),
|
|
596
|
+
theme: {
|
|
597
|
+
requested: themeInfo.requested,
|
|
598
|
+
resolved: themeInfo.resolved,
|
|
599
|
+
available: themeInfo.available,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function buildDesignTokensPayload(designTokensData, { type, category, query, theme } = {}) {
|
|
606
|
+
if (!designTokensData) {
|
|
607
|
+
return buildTokenErrorPayload(
|
|
608
|
+
'DESIGN_TOKENS_DATA_UNAVAILABLE',
|
|
609
|
+
'Design tokens data not available. Run: npm run mcp:extract-tokens',
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const themeInfo = resolveTokenTheme(theme);
|
|
614
|
+
if (!themeInfo.ok) {
|
|
615
|
+
return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let tokens = Array.isArray(designTokensData.tokens) ? designTokensData.tokens : [];
|
|
619
|
+
if (type) tokens = tokens.filter((t) => t.type === type);
|
|
620
|
+
if (category) tokens = tokens.filter((t) => t.category === category);
|
|
621
|
+
if (query) {
|
|
622
|
+
const q = String(query).toLowerCase();
|
|
623
|
+
tokens = tokens.filter((t) => String(t.name ?? '').toLowerCase().includes(q));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
isError: false,
|
|
628
|
+
payload: {
|
|
629
|
+
total: tokens.length,
|
|
630
|
+
tokens,
|
|
631
|
+
summary: designTokensData.summary,
|
|
632
|
+
theme: {
|
|
633
|
+
requested: themeInfo.requested,
|
|
634
|
+
resolved: themeInfo.resolved,
|
|
635
|
+
available: themeInfo.available,
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function isPlainObject(value) {
|
|
642
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function toPluginErrorMessage(name, reason) {
|
|
646
|
+
return `Invalid plugin (${name}): ${reason}`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
|
|
651
|
+
* @typedef {{
|
|
652
|
+
* fileName: string,
|
|
653
|
+
* path: string,
|
|
654
|
+
* }} WcfMcpDataSourceConfig
|
|
655
|
+
*/
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
|
|
659
|
+
* @typedef {{
|
|
660
|
+
* name: string,
|
|
661
|
+
* description?: string,
|
|
662
|
+
* inputSchema?: Record<string, unknown>,
|
|
663
|
+
* handler?: (args: Record<string, unknown>, context: { plugin: { name: string, version: string }, helpers: { loadJsonData: Function } }) => unknown,
|
|
664
|
+
* staticPayload?: unknown,
|
|
665
|
+
* }} WcfMcpPluginTool
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
|
|
670
|
+
* @typedef {{
|
|
671
|
+
* name: string,
|
|
672
|
+
* version: string,
|
|
673
|
+
* tools?: WcfMcpPluginTool[],
|
|
674
|
+
* dataSources?: WcfMcpDataSourceConfig[],
|
|
675
|
+
* }} WcfMcpPlugin
|
|
676
|
+
*/
|
|
677
|
+
|
|
678
|
+
function normalizePluginDataSources(pluginName, dataSources) {
|
|
679
|
+
if (!Array.isArray(dataSources)) return [];
|
|
680
|
+
const out = [];
|
|
681
|
+
for (const entry of dataSources) {
|
|
682
|
+
if (!isPlainObject(entry)) {
|
|
683
|
+
throw new Error(toPluginErrorMessage(pluginName, 'dataSources entries must be objects'));
|
|
684
|
+
}
|
|
685
|
+
const fileName = String(entry.fileName ?? '').trim();
|
|
686
|
+
const sourcePath = String(entry.path ?? '').trim();
|
|
687
|
+
if (!fileName || !sourcePath) {
|
|
688
|
+
throw new Error(toPluginErrorMessage(pluginName, 'dataSources entries require fileName and path'));
|
|
689
|
+
}
|
|
690
|
+
if (!PLUGIN_DATA_SOURCE_KEYS.has(fileName)) {
|
|
691
|
+
throw new Error(toPluginErrorMessage(pluginName, `unsupported data source key: ${fileName}`));
|
|
692
|
+
}
|
|
693
|
+
out.push({ fileName, path: sourcePath });
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizePluginTools(pluginName, tools) {
|
|
699
|
+
if (!Array.isArray(tools)) return [];
|
|
700
|
+
const out = [];
|
|
701
|
+
for (const rawTool of tools) {
|
|
702
|
+
if (!isPlainObject(rawTool)) {
|
|
703
|
+
throw new Error(toPluginErrorMessage(pluginName, 'tools entries must be objects'));
|
|
704
|
+
}
|
|
705
|
+
const name = String(rawTool.name ?? '').trim();
|
|
706
|
+
if (!name) throw new Error(toPluginErrorMessage(pluginName, 'tool.name is required'));
|
|
707
|
+
const hasHandler = typeof rawTool.handler === 'function';
|
|
708
|
+
const hasStaticPayload = Object.prototype.hasOwnProperty.call(rawTool, 'staticPayload');
|
|
709
|
+
if (!hasHandler && !hasStaticPayload) {
|
|
710
|
+
throw new Error(toPluginErrorMessage(pluginName, `tool "${name}" needs handler or staticPayload`));
|
|
711
|
+
}
|
|
712
|
+
// When both are specified, handler takes priority (contract v1: handler-wins)
|
|
713
|
+
// staticPayload is ignored silently.
|
|
714
|
+
const description = String(rawTool.description ?? '').trim() ||
|
|
715
|
+
`Plugin tool provided by ${pluginName}. ${PLUGIN_TOOL_NOTICE}`;
|
|
716
|
+
const inputSchema = isPlainObject(rawTool.inputSchema) ? rawTool.inputSchema : {};
|
|
717
|
+
out.push({
|
|
718
|
+
name,
|
|
719
|
+
description,
|
|
720
|
+
inputSchema,
|
|
721
|
+
handler: hasHandler ? rawTool.handler : undefined,
|
|
722
|
+
staticPayload: hasStaticPayload ? rawTool.staticPayload : undefined,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function normalizePlugins(plugins = []) {
|
|
729
|
+
if (!Array.isArray(plugins)) throw new Error('Invalid plugin configuration: plugins must be an array');
|
|
730
|
+
const normalized = [];
|
|
731
|
+
const seenPluginNames = new Set();
|
|
732
|
+
const seenToolNames = new Set(BUILTIN_TOOL_NAMES);
|
|
733
|
+
|
|
734
|
+
for (const rawPlugin of plugins) {
|
|
735
|
+
if (!isPlainObject(rawPlugin)) throw new Error('Invalid plugin configuration: each plugin must be an object');
|
|
736
|
+
const name = String(rawPlugin.name ?? '').trim();
|
|
737
|
+
const version = String(rawPlugin.version ?? '').trim();
|
|
738
|
+
if (!name || !version) throw new Error('Invalid plugin configuration: plugin.name and plugin.version are required');
|
|
739
|
+
if (seenPluginNames.has(name)) throw new Error(`Duplicate plugin name: ${name}`);
|
|
740
|
+
seenPluginNames.add(name);
|
|
741
|
+
|
|
742
|
+
const tools = normalizePluginTools(name, rawPlugin.tools);
|
|
743
|
+
for (const tool of tools) {
|
|
744
|
+
if (seenToolNames.has(tool.name)) {
|
|
745
|
+
throw new Error(toPluginErrorMessage(name, `tool name collision: ${tool.name}`));
|
|
746
|
+
}
|
|
747
|
+
seenToolNames.add(tool.name);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const dataSources = normalizePluginDataSources(name, rawPlugin.dataSources);
|
|
751
|
+
normalized.push({ name, version, tools, dataSources });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return normalized;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function buildPluginDataSourceMap(plugins = []) {
|
|
758
|
+
const out = new Map();
|
|
759
|
+
for (const plugin of plugins) {
|
|
760
|
+
const pluginName = String(plugin?.name ?? 'unknown-plugin');
|
|
761
|
+
const dataSources = Array.isArray(plugin?.dataSources) ? plugin.dataSources : [];
|
|
762
|
+
for (const source of dataSources) {
|
|
763
|
+
const fileName = String(source?.fileName ?? '').trim();
|
|
764
|
+
const sourcePath = String(source?.path ?? '').trim();
|
|
765
|
+
if (!fileName || !sourcePath) continue;
|
|
766
|
+
if (out.has(fileName)) {
|
|
767
|
+
const prev = out.get(fileName);
|
|
768
|
+
throw new Error(`Duplicate data source override for ${fileName} (${prev.pluginName}, ${pluginName})`);
|
|
769
|
+
}
|
|
770
|
+
out.set(fileName, { path: sourcePath, pluginName });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return out;
|
|
774
|
+
}
|
|
775
|
+
|
|
207
776
|
// ---------------------------------------------------------------------------
|
|
208
777
|
// Helpers (exported for testing)
|
|
209
778
|
// ---------------------------------------------------------------------------
|
|
@@ -272,10 +841,16 @@ export function levenshteinDistance(left, right) {
|
|
|
272
841
|
return prev[b.length];
|
|
273
842
|
}
|
|
274
843
|
|
|
275
|
-
export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
844
|
+
export function suggestUnknownElementTagName(tagName, cemIndex, prefix) {
|
|
276
845
|
const target = String(tagName ?? '').trim().toLowerCase();
|
|
277
846
|
if (!target || !target.includes('-')) return undefined;
|
|
278
847
|
|
|
848
|
+
// Try prefix-prepend before Levenshtein (e.g. input-text → dads-input-text)
|
|
849
|
+
if (prefix && cemIndex instanceof Map) {
|
|
850
|
+
const prefixed = `${String(prefix).toLowerCase()}-${target}`;
|
|
851
|
+
if (cemIndex.has(prefixed)) return prefixed;
|
|
852
|
+
}
|
|
853
|
+
|
|
279
854
|
let bestTag;
|
|
280
855
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
281
856
|
const candidateSource = cemIndex instanceof Map ? cemIndex.keys() : [];
|
|
@@ -295,15 +870,19 @@ export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
|
295
870
|
return bestTag;
|
|
296
871
|
}
|
|
297
872
|
|
|
298
|
-
export function buildDiagnosticSuggestion({ diagnostic, cemIndex }) {
|
|
873
|
+
export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
|
|
299
874
|
const code = String(diagnostic?.code ?? '');
|
|
300
875
|
if (!code) return undefined;
|
|
301
876
|
|
|
302
877
|
if (code === 'unknownElement') {
|
|
303
|
-
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex);
|
|
878
|
+
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex, prefix);
|
|
304
879
|
return tagName ? `Did you mean "${tagName}"?` : undefined;
|
|
305
880
|
}
|
|
306
881
|
|
|
882
|
+
if (code === 'canonicalLowercaseRecommendation') {
|
|
883
|
+
return diagnostic?.hint ?? undefined;
|
|
884
|
+
}
|
|
885
|
+
|
|
307
886
|
if (code === 'forbiddenAttribute' && String(diagnostic?.attrName ?? '').toLowerCase() === 'placeholder') {
|
|
308
887
|
return 'Use aria-label or aria-describedby support text instead of placeholder.';
|
|
309
888
|
}
|
|
@@ -480,18 +1059,29 @@ export function findDeclByComponentId(indexes, componentIdRaw) {
|
|
|
480
1059
|
return undefined;
|
|
481
1060
|
}
|
|
482
1061
|
|
|
483
|
-
|
|
1062
|
+
/**
|
|
1063
|
+
* Generic helper: remap tag-keyed Map to a different prefix.
|
|
1064
|
+
* Used by validate_markup to build prefix-aware CEM/enum/slot maps.
|
|
1065
|
+
*/
|
|
1066
|
+
export function applyPrefixToTagMap(map, prefix) {
|
|
484
1067
|
const p = normalizePrefix(prefix);
|
|
485
|
-
if (p === CANONICAL_PREFIX) return
|
|
1068
|
+
if (p === CANONICAL_PREFIX) return map;
|
|
486
1069
|
|
|
487
1070
|
const out = new Map();
|
|
488
|
-
for (const [tag,
|
|
489
|
-
|
|
490
|
-
out.set(nextTag, meta);
|
|
1071
|
+
for (const [tag, value] of map.entries()) {
|
|
1072
|
+
out.set(withPrefix(tag, p), value);
|
|
491
1073
|
}
|
|
492
1074
|
return out;
|
|
493
1075
|
}
|
|
494
1076
|
|
|
1077
|
+
function mergeWithPrefixed(canonicalMap, prefix) {
|
|
1078
|
+
const prefixed = applyPrefixToTagMap(canonicalMap, prefix);
|
|
1079
|
+
if (prefixed === canonicalMap) return canonicalMap;
|
|
1080
|
+
const combined = new Map(canonicalMap);
|
|
1081
|
+
for (const [k, v] of prefixed.entries()) combined.set(k, v);
|
|
1082
|
+
return combined;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
495
1085
|
export function applyPrefixToHtml(html, prefix) {
|
|
496
1086
|
const p = normalizePrefix(prefix);
|
|
497
1087
|
if (p === CANONICAL_PREFIX) return String(html ?? '');
|
|
@@ -534,7 +1124,8 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
|
534
1124
|
export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix } = {}) {
|
|
535
1125
|
const p = normalizePrefix(prefix);
|
|
536
1126
|
const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
537
|
-
const
|
|
1127
|
+
const limitExplicit = Number.isInteger(limit);
|
|
1128
|
+
const pageSize = limitExplicit ? Math.max(1, Math.min(limit, 200)) : 20;
|
|
538
1129
|
const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
539
1130
|
|
|
540
1131
|
let items = indexes.decls.map(({ decl, tagName, modulePath }) => ({
|
|
@@ -565,13 +1156,20 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
|
|
|
565
1156
|
const total = items.length;
|
|
566
1157
|
const paged = items.slice(pageOffset, pageOffset + pageSize);
|
|
567
1158
|
|
|
568
|
-
|
|
1159
|
+
const result = {
|
|
569
1160
|
total,
|
|
570
1161
|
limit: pageSize,
|
|
571
1162
|
offset: pageOffset,
|
|
572
1163
|
hasMore: pageOffset + paged.length < total,
|
|
573
1164
|
items: paged,
|
|
574
1165
|
};
|
|
1166
|
+
|
|
1167
|
+
// DIG-19: Add migration notice when limit is not explicitly provided
|
|
1168
|
+
if (!limitExplicit && total > pageSize) {
|
|
1169
|
+
result._notice = 'Default pagination changed to 20 items. Set limit:200 for all results.';
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return result;
|
|
575
1173
|
}
|
|
576
1174
|
|
|
577
1175
|
export function parseIconNamesFromDescription(description) {
|
|
@@ -897,6 +1495,123 @@ export function queryAccessibilityIndex(
|
|
|
897
1495
|
};
|
|
898
1496
|
}
|
|
899
1497
|
|
|
1498
|
+
function buildComponentsResourcePayload(indexes) {
|
|
1499
|
+
const page = buildComponentSummaries(indexes, { limit: 200 });
|
|
1500
|
+
const componentsByCategory = {};
|
|
1501
|
+
for (const item of page.items) {
|
|
1502
|
+
const category = String(item?.category ?? 'Other');
|
|
1503
|
+
componentsByCategory[category] = (componentsByCategory[category] ?? 0) + 1;
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
total: page.total,
|
|
1507
|
+
componentsByCategory,
|
|
1508
|
+
components: page.items,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function buildTokensResourcePayload(designTokensData) {
|
|
1513
|
+
if (!Array.isArray(designTokensData?.tokens)) {
|
|
1514
|
+
return {
|
|
1515
|
+
isError: true,
|
|
1516
|
+
error: {
|
|
1517
|
+
code: 'DESIGN_TOKENS_DATA_UNAVAILABLE',
|
|
1518
|
+
message: 'Design tokens data not available. Run: npm run mcp:extract-tokens',
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const tokens = designTokensData.tokens;
|
|
1524
|
+
const tokenTypes = [...new Set(tokens
|
|
1525
|
+
.map((token) => String(token?.type ?? '').trim())
|
|
1526
|
+
.filter(Boolean))].sort();
|
|
1527
|
+
const tokenCategories = [...new Set(tokens
|
|
1528
|
+
.map((token) => String(token?.category ?? '').trim())
|
|
1529
|
+
.filter(Boolean))].sort();
|
|
1530
|
+
|
|
1531
|
+
return {
|
|
1532
|
+
isError: false,
|
|
1533
|
+
payload: {
|
|
1534
|
+
total: tokens.length,
|
|
1535
|
+
summary: designTokensData.summary ?? {},
|
|
1536
|
+
themes: designTokensData.themes ?? { default: 'light', available: ['light'] },
|
|
1537
|
+
tokenTypes,
|
|
1538
|
+
tokenCategories,
|
|
1539
|
+
sample: tokens.slice(0, 20).map(toTokenSummary),
|
|
1540
|
+
},
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function buildGuidelinesResourcePayload(guidelinesIndexData, rawTopic) {
|
|
1545
|
+
const topic = String(rawTopic ?? '').trim().toLowerCase();
|
|
1546
|
+
if (!GUIDELINE_TOPIC_SET.has(topic)) {
|
|
1547
|
+
return {
|
|
1548
|
+
isError: true,
|
|
1549
|
+
error: {
|
|
1550
|
+
code: 'INVALID_GUIDELINE_TOPIC',
|
|
1551
|
+
message: `Unsupported topic: ${topic}. Allowed values are ${GUIDELINE_TOPICS.join(', ')}.`,
|
|
1552
|
+
},
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (!Array.isArray(guidelinesIndexData?.documents)) {
|
|
1557
|
+
return {
|
|
1558
|
+
isError: true,
|
|
1559
|
+
error: {
|
|
1560
|
+
code: 'GUIDELINES_INDEX_UNAVAILABLE',
|
|
1561
|
+
message: 'Guidelines index not available. Run: npm run mcp:index-guidelines',
|
|
1562
|
+
},
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const documents = guidelinesIndexData.documents
|
|
1567
|
+
.filter((doc) => topic === 'all' || String(doc?.topic ?? '').toLowerCase() === topic)
|
|
1568
|
+
.map((doc) => {
|
|
1569
|
+
const sections = Array.isArray(doc?.sections) ? doc.sections : [];
|
|
1570
|
+
return {
|
|
1571
|
+
id: String(doc?.id ?? ''),
|
|
1572
|
+
title: String(doc?.title ?? ''),
|
|
1573
|
+
topic: String(doc?.topic ?? ''),
|
|
1574
|
+
sectionCount: sections.length,
|
|
1575
|
+
sections: sections.map((section) => ({
|
|
1576
|
+
heading: String(section?.heading ?? ''),
|
|
1577
|
+
startLine: Number.isInteger(section?.startLine) ? section.startLine : undefined,
|
|
1578
|
+
})),
|
|
1579
|
+
};
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
return {
|
|
1583
|
+
isError: false,
|
|
1584
|
+
payload: {
|
|
1585
|
+
topic,
|
|
1586
|
+
totalDocuments: documents.length,
|
|
1587
|
+
topicCounts: guidelinesIndexData.topicCounts ?? {},
|
|
1588
|
+
documents,
|
|
1589
|
+
},
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function buildFigmaToWcfPromptText({ figmaUrl, userIntent }) {
|
|
1594
|
+
const url = String(figmaUrl ?? '').trim();
|
|
1595
|
+
const intent = String(userIntent ?? '').trim();
|
|
1596
|
+
|
|
1597
|
+
return [
|
|
1598
|
+
`Figma URL: ${url}`,
|
|
1599
|
+
intent ? `Implementation goal: ${intent}` : 'Implementation goal: (not specified)',
|
|
1600
|
+
'',
|
|
1601
|
+
'Use the workflow below in this exact order:',
|
|
1602
|
+
'1. get_design_system_overview',
|
|
1603
|
+
'2. get_design_tokens',
|
|
1604
|
+
'3. get_component_api',
|
|
1605
|
+
'4. generate_usage_snippet (or get_pattern_recipe)',
|
|
1606
|
+
'5. validate_markup',
|
|
1607
|
+
'',
|
|
1608
|
+
'Output requirements:',
|
|
1609
|
+
'- Split the UI into sections before writing code.',
|
|
1610
|
+
'- For each section, name concrete components and token variables.',
|
|
1611
|
+
'- Provide final validation notes and required fixes.',
|
|
1612
|
+
].join('\n');
|
|
1613
|
+
}
|
|
1614
|
+
|
|
900
1615
|
function resolveDeclByComponent(indexes, component, prefix) {
|
|
901
1616
|
const byTagOrClass =
|
|
902
1617
|
pickDecl(indexes, { tagName: component, prefix }) ??
|
|
@@ -909,7 +1624,50 @@ function resolveDeclByComponent(indexes, component, prefix) {
|
|
|
909
1624
|
};
|
|
910
1625
|
}
|
|
911
1626
|
|
|
912
|
-
|
|
1627
|
+
const byComponentId = findDeclByComponentId(indexes, component);
|
|
1628
|
+
if (byComponentId) return byComponentId;
|
|
1629
|
+
|
|
1630
|
+
// Auto-prefix: try with canonical prefix if bare name was given (DIG-15)
|
|
1631
|
+
const comp = typeof component === 'string' ? component.trim().toLowerCase() : '';
|
|
1632
|
+
const p = normalizePrefix(prefix);
|
|
1633
|
+
if (comp && !comp.startsWith(p)) {
|
|
1634
|
+
const prefixed = `${p}-${comp}`;
|
|
1635
|
+
const byPrefixed = pickDecl(indexes, { tagName: prefixed, prefix: p });
|
|
1636
|
+
if (byPrefixed) {
|
|
1637
|
+
const canonicalTag = typeof byPrefixed.tagName === 'string' ? byPrefixed.tagName.toLowerCase() : undefined;
|
|
1638
|
+
return {
|
|
1639
|
+
decl: byPrefixed,
|
|
1640
|
+
modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return undefined;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function buildComponentNotFoundError(component, indexes, prefix) {
|
|
1649
|
+
const comp = typeof component === 'string' ? component.trim() : '';
|
|
1650
|
+
const p = normalizePrefix(prefix);
|
|
1651
|
+
const suggestions = [];
|
|
1652
|
+
|
|
1653
|
+
// Try suggesting with prefix
|
|
1654
|
+
if (comp && !comp.toLowerCase().startsWith(p)) {
|
|
1655
|
+
const prefixed = `${p}-${comp.toLowerCase()}`;
|
|
1656
|
+
if (indexes.byTag.has(prefixed)) {
|
|
1657
|
+
suggestions.push(prefixed);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Levenshtein-based suggestion
|
|
1662
|
+
const suggested = suggestUnknownElementTagName(comp.includes('-') ? comp : `${p}-${comp}`, indexes.byTag);
|
|
1663
|
+
if (suggested && !suggestions.includes(suggested)) {
|
|
1664
|
+
suggestions.push(suggested);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const msg = suggestions.length > 0
|
|
1668
|
+
? `Component not found: ${comp}. Did you mean: ${suggestions.join(', ')}?`
|
|
1669
|
+
: `Component not found: ${comp}`;
|
|
1670
|
+
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
913
1671
|
}
|
|
914
1672
|
|
|
915
1673
|
// ---------------------------------------------------------------------------
|
|
@@ -918,45 +1676,216 @@ function resolveDeclByComponent(indexes, component, prefix) {
|
|
|
918
1676
|
//
|
|
919
1677
|
// loadJsonData(fileName: string) → Promise<object>
|
|
920
1678
|
// loadValidator() → Promise<{ collectCemCustomElements, validateTextAgainstCem }>
|
|
1679
|
+
// options?: {
|
|
1680
|
+
// plugins?: WcfMcpPlugin[],
|
|
1681
|
+
// loadJsonDataFromPath?: (path: string, fileName: string, pluginName?: string) => Promise<object>
|
|
1682
|
+
// loadTextData?: (fileName: string) => Promise<string>
|
|
1683
|
+
// }
|
|
921
1684
|
// ---------------------------------------------------------------------------
|
|
922
1685
|
|
|
923
|
-
export async function createMcpServer(loadJsonData, loadValidator) {
|
|
924
|
-
const
|
|
1686
|
+
export async function createMcpServer(loadJsonData, loadValidator, options = {}) {
|
|
1687
|
+
const plugins = normalizePlugins(options?.plugins ?? []);
|
|
1688
|
+
const pluginDataSourceMap = buildPluginDataSourceMap(plugins);
|
|
1689
|
+
const loadJsonDataFromPath = typeof options?.loadJsonDataFromPath === 'function'
|
|
1690
|
+
? options.loadJsonDataFromPath
|
|
1691
|
+
: null;
|
|
1692
|
+
const loadTextData = typeof options?.loadTextData === 'function'
|
|
1693
|
+
? options.loadTextData
|
|
1694
|
+
: null;
|
|
1695
|
+
|
|
1696
|
+
const loadJson = async (fileName) => {
|
|
1697
|
+
const override = pluginDataSourceMap.get(fileName);
|
|
1698
|
+
if (!override) return loadJsonData(fileName);
|
|
1699
|
+
if (!loadJsonDataFromPath) {
|
|
1700
|
+
throw new Error(`Plugin data source override for ${fileName} requires loadJsonDataFromPath`);
|
|
1701
|
+
}
|
|
1702
|
+
return loadJsonDataFromPath(override.path, fileName, override.pluginName);
|
|
1703
|
+
};
|
|
1704
|
+
const loadText = async (fileName) => {
|
|
1705
|
+
if (!loadTextData) throw new Error(`Text data loader not configured for ${fileName}`);
|
|
1706
|
+
return loadTextData(fileName);
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
const manifest = await loadJson('custom-elements.json');
|
|
925
1710
|
const indexes = buildIndexes(manifest);
|
|
926
1711
|
const {
|
|
927
1712
|
collectCemCustomElements,
|
|
928
1713
|
validateTextAgainstCem,
|
|
929
1714
|
detectTokenMisuseInInlineStyles = () => [],
|
|
930
1715
|
detectAccessibilityMisuseInMarkup = () => [],
|
|
1716
|
+
buildEnumAttributeMap = () => new Map(),
|
|
1717
|
+
detectEnumValueMisuse = () => [],
|
|
1718
|
+
buildSlotNameMap = () => new Map(),
|
|
1719
|
+
detectInvalidSlotName = () => [],
|
|
1720
|
+
detectMissingRequiredAttributes = () => [],
|
|
1721
|
+
detectOrphanedChildComponents = () => [],
|
|
1722
|
+
detectEmptyInteractiveElement = () => [],
|
|
1723
|
+
detectNonLowercaseAttributes = () => [],
|
|
1724
|
+
detectCdnReferences = () => [],
|
|
1725
|
+
detectMissingRuntimeScaffold = () => [],
|
|
931
1726
|
} = await loadValidator();
|
|
932
1727
|
const canonicalCemIndex = collectCemCustomElements(manifest);
|
|
933
|
-
const
|
|
934
|
-
const
|
|
1728
|
+
const canonicalEnumMap = buildEnumAttributeMap(manifest);
|
|
1729
|
+
const canonicalSlotMap = buildSlotNameMap(manifest);
|
|
1730
|
+
const installRegistry = await loadJson('install-registry.json');
|
|
1731
|
+
const patternRegistry = await loadJson('pattern-registry.json');
|
|
935
1732
|
const { patterns } = loadPatternRegistryShape(patternRegistry);
|
|
936
1733
|
const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
|
|
937
1734
|
|
|
938
1735
|
// Load optional data files (design tokens, guidelines index)
|
|
939
1736
|
let designTokensData = null;
|
|
940
1737
|
try {
|
|
941
|
-
designTokensData = await
|
|
1738
|
+
designTokensData = await loadJson('design-tokens.json');
|
|
942
1739
|
} catch {
|
|
943
1740
|
// design-tokens.json may not exist yet
|
|
944
1741
|
}
|
|
945
1742
|
|
|
946
1743
|
let guidelinesIndexData = null;
|
|
947
1744
|
try {
|
|
948
|
-
guidelinesIndexData = await
|
|
1745
|
+
guidelinesIndexData = await loadJson('guidelines-index.json');
|
|
949
1746
|
} catch {
|
|
950
1747
|
// guidelines-index.json may not exist yet
|
|
951
1748
|
}
|
|
1749
|
+
let llmsFullText = null;
|
|
1750
|
+
try {
|
|
1751
|
+
llmsFullText = await loadText('llms-full.txt');
|
|
1752
|
+
} catch {
|
|
1753
|
+
// llms-full.txt may not exist in local setup
|
|
1754
|
+
}
|
|
952
1755
|
|
|
953
1756
|
const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
|
|
954
1757
|
|
|
1758
|
+
const VENDOR_DIR = 'vendor-runtime';
|
|
1759
|
+
const PREFIX_STRIP_RE = /^[^-]+-/;
|
|
1760
|
+
|
|
955
1761
|
const server = new McpServer({
|
|
956
1762
|
name: 'web-components-factory-design-system',
|
|
957
|
-
version: '0.
|
|
1763
|
+
version: '0.5.0',
|
|
958
1764
|
});
|
|
959
1765
|
|
|
1766
|
+
server.registerPrompt(
|
|
1767
|
+
FIGMA_TO_WCF_PROMPT,
|
|
1768
|
+
{
|
|
1769
|
+
title: 'Figma To WCF',
|
|
1770
|
+
description:
|
|
1771
|
+
'Guided prompt for converting a Figma URL into WCF implementation steps with a strict tool order.',
|
|
1772
|
+
argsSchema: {
|
|
1773
|
+
figmaUrl: z.string().trim().url().describe('Figma URL (design or board link)'),
|
|
1774
|
+
userIntent: z.string().optional().describe('Optional implementation intent / screen purpose'),
|
|
1775
|
+
},
|
|
1776
|
+
},
|
|
1777
|
+
async ({ figmaUrl, userIntent }) => ({
|
|
1778
|
+
messages: [{
|
|
1779
|
+
role: 'user',
|
|
1780
|
+
content: {
|
|
1781
|
+
type: 'text',
|
|
1782
|
+
text: buildFigmaToWcfPromptText({ figmaUrl, userIntent }),
|
|
1783
|
+
},
|
|
1784
|
+
}],
|
|
1785
|
+
}),
|
|
1786
|
+
);
|
|
1787
|
+
|
|
1788
|
+
server.registerResource(
|
|
1789
|
+
'wcf_components',
|
|
1790
|
+
WCF_RESOURCE_URIS.components,
|
|
1791
|
+
{
|
|
1792
|
+
title: 'WCF Component Catalog',
|
|
1793
|
+
description: 'Component catalog snapshot with categories and API entry points.',
|
|
1794
|
+
mimeType: 'application/json',
|
|
1795
|
+
},
|
|
1796
|
+
async () => {
|
|
1797
|
+
const payload = buildComponentsResourcePayload(indexes);
|
|
1798
|
+
return {
|
|
1799
|
+
contents: [{
|
|
1800
|
+
uri: WCF_RESOURCE_URIS.components,
|
|
1801
|
+
mimeType: 'application/json',
|
|
1802
|
+
text: JSON.stringify(payload, null, 2),
|
|
1803
|
+
}],
|
|
1804
|
+
};
|
|
1805
|
+
},
|
|
1806
|
+
);
|
|
1807
|
+
|
|
1808
|
+
server.registerResource(
|
|
1809
|
+
'wcf_tokens',
|
|
1810
|
+
WCF_RESOURCE_URIS.tokens,
|
|
1811
|
+
{
|
|
1812
|
+
title: 'WCF Design Tokens',
|
|
1813
|
+
description: 'Token summary resource for colors, spacing, typography, radius, and shadows.',
|
|
1814
|
+
mimeType: 'application/json',
|
|
1815
|
+
},
|
|
1816
|
+
async () => {
|
|
1817
|
+
const result = buildTokensResourcePayload(designTokensData);
|
|
1818
|
+
const payload = result.isError ? { error: result.error } : result.payload;
|
|
1819
|
+
return {
|
|
1820
|
+
contents: [{
|
|
1821
|
+
uri: WCF_RESOURCE_URIS.tokens,
|
|
1822
|
+
mimeType: 'application/json',
|
|
1823
|
+
text: JSON.stringify(payload, null, 2),
|
|
1824
|
+
}],
|
|
1825
|
+
};
|
|
1826
|
+
},
|
|
1827
|
+
);
|
|
1828
|
+
|
|
1829
|
+
server.registerResource(
|
|
1830
|
+
'wcf_guidelines',
|
|
1831
|
+
new ResourceTemplate(WCF_RESOURCE_URIS.guidelinesTemplate, {
|
|
1832
|
+
list: async () => ({
|
|
1833
|
+
resources: GUIDELINE_TOPICS.map((topic) => ({
|
|
1834
|
+
uri: `wcf://guidelines/${topic}`,
|
|
1835
|
+
name: `wcf guidelines (${topic})`,
|
|
1836
|
+
description: `Guideline summary for topic=${topic}`,
|
|
1837
|
+
})),
|
|
1838
|
+
}),
|
|
1839
|
+
complete: {
|
|
1840
|
+
topic: async (value) => {
|
|
1841
|
+
const query = String(value ?? '').trim().toLowerCase();
|
|
1842
|
+
return GUIDELINE_TOPICS.filter((topic) => topic.startsWith(query));
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
}),
|
|
1846
|
+
{
|
|
1847
|
+
title: 'WCF Guidelines',
|
|
1848
|
+
description: 'Topic-scoped guideline resource (accessibility|css|patterns|all).',
|
|
1849
|
+
mimeType: 'application/json',
|
|
1850
|
+
},
|
|
1851
|
+
async (_uri, variables) => {
|
|
1852
|
+
const topic = String(variables?.topic ?? '').trim().toLowerCase();
|
|
1853
|
+
const result = buildGuidelinesResourcePayload(guidelinesIndexData, topic);
|
|
1854
|
+
if (result.isError) {
|
|
1855
|
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
contents: [{
|
|
1859
|
+
uri: `wcf://guidelines/${topic}`,
|
|
1860
|
+
mimeType: 'application/json',
|
|
1861
|
+
text: JSON.stringify(result.payload, null, 2),
|
|
1862
|
+
}],
|
|
1863
|
+
};
|
|
1864
|
+
},
|
|
1865
|
+
);
|
|
1866
|
+
|
|
1867
|
+
server.registerResource(
|
|
1868
|
+
'wcf_llms_full',
|
|
1869
|
+
WCF_RESOURCE_URIS.llmsFull,
|
|
1870
|
+
{
|
|
1871
|
+
title: 'WCF llms-full',
|
|
1872
|
+
description: 'LLM reference corpus for WCF usage, generated from repository docs.',
|
|
1873
|
+
mimeType: 'text/plain',
|
|
1874
|
+
},
|
|
1875
|
+
async () => {
|
|
1876
|
+
if (typeof llmsFullText !== 'string' || llmsFullText.length === 0) {
|
|
1877
|
+
throw new Error('LLMS_FULL_UNAVAILABLE: llms-full.txt is not available.');
|
|
1878
|
+
}
|
|
1879
|
+
return {
|
|
1880
|
+
contents: [{
|
|
1881
|
+
uri: WCF_RESOURCE_URIS.llmsFull,
|
|
1882
|
+
mimeType: 'text/plain',
|
|
1883
|
+
text: llmsFullText,
|
|
1884
|
+
}],
|
|
1885
|
+
};
|
|
1886
|
+
},
|
|
1887
|
+
);
|
|
1888
|
+
|
|
960
1889
|
// -----------------------------------------------------------------------
|
|
961
1890
|
// Tool: get_design_system_overview
|
|
962
1891
|
// -----------------------------------------------------------------------
|
|
@@ -981,13 +1910,72 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
981
1910
|
|
|
982
1911
|
const overview = {
|
|
983
1912
|
name: 'DADS Web Components (wcf)',
|
|
984
|
-
version: '0.
|
|
1913
|
+
version: '0.5.0',
|
|
985
1914
|
prefix: CANONICAL_PREFIX,
|
|
986
1915
|
totalComponents: indexes.decls.length,
|
|
987
1916
|
componentsByCategory: categoryCount,
|
|
988
1917
|
totalPatterns: patternList.length,
|
|
989
1918
|
patterns: patternList,
|
|
1919
|
+
setupInfo: {
|
|
1920
|
+
npmPackage: 'web-components-factory',
|
|
1921
|
+
installCommand: 'npm install web-components-factory',
|
|
1922
|
+
vendorRuntimePath: `${VENDOR_DIR}/`,
|
|
1923
|
+
htmlBoilerplate: '<script type="module" src="vendor-runtime/src/autoload.js"></script>',
|
|
1924
|
+
noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
|
|
1925
|
+
noCDN: true,
|
|
1926
|
+
deliveryModel: 'vendor-local',
|
|
1927
|
+
distribution: {
|
|
1928
|
+
selfHosted: true,
|
|
1929
|
+
cdn: false,
|
|
1930
|
+
strategy: 'vendor-importmap',
|
|
1931
|
+
quickStart: 'npx web-components-factory init --prefix <prefix> --dir <dir>',
|
|
1932
|
+
description:
|
|
1933
|
+
'Components are installed locally via the wcf CLI. No CDN is available. All assets are served from the project directory using import maps and a boot script.',
|
|
1934
|
+
},
|
|
1935
|
+
importMapHint: 'WCF uses <script type="importmap"> for module resolution. Each component tag name maps to a local JS file: { "<prefix>-<component>": "./<dir>/components/<component>.js" }. The wcf CLI generates importmap.snippet.json automatically via `wcf init`.',
|
|
1936
|
+
bootScript: '<dir>/boot.js — sets the component prefix via setConfig(), then loads wc-autoloader.js which scans the DOM for custom element tags and dynamically imports them via the import map.',
|
|
1937
|
+
vendorSetup: {
|
|
1938
|
+
init: 'wcf init --prefix <prefix> --dir <dir>',
|
|
1939
|
+
add: 'wcf add <componentId> --prefix <prefix> --out <dir>',
|
|
1940
|
+
workflow: '1. wcf init で初期化(boot.js, importmap.snippet.json, autoloader を生成) → 2. wcf add で各コンポーネントを追加 → import map と boot.js が自動生成される',
|
|
1941
|
+
},
|
|
1942
|
+
htmlSetup: [
|
|
1943
|
+
'<script type="importmap">',
|
|
1944
|
+
'{',
|
|
1945
|
+
' "imports": {',
|
|
1946
|
+
' "<prefix>-button": "./<dir>/components/button.js",',
|
|
1947
|
+
' "<prefix>-card": "./<dir>/components/card.js"',
|
|
1948
|
+
' }',
|
|
1949
|
+
'}',
|
|
1950
|
+
'</script>',
|
|
1951
|
+
'<script type="module" src="./<dir>/boot.js"></script>',
|
|
1952
|
+
].join('\n'),
|
|
1953
|
+
},
|
|
990
1954
|
ideSetupTemplates: IDE_SETUP_TEMPLATES,
|
|
1955
|
+
availablePrompts: [
|
|
1956
|
+
{
|
|
1957
|
+
name: FIGMA_TO_WCF_PROMPT,
|
|
1958
|
+
purpose: 'Figma-to-WCF conversion workflow prompt',
|
|
1959
|
+
},
|
|
1960
|
+
],
|
|
1961
|
+
availableResources: [
|
|
1962
|
+
{
|
|
1963
|
+
uri: WCF_RESOURCE_URIS.components,
|
|
1964
|
+
purpose: 'Component catalog snapshot',
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
uri: WCF_RESOURCE_URIS.tokens,
|
|
1968
|
+
purpose: 'Token summary snapshot',
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
uri: WCF_RESOURCE_URIS.guidelinesTemplate,
|
|
1972
|
+
purpose: 'Topic-based guideline summaries',
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
uri: WCF_RESOURCE_URIS.llmsFull,
|
|
1976
|
+
purpose: 'Full LLM reference text for WCF',
|
|
1977
|
+
},
|
|
1978
|
+
],
|
|
991
1979
|
availableTools: [
|
|
992
1980
|
{ name: 'get_design_system_overview', purpose: 'This overview (start here)' },
|
|
993
1981
|
{ name: 'list_components', purpose: 'Browse components with progressive disclosure and filters' },
|
|
@@ -1000,23 +1988,51 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1000
1988
|
{ name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
|
|
1001
1989
|
{ name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
|
|
1002
1990
|
{ name: 'get_design_tokens', purpose: 'Query design tokens (colors, spacing, typography, radius, shadows)' },
|
|
1991
|
+
{ name: 'get_design_token_detail', purpose: 'Get details, relationships, and usage examples for one token' },
|
|
1003
1992
|
{ name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
|
|
1004
1993
|
{ name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
|
|
1005
1994
|
],
|
|
1006
1995
|
recommendedWorkflow: [
|
|
1007
1996
|
'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.
|
|
1997
|
+
'2. figma_to_wcf (optional) → bootstrap the Figma-to-WCF tool sequence',
|
|
1998
|
+
'3. wcf://components and wcf://tokens resources → preload catalog/token context',
|
|
1999
|
+
'4. search_guidelines → find relevant guidelines',
|
|
2000
|
+
'5. get_design_tokens → get correct token values',
|
|
2001
|
+
'6. get_design_token_detail → inspect one token with references/referencedBy and usage examples',
|
|
2002
|
+
'7. get_accessibility_docs → fetch component-level accessibility checklist',
|
|
2003
|
+
'8. list_components (category/query + pagination) → shortlist components',
|
|
2004
|
+
'9. search_icons (optional) → find icon names quickly',
|
|
2005
|
+
'10. get_component_api → check attributes, slots, events, CSS parts',
|
|
2006
|
+
'11. generate_usage_snippet or get_pattern_recipe → get code',
|
|
2007
|
+
'12. validate_markup → verify your HTML and use suggestions to self-correct',
|
|
2008
|
+
'13. get_install_recipe → get import/install instructions',
|
|
1017
2009
|
],
|
|
2010
|
+
experimental: {
|
|
2011
|
+
plugins: {
|
|
2012
|
+
enabled: plugins.length > 0,
|
|
2013
|
+
note: PLUGIN_TOOL_NOTICE,
|
|
2014
|
+
pluginCount: plugins.length,
|
|
2015
|
+
pluginToolCount: plugins.reduce((sum, plugin) => sum + (plugin.tools?.length ?? 0), 0),
|
|
2016
|
+
plugins: plugins.map((plugin) => ({
|
|
2017
|
+
name: plugin.name,
|
|
2018
|
+
version: plugin.version,
|
|
2019
|
+
toolCount: plugin.tools?.length ?? 0,
|
|
2020
|
+
dataSourceOverrides: plugin.dataSources?.map((source) => source.fileName) ?? [],
|
|
2021
|
+
})),
|
|
2022
|
+
},
|
|
2023
|
+
},
|
|
1018
2024
|
};
|
|
1019
2025
|
|
|
2026
|
+
for (const plugin of plugins) {
|
|
2027
|
+
const tools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
2028
|
+
for (const tool of tools) {
|
|
2029
|
+
overview.availableTools.push({
|
|
2030
|
+
name: tool.name,
|
|
2031
|
+
purpose: `${tool.description} (plugin: ${plugin.name})`,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1020
2036
|
return {
|
|
1021
2037
|
content: [{ type: 'text', text: JSON.stringify(overview, null, 2) }],
|
|
1022
2038
|
};
|
|
@@ -1030,22 +2046,30 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1030
2046
|
'list_components',
|
|
1031
2047
|
{
|
|
1032
2048
|
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.',
|
|
2049
|
+
'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
2050
|
inputSchema: {
|
|
1035
2051
|
category: z
|
|
1036
2052
|
.enum(['Form', 'Actions', 'Navigation', 'Content', 'Display', 'Layout', 'Other'])
|
|
1037
2053
|
.optional()
|
|
1038
2054
|
.describe('Filter by component category'),
|
|
1039
2055
|
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 (
|
|
2056
|
+
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
|
|
1041
2057
|
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
1042
2058
|
prefix: z.string().optional(),
|
|
1043
2059
|
},
|
|
1044
2060
|
},
|
|
1045
2061
|
async ({ category, query, limit, offset, prefix }) => {
|
|
1046
|
-
const
|
|
2062
|
+
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix });
|
|
2063
|
+
const payload = {
|
|
2064
|
+
items: page.items,
|
|
2065
|
+
total: page.total,
|
|
2066
|
+
limit: page.limit,
|
|
2067
|
+
offset: page.offset,
|
|
2068
|
+
hasMore: page.hasMore,
|
|
2069
|
+
};
|
|
2070
|
+
if (page._notice) payload._notice = page._notice;
|
|
1047
2071
|
return {
|
|
1048
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
2072
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1049
2073
|
};
|
|
1050
2074
|
},
|
|
1051
2075
|
);
|
|
@@ -1082,27 +2106,34 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1082
2106
|
description:
|
|
1083
2107
|
'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
2108
|
inputSchema: {
|
|
1085
|
-
tagName: z.string().optional(),
|
|
1086
|
-
className: z.string().optional(),
|
|
2109
|
+
tagName: z.string().optional().describe('Tag name (e.g., "dads-button")'),
|
|
2110
|
+
className: z.string().optional().describe('Class name (e.g., "DadsButton")'),
|
|
2111
|
+
component: z.string().optional().describe('Any identifier: tagName, className, or bare name (e.g., "button")'),
|
|
1087
2112
|
prefix: z.string().optional(),
|
|
1088
2113
|
},
|
|
1089
2114
|
},
|
|
1090
|
-
async ({ tagName, className, prefix }) => {
|
|
1091
|
-
const
|
|
2115
|
+
async ({ tagName, className, component, prefix }) => {
|
|
2116
|
+
const p = normalizePrefix(prefix);
|
|
2117
|
+
let decl;
|
|
2118
|
+
let modulePath;
|
|
2119
|
+
|
|
2120
|
+
if (component) {
|
|
2121
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
2122
|
+
decl = resolved?.decl;
|
|
2123
|
+
modulePath = resolved?.modulePath;
|
|
2124
|
+
} else {
|
|
2125
|
+
decl = pickDecl(indexes, { tagName, className, prefix: p });
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1092
2128
|
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
|
-
};
|
|
2129
|
+
const identifier = component || tagName || className || '';
|
|
2130
|
+
return buildComponentNotFoundError(identifier, indexes, p);
|
|
1102
2131
|
}
|
|
1103
2132
|
|
|
1104
2133
|
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
1105
|
-
|
|
2134
|
+
if (!modulePath) {
|
|
2135
|
+
modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
2136
|
+
}
|
|
1106
2137
|
const api = serializeApi(decl, modulePath, prefix);
|
|
1107
2138
|
const relatedComponents = getRelatedComponentsForTag({
|
|
1108
2139
|
canonicalTagName: canonicalTag,
|
|
@@ -1117,6 +2148,14 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1117
2148
|
if (accessibilityChecklist) {
|
|
1118
2149
|
api.accessibilityChecklist = accessibilityChecklist;
|
|
1119
2150
|
}
|
|
2151
|
+
const interactionExamples = canonicalTag ? INTERACTION_EXAMPLES_MAP[canonicalTag] : undefined;
|
|
2152
|
+
if (interactionExamples) {
|
|
2153
|
+
api.interactionExamples = interactionExamples;
|
|
2154
|
+
}
|
|
2155
|
+
const layoutBehavior = canonicalTag ? LAYOUT_BEHAVIOR_MAP[canonicalTag] : undefined;
|
|
2156
|
+
if (layoutBehavior) {
|
|
2157
|
+
api.layoutBehavior = layoutBehavior;
|
|
2158
|
+
}
|
|
1120
2159
|
|
|
1121
2160
|
return buildJsonToolResponse(api);
|
|
1122
2161
|
},
|
|
@@ -1136,19 +2175,16 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1136
2175
|
},
|
|
1137
2176
|
},
|
|
1138
2177
|
async ({ component, prefix }) => {
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
2178
|
+
const p = normalizePrefix(prefix);
|
|
2179
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
2180
|
+
const decl = resolved?.decl;
|
|
1142
2181
|
|
|
1143
2182
|
if (!decl) {
|
|
1144
|
-
return
|
|
1145
|
-
content: [{ type: 'text', text: `Component not found: ${component}` }],
|
|
1146
|
-
isError: true,
|
|
1147
|
-
};
|
|
2183
|
+
return buildComponentNotFoundError(component, indexes, p);
|
|
1148
2184
|
}
|
|
1149
2185
|
|
|
1150
2186
|
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
1151
|
-
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
2187
|
+
const modulePath = resolved?.modulePath ?? (canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined);
|
|
1152
2188
|
const api = serializeApi(decl, modulePath, prefix);
|
|
1153
2189
|
const snippet = generateSnippet(api, prefix);
|
|
1154
2190
|
|
|
@@ -1206,6 +2242,11 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1206
2242
|
const deps = Array.isArray(install.deps) ? install.deps : [];
|
|
1207
2243
|
const tags = Array.isArray(install.tags) ? install.tags : [];
|
|
1208
2244
|
|
|
2245
|
+
// Resolve transitive dependencies via BFS
|
|
2246
|
+
const transitiveDeps = componentId
|
|
2247
|
+
? resolveComponentClosure({ installRegistry }, [componentId]).filter((id) => id !== componentId)
|
|
2248
|
+
: [];
|
|
2249
|
+
|
|
1209
2250
|
const tagNames =
|
|
1210
2251
|
tags.length > 0 ? tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : [api.tagName];
|
|
1211
2252
|
|
|
@@ -1228,11 +2269,24 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1228
2269
|
componentId,
|
|
1229
2270
|
tagNames,
|
|
1230
2271
|
deps,
|
|
2272
|
+
transitiveDeps,
|
|
1231
2273
|
define,
|
|
1232
2274
|
defineHint,
|
|
1233
2275
|
source: install.source,
|
|
1234
2276
|
usageSnippet,
|
|
2277
|
+
usageContext: 'body-only',
|
|
1235
2278
|
installHint: componentId ? `wcf add ${componentId}` : undefined,
|
|
2279
|
+
vendorHint: (() => {
|
|
2280
|
+
const im = tagNames.length > 0
|
|
2281
|
+
? JSON.stringify({ imports: Object.fromEntries(tagNames.map((t) => [t, `./<dir>/components/${t.replace(/^[^-]+-/, '')}.js`])) })
|
|
2282
|
+
: undefined;
|
|
2283
|
+
return {
|
|
2284
|
+
install: componentId ? `wcf add ${componentId} --prefix <prefix> --out <dir>` : undefined,
|
|
2285
|
+
importMap: im,
|
|
2286
|
+
importmap: im, // @deprecated — use importMap; will be removed in v1.0
|
|
2287
|
+
boot: '<dir>/boot.js -- loads autoloader that registers components via import map',
|
|
2288
|
+
};
|
|
2289
|
+
})(),
|
|
1236
2290
|
},
|
|
1237
2291
|
null,
|
|
1238
2292
|
2,
|
|
@@ -1250,7 +2304,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1250
2304
|
'validate_markup',
|
|
1251
2305
|
{
|
|
1252
2306
|
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.',
|
|
2307
|
+
'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
2308
|
inputSchema: {
|
|
1255
2309
|
html: z.string(),
|
|
1256
2310
|
prefix: z.string().optional(),
|
|
@@ -1259,11 +2313,12 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1259
2313
|
async ({ html, prefix }) => {
|
|
1260
2314
|
const p = normalizePrefix(prefix);
|
|
1261
2315
|
let cemIndex = canonicalCemIndex;
|
|
2316
|
+
let enumMap = canonicalEnumMap;
|
|
2317
|
+
let slotMap = canonicalSlotMap;
|
|
1262
2318
|
if (p !== CANONICAL_PREFIX) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
cemIndex = combined;
|
|
2319
|
+
cemIndex = mergeWithPrefixed(canonicalCemIndex, p);
|
|
2320
|
+
enumMap = mergeWithPrefixed(canonicalEnumMap, p);
|
|
2321
|
+
slotMap = mergeWithPrefixed(canonicalSlotMap, p);
|
|
1267
2322
|
}
|
|
1268
2323
|
|
|
1269
2324
|
const cemDiagnostics = validateTextAgainstCem({
|
|
@@ -1276,6 +2331,13 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1276
2331
|
},
|
|
1277
2332
|
});
|
|
1278
2333
|
|
|
2334
|
+
const enumDiagnostics = detectEnumValueMisuse({
|
|
2335
|
+
filePath: '<markup>',
|
|
2336
|
+
text: html,
|
|
2337
|
+
enumMap,
|
|
2338
|
+
severity: 'error',
|
|
2339
|
+
});
|
|
2340
|
+
|
|
1279
2341
|
const tokenMisuseDiagnostics = detectTokenMisuseInInlineStyles({
|
|
1280
2342
|
filePath: '<markup>',
|
|
1281
2343
|
text: html,
|
|
@@ -1289,8 +2351,68 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1289
2351
|
severity: 'warning',
|
|
1290
2352
|
});
|
|
1291
2353
|
|
|
1292
|
-
const
|
|
1293
|
-
|
|
2354
|
+
const slotDiagnostics = detectInvalidSlotName({
|
|
2355
|
+
filePath: '<markup>',
|
|
2356
|
+
text: html,
|
|
2357
|
+
slotMap,
|
|
2358
|
+
severity: 'error',
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
const requiredAttrDiagnostics = detectMissingRequiredAttributes({
|
|
2362
|
+
filePath: '<markup>',
|
|
2363
|
+
text: html,
|
|
2364
|
+
prefix: p,
|
|
2365
|
+
severity: 'error',
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
const orphanDiagnostics = detectOrphanedChildComponents({
|
|
2369
|
+
filePath: '<markup>',
|
|
2370
|
+
text: html,
|
|
2371
|
+
prefix: p,
|
|
2372
|
+
severity: 'warning',
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
const emptyInteractiveDiagnostics = detectEmptyInteractiveElement({
|
|
2376
|
+
filePath: '<markup>',
|
|
2377
|
+
text: html,
|
|
2378
|
+
prefix: p,
|
|
2379
|
+
severity: 'warning',
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
const lowercaseDiagnostics = detectNonLowercaseAttributes({
|
|
2383
|
+
filePath: '<markup>',
|
|
2384
|
+
text: html,
|
|
2385
|
+
cem: cemIndex,
|
|
2386
|
+
severity: 'warning',
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
const cdnDiagnostics = detectCdnReferences({
|
|
2390
|
+
filePath: '<markup>',
|
|
2391
|
+
text: html,
|
|
2392
|
+
severity: 'warning',
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
const scaffoldDiagnostics = detectMissingRuntimeScaffold({
|
|
2396
|
+
filePath: '<markup>',
|
|
2397
|
+
text: html,
|
|
2398
|
+
severity: 'warning',
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
const allRawDiagnostics = [
|
|
2402
|
+
...cemDiagnostics,
|
|
2403
|
+
...enumDiagnostics,
|
|
2404
|
+
...slotDiagnostics,
|
|
2405
|
+
...requiredAttrDiagnostics,
|
|
2406
|
+
...orphanDiagnostics,
|
|
2407
|
+
...emptyInteractiveDiagnostics,
|
|
2408
|
+
...lowercaseDiagnostics,
|
|
2409
|
+
...tokenMisuseDiagnostics,
|
|
2410
|
+
...accessibilityDiagnostics,
|
|
2411
|
+
...cdnDiagnostics,
|
|
2412
|
+
...scaffoldDiagnostics,
|
|
2413
|
+
];
|
|
2414
|
+
const diagnostics = allRawDiagnostics.map((d) => {
|
|
2415
|
+
const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex, prefix: p });
|
|
1294
2416
|
return {
|
|
1295
2417
|
file: d.file,
|
|
1296
2418
|
range: d.range,
|
|
@@ -1334,6 +2456,70 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1334
2456
|
},
|
|
1335
2457
|
);
|
|
1336
2458
|
|
|
2459
|
+
// -----------------------------------------------------------------------
|
|
2460
|
+
// Helper: buildFullPageHtml
|
|
2461
|
+
// -----------------------------------------------------------------------
|
|
2462
|
+
function escapeHtmlTitle(s) {
|
|
2463
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Build import map entries from a component closure.
|
|
2468
|
+
* @param {string[]} closure - Component IDs
|
|
2469
|
+
* @param {Object} components - Component metadata (from install registry)
|
|
2470
|
+
* @param {string} prefix - Tag name prefix
|
|
2471
|
+
* @param {string} dir - Directory placeholder or concrete path
|
|
2472
|
+
* @returns {Object} Import map entries { prefixedTag: path }
|
|
2473
|
+
*/
|
|
2474
|
+
function buildImportMapEntries(closure, components, prefix, dir) {
|
|
2475
|
+
return Object.fromEntries(
|
|
2476
|
+
closure.flatMap((cid) => {
|
|
2477
|
+
const meta = components[cid];
|
|
2478
|
+
const tags = Array.isArray(meta?.tags) ? meta.tags : [cid];
|
|
2479
|
+
return tags.map((t) => {
|
|
2480
|
+
const lower = String(t).toLowerCase();
|
|
2481
|
+
const suffix = lower.replace(PREFIX_STRIP_RE, '');
|
|
2482
|
+
return [withPrefix(lower, prefix), `./${dir}/components/${suffix}.js`];
|
|
2483
|
+
});
|
|
2484
|
+
}),
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Build a complete HTML5 page from pattern data, resolving scaffoldHint
|
|
2490
|
+
* placeholders into concrete paths.
|
|
2491
|
+
* @param {Object} opts
|
|
2492
|
+
* @param {string} opts.html - Pattern HTML body
|
|
2493
|
+
* @param {string} opts.title - Page title
|
|
2494
|
+
* @param {Object} opts.importMapEntries - Import map entries { tag: path }
|
|
2495
|
+
* @param {string} [opts.dir] - Directory for JS assets
|
|
2496
|
+
* @param {string} [opts.lang='ja'] - HTML lang attribute
|
|
2497
|
+
* @returns {string} Complete HTML5 document
|
|
2498
|
+
*/
|
|
2499
|
+
function buildFullPageHtml({ html, title, importMapEntries, dir = VENDOR_DIR, lang = 'ja' }) {
|
|
2500
|
+
const importMapJson = JSON.stringify({ imports: importMapEntries }, null, 2);
|
|
2501
|
+
return [
|
|
2502
|
+
'<!DOCTYPE html>',
|
|
2503
|
+
`<html lang="${lang}">`,
|
|
2504
|
+
'<head>',
|
|
2505
|
+
' <meta charset="UTF-8">',
|
|
2506
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
2507
|
+
` <title>${escapeHtmlTitle(title)}</title>`,
|
|
2508
|
+
` <!-- distribution: selfHosted=true, strategy=vendor-importmap -->`,
|
|
2509
|
+
` <!-- Do NOT replace these local paths with CDN URLs. This design system is self-hosted. -->`,
|
|
2510
|
+
` <script type="importmap">`,
|
|
2511
|
+
`${importMapJson}`,
|
|
2512
|
+
` </script>`,
|
|
2513
|
+
` <script type="module" src="./${dir}/boot.js"></script>`,
|
|
2514
|
+
'</head>',
|
|
2515
|
+
'<body>',
|
|
2516
|
+
` <noscript>このページの機能にはJavaScriptが必要です。</noscript>`,
|
|
2517
|
+
` ${html}`,
|
|
2518
|
+
'</body>',
|
|
2519
|
+
'</html>',
|
|
2520
|
+
].join('\n');
|
|
2521
|
+
}
|
|
2522
|
+
|
|
1337
2523
|
// -----------------------------------------------------------------------
|
|
1338
2524
|
// Tool: get_pattern_recipe
|
|
1339
2525
|
// -----------------------------------------------------------------------
|
|
@@ -1341,13 +2527,14 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1341
2527
|
'get_pattern_recipe',
|
|
1342
2528
|
{
|
|
1343
2529
|
description:
|
|
1344
|
-
'Get a complete pattern recipe with component dependencies and HTML. When: building a page layout from a pattern. Returns: dependency tree, install commands, and resolved HTML. After: use validate_markup to verify the generated HTML.',
|
|
2530
|
+
'Get a complete pattern recipe with component dependencies and HTML. When: building a page layout from a pattern. Returns: dependency tree, install commands, and resolved HTML. After: use validate_markup to verify the generated HTML. Use include: ["fullPage"] to get a complete HTML5 page ready for browser rendering.',
|
|
1345
2531
|
inputSchema: {
|
|
1346
2532
|
patternId: z.string(),
|
|
1347
2533
|
prefix: z.string().optional(),
|
|
2534
|
+
include: z.array(z.enum(['fullPage'])).optional(),
|
|
1348
2535
|
},
|
|
1349
2536
|
},
|
|
1350
|
-
async ({ patternId, prefix }) => {
|
|
2537
|
+
async ({ patternId, prefix, include }) => {
|
|
1351
2538
|
const id = String(patternId ?? '').trim();
|
|
1352
2539
|
const p = normalizePrefix(prefix);
|
|
1353
2540
|
const pat = patterns[id];
|
|
@@ -1378,28 +2565,59 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1378
2565
|
const canonicalHtml = String(pat.html ?? '');
|
|
1379
2566
|
const html = applyPrefixToHtml(canonicalHtml, p);
|
|
1380
2567
|
|
|
2568
|
+
const entryHints = Array.isArray(pat.entryHints) ? [...pat.entryHints] : ['boot'];
|
|
2569
|
+
|
|
2570
|
+
const importMapEntries = buildImportMapEntries(closure, components, p, '<dir>');
|
|
2571
|
+
|
|
2572
|
+
const scaffoldHint = {
|
|
2573
|
+
doctype: '<!DOCTYPE html>',
|
|
2574
|
+
importMap: `<script type="importmap">\n${JSON.stringify({ imports: importMapEntries }, null, 2)}\n</script>`,
|
|
2575
|
+
bootScript: '<script type="module" src="./<dir>/boot.js"></script>',
|
|
2576
|
+
noscript: '<noscript>このページの機能にはJavaScriptが必要です。</noscript>',
|
|
2577
|
+
serveOverHttp: 'Import maps require HTTP/HTTPS. Use a local dev server (e.g. npx serve .) instead of opening the HTML file directly via file:// protocol.',
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// Build fullPageHtml when requested via include: ['fullPage']
|
|
2581
|
+
const includeArr = Array.isArray(include) ? include : [];
|
|
2582
|
+
let fullPageHtml;
|
|
2583
|
+
if (includeArr.includes('fullPage')) {
|
|
2584
|
+
const resolvedImportMap = buildImportMapEntries(closure, components, p, VENDOR_DIR);
|
|
2585
|
+
fullPageHtml = buildFullPageHtml({
|
|
2586
|
+
html,
|
|
2587
|
+
title: pat.title ?? pat.id,
|
|
2588
|
+
importMapEntries: resolvedImportMap,
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
const result = {
|
|
2593
|
+
pattern: {
|
|
2594
|
+
id: pat.id,
|
|
2595
|
+
title: pat.title,
|
|
2596
|
+
description: pat.description,
|
|
2597
|
+
},
|
|
2598
|
+
prefix: p,
|
|
2599
|
+
requires,
|
|
2600
|
+
components: closure,
|
|
2601
|
+
install,
|
|
2602
|
+
html,
|
|
2603
|
+
canonicalHtml,
|
|
2604
|
+
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
2605
|
+
entryHints,
|
|
2606
|
+
scaffoldHint,
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
if (fullPageHtml !== undefined) {
|
|
2610
|
+
result.fullPageHtml = fullPageHtml;
|
|
2611
|
+
result.vendorSetup = {
|
|
2612
|
+
command: `npx web-components-factory init --prefix ${p} --dir ${VENDOR_DIR} && npx web-components-factory add ${closure.join(' ')} --prefix ${p} --out ${VENDOR_DIR}`,
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
|
|
1381
2616
|
return {
|
|
1382
2617
|
content: [
|
|
1383
2618
|
{
|
|
1384
2619
|
type: 'text',
|
|
1385
|
-
text: JSON.stringify(
|
|
1386
|
-
{
|
|
1387
|
-
pattern: {
|
|
1388
|
-
id: pat.id,
|
|
1389
|
-
title: pat.title,
|
|
1390
|
-
description: pat.description,
|
|
1391
|
-
},
|
|
1392
|
-
prefix: p,
|
|
1393
|
-
requires,
|
|
1394
|
-
components: closure,
|
|
1395
|
-
install,
|
|
1396
|
-
html,
|
|
1397
|
-
canonicalHtml,
|
|
1398
|
-
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
1399
|
-
},
|
|
1400
|
-
null,
|
|
1401
|
-
2,
|
|
1402
|
-
),
|
|
2620
|
+
text: JSON.stringify(result, null, 2),
|
|
1403
2621
|
},
|
|
1404
2622
|
],
|
|
1405
2623
|
};
|
|
@@ -1454,35 +2672,48 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1454
2672
|
.describe('Filter by token category'),
|
|
1455
2673
|
query: z.string().optional()
|
|
1456
2674
|
.describe('Search token names (partial match)'),
|
|
2675
|
+
theme: z.enum(['light', 'dark', 'all']).optional()
|
|
2676
|
+
.describe('Theme filter (currently light only; dark/all return an error due to NG-06)'),
|
|
1457
2677
|
},
|
|
1458
2678
|
},
|
|
1459
|
-
async ({ type, category, query }) => {
|
|
1460
|
-
|
|
2679
|
+
async ({ type, category, query, theme }) => {
|
|
2680
|
+
const { isError, payload } = buildDesignTokensPayload(designTokensData, { type, category, query, theme });
|
|
2681
|
+
if (isError) {
|
|
1461
2682
|
return {
|
|
1462
|
-
content: [{ type: 'text', text:
|
|
2683
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1463
2684
|
isError: true,
|
|
1464
2685
|
};
|
|
1465
2686
|
}
|
|
2687
|
+
return buildJsonToolResponse(payload);
|
|
2688
|
+
},
|
|
2689
|
+
);
|
|
1466
2690
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2691
|
+
// -----------------------------------------------------------------------
|
|
2692
|
+
// Tool: get_design_token_detail
|
|
2693
|
+
// -----------------------------------------------------------------------
|
|
2694
|
+
server.registerTool(
|
|
2695
|
+
'get_design_token_detail',
|
|
2696
|
+
{
|
|
2697
|
+
description:
|
|
2698
|
+
'Get details for one design token. ' +
|
|
2699
|
+
'When: you already found a token and need its references, referencedBy, and usage examples. ' +
|
|
2700
|
+
'Returns: token detail object with relationships and example CSS snippets. ' +
|
|
2701
|
+
'After: apply the cssVariable in your implementation or validate related semantic aliases.',
|
|
2702
|
+
inputSchema: {
|
|
2703
|
+
name: z.string()
|
|
2704
|
+
.describe('Token name or css variable (e.g. --color-primary or var(--color-primary))'),
|
|
2705
|
+
theme: z.enum(['light', 'dark', 'all']).optional()
|
|
2706
|
+
.describe('Theme selector (currently only light is supported due to NG-06)'),
|
|
2707
|
+
},
|
|
2708
|
+
},
|
|
2709
|
+
async ({ name, theme }) => {
|
|
2710
|
+
const { isError, payload } = buildDesignTokenDetailPayload(designTokensData, name, theme);
|
|
2711
|
+
if (isError) {
|
|
2712
|
+
return {
|
|
2713
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
2714
|
+
isError: true,
|
|
2715
|
+
};
|
|
1478
2716
|
}
|
|
1479
|
-
|
|
1480
|
-
const payload = {
|
|
1481
|
-
total: tokens.length,
|
|
1482
|
-
tokens,
|
|
1483
|
-
summary: designTokensData.summary,
|
|
1484
|
-
};
|
|
1485
|
-
|
|
1486
2717
|
return buildJsonToolResponse(payload);
|
|
1487
2718
|
},
|
|
1488
2719
|
);
|
|
@@ -1582,6 +2813,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1582
2813
|
const max = maxResults ?? 5;
|
|
1583
2814
|
const documents = Array.isArray(guidelinesIndexData.documents) ? guidelinesIndexData.documents : [];
|
|
1584
2815
|
const q = query.toLowerCase();
|
|
2816
|
+
const expandedTerms = expandQueryWithSynonyms(q);
|
|
1585
2817
|
|
|
1586
2818
|
// Score and rank sections
|
|
1587
2819
|
const results = [];
|
|
@@ -1595,6 +2827,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1595
2827
|
const heading = String(section.heading ?? '').toLowerCase();
|
|
1596
2828
|
const keywords = Array.isArray(section.keywords) ? section.keywords : [];
|
|
1597
2829
|
const snippet = String(section.snippet ?? '').toLowerCase();
|
|
2830
|
+
const body = String(section.body ?? '').toLowerCase();
|
|
1598
2831
|
|
|
1599
2832
|
// Heading match: weight 3
|
|
1600
2833
|
if (heading.includes(q)) score += 3;
|
|
@@ -1610,6 +2843,37 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1610
2843
|
// Snippet match: weight 1
|
|
1611
2844
|
if (snippet.includes(q)) score += 1;
|
|
1612
2845
|
|
|
2846
|
+
// Body text match: weight 1, plus boost for multiple occurrences
|
|
2847
|
+
if (body && body.includes(q)) {
|
|
2848
|
+
score += 1;
|
|
2849
|
+
// Count additional occurrences in body for boost (cap at +2)
|
|
2850
|
+
let idx = body.indexOf(q);
|
|
2851
|
+
let occurrences = 0;
|
|
2852
|
+
while (idx !== -1 && occurrences < 3) {
|
|
2853
|
+
occurrences++;
|
|
2854
|
+
idx = body.indexOf(q, idx + q.length);
|
|
2855
|
+
}
|
|
2856
|
+
if (occurrences > 1) score += Math.min(occurrences - 1, 2);
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// Synonym expansion match: check all expanded terms, cap total synonym contribution at +2
|
|
2860
|
+
if (expandedTerms.length > 1) {
|
|
2861
|
+
let synScore = 0;
|
|
2862
|
+
const lowerKeywords = keywords.map((kw) => String(kw).toLowerCase());
|
|
2863
|
+
for (let i = 1; i < expandedTerms.length && synScore < 2; i++) {
|
|
2864
|
+
const syn = expandedTerms[i];
|
|
2865
|
+
if (heading.includes(syn)) { synScore += 1; continue; }
|
|
2866
|
+
if (snippet.includes(syn) || body.includes(syn)) { synScore += 1; continue; }
|
|
2867
|
+
for (const kw of lowerKeywords) {
|
|
2868
|
+
if (kw.includes(syn)) {
|
|
2869
|
+
synScore += 1;
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
score += synScore;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
1613
2877
|
if (score > 0) {
|
|
1614
2878
|
results.push({
|
|
1615
2879
|
score,
|
|
@@ -1635,9 +2899,81 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
1635
2899
|
results: topResults,
|
|
1636
2900
|
};
|
|
1637
2901
|
|
|
2902
|
+
// Zero-result fallback: suggest alternative queries and tools
|
|
2903
|
+
if (results.length === 0) {
|
|
2904
|
+
const synonymExpansions = expandedTerms.filter((t) => t !== q);
|
|
2905
|
+
payload.suggestions = {
|
|
2906
|
+
alternativeQueries: synonymExpansions.length > 0 ? synonymExpansions : [],
|
|
2907
|
+
alternativeTools: [
|
|
2908
|
+
{ tool: 'get_accessibility_docs', hint: 'For component-specific a11y checks' },
|
|
2909
|
+
{ tool: 'get_component_api', hint: 'For component API details' },
|
|
2910
|
+
],
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
|
|
1638
2914
|
return buildJsonToolResponse(payload);
|
|
1639
2915
|
},
|
|
1640
2916
|
);
|
|
1641
2917
|
|
|
1642
|
-
|
|
2918
|
+
for (const plugin of plugins) {
|
|
2919
|
+
const pluginTools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
2920
|
+
for (const tool of pluginTools) {
|
|
2921
|
+
server.registerTool(
|
|
2922
|
+
tool.name,
|
|
2923
|
+
{
|
|
2924
|
+
description: tool.description,
|
|
2925
|
+
inputSchema: tool.inputSchema ?? {},
|
|
2926
|
+
},
|
|
2927
|
+
async (args) => {
|
|
2928
|
+
try {
|
|
2929
|
+
if (typeof tool.handler === 'function') {
|
|
2930
|
+
const result = await tool.handler(args, {
|
|
2931
|
+
plugin: { name: plugin.name, version: plugin.version },
|
|
2932
|
+
helpers: {
|
|
2933
|
+
loadJsonData: loadJson,
|
|
2934
|
+
buildJsonToolResponse,
|
|
2935
|
+
normalizePrefix,
|
|
2936
|
+
withPrefix,
|
|
2937
|
+
toCanonicalTagName,
|
|
2938
|
+
},
|
|
2939
|
+
});
|
|
2940
|
+
if (isPlainObject(result) && Array.isArray(result.content)) {
|
|
2941
|
+
return result;
|
|
2942
|
+
}
|
|
2943
|
+
return buildJsonToolResponse(result ?? {});
|
|
2944
|
+
}
|
|
2945
|
+
return buildJsonToolResponse(tool.staticPayload ?? {});
|
|
2946
|
+
} catch (error) {
|
|
2947
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2948
|
+
return {
|
|
2949
|
+
content: [{
|
|
2950
|
+
type: 'text',
|
|
2951
|
+
text: JSON.stringify({
|
|
2952
|
+
error: {
|
|
2953
|
+
code: 'PLUGIN_TOOL_RUNTIME_ERROR',
|
|
2954
|
+
message: `Plugin tool failed (${tool.name}): ${message}`,
|
|
2955
|
+
plugin: plugin.name,
|
|
2956
|
+
},
|
|
2957
|
+
}, null, 2),
|
|
2958
|
+
}],
|
|
2959
|
+
isError: true,
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
},
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
return {
|
|
2968
|
+
server,
|
|
2969
|
+
pluginRuntime: {
|
|
2970
|
+
pluginCount: plugins.length,
|
|
2971
|
+
pluginToolCount: plugins.reduce((sum, plugin) => sum + (plugin.tools?.length ?? 0), 0),
|
|
2972
|
+
dataSourceOverrides: [...pluginDataSourceMap.entries()].map(([fileName, item]) => ({
|
|
2973
|
+
fileName,
|
|
2974
|
+
path: item.path,
|
|
2975
|
+
pluginName: item.pluginName,
|
|
2976
|
+
})),
|
|
2977
|
+
},
|
|
2978
|
+
};
|
|
1643
2979
|
}
|