@monoharada/wcf-mcp 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core.mjs CHANGED
@@ -1,1967 +1,112 @@
1
1
  /**
2
- * core.mjs — Shared MCP server logic.
2
+ * core.mjs — Facade re-exporting all public and internal symbols.
3
3
  *
4
4
  * Both `server.mjs` (standalone / npx) and `scripts/mcp/design-system-mcp.mjs`
5
5
  * (repo-local) import `createMcpServer()` from here so that tool definitions
6
6
  * and helper functions live in exactly one place.
7
- */
8
-
9
- import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
10
- import { z } from 'zod';
11
-
12
- // ---------------------------------------------------------------------------
13
- // Constants
14
- // ---------------------------------------------------------------------------
15
-
16
- export const CANONICAL_PREFIX = 'dads';
17
- export const MAX_PREFIX_LENGTH = 64;
18
- export const STRUCTURED_CONTENT_DISABLE_FLAG = 'WCF_MCP_DISABLE_STRUCTURED_CONTENT';
19
- export const MAX_TOOL_RESULT_BYTES = 100 * 1024;
20
- export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1.1).';
21
- export const PLUGIN_CONTRACT_VERSION = '1.1.0';
22
-
23
- /**
24
- * Convert a plugin tool's inputSchema to a passthrough Zod schema.
25
- * Handles three cases:
26
- * - Already a Zod schema instance (has _def or _zod) → apply .passthrough()
27
- * - Plain object (raw shape map) → wrap with z.object().passthrough()
28
- * - Falsy / empty → z.object({}).passthrough()
29
- */
30
- function toPassthroughSchema(schema) {
31
- if (schema && (schema._def || schema._zod)) {
32
- // Already a Zod schema — apply passthrough if it's an object type
33
- return typeof schema.passthrough === 'function'
34
- ? schema.passthrough()
35
- : schema;
36
- }
37
- return z.object(schema ?? {}).passthrough();
38
- }
39
-
40
- export const CATEGORY_MAP = {
41
- 'dads-input-text': 'Form',
42
- 'dads-textarea': 'Form',
43
- 'dads-select': 'Form',
44
- 'dads-checkbox': 'Form',
45
- 'dads-radio': 'Form',
46
- 'dads-switch': 'Form',
47
- 'dads-combobox': 'Form',
48
- 'dads-date-picker': 'Form',
49
- 'dads-file-upload': 'Form',
50
- 'dads-fieldset': 'Form',
51
- 'dads-search-box': 'Form',
52
- 'dads-calendar': 'Form',
53
- 'dads-button': 'Actions',
54
- 'dads-dialog': 'Actions',
55
- 'dads-drawer': 'Actions',
56
- 'dads-disclosure': 'Actions',
57
- 'dads-accordion-details': 'Actions',
58
- 'dads-accordion-item-details': 'Actions',
59
- 'dads-breadcrumb': 'Navigation',
60
- 'dads-breadcrumb-item': 'Navigation',
61
- 'dads-page-navigation': 'Navigation',
62
- 'dads-step-navigation': 'Navigation',
63
- 'dads-step-navigation-item': 'Navigation',
64
- 'dads-menu-list': 'Navigation',
65
- 'dads-menu-list-item': 'Navigation',
66
- 'dads-menu-list-box': 'Navigation',
67
- 'dads-tab': 'Navigation',
68
- 'dads-global-menu': 'Navigation',
69
- 'dads-global-menu-item': 'Navigation',
70
- 'dads-language-selector': 'Navigation',
71
- 'dads-hamburger-menu-button': 'Navigation',
72
- 'dads-utility-link': 'Navigation',
73
- 'dads-mobile-menu': 'Navigation',
74
- 'dads-card': 'Content',
75
- 'dads-heading': 'Content',
76
- 'dads-text': 'Content',
77
- 'dads-blockquote': 'Content',
78
- 'dads-code-block': 'Content',
79
- 'dads-divider': 'Content',
80
- 'dads-list': 'Content',
81
- 'dads-list-item': 'Content',
82
- 'dads-description-list': 'Content',
83
- 'dads-resource-list': 'Content',
84
- 'dads-table': 'Content',
85
- 'dads-table-control': 'Content',
86
- 'dads-avatar': 'Display',
87
- 'dads-icon': 'Display',
88
- 'dads-chip-label': 'Display',
89
- 'dads-chip-tag': 'Display',
90
- 'dads-notification-banner': 'Display',
91
- 'dads-emergency-banner': 'Display',
92
- 'dads-carousel': 'Display',
93
- 'dads-layout-shell': 'Layout',
94
- 'dads-layout-sidebar': 'Layout',
95
- 'dads-layout-aside': 'Layout',
96
- 'dads-header-container': 'Layout',
97
- 'dads-device-mock': 'Display',
98
- 'dads-progress-indicator': 'Display',
99
- 'dads-spinner': 'Display',
100
- 'dads-progress-bar': 'Display',
101
- 'dads-loading-icon': 'Display',
102
- };
103
-
104
- const TOKEN_MISUSE_ALLOWED_TYPES = Object.freeze(new Set(['color', 'spacing']));
105
- const STRUCTURED_CONTENT_DISABLE_TRUE_VALUES = Object.freeze(new Set(['1', 'true', 'yes', 'on']));
106
- const WCAG_LEVELS = Object.freeze(new Set(['A', 'AA', 'AAA', 'all']));
107
- const TOKEN_THEMES = Object.freeze(new Set(['light', 'dark', 'all']));
108
- const GUIDELINE_TOPICS = Object.freeze(['accessibility', 'css', 'patterns', 'all']);
109
- const GUIDELINE_TOPIC_SET = Object.freeze(new Set(GUIDELINE_TOPICS));
110
- const PLUGIN_DATA_SOURCE_KEYS = Object.freeze(new Set([
111
- 'custom-elements.json',
112
- 'install-registry.json',
113
- 'pattern-registry.json',
114
- 'design-tokens.json',
115
- 'guidelines-index.json',
116
- ]));
117
- const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
118
- 'get_design_system_overview',
119
- 'list_components',
120
- 'search_icons',
121
- 'get_component_api',
122
- 'generate_usage_snippet',
123
- 'get_install_recipe',
124
- 'validate_markup',
125
- 'list_patterns',
126
- 'get_pattern_recipe',
127
- 'generate_pattern_snippet',
128
- 'get_design_tokens',
129
- 'get_design_token_detail',
130
- 'get_accessibility_docs',
131
- 'search_guidelines',
132
- 'generate_full_page_html',
133
- 'get_component_selector_guide',
134
- ]));
135
- const A11Y_CATEGORY_LEVEL_MAP = Object.freeze({
136
- semantics: 'A',
137
- keyboard: 'A',
138
- labels: 'A',
139
- states: 'AA',
140
- zoom: 'AA',
141
- motion: 'AA',
142
- callouts: 'AA',
143
- guideline: 'A',
144
- });
145
- const NPX_TEMPLATE = Object.freeze({
146
- command: 'npx',
147
- args: ['@monoharada/wcf-mcp'],
148
- });
149
- export const FIGMA_TO_WCF_PROMPT = 'figma_to_wcf';
150
- export const WCF_RESOURCE_URIS = Object.freeze({
151
- components: 'wcf://components',
152
- tokens: 'wcf://tokens',
153
- guidelinesTemplate: 'wcf://guidelines/{topic}',
154
- llmsFull: 'wcf://llms-full',
155
- skills: 'wcf://skills',
156
- });
157
-
158
- /** Normalize a skill entry to summary fields (omit compat/manifest for wcf://skills). */
159
- function normalizeSkillSummary(s) {
160
- return {
161
- name: s.name,
162
- description: s.description ?? '',
163
- status: s.status ?? 'active',
164
- path: s.path ?? '',
165
- entry: s.entry ?? 'SKILL.md',
166
- clients: Array.isArray(s.clients) ? s.clients : [],
167
- tags: Array.isArray(s.tags) ? s.tags : [],
168
- version: typeof s.version === 'string' ? s.version : '0.0.0',
169
- dependencies: Array.isArray(s.dependencies) ? s.dependencies : [],
170
- };
171
- }
172
-
173
- // Unidirectional synonym table: key → expands to include these terms (DIG-09)
174
- // Searching "keyboard" also matches "focus", "tab" etc. but NOT reverse.
175
- const SYNONYM_TABLE = new Map([
176
- ['aria-live', ['role=alert', 'aria-describedby', 'live region', 'error text']],
177
- ['keyboard', ['focus', 'tab', 'tabindex', 'key event', 'focus trap']],
178
- ['contrast', ['color', 'wcag', 'color contrast']],
179
- ['spacing', ['margin', 'padding', 'gap', 'spacing token', '--spacing']],
180
- ['skip-navigation', ['skip-link', 'landmark', 'skip nav']],
181
- ['heading', ['heading hierarchy', 'h1', 'heading level']],
182
- ['form', ['input', 'validation', 'required', 'label']],
183
- ['part', ['::part', 'css part', 'shadow dom styling']],
184
- ['layout', ['grid', 'flexbox', 'layout-shell', 'responsive', 'breakpoint']],
185
- ['responsive', ['media query', 'breakpoint', 'viewport', 'mobile']],
186
- ['error', ['validation', 'aria-invalid', 'aria-describedby', 'error text']],
187
- ['focus', ['focus-visible', 'focus ring', 'outline', 'tabindex', 'keyboard']],
188
- ['token', ['design token', 'css variable', 'custom property', 'spacing token']],
189
- ['div-soup', ['wrapper', 'unnecessary div', 'minimal dom']],
190
- ]);
191
-
192
- // Icon alias table: common alias → canonical icon names (DD-18)
193
- // Maps user-friendly search terms to actual icon names in the CEM catalog.
194
- const ICON_ALIAS_TABLE = new Map([
195
- ['x', ['close', 'cancel']],
196
- ['trash', ['delete']],
197
- ['pencil', ['edit']],
198
- ['magnifying', ['search']],
199
- ['gear', ['settings']],
200
- ['plus', ['add']],
201
- ['minus', ['subtract']],
202
- ['tick', ['check', 'checkmark']],
203
- ['alert', ['warning', 'attention']],
204
- ['info', ['information', 'help']],
205
- ['hamburger', ['menu']],
206
- ['back', ['arrowBack', 'arrowLeft']],
207
- ['forward', ['arrowForward', 'arrowRight']],
208
- ['eye', ['visibility']],
209
- ['user', ['person']],
210
- ['file', ['document']],
211
- ['bell', ['notification']],
212
- ]);
213
-
214
- // Interaction examples for form components (P-04 / #206)
215
- const INTERACTION_EXAMPLES_MAP = Object.freeze({
216
- 'dads-input-text': [
217
- { scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "hello";' },
218
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
219
- { scenario: 'Clear validation error', trigger: 'attribute', code: 'el.error = false; el.errorText = "";' },
220
- { scenario: 'Listen to value change', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
221
- ],
222
- 'dads-textarea': [
223
- { scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "long text...";' },
224
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "入力できる文字数を超えています";' },
225
- { scenario: 'Listen to input event', trigger: 'event', code: "el.addEventListener('input', (e) => { console.log(e.target.value); });" },
226
- ],
227
- 'dads-select': [
228
- { scenario: 'Set selected value', trigger: 'property', code: 'el.value = "option1";' },
229
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
230
- { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
231
- ],
232
- 'dads-checkbox': [
233
- { scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
234
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
235
- { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.checked); });" },
236
- ],
237
- 'dads-radio': [
238
- { scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
239
- { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
240
- ],
241
- 'dads-combobox': [
242
- { scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "selected-option";' },
243
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
244
- { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
245
- ],
246
- 'dads-date-picker': [
247
- { scenario: 'Set date value', trigger: 'property', code: 'el.value = "2024-01-15";' },
248
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
249
- { scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
250
- ],
251
- 'dads-file-upload': [
252
- { scenario: 'Listen to file selection', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.files); });" },
253
- { scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
254
- ],
255
- });
256
-
257
- // Layout behavior metadata for layout/display components (P-05 / #207)
258
- const LAYOUT_BEHAVIOR_MAP = Object.freeze({
259
- 'dads-layout-shell': {
260
- responsive: {
261
- breakpoints: { desktop: '80rem', tablet: '48rem' },
262
- modes: ['auto', 'desktop', 'tablet', 'mobile'],
263
- defaultMode: 'auto',
264
- description: 'Automatically switches between desktop/tablet/mobile layouts based on viewport width when mode="auto".',
265
- },
266
- overflow: {
267
- strategy: 'slot-driven',
268
- description: 'Slots (header, sidebar, aside, footer) are auto-hidden when empty. Sidebar collapses to rail on tablet.',
269
- },
270
- constraints: {
271
- patterns: ['website', 'app-shell', 'master-detail', 'left-header-pane', 'three-pane', 'three-pane-shell'],
272
- defaultPattern: 'app-shell',
273
- mobileSidebarOptions: ['hidden', 'top', 'bottom'],
274
- description: 'Choose a pattern attribute to control layout structure. Pair with mode and mobile-sidebar for full control.',
275
- },
276
- },
277
- 'dads-layout-sidebar': {
278
- responsive: {
279
- description: 'Designed to be placed inside dads-layout-shell sidebar slot. Width is controlled by the parent shell.',
280
- },
281
- constraints: {
282
- description: 'Simple container for sidebar content. Use inside dads-layout-shell for responsive behavior.',
283
- },
284
- },
285
- 'dads-device-mock': {
286
- responsive: {
287
- devices: ['desktop', 'tablet', 'mobile'],
288
- defaultDevice: 'mobile',
289
- description: 'Renders a device frame (desktop/tablet/mobile) around slotted content. Set device attribute to switch.',
290
- },
291
- constraints: {
292
- visibleHeight: 'Use visible-height attribute to clip the mock to a specific height (e.g. "220px").',
293
- description: 'Display-only component for previewing content in device frames. Not a layout container.',
294
- },
295
- },
296
- });
297
-
298
- export function expandQueryWithSynonyms(query) {
299
- const q = String(query ?? '').toLowerCase().trim();
300
- if (!q) return [q];
301
- const terms = [q];
302
- for (const [key, synonyms] of SYNONYM_TABLE) {
303
- if (q.includes(key)) {
304
- for (const syn of synonyms) {
305
- if (!terms.includes(syn)) terms.push(syn);
306
- }
307
- }
308
- }
309
- return terms;
310
- }
311
-
312
- export const IDE_SETUP_TEMPLATES = Object.freeze([
313
- {
314
- ide: 'Claude Desktop',
315
- configPath: 'claude_desktop_config.json',
316
- snippet: {
317
- mcpServers: {
318
- wcf: NPX_TEMPLATE,
319
- },
320
- },
321
- },
322
- {
323
- ide: 'Claude Code',
324
- configPath: '.mcp.json',
325
- snippet: {
326
- mcpServers: {
327
- wcf: NPX_TEMPLATE,
328
- },
329
- },
330
- },
331
- {
332
- ide: 'Cursor',
333
- configPath: '.cursor/mcp.json',
334
- snippet: {
335
- mcpServers: {
336
- wcf: NPX_TEMPLATE,
337
- },
338
- },
339
- },
340
- {
341
- ide: 'VS Code (GitHub Copilot)',
342
- configPath: '.vscode/mcp.json',
343
- snippet: {
344
- mcpServers: {
345
- wcf: NPX_TEMPLATE,
346
- },
347
- },
348
- },
349
- {
350
- ide: 'Windsurf',
351
- configPath: '.windsurf/mcp_config.json',
352
- snippet: {
353
- mcpServers: {
354
- wcf: NPX_TEMPLATE,
355
- },
356
- },
357
- },
358
- ]);
359
-
360
- export function isStructuredContentDisabled(env = process.env) {
361
- const raw = String(env?.[STRUCTURED_CONTENT_DISABLE_FLAG] ?? '').trim().toLowerCase();
362
- return STRUCTURED_CONTENT_DISABLE_TRUE_VALUES.has(raw);
363
- }
364
-
365
- export function toStructuredContent(data) {
366
- return {
367
- type: 'application/json',
368
- data,
369
- };
370
- }
371
-
372
- export function measureToolResultBytes(result) {
373
- return Buffer.byteLength(JSON.stringify(result), 'utf8');
374
- }
375
-
376
- export function buildJsonToolResponse(payload, { env = process.env } = {}) {
377
- const content = [{
378
- type: 'text',
379
- text: JSON.stringify(payload, null, 2),
380
- }];
381
-
382
- if (isStructuredContentDisabled(env)) {
383
- return { content };
384
- }
385
-
386
- const withStructuredContent = {
387
- content,
388
- structuredContent: toStructuredContent(payload),
389
- };
390
-
391
- // Keep response size under the 100KB guardrail even when structuredContent is enabled.
392
- if (measureToolResultBytes(withStructuredContent) > MAX_TOOL_RESULT_BYTES) {
393
- return { content };
394
- }
395
-
396
- return withStructuredContent;
397
- }
398
-
399
- export function normalizeTokenValue(value) {
400
- if (typeof value === 'string') return value.trim().toLowerCase().replace(/\s+/g, ' ');
401
- if (typeof value === 'number') return String(value);
402
- return '';
403
- }
404
-
405
- export function normalizeCssVariable(value) {
406
- if (typeof value !== 'string') return '';
407
-
408
- const raw = value.trim();
409
- if (!raw) return '';
410
- if (raw.startsWith('--')) return raw;
411
-
412
- const varMatch = /^var\(\s*(--[^,\s)]+)\s*(?:,\s*[^)]+)?\)$/.exec(raw);
413
- if (varMatch) return varMatch[1];
414
-
415
- return '';
416
- }
417
-
418
- export function buildTokenSuggestionMap(designTokensData) {
419
- if (!Array.isArray(designTokensData?.tokens)) return new Map();
420
-
421
- const out = new Map();
422
- for (const token of designTokensData.tokens) {
423
- const type = String(token?.type ?? '').toLowerCase();
424
- if (!TOKEN_MISUSE_ALLOWED_TYPES.has(type)) continue;
425
-
426
- const cssVariable = normalizeCssVariable(token?.cssVariable);
427
- if (!cssVariable) continue;
428
-
429
- const normalized = normalizeTokenValue(token?.value);
430
- if (normalized && !out.has(normalized)) out.set(normalized, cssVariable);
431
- }
432
- return out;
433
- }
434
-
435
- export function normalizeTokenIdentifier(value) {
436
- const raw = String(value ?? '').trim().toLowerCase();
437
- if (!raw) return '';
438
- const cssVariable = normalizeCssVariable(raw);
439
- if (cssVariable) return cssVariable;
440
- if (raw.startsWith('--')) return raw;
441
- return `--${raw.replace(/^[-]+/, '')}`;
442
- }
443
-
444
- export function resolveTokenTheme(theme) {
445
- const requested = String(theme ?? 'light').trim().toLowerCase() || 'light';
446
- if (!TOKEN_THEMES.has(requested)) {
447
- return {
448
- ok: false,
449
- errorCode: 'INVALID_THEME',
450
- message: `Unsupported theme: ${requested}. Allowed values are light, dark, all.`,
451
- };
452
- }
453
- if (requested !== 'light') {
454
- return {
455
- ok: false,
456
- errorCode: 'INVALID_THEME',
457
- message: `Theme "${requested}" is not available yet. Use theme="light" (NG-06).`,
458
- };
459
- }
460
- return {
461
- ok: true,
462
- requested,
463
- resolved: 'light',
464
- available: ['light'],
465
- };
466
- }
467
-
468
- export function extractReferencedTokenNames(value) {
469
- if (typeof value !== 'string') return [];
470
- const refs = [];
471
- const re = /var\(\s*(--[^,\s)]+)\s*(?:,\s*[^)]+)?\)/g;
472
- let match;
473
- while ((match = re.exec(value))) {
474
- const tokenName = normalizeTokenIdentifier(match[1]);
475
- if (tokenName) refs.push(tokenName);
476
- }
477
- return [...new Set(refs)];
478
- }
479
-
480
- export function buildTokenRelationshipIndex(designTokensData) {
481
- const byToken = {};
482
- const tokens = Array.isArray(designTokensData?.tokens) ? designTokensData.tokens : [];
483
- const fromData = designTokensData?.relationships?.byToken;
484
- if (fromData && typeof fromData === 'object') {
485
- for (const [rawName, rawRel] of Object.entries(fromData)) {
486
- const name = normalizeTokenIdentifier(rawName);
487
- if (!name) continue;
488
- const refs = Array.isArray(rawRel?.references)
489
- ? rawRel.references.map((r) => normalizeTokenIdentifier(r)).filter(Boolean)
490
- : [];
491
- const referencedBy = Array.isArray(rawRel?.referencedBy)
492
- ? rawRel.referencedBy.map((r) => normalizeTokenIdentifier(r)).filter(Boolean)
493
- : [];
494
- byToken[name] = {
495
- references: [...new Set(refs)].sort(),
496
- referencedBy: [...new Set(referencedBy)].sort(),
497
- };
498
- }
499
- }
500
-
501
- if (Object.keys(byToken).length > 0) {
502
- return { byToken };
503
- }
504
-
505
- for (const token of tokens) {
506
- const name = normalizeTokenIdentifier(token?.name);
507
- if (!name) continue;
508
- if (!byToken[name]) byToken[name] = { references: [], referencedBy: [] };
509
- const refs = extractReferencedTokenNames(token?.value);
510
- byToken[name].references = refs;
511
- }
512
-
513
- for (const [sourceName, relation] of Object.entries(byToken)) {
514
- for (const refName of relation.references) {
515
- if (!byToken[refName]) byToken[refName] = { references: [], referencedBy: [] };
516
- byToken[refName].referencedBy.push(sourceName);
517
- }
518
- }
519
-
520
- for (const relation of Object.values(byToken)) {
521
- relation.references = [...new Set(relation.references)].sort();
522
- relation.referencedBy = [...new Set(relation.referencedBy)].sort();
523
- }
524
-
525
- return { byToken };
526
- }
527
-
528
- /**
529
- * Extract which components reference which design tokens via var() in CEM cssProperties.
530
- * Returns Map<tokenName, Set<componentTagName>>.
531
- * DD-25: var(--token, fallback) fallback values are not extracted (known limitation).
532
- */
533
- export function buildComponentTokenReferencedBy(manifest) {
534
- const result = new Map();
535
- const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
536
- const varRe = /var\((--[\w-]+)/g;
537
- for (const mod of modules) {
538
- const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
539
- for (const decl of declarations) {
540
- const tag = decl?.tagName;
541
- if (typeof tag !== 'string') continue;
542
- const cssProps = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
543
- for (const prop of cssProps) {
544
- const defaultVal = typeof prop?.default === 'string' ? prop.default : '';
545
- let m;
546
- while ((m = varRe.exec(defaultVal)) !== null) {
547
- const tokenName = normalizeTokenIdentifier(m[1]);
548
- if (!tokenName) continue;
549
- if (!result.has(tokenName)) result.set(tokenName, new Set());
550
- result.get(tokenName).add(tag);
551
- }
552
- // Also index the css property name itself as a token → component mapping
553
- const propName = normalizeTokenIdentifier(prop?.name);
554
- if (propName) {
555
- if (!result.has(propName)) result.set(propName, new Set());
556
- result.get(propName).add(tag);
557
- }
558
- }
559
- }
560
- }
561
- return result;
562
- }
563
-
564
- function toTokenSummary(token) {
565
- return {
566
- name: String(token?.name ?? ''),
567
- value: String(token?.value ?? ''),
568
- type: String(token?.type ?? ''),
569
- category: String(token?.category ?? ''),
570
- cssVariable: String(token?.cssVariable ?? ''),
571
- };
572
- }
573
-
574
- export function suggestTokenNames(targetName, tokens, maxSuggestions = 5) {
575
- const target = normalizeTokenIdentifier(targetName);
576
- if (!target) return [];
577
- const allNames = [...new Set(tokens
578
- .map((token) => normalizeTokenIdentifier(token?.name))
579
- .filter(Boolean))];
580
-
581
- const startsWith = allNames.filter((name) => name.startsWith(target));
582
- if (startsWith.length >= maxSuggestions) return startsWith.slice(0, maxSuggestions);
583
-
584
- const includes = allNames.filter((name) => name.includes(target) && !startsWith.includes(name));
585
- const ranked = allNames
586
- .filter((name) => !startsWith.includes(name) && !includes.includes(name))
587
- .map((name) => ({ name, distance: levenshteinDistance(target, name) }))
588
- .sort((left, right) => left.distance - right.distance || left.name.localeCompare(right.name))
589
- .map((entry) => entry.name);
590
-
591
- return [...startsWith, ...includes, ...ranked].slice(0, maxSuggestions);
592
- }
593
-
594
- function buildUsageExamples(token) {
595
- const cssVar = String(token?.cssVariable ?? '');
596
- const type = String(token?.type ?? '').toLowerCase();
597
- if (!cssVar) return [];
598
- if (type === 'color') {
599
- return [
600
- `.example { color: ${cssVar}; }`,
601
- `.example { background-color: ${cssVar}; }`,
602
- ];
603
- }
604
- if (type === 'spacing') {
605
- return [
606
- `.example { padding: ${cssVar}; }`,
607
- `.example { gap: ${cssVar}; }`,
608
- ];
609
- }
610
- if (type === 'typography') {
611
- return [
612
- `.example { font-size: ${cssVar}; }`,
613
- `.example { line-height: ${cssVar}; }`,
614
- ];
615
- }
616
- if (type === 'radius') {
617
- return [`.example { border-radius: ${cssVar}; }`];
618
- }
619
- if (type === 'shadow') {
620
- return [`.example { box-shadow: ${cssVar}; }`];
621
- }
622
- return [`.example { --token-value: ${cssVar}; }`];
623
- }
624
-
625
- function buildTokenErrorPayload(code, message, extra = {}) {
626
- return {
627
- isError: true,
628
- payload: {
629
- error: { code, message },
630
- ...extra,
631
- },
632
- };
633
- }
634
-
635
- export function buildDesignTokenDetailPayload(designTokensData, name, theme) {
636
- if (!Array.isArray(designTokensData?.tokens)) {
637
- return buildTokenErrorPayload(
638
- 'DESIGN_TOKENS_DATA_UNAVAILABLE',
639
- 'Design tokens data not available. Run: npm run mcp:extract-tokens',
640
- );
641
- }
642
-
643
- const themeInfo = resolveTokenTheme(theme);
644
- if (!themeInfo.ok) {
645
- return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
646
- }
647
-
648
- const normalizedName = normalizeTokenIdentifier(name);
649
- if (!normalizedName) {
650
- return buildTokenErrorPayload('INVALID_TOKEN_INPUT', 'Token name is required.');
651
- }
652
-
653
- const tokens = designTokensData.tokens;
654
- const token = tokens.find((item) => normalizeTokenIdentifier(item?.name) === normalizedName);
655
- if (!token) {
656
- return buildTokenErrorPayload(
657
- 'TOKEN_NOT_FOUND',
658
- `Token not found: ${normalizedName}`,
659
- { suggestions: suggestTokenNames(normalizedName, tokens) },
660
- );
661
- }
662
-
663
- const relationshipIndex = buildTokenRelationshipIndex(designTokensData);
664
- const relation = relationshipIndex.byToken[normalizedName] ?? { references: [], referencedBy: [] };
665
- const tokenByName = new Map(tokens
666
- .map((item) => [normalizeTokenIdentifier(item?.name), item])
667
- .filter(([tokenName]) => tokenName));
668
- const references = relation.references
669
- .map((tokenName) => tokenByName.get(tokenName))
670
- .filter(Boolean)
671
- .map(toTokenSummary);
672
- const referencedBy = relation.referencedBy
673
- .map((tokenName) => tokenByName.get(tokenName))
674
- .filter(Boolean)
675
- .map(toTokenSummary);
676
- const relatedTokens = referencedBy
677
- .filter((item) => String(item.category).toLowerCase() === 'semantic')
678
- .map((item) => item.name);
679
-
680
- return {
681
- isError: false,
682
- payload: {
683
- token: {
684
- ...toTokenSummary(token),
685
- group: token?.group ?? null,
686
- },
687
- references,
688
- referencedBy,
689
- relatedTokens,
690
- usageExamples: buildUsageExamples(token),
691
- theme: {
692
- requested: themeInfo.requested,
693
- resolved: themeInfo.resolved,
694
- available: themeInfo.available,
695
- },
696
- },
697
- };
698
- }
699
-
700
- export function buildDesignTokensPayload(designTokensData, { type, category, query, theme } = {}) {
701
- if (!designTokensData) {
702
- return buildTokenErrorPayload(
703
- 'DESIGN_TOKENS_DATA_UNAVAILABLE',
704
- 'Design tokens data not available. Run: npm run mcp:extract-tokens',
705
- );
706
- }
707
-
708
- const themeInfo = resolveTokenTheme(theme);
709
- if (!themeInfo.ok) {
710
- return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
711
- }
712
-
713
- let tokens = Array.isArray(designTokensData.tokens) ? designTokensData.tokens : [];
714
- if (type) tokens = tokens.filter((t) => t.type === type);
715
- if (category) tokens = tokens.filter((t) => t.category === category);
716
- if (query) {
717
- const q = String(query).toLowerCase();
718
- tokens = tokens.filter((t) => String(t.name ?? '').toLowerCase().includes(q));
719
- }
720
-
721
- return {
722
- isError: false,
723
- payload: {
724
- total: tokens.length,
725
- tokens,
726
- summary: designTokensData.summary,
727
- theme: {
728
- requested: themeInfo.requested,
729
- resolved: themeInfo.resolved,
730
- available: themeInfo.available,
731
- },
732
- },
733
- };
734
- }
735
-
736
- function isPlainObject(value) {
737
- return value !== null && typeof value === 'object' && !Array.isArray(value);
738
- }
739
-
740
- function toPluginErrorMessage(name, reason) {
741
- return `Invalid plugin (${name}): ${reason}`;
742
- }
743
-
744
- /**
745
- * Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
746
- * @typedef {{
747
- * fileName: string,
748
- * path: string,
749
- * }} WcfMcpDataSourceConfig
750
- */
751
-
752
- /**
753
- * Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
754
- * @typedef {{
755
- * name: string,
756
- * description?: string,
757
- * inputSchema?: Record<string, unknown>,
758
- * handler?: (args: Record<string, unknown>, context: { plugin: { name: string, version: string }, helpers: { loadJsonData: Function, loadTextData: Function } }) => unknown,
759
- * staticPayload?: unknown,
760
- * }} WcfMcpPluginTool
761
- */
762
-
763
- /**
764
- * Plugin contract v1 — stable interface. See docs/plugin-contract-v1.md.
765
- * @typedef {{
766
- * name: string,
767
- * version: string,
768
- * tools?: WcfMcpPluginTool[],
769
- * dataSources?: WcfMcpDataSourceConfig[],
770
- * }} WcfMcpPlugin
771
- */
772
-
773
- function normalizePluginDataSources(pluginName, dataSources) {
774
- if (!Array.isArray(dataSources)) return [];
775
- const out = [];
776
- for (const entry of dataSources) {
777
- if (!isPlainObject(entry)) {
778
- throw new Error(toPluginErrorMessage(pluginName, 'dataSources entries must be objects'));
779
- }
780
- const fileName = String(entry.fileName ?? '').trim();
781
- const sourcePath = String(entry.path ?? '').trim();
782
- if (!fileName || !sourcePath) {
783
- throw new Error(toPluginErrorMessage(pluginName, 'dataSources entries require fileName and path'));
784
- }
785
- if (!PLUGIN_DATA_SOURCE_KEYS.has(fileName)) {
786
- throw new Error(toPluginErrorMessage(pluginName, `unsupported data source key: ${fileName}`));
787
- }
788
- out.push({ fileName, path: sourcePath });
789
- }
790
- return out;
791
- }
792
-
793
- function normalizePluginTools(pluginName, tools) {
794
- if (!Array.isArray(tools)) return [];
795
- const out = [];
796
- for (const rawTool of tools) {
797
- if (!isPlainObject(rawTool)) {
798
- throw new Error(toPluginErrorMessage(pluginName, 'tools entries must be objects'));
799
- }
800
- const name = String(rawTool.name ?? '').trim();
801
- if (!name) throw new Error(toPluginErrorMessage(pluginName, 'tool.name is required'));
802
- const hasHandler = typeof rawTool.handler === 'function';
803
- const hasStaticPayload = Object.prototype.hasOwnProperty.call(rawTool, 'staticPayload');
804
- if (!hasHandler && !hasStaticPayload) {
805
- throw new Error(toPluginErrorMessage(pluginName, `tool "${name}" needs handler or staticPayload`));
806
- }
807
- // When both are specified, handler takes priority (contract v1: handler-wins)
808
- // staticPayload is ignored silently.
809
- const description = String(rawTool.description ?? '').trim() ||
810
- `Plugin tool provided by ${pluginName}. ${PLUGIN_TOOL_NOTICE}`;
811
- const inputSchema = isPlainObject(rawTool.inputSchema) ? rawTool.inputSchema : {};
812
- out.push({
813
- name,
814
- description,
815
- inputSchema,
816
- handler: hasHandler ? rawTool.handler : undefined,
817
- staticPayload: hasStaticPayload ? rawTool.staticPayload : undefined,
818
- });
819
- }
820
- return out;
821
- }
822
-
823
- export function normalizePlugins(plugins = []) {
824
- if (!Array.isArray(plugins)) throw new Error('Invalid plugin configuration: plugins must be an array');
825
- const normalized = [];
826
- const seenPluginNames = new Set();
827
- const seenToolNames = new Set(BUILTIN_TOOL_NAMES);
828
-
829
- for (const rawPlugin of plugins) {
830
- if (!isPlainObject(rawPlugin)) throw new Error('Invalid plugin configuration: each plugin must be an object');
831
- const name = String(rawPlugin.name ?? '').trim();
832
- const version = String(rawPlugin.version ?? '').trim();
833
- if (!name || !version) throw new Error('Invalid plugin configuration: plugin.name and plugin.version are required');
834
- if (seenPluginNames.has(name)) throw new Error(`Duplicate plugin name: ${name}`);
835
- seenPluginNames.add(name);
836
-
837
- const tools = normalizePluginTools(name, rawPlugin.tools);
838
- for (const tool of tools) {
839
- if (seenToolNames.has(tool.name)) {
840
- throw new Error(toPluginErrorMessage(name, `tool name collision: ${tool.name}`));
841
- }
842
- seenToolNames.add(tool.name);
843
- }
844
-
845
- const dataSources = normalizePluginDataSources(name, rawPlugin.dataSources);
846
- normalized.push({ name, version, tools, dataSources });
847
- }
848
-
849
- return normalized;
850
- }
851
-
852
- export function buildPluginDataSourceMap(plugins = []) {
853
- const out = new Map();
854
- for (const plugin of plugins) {
855
- const pluginName = String(plugin?.name ?? 'unknown-plugin');
856
- const dataSources = Array.isArray(plugin?.dataSources) ? plugin.dataSources : [];
857
- for (const source of dataSources) {
858
- const fileName = String(source?.fileName ?? '').trim();
859
- const sourcePath = String(source?.path ?? '').trim();
860
- if (!fileName || !sourcePath) continue;
861
- if (out.has(fileName)) {
862
- const prev = out.get(fileName);
863
- throw new Error(`Duplicate data source override for ${fileName} (${prev.pluginName}, ${pluginName})`);
864
- }
865
- out.set(fileName, { path: sourcePath, pluginName });
866
- }
867
- }
868
- return out;
869
- }
870
-
871
- // ---------------------------------------------------------------------------
872
- // Helpers (exported for testing)
873
- // ---------------------------------------------------------------------------
874
-
875
- export function getCategory(tagName) {
876
- return CATEGORY_MAP[tagName] ?? 'Other';
877
- }
878
-
879
- function normalizePrefixRaw(prefix) {
880
- if (typeof prefix !== 'string' || prefix.trim() === '') return CANONICAL_PREFIX;
881
- return prefix.trim().toLowerCase();
882
- }
883
-
884
- export function normalizePrefix(prefix) {
885
- return normalizePrefixRaw(prefix).slice(0, MAX_PREFIX_LENGTH);
886
- }
887
-
888
- export function withPrefix(tagName, prefix) {
889
- if (typeof tagName !== 'string') return tagName;
890
- const p = normalizePrefix(prefix);
891
- if (p === CANONICAL_PREFIX) return tagName;
892
- const from = `${CANONICAL_PREFIX}-`;
893
- if (!tagName.startsWith(from)) return tagName;
894
- return `${p}-${tagName.slice(from.length)}`;
895
- }
896
-
897
- export function toCanonicalTagName(tagName, prefix) {
898
- if (typeof tagName !== 'string') return undefined;
899
- const raw = tagName.trim().toLowerCase();
900
- if (!raw) return undefined;
901
- if (raw.startsWith(`${CANONICAL_PREFIX}-`)) return raw;
902
-
903
- const candidates = [...new Set([normalizePrefix(prefix), normalizePrefixRaw(prefix)])];
904
- for (const p of candidates) {
905
- if (p !== CANONICAL_PREFIX && raw.startsWith(`${p}-`)) {
906
- return `${CANONICAL_PREFIX}-${raw.slice(p.length + 1)}`;
907
- }
908
- }
909
-
910
- return raw;
911
- }
912
-
913
- export function levenshteinDistance(left, right) {
914
- const a = String(left ?? '');
915
- const b = String(right ?? '');
916
- if (a === b) return 0;
917
- if (!a.length) return b.length;
918
- if (!b.length) return a.length;
919
-
920
- const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
921
- const curr = Array.from({ length: b.length + 1 }, () => 0);
922
-
923
- for (let i = 1; i <= a.length; i += 1) {
924
- curr[0] = i;
925
- for (let j = 1; j <= b.length; j += 1) {
926
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
927
- curr[j] = Math.min(
928
- curr[j - 1] + 1,
929
- prev[j] + 1,
930
- prev[j - 1] + cost,
931
- );
932
- }
933
- for (let j = 0; j <= b.length; j += 1) prev[j] = curr[j];
934
- }
935
-
936
- return prev[b.length];
937
- }
938
-
939
- export function suggestUnknownElementTagName(tagName, cemIndex, prefix) {
940
- const target = String(tagName ?? '').trim().toLowerCase();
941
- if (!target || !target.includes('-')) return undefined;
942
-
943
- // Try prefix-prepend before Levenshtein (e.g. input-text → dads-input-text)
944
- if (prefix && cemIndex instanceof Map) {
945
- const prefixed = `${String(prefix).toLowerCase()}-${target}`;
946
- if (cemIndex.has(prefixed)) return prefixed;
947
- }
948
-
949
- let bestTag;
950
- let bestDistance = Number.POSITIVE_INFINITY;
951
- const candidateSource = cemIndex instanceof Map ? cemIndex.keys() : [];
952
- for (const rawCandidate of candidateSource) {
953
- const candidate = String(rawCandidate ?? '').toLowerCase();
954
- if (!candidate || !candidate.includes('-') || candidate === target) continue;
955
- const distance = levenshteinDistance(target, candidate);
956
- if (distance < bestDistance) {
957
- bestDistance = distance;
958
- bestTag = candidate;
959
- }
960
- }
961
-
962
- if (!bestTag) return undefined;
963
- const maxDistance = Math.max(1, Math.ceil(target.length * 0.3));
964
- if (bestDistance > maxDistance) return undefined;
965
- return bestTag;
966
- }
967
-
968
- export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
969
- const code = String(diagnostic?.code ?? '');
970
- if (!code) return undefined;
971
-
972
- if (code === 'unknownElement') {
973
- const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex, prefix);
974
- return tagName ? `Did you mean "${tagName}"?` : undefined;
975
- }
976
-
977
- if (code === 'canonicalLowercaseRecommendation') {
978
- return diagnostic?.hint ?? undefined;
979
- }
980
-
981
- if (code === 'forbiddenAttribute' && String(diagnostic?.attrName ?? '').toLowerCase() === 'placeholder') {
982
- return 'Use aria-label or aria-describedby support text instead of placeholder.';
983
- }
984
-
985
- if (code === 'ariaLiveNotRecommended') {
986
- return 'Remove aria-live and connect support or error text via aria-describedby.';
987
- }
988
-
989
- if (code === 'roleAlertNotRecommended') {
990
- return 'Use role="alert" only for urgent live updates; otherwise use static text associated via aria-describedby.';
991
- }
992
-
993
- if (code === 'emptyLabel') {
994
- return diagnostic?.hint ?? 'Provide a meaningful label value for accessibility.';
995
- }
996
-
997
- if (code === 'emptyAriaLabel') {
998
- return diagnostic?.hint ?? 'Provide a meaningful aria-label value or use a visible <label> element.';
999
- }
1000
-
1001
- return undefined;
1002
- }
1003
-
1004
- export function findCustomElementDeclarations(manifest) {
1005
- const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
1006
- const decls = [];
1007
-
1008
- for (const mod of modules) {
1009
- const modulePath = typeof mod?.path === 'string' ? mod.path : undefined;
1010
- const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
1011
- for (const decl of declarations) {
1012
- if (!decl || typeof decl !== 'object') continue;
1013
- const tagName = typeof decl.tagName === 'string' ? decl.tagName : undefined;
1014
- const isCustomElement = decl.customElement === true || decl.kind === 'custom-element';
1015
- if (!isCustomElement || !tagName) continue;
1016
-
1017
- decls.push({ decl, tagName: tagName.toLowerCase(), modulePath });
1018
- }
1019
- }
1020
-
1021
- return decls;
1022
- }
1023
-
1024
- export function buildIndexes(manifest) {
1025
- const decls = findCustomElementDeclarations(manifest);
1026
-
1027
- const byTag = new Map();
1028
- const byClass = new Map();
1029
- const modulePathByTag = new Map();
1030
-
1031
- for (const { decl, tagName, modulePath } of decls) {
1032
- if (!byTag.has(tagName)) byTag.set(tagName, decl);
1033
- if (typeof decl?.name === 'string' && !byClass.has(decl.name)) byClass.set(decl.name, decl);
1034
- if (!modulePathByTag.has(tagName)) modulePathByTag.set(tagName, modulePath);
1035
- }
1036
-
1037
- return { byTag, byClass, modulePathByTag, decls };
1038
- }
1039
-
1040
- /**
1041
- * Extracts the primary component prefix from CEM indexes.
1042
- * Returns the most common prefix among all tagNames (e.g. 'dads' from 'dads-button').
1043
- * Falls back to CANONICAL_PREFIX if no tagNames are found.
1044
- */
1045
- export function extractPrefixFromIndexes(indexes) {
1046
- const counts = new Map();
1047
- for (const { tagName } of indexes.decls) {
1048
- const i = tagName.indexOf('-');
1049
- if (i > 0) {
1050
- const p = tagName.slice(0, i);
1051
- counts.set(p, (counts.get(p) ?? 0) + 1);
1052
- }
1053
- }
1054
- let best = CANONICAL_PREFIX;
1055
- let bestCount = 0;
1056
- for (const [p, c] of counts) {
1057
- if (c > bestCount) { best = p; bestCount = c; }
1058
- }
1059
- return best;
1060
- }
1061
-
1062
- /**
1063
- * Build a full HTML page from a fragment.
1064
- * @param {{ html: string; prefix: string; cemIndex: Map }} params
1065
- */
1066
- export function buildFullPageHtml({ html, prefix, cemIndex }) {
1067
- // Extract custom element tags from the HTML fragment
1068
- const tagRe = /<([a-z][a-z0-9]*-[a-z0-9-]*)\b/gi;
1069
- const tags = new Set();
1070
- let m;
1071
- while ((m = tagRe.exec(html))) {
1072
- tags.add(String(m[1]).toLowerCase());
1073
- }
1074
-
1075
- // Build import map entries for recognized components
1076
- const importEntries = {};
1077
- for (const tag of tags) {
1078
- if (cemIndex.has(tag)) {
1079
- const suffix = tag.replace(/^[^-]+-/, '');
1080
- importEntries[tag] = `./<dir>/components/${suffix}.js`;
1081
- }
1082
- }
1083
-
1084
- const importMapJson = JSON.stringify({ imports: importEntries }, null, 2);
1085
-
1086
- const lines = [
1087
- '<!DOCTYPE html>',
1088
- `<html lang="ja">`,
1089
- '<head>',
1090
- ' <meta charset="utf-8">',
1091
- ' <meta name="viewport" content="width=device-width, initial-scale=1">',
1092
- ` <title>WCF Preview</title>`,
1093
- ` <link rel="stylesheet" href="./<dir>/styles/tokens.css">`,
1094
- ` <script type="importmap">`,
1095
- importMapJson,
1096
- ` </script>`,
1097
- '</head>',
1098
- '<body>',
1099
- html,
1100
- ` <script type="module" src="./<dir>/boot.js"></script>`,
1101
- '</body>',
1102
- '</html>',
1103
- ];
1104
-
1105
- return { fullHtml: lines.join('\n'), importEntries };
1106
- }
1107
-
1108
- export function pickDecl({ byTag, byClass }, { tagName, className, prefix }) {
1109
- if (typeof tagName === 'string' && tagName.trim() !== '') {
1110
- const canonical = toCanonicalTagName(tagName, prefix);
1111
- if (canonical && byTag.has(canonical)) return byTag.get(canonical);
1112
- }
1113
-
1114
- if (typeof className === 'string' && className.trim() !== '' && byClass.has(className.trim())) {
1115
- return byClass.get(className.trim());
1116
- }
1117
-
1118
- return undefined;
1119
- }
1120
-
1121
- export function serializeApi(decl, modulePath, prefix) {
1122
- const tagName = typeof decl?.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
1123
- const outTag = tagName ? withPrefix(tagName, prefix) : undefined;
1124
-
1125
- const attributes = Array.isArray(decl?.attributes) ? decl.attributes : [];
1126
- const slots = Array.isArray(decl?.slots) ? decl.slots : [];
1127
- const events = Array.isArray(decl?.events) ? decl.events : [];
1128
- const cssParts = Array.isArray(decl?.cssParts) ? decl.cssParts : [];
1129
- const cssProperties = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
1130
-
1131
- return {
1132
- tagName: outTag,
1133
- className: typeof decl?.name === 'string' ? decl.name : undefined,
1134
- description: typeof decl?.description === 'string' ? decl.description : undefined,
1135
- modulePath,
1136
- custom: decl?.custom,
1137
- attributes: attributes.map((a) => ({
1138
- name: a?.name,
1139
- type: a?.type?.text,
1140
- default: a?.default ?? null,
1141
- description: a?.description,
1142
- inheritedFrom: a?.inheritedFrom,
1143
- deprecated: a?.deprecated,
1144
- })),
1145
- slots: slots.map((s) => ({
1146
- name: s?.name,
1147
- description: s?.description,
1148
- })),
1149
- events: events.map((e) => ({
1150
- name: e?.name,
1151
- type: e?.type?.text,
1152
- description: e?.description,
1153
- inheritedFrom: e?.inheritedFrom,
1154
- deprecated: e?.deprecated,
1155
- })),
1156
- cssParts: cssParts.map((p) => ({
1157
- name: p?.name,
1158
- description: p?.description,
1159
- })),
1160
- cssProperties: cssProperties.map((p) => ({
1161
- name: p?.name,
1162
- default: p?.default,
1163
- description: p?.description,
1164
- })),
1165
- };
1166
- }
1167
-
1168
- /**
1169
- * Generic fallback values for common attributes when CEM default is missing.
1170
- * Attribute-name-based (not component-specific). `type` is excluded to avoid
1171
- * conflicts between button (type="button") and input (type="text").
1172
- * `variant` is also excluded — its valid values differ per component,
1173
- * so the first enum value is used instead (see generateSnippet).
1174
- */
1175
- const SNIPPET_FALLBACK_VALUES = {
1176
- label: 'ラベル',
1177
- name: 'field1',
1178
- value: 'サンプル値',
1179
- 'support-text': '説明テキスト',
1180
- };
1181
-
1182
- export function generateSnippet(api, prefix) {
1183
- // Use custom snippet if injected by CEM plugin (e.g. data-* driven components)
1184
- const customSnippet = api.custom?.usageSnippet;
1185
- if (typeof customSnippet === 'string' && customSnippet.trim()) {
1186
- const p = normalizePrefix(prefix);
1187
- if (p !== CANONICAL_PREFIX) {
1188
- return customSnippet.replace(
1189
- new RegExp(`<\\s*(\\/?)\\s*${CANONICAL_PREFIX}-([a-z0-9-]+)(?=[\\s/>])`, 'gi'),
1190
- (_m, slash, rest) => `<${slash ?? ''}${p}-${String(rest).toLowerCase()}`,
1191
- );
1192
- }
1193
- return customSnippet;
1194
- }
1195
-
1196
- const tag = api.tagName ?? withPrefix(String(api.className ?? 'dads-component'), prefix);
1197
- const attrs = Array.isArray(api.attributes) ? api.attributes : [];
1198
- const slots = Array.isArray(api.slots) ? api.slots : [];
1199
-
1200
- const attrPriority = [
1201
- 'label',
1202
- 'support-text',
1203
- 'value',
1204
- 'name',
1205
- 'type',
1206
- 'variant',
1207
- 'size',
1208
- 'required',
1209
- 'disabled',
1210
- 'readonly',
1211
- ];
1212
-
1213
- const attrByName = new Map(attrs.map((a) => [String(a?.name ?? ''), a]));
1214
- const lines = [];
1215
-
1216
- for (const name of attrPriority) {
1217
- const a = attrByName.get(name);
1218
- if (!a) continue;
1219
- const t = String(a.type ?? '').toLowerCase();
1220
- const isBoolean = t.includes('boolean');
1221
- if (isBoolean) {
1222
- lines.push(` ${name}`);
1223
- } else {
1224
- let defaultVal;
1225
- if (typeof a.default === 'string') {
1226
- defaultVal = a.default.replace(/^['"]|['"]$/g, '');
1227
- } else if (SNIPPET_FALLBACK_VALUES[name] !== undefined) {
1228
- defaultVal = SNIPPET_FALLBACK_VALUES[name];
1229
- } else {
1230
- // For enum types, use the first enum value as fallback
1231
- const enumMatch = t.match(/^'([^']+)'/);
1232
- if (enumMatch) {
1233
- defaultVal = enumMatch[1];
1234
- } else {
1235
- // Fallback: extract first value from description pattern like "(solid | outlined | text)"
1236
- const desc = String(a.description ?? '');
1237
- const descEnum = desc.match(/\(([^)]+)\)/);
1238
- if (descEnum) {
1239
- const first = descEnum[1].split(/\s*[||]\s*/)[0]?.trim();
1240
- defaultVal = first || '';
1241
- } else {
1242
- defaultVal = '';
1243
- }
1244
- }
1245
- }
1246
- lines.push(` ${name}="${defaultVal}"`);
1247
- }
1248
- if (lines.length >= 4) break;
1249
- }
1250
-
1251
- const open = lines.length > 0 ? `<${tag}\n${lines.join('\n')}\n>` : `<${tag}>`;
1252
- const slotNames = slots
1253
- .map((s) => String(s?.name ?? '').trim())
1254
- .filter((s) => s !== '');
1255
- const slotComment =
1256
- slotNames.length > 0 ? `\n <!-- slots: ${slotNames.join(', ')} -->\n` : '\n';
1257
-
1258
- return `${open}${slotComment}</${tag}>`;
1259
- }
1260
-
1261
- export function findDeclByComponentId(indexes, componentIdRaw) {
1262
- const componentId = typeof componentIdRaw === 'string' ? componentIdRaw.trim() : '';
1263
- if (!componentId) return undefined;
1264
- for (const { decl, modulePath } of indexes.decls) {
1265
- const installId = decl?.custom?.install?.id;
1266
- const inferredId = decl?.custom?.componentId;
1267
- const id = typeof installId === 'string' ? installId : typeof inferredId === 'string' ? inferredId : undefined;
1268
- if (id === componentId) return { decl, modulePath };
1269
- }
1270
- return undefined;
1271
- }
1272
-
1273
- /**
1274
- * Generic helper: remap tag-keyed Map to a different prefix.
1275
- * Used by validate_markup to build prefix-aware CEM/enum/slot maps.
1276
- */
1277
- export function applyPrefixToTagMap(map, prefix) {
1278
- const p = normalizePrefix(prefix);
1279
- if (p === CANONICAL_PREFIX) return map;
1280
-
1281
- const out = new Map();
1282
- for (const [tag, value] of map.entries()) {
1283
- out.set(withPrefix(tag, p), value);
1284
- }
1285
- return out;
1286
- }
1287
-
1288
- function mergeWithPrefixed(canonicalMap, prefix) {
1289
- const prefixed = applyPrefixToTagMap(canonicalMap, prefix);
1290
- if (prefixed === canonicalMap) return canonicalMap;
1291
- const combined = new Map(canonicalMap);
1292
- for (const [k, v] of prefixed.entries()) combined.set(k, v);
1293
- return combined;
1294
- }
1295
-
1296
- export function applyPrefixToHtml(html, prefix) {
1297
- const p = normalizePrefix(prefix);
1298
- if (p === CANONICAL_PREFIX) return String(html ?? '');
1299
- const from = `${CANONICAL_PREFIX}-`;
1300
- const to = `${p}-`;
1301
-
1302
- return String(html ?? '').replace(
1303
- new RegExp(`<\\s*(\\/?)\\s*${from}([a-z0-9-]+)(?=[\\s/>])`, 'gi'),
1304
- (_m, slash, rest) => `<${slash ?? ''}${to}${String(rest).toLowerCase()}`,
1305
- );
1306
- }
1307
-
1308
- export function loadPatternRegistryShape(raw) {
1309
- if (!raw || typeof raw !== 'object') return { patterns: {} };
1310
- const patterns = raw.patterns && typeof raw.patterns === 'object' ? raw.patterns : {};
1311
- return { patterns };
1312
- }
1313
-
1314
- export function resolveComponentClosure({ installRegistry }, componentIds) {
1315
- const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
1316
- const queue = [...new Set(componentIds.map((c) => String(c ?? '').trim()).filter(Boolean))];
1317
- const out = new Set();
1318
-
1319
- while (queue.length > 0) {
1320
- const id = queue.shift();
1321
- if (!id || out.has(id)) continue;
1322
- out.add(id);
1323
-
1324
- const meta = components[id];
1325
- const deps = Array.isArray(meta?.deps) ? meta.deps : [];
1326
- for (const d of deps) {
1327
- const dep = String(d ?? '').trim();
1328
- if (dep && !out.has(dep)) queue.push(dep);
1329
- }
1330
- }
1331
-
1332
- return [...out];
1333
- }
1334
-
1335
- /**
1336
- * Build a frequency map: componentId → count of patterns that require it.
1337
- * Counts from pattern-registry.json `requires` arrays only.
1338
- */
1339
- export function buildPatternFrequencyMap(patterns) {
1340
- const freq = new Map();
1341
- if (!patterns || typeof patterns !== 'object') return freq;
1342
- for (const pat of Object.values(patterns)) {
1343
- const requires = Array.isArray(pat?.requires) ? pat.requires : [];
1344
- for (const id of requires) {
1345
- const key = String(id ?? '').trim();
1346
- if (key) freq.set(key, (freq.get(key) ?? 0) + 1);
1347
- }
1348
- }
1349
- return freq;
1350
- }
1351
-
1352
- /**
1353
- * Convert a tag from the current prefix to canonical prefix using string ops
1354
- * (no regex, safe for arbitrary prefix values).
1355
- */
1356
- function toCanonicalTag(tag, currentPrefix) {
1357
- const cp = `${currentPrefix}-`;
1358
- if (tag.startsWith(cp)) {
1359
- return `${CANONICAL_PREFIX}-${tag.slice(cp.length)}`;
1360
- }
1361
- return tag;
1362
- }
1363
-
1364
- export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency } = {}) {
1365
- const p = normalizePrefix(prefix);
1366
- const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
1367
- const limitExplicit = Number.isInteger(limit);
1368
- const pageSize = limitExplicit ? Math.max(1, Math.min(limit, 200)) : 20;
1369
- const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
1370
-
1371
- let items = indexes.decls.map(({ decl, tagName, modulePath }) => ({
1372
- tagName: withPrefix(tagName, p),
1373
- className: typeof decl?.name === 'string' ? decl.name : undefined,
1374
- description: typeof decl?.description === 'string' ? decl.description : undefined,
1375
- category: getCategory(tagName),
1376
- modulePath,
1377
- }));
1378
-
1379
- // patternId filter: restrict to components required by a specific pattern
1380
- if (typeof patternId === 'string' && patternId.trim()) {
1381
- const pats = patterns && typeof patterns === 'object' ? patterns : {};
1382
- const pat = pats[patternId.trim()];
1383
- if (pat && Array.isArray(pat.requires)) {
1384
- const requiredIds = new Set(pat.requires.map((r) => String(r ?? '').trim()).filter(Boolean));
1385
- const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
1386
- items = items.filter((item) => {
1387
- // Map tagName to componentId via install registry
1388
- const canonicalTag = toCanonicalTag(item.tagName, p);
1389
- const componentId = tags[canonicalTag];
1390
- return componentId && requiredIds.has(componentId);
1391
- });
1392
- } else {
1393
- items = [];
1394
- }
1395
- }
1396
-
1397
- if (category) {
1398
- items = items.filter((item) => item.category === category);
1399
- }
1400
-
1401
- if (q) {
1402
- items = items.filter((item) => {
1403
- const haystacks = [
1404
- item.tagName,
1405
- item.className,
1406
- item.description,
1407
- item.category,
1408
- item.modulePath,
1409
- ];
1410
- return haystacks.some((value) => String(value ?? '').toLowerCase().includes(q));
1411
- });
1412
- }
1413
-
1414
- // frequency sort: order by pattern usage count descending
1415
- if (sort === 'frequency') {
1416
- const freq = patternFrequency instanceof Map ? patternFrequency : new Map();
1417
- const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
1418
- items = items.map((item) => {
1419
- const canonicalTag = toCanonicalTag(item.tagName, p);
1420
- const componentId = tags[canonicalTag] ?? '';
1421
- return { ...item, frequency: freq.get(componentId) ?? 0 };
1422
- });
1423
- items.sort((a, b) => b.frequency - a.frequency);
1424
- }
1425
-
1426
- const total = items.length;
1427
- const paged = items.slice(pageOffset, pageOffset + pageSize);
1428
-
1429
- const result = {
1430
- total,
1431
- limit: pageSize,
1432
- offset: pageOffset,
1433
- hasMore: pageOffset + paged.length < total,
1434
- items: paged,
1435
- };
1436
-
1437
- // DIG-19: Add migration notice when limit is not explicitly provided
1438
- if (!limitExplicit && total > pageSize) {
1439
- result._notice = 'Default pagination changed to 20 items. Set limit:200 for all results.';
1440
- }
1441
-
1442
- return result;
1443
- }
1444
-
1445
- export function parseIconNamesFromDescription(description) {
1446
- if (typeof description !== 'string' || description.trim() === '') return [];
1447
-
1448
- const markerMatch = description.match(/iconPathsのキー[::]\s*([^))\n]+)/u);
1449
- if (!markerMatch) return [];
1450
-
1451
- return [...new Set(
1452
- markerMatch[1]
1453
- .split(/[,、]/)
1454
- .map((name) => name.trim())
1455
- .map((name) => name.replace(/[`'"]/g, ''))
1456
- .filter(Boolean),
1457
- )];
1458
- }
1459
-
1460
- export function parseIconNamesFromType(typeText) {
1461
- if (typeof typeText !== 'string' || typeText.trim() === '') return [];
1462
- const out = [];
1463
- const regex = /'([^']+)'|"([^"]+)"|`([^`]+)`/g;
1464
- let match;
1465
- while ((match = regex.exec(typeText)) !== null) {
1466
- const value = match[1] ?? match[2] ?? match[3];
1467
- if (typeof value === 'string' && value.trim() !== '') out.push(value.trim());
1468
- }
1469
- return [...new Set(out)];
1470
- }
1471
-
1472
- export function extractIconNames(indexes) {
1473
- const decl = indexes.byTag.get('dads-icon');
1474
- if (!decl) return [];
1475
-
1476
- const attributes = Array.isArray(decl?.attributes) ? decl.attributes : [];
1477
- const nameAttr = attributes.find((attr) => String(attr?.name ?? '') === 'name');
1478
- if (!nameAttr) return [];
1479
-
1480
- const fromDescription = parseIconNamesFromDescription(nameAttr?.description);
1481
- const fromType = parseIconNamesFromType(nameAttr?.type?.text);
1482
-
1483
- return [...new Set([...fromDescription, ...fromType])];
1484
- }
1485
-
1486
- export function buildIconCatalog(indexes, prefix) {
1487
- const p = normalizePrefix(prefix);
1488
- const tag = withPrefix('dads-icon', p);
1489
- const names = extractIconNames(indexes).sort((left, right) => left.localeCompare(right));
1490
-
1491
- return names.map((name) => ({
1492
- name,
1493
- variants: ['default'],
1494
- usageExample: `<${tag} name="${name}" size="20"></${tag}>`,
1495
- }));
1496
- }
1497
-
1498
- export function searchIconCatalog(indexes, { query, limit, offset, prefix } = {}) {
1499
- const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
1500
- const pageSize = Number.isInteger(limit) ? Math.max(1, Math.min(limit, 100)) : 20;
1501
- const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
1502
-
1503
- let icons = buildIconCatalog(indexes, prefix);
1504
- if (q) {
1505
- // Expand query with icon aliases (DD-18)
1506
- const searchTerms = [q];
1507
- const aliases = ICON_ALIAS_TABLE.get(q);
1508
- if (aliases) {
1509
- for (const alias of aliases) {
1510
- if (!searchTerms.includes(alias)) searchTerms.push(alias);
1511
- }
1512
- }
1513
- icons = icons.filter((icon) => {
1514
- const name = icon.name.toLowerCase();
1515
- return searchTerms.some((term) => name.includes(term));
1516
- });
1517
- }
1518
-
1519
- const total = icons.length;
1520
- const paged = icons.slice(pageOffset, pageOffset + pageSize);
1521
-
1522
- return {
1523
- total,
1524
- limit: pageSize,
1525
- offset: pageOffset,
1526
- hasMore: pageOffset + paged.length < total,
1527
- icons: paged,
1528
- };
1529
- }
1530
-
1531
- export function buildRelatedComponentMap(installRegistry, patterns) {
1532
- const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
1533
- const patternList = Object.values(patterns ?? {});
1534
- const related = new Map();
1535
-
1536
- const addRelation = (fromId, toId, via) => {
1537
- const from = String(fromId ?? '').trim();
1538
- const to = String(toId ?? '').trim();
1539
- if (!from || !to || from === to) return;
1540
-
1541
- if (!related.has(from)) related.set(from, new Map());
1542
- const relMap = related.get(from);
1543
- if (!relMap.has(to)) relMap.set(to, new Set());
1544
- relMap.get(to).add(via);
1545
- };
1546
-
1547
- for (const pattern of patternList) {
1548
- const patternId = String(pattern?.id ?? '').trim() || 'pattern';
1549
- const requires = [...new Set((Array.isArray(pattern?.requires) ? pattern.requires : []).map((id) => String(id ?? '').trim()).filter(Boolean))];
1550
-
1551
- for (const fromId of requires) {
1552
- for (const toId of requires) {
1553
- addRelation(fromId, toId, patternId);
1554
- }
1555
- }
1556
- }
1557
-
1558
- for (const [componentId, meta] of Object.entries(components)) {
1559
- const deps = Array.isArray(meta?.deps) ? meta.deps : [];
1560
- for (const dep of deps) {
1561
- const depId = String(dep ?? '').trim();
1562
- addRelation(componentId, depId, 'dependency');
1563
- addRelation(depId, componentId, 'dependencyOf');
1564
- }
1565
- }
1566
-
1567
- return related;
1568
- }
1569
-
1570
- export function getRelatedComponentsForTag({ canonicalTagName, installRegistry, relatedMap, prefix, maxResults = 12 }) {
1571
- const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
1572
- const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
1573
- const componentId = typeof canonicalTagName === 'string' ? tags[canonicalTagName] : undefined;
1574
- if (typeof componentId !== 'string' || componentId === '') return [];
1575
-
1576
- const relMap = relatedMap?.get(componentId);
1577
- if (!relMap) return [];
1578
-
1579
- const out = [];
1580
- for (const [relatedId, via] of relMap.entries()) {
1581
- const relatedMeta = components[relatedId];
1582
- if (!relatedMeta || typeof relatedMeta !== 'object') continue;
1583
-
1584
- const canonicalTags = Array.isArray(relatedMeta.tags)
1585
- ? relatedMeta.tags.map((tag) => String(tag ?? '').toLowerCase()).filter(Boolean)
1586
- : [];
1587
-
1588
- out.push({
1589
- componentId: relatedId,
1590
- tagNames: canonicalTags.map((tag) => withPrefix(tag, prefix)),
1591
- via: [...via],
1592
- });
1593
- }
1594
-
1595
- out.sort((left, right) => String(left.componentId).localeCompare(String(right.componentId)));
1596
- return out.slice(0, Math.max(1, maxResults));
1597
- }
1598
-
1599
- export function normalizeWcagLevel(level) {
1600
- const raw = typeof level === 'string' ? level.trim().toUpperCase() : '';
1601
- if (!raw || raw === 'ALL') return 'all';
1602
- return WCAG_LEVELS.has(raw) ? raw : 'all';
1603
- }
1604
-
1605
- function getWcagLevelForA11yTopic(topic) {
1606
- const key = String(topic ?? '').trim().toLowerCase();
1607
- return A11Y_CATEGORY_LEVEL_MAP[key] ?? 'A';
1608
- }
1609
-
1610
- function toChecklistItemsFromCategories(categories) {
1611
- if (!categories || typeof categories !== 'object') return [];
1612
-
1613
- const out = [];
1614
- for (const [topic, checks] of Object.entries(categories)) {
1615
- if (!Array.isArray(checks)) continue;
1616
- const wcagLevel = getWcagLevelForA11yTopic(topic);
1617
- for (const check of checks) {
1618
- const text = String(check ?? '').trim();
1619
- if (!text) continue;
1620
- out.push({
1621
- topic: String(topic),
1622
- wcagLevel,
1623
- check: text,
1624
- });
1625
- }
1626
- }
1627
- return out;
1628
- }
1629
-
1630
- function toChecklistItemsFromCallouts(callouts) {
1631
- if (!Array.isArray(callouts)) return [];
1632
-
1633
- const out = [];
1634
- for (const callout of callouts) {
1635
- const parts = [
1636
- callout?.title,
1637
- callout?.label,
1638
- callout?.description,
1639
- ...(Array.isArray(callout?.highlights) ? callout.highlights : []),
1640
- ]
1641
- .map((value) => String(value ?? '').trim())
1642
- .filter(Boolean);
1643
-
1644
- if (parts.length === 0) continue;
1645
- out.push({
1646
- topic: 'callouts',
1647
- wcagLevel: getWcagLevelForA11yTopic('callouts'),
1648
- check: parts.join(' — '),
1649
- });
1650
- }
1651
- return out;
1652
- }
1653
-
1654
- export function extractAccessibilityChecklist(decl, { prefix } = {}) {
1655
- const annotations = decl?.custom?.a11yAnnotations;
1656
- if (!annotations || typeof annotations !== 'object') return undefined;
1657
-
1658
- const items = [
1659
- ...toChecklistItemsFromCategories(annotations.categories),
1660
- ...toChecklistItemsFromCallouts(annotations.callouts),
1661
- ];
1662
- if (items.length === 0) return undefined;
1663
-
1664
- const unique = new Map();
1665
- for (const item of items) {
1666
- const key = `${item.topic}|${item.wcagLevel}|${item.check}`;
1667
- if (!unique.has(key)) unique.set(key, item);
1668
- }
1669
-
1670
- return {
1671
- summary: String(annotations.summary ?? '').trim() || 'Component accessibility checklist',
1672
- version: Number.isInteger(annotations.version) ? annotations.version : 1,
1673
- totalChecks: unique.size,
1674
- items: [...unique.values()],
1675
- componentTagName:
1676
- typeof decl?.tagName === 'string' ? withPrefix(decl.tagName.toLowerCase(), prefix) : undefined,
1677
- };
1678
- }
1679
-
1680
- export function buildAccessibilityIndex(indexes, guidelinesIndexData, { prefix } = {}) {
1681
- const out = [];
1682
-
1683
- for (const { decl, tagName } of indexes.decls) {
1684
- const checklist = extractAccessibilityChecklist(decl, { prefix });
1685
- if (!checklist) continue;
1686
- const className = typeof decl?.name === 'string' ? decl.name : undefined;
1687
-
1688
- for (const item of checklist.items) {
1689
- out.push({
1690
- source: 'component',
1691
- componentTagName: withPrefix(tagName, prefix),
1692
- componentClassName: className,
1693
- topic: item.topic,
1694
- wcagLevel: item.wcagLevel,
1695
- check: item.check,
1696
- });
1697
- }
1698
- }
1699
-
1700
- const docs = Array.isArray(guidelinesIndexData?.documents)
1701
- ? guidelinesIndexData.documents.filter((doc) => doc?.topic === 'accessibility')
1702
- : [];
1703
-
1704
- for (const doc of docs) {
1705
- const sections = Array.isArray(doc?.sections) ? doc.sections : [];
1706
- for (const section of sections) {
1707
- const heading = String(section?.heading ?? '').trim();
1708
- const snippet = String(section?.snippet ?? '').trim();
1709
- if (!heading && !snippet) continue;
1710
-
1711
- out.push({
1712
- source: 'guideline',
1713
- documentId: String(doc?.id ?? ''),
1714
- title: String(doc?.title ?? ''),
1715
- heading,
1716
- topic: 'guideline',
1717
- wcagLevel: getWcagLevelForA11yTopic('guideline'),
1718
- check: snippet || heading,
1719
- });
1720
- }
1721
- }
1722
-
1723
- return out;
1724
- }
1725
-
1726
- export function queryAccessibilityIndex(
1727
- entries,
1728
- { componentTagName, topic, wcagLevel, maxResults = 20 } = {},
1729
- ) {
1730
- const normalizedTopic = String(topic ?? '').trim().toLowerCase() || 'all';
1731
- const normalizedWcagLevel = normalizeWcagLevel(wcagLevel);
1732
- const pageSize = Number.isInteger(maxResults) ? Math.max(1, Math.min(maxResults, 100)) : 20;
1733
- const source = Array.isArray(entries) ? entries : [];
1734
- const results = [];
1735
- const shouldBalanceSources = !componentTagName && normalizedTopic === 'all';
1736
- const guidelineCandidates = [];
1737
- const componentCandidates = [];
1738
- const otherCandidates = [];
1739
- let totalHits = 0;
1740
-
1741
- for (const entry of source) {
1742
- if (componentTagName && entry.componentTagName !== componentTagName) continue;
1743
- if (normalizedTopic !== 'all' && String(entry.topic ?? '').toLowerCase() !== normalizedTopic) continue;
1744
- if (normalizedWcagLevel !== 'all' && String(entry.wcagLevel ?? '').toUpperCase() !== normalizedWcagLevel) continue;
1745
-
1746
- totalHits += 1;
1747
- if (!shouldBalanceSources) {
1748
- if (results.length < pageSize) results.push(entry);
1749
- continue;
1750
- }
1751
-
1752
- if (String(entry.source ?? '') === 'guideline') {
1753
- if (guidelineCandidates.length < pageSize) guidelineCandidates.push(entry);
1754
- } else if (String(entry.source ?? '') === 'component') {
1755
- if (componentCandidates.length < pageSize) componentCandidates.push(entry);
1756
- } else if (otherCandidates.length < pageSize) {
1757
- otherCandidates.push(entry);
1758
- }
1759
- }
1760
-
1761
- if (shouldBalanceSources) {
1762
- while (results.length < pageSize) {
1763
- const beforeLength = results.length;
1764
- if (guidelineCandidates.length > 0) results.push(guidelineCandidates.shift());
1765
- if (results.length < pageSize && componentCandidates.length > 0) results.push(componentCandidates.shift());
1766
- if (results.length < pageSize && otherCandidates.length > 0) results.push(otherCandidates.shift());
1767
- if (results.length === beforeLength) break;
1768
- }
1769
- }
1770
-
1771
- return {
1772
- topic: normalizedTopic,
1773
- wcagLevel: normalizedWcagLevel,
1774
- totalHits,
1775
- results,
1776
- };
1777
- }
1778
-
1779
- function buildComponentsResourcePayload(indexes) {
1780
- const page = buildComponentSummaries(indexes, { limit: 200 });
1781
- const componentsByCategory = {};
1782
- for (const item of page.items) {
1783
- const category = String(item?.category ?? 'Other');
1784
- componentsByCategory[category] = (componentsByCategory[category] ?? 0) + 1;
1785
- }
1786
- return {
1787
- total: page.total,
1788
- componentsByCategory,
1789
- components: page.items,
1790
- };
1791
- }
1792
-
1793
- function buildTokensResourcePayload(designTokensData) {
1794
- if (!Array.isArray(designTokensData?.tokens)) {
1795
- return {
1796
- isError: true,
1797
- error: {
1798
- code: 'DESIGN_TOKENS_DATA_UNAVAILABLE',
1799
- message: 'Design tokens data not available. Run: npm run mcp:extract-tokens',
1800
- },
1801
- };
1802
- }
1803
-
1804
- const tokens = designTokensData.tokens;
1805
- const tokenTypes = [...new Set(tokens
1806
- .map((token) => String(token?.type ?? '').trim())
1807
- .filter(Boolean))].sort();
1808
- const tokenCategories = [...new Set(tokens
1809
- .map((token) => String(token?.category ?? '').trim())
1810
- .filter(Boolean))].sort();
1811
-
1812
- return {
1813
- isError: false,
1814
- payload: {
1815
- total: tokens.length,
1816
- summary: designTokensData.summary ?? {},
1817
- themes: designTokensData.themes ?? { default: 'light', available: ['light'] },
1818
- tokenTypes,
1819
- tokenCategories,
1820
- sample: tokens.slice(0, 20).map(toTokenSummary),
1821
- },
1822
- };
1823
- }
1824
-
1825
- function buildGuidelinesResourcePayload(guidelinesIndexData, rawTopic) {
1826
- const topic = String(rawTopic ?? '').trim().toLowerCase();
1827
- if (!GUIDELINE_TOPIC_SET.has(topic)) {
1828
- return {
1829
- isError: true,
1830
- error: {
1831
- code: 'INVALID_GUIDELINE_TOPIC',
1832
- message: `Unsupported topic: ${topic}. Allowed values are ${GUIDELINE_TOPICS.join(', ')}.`,
1833
- },
1834
- };
1835
- }
1836
-
1837
- if (!Array.isArray(guidelinesIndexData?.documents)) {
1838
- return {
1839
- isError: true,
1840
- error: {
1841
- code: 'GUIDELINES_INDEX_UNAVAILABLE',
1842
- message: 'Guidelines index not available. Run: npm run mcp:index-guidelines',
1843
- },
1844
- };
1845
- }
1846
-
1847
- const documents = guidelinesIndexData.documents
1848
- .filter((doc) => topic === 'all' || String(doc?.topic ?? '').toLowerCase() === topic)
1849
- .map((doc) => {
1850
- const sections = Array.isArray(doc?.sections) ? doc.sections : [];
1851
- return {
1852
- id: String(doc?.id ?? ''),
1853
- title: String(doc?.title ?? ''),
1854
- topic: String(doc?.topic ?? ''),
1855
- sectionCount: sections.length,
1856
- sections: sections.map((section) => ({
1857
- heading: String(section?.heading ?? ''),
1858
- startLine: Number.isInteger(section?.startLine) ? section.startLine : undefined,
1859
- })),
1860
- };
1861
- });
1862
-
1863
- return {
1864
- isError: false,
1865
- payload: {
1866
- topic,
1867
- totalDocuments: documents.length,
1868
- topicCounts: guidelinesIndexData.topicCounts ?? {},
1869
- documents,
1870
- },
1871
- };
1872
- }
1873
-
1874
- function buildFigmaToWcfPromptText({ figmaUrl, userIntent }) {
1875
- const url = String(figmaUrl ?? '').trim();
1876
- const intent = String(userIntent ?? '').trim();
1877
-
1878
- return [
1879
- `Figma URL: ${url}`,
1880
- intent ? `Implementation goal: ${intent}` : 'Implementation goal: (not specified)',
1881
- '',
1882
- 'Use the workflow below in this exact order:',
1883
- '1. get_design_system_overview',
1884
- '2. get_design_tokens',
1885
- '3. get_component_api',
1886
- '4. generate_usage_snippet (or get_pattern_recipe)',
1887
- '5. validate_markup',
1888
- '',
1889
- 'Output requirements:',
1890
- '- Split the UI into sections before writing code.',
1891
- '- For each section, name concrete components and token variables.',
1892
- '- Provide final validation notes and required fixes.',
1893
- ].join('\n');
1894
- }
1895
-
1896
- function resolveDeclByComponent(indexes, component, prefix) {
1897
- const byTagOrClass =
1898
- pickDecl(indexes, { tagName: component, prefix }) ??
1899
- pickDecl(indexes, { className: component, prefix });
1900
- if (byTagOrClass) {
1901
- const canonicalTag = typeof byTagOrClass.tagName === 'string' ? byTagOrClass.tagName.toLowerCase() : undefined;
1902
- return {
1903
- decl: byTagOrClass,
1904
- modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
1905
- };
1906
- }
1907
-
1908
- const byComponentId = findDeclByComponentId(indexes, component);
1909
- if (byComponentId) return byComponentId;
1910
-
1911
- // Auto-prefix: try with canonical prefix if bare name was given (DIG-15)
1912
- const comp = typeof component === 'string' ? component.trim().toLowerCase() : '';
1913
- const p = normalizePrefix(prefix);
1914
- if (comp && !comp.startsWith(p)) {
1915
- const prefixed = `${p}-${comp}`;
1916
- const byPrefixed = pickDecl(indexes, { tagName: prefixed, prefix: p });
1917
- if (byPrefixed) {
1918
- const canonicalTag = typeof byPrefixed.tagName === 'string' ? byPrefixed.tagName.toLowerCase() : undefined;
1919
- return {
1920
- decl: byPrefixed,
1921
- modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
1922
- };
1923
- }
1924
- }
1925
-
1926
- return undefined;
1927
- }
1928
-
1929
- function buildComponentNotFoundError(component, indexes, prefix) {
1930
- const comp = typeof component === 'string' ? component.trim() : '';
1931
- const p = normalizePrefix(prefix);
1932
- const suggestions = [];
1933
-
1934
- // Try suggesting with prefix
1935
- if (comp && !comp.toLowerCase().startsWith(p)) {
1936
- const prefixed = `${p}-${comp.toLowerCase()}`;
1937
- if (indexes.byTag.has(prefixed)) {
1938
- suggestions.push(prefixed);
1939
- }
1940
- }
7
+ *
8
+ * Actual logic lives in core/ sub-modules (DD-02, DD-09).
9
+ */
1941
10
 
1942
- // Levenshtein-based suggestion
1943
- const suggested = suggestUnknownElementTagName(comp.includes('-') ? comp : `${p}-${comp}`, indexes.byTag);
1944
- if (suggested && !suggestions.includes(suggested)) {
1945
- suggestions.push(suggested);
1946
- }
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { registerAll } from './core/register.mjs';
13
+
14
+ // --- core/constants.mjs ---
15
+ export {
16
+ CANONICAL_PREFIX,
17
+ MAX_PREFIX_LENGTH,
18
+ STRUCTURED_CONTENT_DISABLE_FLAG,
19
+ MAX_TOOL_RESULT_BYTES,
20
+ PLUGIN_TOOL_NOTICE,
21
+ CATEGORY_MAP,
22
+ FIGMA_TO_WCF_PROMPT,
23
+ WCF_RESOURCE_URIS,
24
+ IDE_SETUP_TEMPLATES,
25
+ } from './core/constants.mjs';
26
+
27
+ // --- core/response.mjs ---
28
+ export {
29
+ expandQueryWithSynonyms,
30
+ isStructuredContentDisabled,
31
+ toStructuredContent,
32
+ measureToolResultBytes,
33
+ buildJsonToolResponse,
34
+ buildJsonToolErrorResponse,
35
+ } from './core/response.mjs';
36
+
37
+ // --- core/prefix.mjs ---
38
+ export {
39
+ getCategory,
40
+ normalizePrefix,
41
+ withPrefix,
42
+ toCanonicalTagName,
43
+ levenshteinDistance,
44
+ suggestUnknownElementTagName,
45
+ buildDiagnosticSuggestion,
46
+ applyPrefixToTagMap,
47
+ applyPrefixToHtml,
48
+ } from './core/prefix.mjs';
49
+
50
+ // --- core/plugins.mjs ---
51
+ export {
52
+ PLUGIN_CONTRACT_VERSION,
53
+ normalizePlugins,
54
+ buildPluginDataSourceMap,
55
+ } from './core/plugins.mjs';
56
+
57
+ // --- core/tokens.mjs ---
58
+ export {
59
+ normalizeTokenValue,
60
+ normalizeCssVariable,
61
+ buildTokenSuggestionMap,
62
+ normalizeTokenIdentifier,
63
+ resolveTokenTheme,
64
+ extractReferencedTokenNames,
65
+ buildTokenRelationshipIndex,
66
+ buildComponentTokenReferencedBy,
67
+ suggestTokenNames,
68
+ buildDesignTokenDetailPayload,
69
+ buildDesignTokensPayload,
70
+ } from './core/tokens.mjs';
71
+
72
+ // --- core/cem.mjs ---
73
+ export {
74
+ findCustomElementDeclarations,
75
+ buildIndexes,
76
+ extractPrefixFromIndexes,
77
+ buildFullPageHtml,
78
+ pickDecl,
79
+ serializeApi,
80
+ generateSnippet,
81
+ findDeclByComponentId,
82
+ loadPatternRegistryShape,
83
+ resolveComponentClosure,
84
+ buildPatternFrequencyMap,
85
+ buildComponentSummaries,
86
+ parseIconNamesFromDescription,
87
+ parseIconNamesFromType,
88
+ extractIconNames,
89
+ buildIconCatalog,
90
+ searchIconCatalog,
91
+ buildRelatedComponentMap,
92
+ getRelatedComponentsForTag,
93
+ normalizeWcagLevel,
94
+ extractAccessibilityChecklist,
95
+ buildAccessibilityIndex,
96
+ queryAccessibilityIndex,
97
+ } from './core/cem.mjs';
1947
98
 
1948
- const msg = suggestions.length > 0
1949
- ? `Component not found: ${comp}. Did you mean: ${suggestions.join(', ')}?`
1950
- : `Component not found: ${comp}`;
1951
- return { content: [{ type: 'text', text: msg }], isError: true };
1952
- }
99
+ // ---------------------------------------------------------------------------
100
+ // Imports needed within createMcpServer orchestration
101
+ // ---------------------------------------------------------------------------
102
+ import { normalizePlugins, buildPluginDataSourceMap } from './core/plugins.mjs';
103
+ import { buildIndexes, extractPrefixFromIndexes, loadPatternRegistryShape, buildRelatedComponentMap, buildPatternFrequencyMap } from './core/cem.mjs';
104
+ import { buildTokenSuggestionMap, buildComponentTokenReferencedBy } from './core/tokens.mjs';
105
+ import { MAX_TOOL_RESULT_BYTES, PACKAGE_VERSION } from './core/constants.mjs';
1953
106
 
1954
107
  // ---------------------------------------------------------------------------
1955
108
  // createMcpServer — builds the McpServer with all tools registered, but does
1956
109
  // NOT connect a transport. Callers choose their own transport.
1957
- //
1958
- // loadJsonData(fileName: string) → Promise<object>
1959
- // loadValidator() → Promise<{ collectCemCustomElements, validateTextAgainstCem }>
1960
- // options?: {
1961
- // plugins?: WcfMcpPlugin[],
1962
- // loadJsonDataFromPath?: (path: string, fileName: string, pluginName?: string) => Promise<object>
1963
- // loadTextData?: (fileName: string) => Promise<string>
1964
- // }
1965
110
  // ---------------------------------------------------------------------------
1966
111
 
1967
112
  export async function createMcpServer(loadJsonData, loadValidator, options = {}) {
@@ -2015,7 +160,6 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2015
160
  const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
2016
161
  const patternFrequency = buildPatternFrequencyMap(patterns);
2017
162
 
2018
- // Load optional data files (design tokens, guidelines index)
2019
163
  let designTokensData = null;
2020
164
  try {
2021
165
  designTokensData = await loadJson('design-tokens.json');
@@ -2039,802 +183,6 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2039
183
  const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
2040
184
  const componentTokenRefMap = buildComponentTokenReferencedBy(manifest);
2041
185
 
2042
- const VENDOR_DIR = 'vendor-runtime';
2043
- const PREFIX_STRIP_RE = /^[^-]+-/;
2044
-
2045
- const server = new McpServer({
2046
- name: 'web-components-factory-design-system',
2047
- version: '0.7.0',
2048
- });
2049
-
2050
- server.registerPrompt(
2051
- FIGMA_TO_WCF_PROMPT,
2052
- {
2053
- title: 'Figma To WCF',
2054
- description:
2055
- 'Guided prompt for converting a Figma URL into WCF implementation steps with a strict tool order.',
2056
- argsSchema: {
2057
- figmaUrl: z.string().trim().url().describe('Figma URL (design or board link)'),
2058
- userIntent: z.string().optional().describe('Optional implementation intent / screen purpose'),
2059
- },
2060
- },
2061
- async ({ figmaUrl, userIntent }) => ({
2062
- messages: [{
2063
- role: 'user',
2064
- content: {
2065
- type: 'text',
2066
- text: buildFigmaToWcfPromptText({ figmaUrl, userIntent }),
2067
- },
2068
- }],
2069
- }),
2070
- );
2071
-
2072
- server.registerResource(
2073
- 'wcf_components',
2074
- WCF_RESOURCE_URIS.components,
2075
- {
2076
- title: 'WCF Component Catalog',
2077
- description: 'Component catalog snapshot with categories and API entry points.',
2078
- mimeType: 'application/json',
2079
- },
2080
- async () => {
2081
- const payload = buildComponentsResourcePayload(indexes);
2082
- return {
2083
- contents: [{
2084
- uri: WCF_RESOURCE_URIS.components,
2085
- mimeType: 'application/json',
2086
- text: JSON.stringify(payload, null, 2),
2087
- }],
2088
- };
2089
- },
2090
- );
2091
-
2092
- server.registerResource(
2093
- 'wcf_tokens',
2094
- WCF_RESOURCE_URIS.tokens,
2095
- {
2096
- title: 'WCF Design Tokens',
2097
- description: 'Token summary resource for colors, spacing, typography, radius, and shadows.',
2098
- mimeType: 'application/json',
2099
- },
2100
- async () => {
2101
- const result = buildTokensResourcePayload(designTokensData);
2102
- const payload = result.isError ? { error: result.error } : result.payload;
2103
- return {
2104
- contents: [{
2105
- uri: WCF_RESOURCE_URIS.tokens,
2106
- mimeType: 'application/json',
2107
- text: JSON.stringify(payload, null, 2),
2108
- }],
2109
- };
2110
- },
2111
- );
2112
-
2113
- server.registerResource(
2114
- 'wcf_guidelines',
2115
- new ResourceTemplate(WCF_RESOURCE_URIS.guidelinesTemplate, {
2116
- list: async () => ({
2117
- resources: GUIDELINE_TOPICS.map((topic) => ({
2118
- uri: `wcf://guidelines/${topic}`,
2119
- name: `wcf guidelines (${topic})`,
2120
- description: `Guideline summary for topic=${topic}`,
2121
- })),
2122
- }),
2123
- complete: {
2124
- topic: async (value) => {
2125
- const query = String(value ?? '').trim().toLowerCase();
2126
- return GUIDELINE_TOPICS.filter((topic) => topic.startsWith(query));
2127
- },
2128
- },
2129
- }),
2130
- {
2131
- title: 'WCF Guidelines',
2132
- description: 'Topic-scoped guideline resource (accessibility|css|patterns|all).',
2133
- mimeType: 'application/json',
2134
- },
2135
- async (_uri, variables) => {
2136
- const topic = String(variables?.topic ?? '').trim().toLowerCase();
2137
- const result = buildGuidelinesResourcePayload(guidelinesIndexData, topic);
2138
- if (result.isError) {
2139
- throw new Error(`${result.error.code}: ${result.error.message}`);
2140
- }
2141
- return {
2142
- contents: [{
2143
- uri: `wcf://guidelines/${topic}`,
2144
- mimeType: 'application/json',
2145
- text: JSON.stringify(result.payload, null, 2),
2146
- }],
2147
- };
2148
- },
2149
- );
2150
-
2151
- server.registerResource(
2152
- 'wcf_llms_full',
2153
- WCF_RESOURCE_URIS.llmsFull,
2154
- {
2155
- title: 'WCF llms-full',
2156
- description: 'LLM reference corpus for WCF usage, generated from repository docs.',
2157
- mimeType: 'text/plain',
2158
- },
2159
- async () => {
2160
- if (typeof llmsFullText !== 'string' || llmsFullText.length === 0) {
2161
- throw new Error('LLMS_FULL_UNAVAILABLE: llms-full.txt is not available.');
2162
- }
2163
- return {
2164
- contents: [{
2165
- uri: WCF_RESOURCE_URIS.llmsFull,
2166
- mimeType: 'text/plain',
2167
- text: llmsFullText,
2168
- }],
2169
- };
2170
- },
2171
- );
2172
-
2173
- // -----------------------------------------------------------------------
2174
- // Resource: wcf://skills
2175
- // -----------------------------------------------------------------------
2176
- server.registerResource(
2177
- 'wcf_skills',
2178
- WCF_RESOURCE_URIS.skills,
2179
- {
2180
- title: 'WCF Skills Catalog',
2181
- description: 'Registered Claude Code / Cursor / Codex skills from skills-registry.json.',
2182
- mimeType: 'application/json',
2183
- },
2184
- async () => {
2185
- const registry = await loadJsonData('skills-registry.json');
2186
- if (!registry || !Array.isArray(registry.skills)) {
2187
- throw new Error('SKILLS_REGISTRY_UNAVAILABLE: skills-registry.json is not available.');
2188
- }
2189
- const skills = registry.skills.map(normalizeSkillSummary);
2190
- return {
2191
- contents: [{
2192
- uri: WCF_RESOURCE_URIS.skills,
2193
- mimeType: 'application/json',
2194
- text: JSON.stringify({ schemaVersion: registry.schemaVersion ?? 2, total: skills.length, skills }, null, 2),
2195
- }],
2196
- };
2197
- },
2198
- );
2199
-
2200
- // -----------------------------------------------------------------------
2201
- // Tool: get_design_system_overview
2202
- // -----------------------------------------------------------------------
2203
- server.registerTool(
2204
- 'get_design_system_overview',
2205
- {
2206
- description:
2207
- '**MUST be called first before using any other tool.** Returns a high-level overview of the design system: name, version, component count by category, available patterns, and recommended tool workflow. Use this to understand what is available before diving into specifics.',
2208
- inputSchema: {},
2209
- },
2210
- async () => {
2211
- const categoryCount = {};
2212
- for (const { tagName } of indexes.decls) {
2213
- const cat = getCategory(tagName);
2214
- categoryCount[cat] = (categoryCount[cat] ?? 0) + 1;
2215
- }
2216
-
2217
- const patternList = Object.values(patterns).map((p) => ({
2218
- id: p?.id,
2219
- title: p?.title,
2220
- }));
2221
-
2222
- const overview = {
2223
- name: 'DADS Web Components (wcf)',
2224
- version: '0.7.0',
2225
- prefix: detectedPrefix,
2226
- totalComponents: indexes.decls.length,
2227
- componentsByCategory: categoryCount,
2228
- totalPatterns: patternList.length,
2229
- patterns: patternList,
2230
- setupInfo: {
2231
- npmPackage: 'web-components-factory',
2232
- installCommand: 'npm install web-components-factory',
2233
- vendorRuntimePath: '<dir>/',
2234
- htmlBoilerplate: [
2235
- '<script type="importmap">',
2236
- `{ "imports": { "${detectedPrefix}-button": "./<dir>/components/button.js" } }`,
2237
- '</script>',
2238
- '<script type="module" src="./<dir>/boot.js"></script>',
2239
- ].join('\n'),
2240
- noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
2241
- noCDN: true,
2242
- deliveryModel: 'vendor-local',
2243
- distribution: {
2244
- selfHosted: true,
2245
- cdn: false,
2246
- strategy: 'vendor-importmap',
2247
- quickStart: 'npx web-components-factory init --prefix <prefix> --dir <dir>',
2248
- description:
2249
- '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.',
2250
- },
2251
- importMapHint: `WCF uses <script type="importmap"> for module resolution. Each component tag name maps to a local JS file: { "${detectedPrefix}-<component>": "./<dir>/components/<component>.js" }. The wcf CLI generates importmap.snippet.json automatically via \`wcf init\`.`,
2252
- 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.',
2253
- detectedPrefix,
2254
- vendorSetup: {
2255
- init: `wcf init --prefix ${detectedPrefix} --dir <dir>`,
2256
- add: `wcf add <componentId> --prefix ${detectedPrefix} --out <dir>`,
2257
- workflow: '1. wcf init で初期化(boot.js, importmap.snippet.json, autoloader を生成) → 2. wcf add で各コンポーネントを追加 → import map と boot.js が自動生成される',
2258
- },
2259
- htmlSetup: [
2260
- '<script type="importmap">',
2261
- '{',
2262
- ' "imports": {',
2263
- ` "${detectedPrefix}-button": "./<dir>/components/button.js",`,
2264
- ` "${detectedPrefix}-card": "./<dir>/components/card.js"`,
2265
- ' }',
2266
- '}',
2267
- '</script>',
2268
- '<script type="module" src="./<dir>/boot.js"></script>',
2269
- ].join('\n'),
2270
- },
2271
- ideSetupTemplates: IDE_SETUP_TEMPLATES,
2272
- availablePrompts: [
2273
- {
2274
- name: FIGMA_TO_WCF_PROMPT,
2275
- purpose: 'Figma-to-WCF conversion workflow prompt',
2276
- },
2277
- ],
2278
- availableResources: [
2279
- {
2280
- uri: WCF_RESOURCE_URIS.components,
2281
- purpose: 'Component catalog snapshot',
2282
- },
2283
- {
2284
- uri: WCF_RESOURCE_URIS.tokens,
2285
- purpose: 'Token summary snapshot',
2286
- },
2287
- {
2288
- uri: WCF_RESOURCE_URIS.guidelinesTemplate,
2289
- purpose: 'Topic-based guideline summaries',
2290
- },
2291
- {
2292
- uri: WCF_RESOURCE_URIS.llmsFull,
2293
- purpose: 'Full LLM reference text for WCF',
2294
- },
2295
- {
2296
- uri: WCF_RESOURCE_URIS.skills,
2297
- purpose: 'Skills catalog snapshot',
2298
- },
2299
- ],
2300
- availableTools: [
2301
- { name: 'get_design_system_overview', purpose: 'This overview (start here)' },
2302
- { name: 'list_components', purpose: 'Browse components with progressive disclosure and filters' },
2303
- { name: 'search_icons', purpose: 'Search icon names and usage examples' },
2304
- { name: 'get_component_api', purpose: 'Full API surface for a single component' },
2305
- { name: 'generate_usage_snippet', purpose: 'Minimal HTML usage example' },
2306
- { name: 'get_install_recipe', purpose: 'Installation instructions and dependency tree' },
2307
- { name: 'validate_markup', purpose: 'Validate HTML against CEM schema' },
2308
- { name: 'generate_full_page_html', purpose: 'Wrap HTML fragment into a complete page with importmap and boot script' },
2309
- { name: 'list_patterns', purpose: 'Browse page-level UI composition patterns' },
2310
- { name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
2311
- { name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
2312
- { name: 'get_design_tokens', purpose: 'Query design tokens (colors, spacing, typography, radius, shadows)' },
2313
- { name: 'get_design_token_detail', purpose: 'Get details, relationships, and usage examples for one token' },
2314
- { name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
2315
- { name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
2316
- { name: 'get_component_selector_guide', purpose: 'Component selection guide by category and use case' },
2317
- ],
2318
- recommendedWorkflow: [
2319
- '1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
2320
- '2. figma_to_wcf (optional) → bootstrap the Figma-to-WCF tool sequence',
2321
- '3. wcf://components and wcf://tokens resources → preload catalog/token context',
2322
- '4. search_guidelines → find relevant guidelines',
2323
- '5. get_design_tokens → get correct token values',
2324
- '6. get_design_token_detail → inspect one token with references/referencedBy and usage examples',
2325
- '7. get_accessibility_docs → fetch component-level accessibility checklist',
2326
- '8. list_components (category/query + pagination) → shortlist components',
2327
- '9. search_icons (optional) → find icon names quickly',
2328
- '10. get_component_api → check attributes, slots, events, CSS parts',
2329
- '11. generate_usage_snippet or get_pattern_recipe → get code',
2330
- '12. validate_markup → verify your HTML and use suggestions to self-correct',
2331
- '13. generate_full_page_html → wrap fragment into a complete preview-ready page',
2332
- '14. get_install_recipe → get import/install instructions',
2333
- ],
2334
- experimental: {
2335
- plugins: {
2336
- enabled: plugins.length > 0,
2337
- note: PLUGIN_TOOL_NOTICE,
2338
- pluginCount: plugins.length,
2339
- pluginToolCount: plugins.reduce((sum, plugin) => sum + (plugin.tools?.length ?? 0), 0),
2340
- plugins: plugins.map((plugin) => ({
2341
- name: plugin.name,
2342
- version: plugin.version,
2343
- toolCount: plugin.tools?.length ?? 0,
2344
- dataSourceOverrides: plugin.dataSources?.map((source) => source.fileName) ?? [],
2345
- })),
2346
- },
2347
- },
2348
- };
2349
-
2350
- for (const plugin of plugins) {
2351
- const tools = Array.isArray(plugin.tools) ? plugin.tools : [];
2352
- for (const tool of tools) {
2353
- overview.availableTools.push({
2354
- name: tool.name,
2355
- purpose: `${tool.description} (plugin: ${plugin.name})`,
2356
- });
2357
- }
2358
- }
2359
-
2360
- return {
2361
- content: [{ type: 'text', text: JSON.stringify(overview, null, 2) }],
2362
- };
2363
- },
2364
- );
2365
-
2366
- // -----------------------------------------------------------------------
2367
- // Tool: list_components
2368
- // -----------------------------------------------------------------------
2369
- server.registerTool(
2370
- 'list_components',
2371
- {
2372
- description:
2373
- '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.',
2374
- inputSchema: {
2375
- category: z
2376
- .enum(['Form', 'Actions', 'Navigation', 'Content', 'Display', 'Layout', 'Other'])
2377
- .optional()
2378
- .describe('Filter by component category'),
2379
- query: z.string().optional().describe('Search by tagName/className/description/category/modulePath'),
2380
- limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
2381
- offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
2382
- prefix: z.string().optional(),
2383
- patternId: z.string().optional().describe('Filter to components required by this pattern'),
2384
- sort: z.enum(['default', 'frequency']).optional().describe('Sort order: "default" (CEM declaration order) or "frequency" (pattern usage count, descending)'),
2385
- },
2386
- },
2387
- async ({ category, query, limit, offset, prefix, patternId, sort }) => {
2388
- const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency });
2389
- const payload = {
2390
- items: page.items,
2391
- total: page.total,
2392
- limit: page.limit,
2393
- offset: page.offset,
2394
- hasMore: page.hasMore,
2395
- };
2396
- if (page._notice) payload._notice = page._notice;
2397
- return {
2398
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
2399
- };
2400
- },
2401
- );
2402
-
2403
- // -----------------------------------------------------------------------
2404
- // Tool: search_icons
2405
- // -----------------------------------------------------------------------
2406
- server.registerTool(
2407
- 'search_icons',
2408
- {
2409
- description:
2410
- 'Search icon catalog by keyword. When: you need a valid icon name for dads-icon or icon-capable components. Returns: { total, limit, offset, hasMore, icons[] } with name, variants, and usageExample. After: use the icon name in generate_usage_snippet or your markup.',
2411
- inputSchema: {
2412
- query: z.string().optional().describe('Search icon names (partial match)'),
2413
- limit: z.number().int().min(1).max(100).optional().describe('Maximum items to return (default: 20)'),
2414
- offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
2415
- prefix: z.string().optional(),
2416
- },
2417
- },
2418
- async ({ query, limit, offset, prefix }) => {
2419
- const payload = searchIconCatalog(indexes, { query, limit, offset, prefix });
2420
- return {
2421
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
2422
- };
2423
- },
2424
- );
2425
-
2426
- // -----------------------------------------------------------------------
2427
- // Tool: get_component_api
2428
- // -----------------------------------------------------------------------
2429
- server.registerTool(
2430
- 'get_component_api',
2431
- {
2432
- description:
2433
- 'Get the full API surface of one or more components (attributes, slots, events, CSS parts, CSS custom properties). When: you need detailed specs for components. Returns: complete component specification (single object or array for batch). After: use generate_usage_snippet for a code example.',
2434
- inputSchema: {
2435
- tagName: z.string().optional().describe('Tag name (e.g., "dads-button")'),
2436
- className: z.string().optional().describe('Class name (e.g., "DadsButton")'),
2437
- component: z.string().optional().describe('Any identifier: tagName, className, or bare name (e.g., "button")'),
2438
- components: z.array(z.string()).max(10).optional().describe('Batch: array of component identifiers (max 10). When provided, component/tagName/className are ignored.'),
2439
- prefix: z.string().optional(),
2440
- },
2441
- },
2442
- async ({ tagName, className, component, components, prefix }) => {
2443
- const p = normalizePrefix(prefix);
2444
-
2445
- // Batch mode: components array takes priority (DD-23)
2446
- if (Array.isArray(components) && components.length > 0) {
2447
- const results = [];
2448
- for (const comp of components) {
2449
- const resolved = resolveDeclByComponent(indexes, comp, p);
2450
- if (!resolved?.decl) {
2451
- results.push({ component: comp, error: `Component not found: ${comp}` });
2452
- continue;
2453
- }
2454
- const { decl: d, modulePath: mp } = resolved;
2455
- const cTag = typeof d.tagName === 'string' ? d.tagName.toLowerCase() : undefined;
2456
- const mPath = mp ?? (cTag ? indexes.modulePathByTag.get(cTag) : undefined);
2457
- const api = serializeApi(d, mPath, prefix);
2458
- const related = getRelatedComponentsForTag({
2459
- canonicalTagName: cTag,
2460
- installRegistry,
2461
- relatedMap: relatedComponentMap,
2462
- prefix,
2463
- });
2464
- if (related.length > 0) api.relatedComponents = related;
2465
- const a11y = extractAccessibilityChecklist(d, { prefix });
2466
- if (a11y) api.accessibilityChecklist = a11y;
2467
- results.push(api);
2468
- }
2469
- const resultJson = JSON.stringify(results, null, 2);
2470
- if (measureToolResultBytes(resultJson) > MAX_TOOL_RESULT_BYTES) {
2471
- return {
2472
- content: [{ type: 'text', text: JSON.stringify({ error: 'Batch result exceeds size limit. Reduce the number of components.' }) }],
2473
- isError: true,
2474
- };
2475
- }
2476
- return buildJsonToolResponse(results);
2477
- }
2478
-
2479
- // Single mode (existing behavior)
2480
- let decl;
2481
- let modulePath;
2482
-
2483
- if (component) {
2484
- const resolved = resolveDeclByComponent(indexes, component, p);
2485
- decl = resolved?.decl;
2486
- modulePath = resolved?.modulePath;
2487
- } else {
2488
- decl = pickDecl(indexes, { tagName, className, prefix: p });
2489
- }
2490
-
2491
- if (!decl) {
2492
- const identifier = component || tagName || className || '';
2493
- return buildComponentNotFoundError(identifier, indexes, p);
2494
- }
2495
-
2496
- const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
2497
- if (!modulePath) {
2498
- modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
2499
- }
2500
- const api = serializeApi(decl, modulePath, prefix);
2501
- const relatedComponents = getRelatedComponentsForTag({
2502
- canonicalTagName: canonicalTag,
2503
- installRegistry,
2504
- relatedMap: relatedComponentMap,
2505
- prefix,
2506
- });
2507
- if (relatedComponents.length > 0) {
2508
- api.relatedComponents = relatedComponents;
2509
- }
2510
- const accessibilityChecklist = extractAccessibilityChecklist(decl, { prefix });
2511
- if (accessibilityChecklist) {
2512
- api.accessibilityChecklist = accessibilityChecklist;
2513
- }
2514
- const interactionExamples = canonicalTag ? INTERACTION_EXAMPLES_MAP[canonicalTag] : undefined;
2515
- if (interactionExamples) {
2516
- api.interactionExamples = interactionExamples;
2517
- }
2518
- const layoutBehavior = canonicalTag ? LAYOUT_BEHAVIOR_MAP[canonicalTag] : undefined;
2519
- if (layoutBehavior) {
2520
- api.layoutBehavior = layoutBehavior;
2521
- }
2522
-
2523
- return buildJsonToolResponse(api);
2524
- },
2525
- );
2526
-
2527
- // -----------------------------------------------------------------------
2528
- // Tool: generate_usage_snippet
2529
- // -----------------------------------------------------------------------
2530
- server.registerTool(
2531
- 'generate_usage_snippet',
2532
- {
2533
- description:
2534
- 'Generate a minimal HTML usage example for a component. When: you need a quick code snippet to start with. Returns: ready-to-use HTML string with key attributes pre-filled.',
2535
- inputSchema: {
2536
- component: z.string(),
2537
- prefix: z.string().optional(),
2538
- },
2539
- },
2540
- async ({ component, prefix }) => {
2541
- const p = normalizePrefix(prefix);
2542
- const resolved = resolveDeclByComponent(indexes, component, p);
2543
- const decl = resolved?.decl;
2544
-
2545
- if (!decl) {
2546
- return buildComponentNotFoundError(component, indexes, p);
2547
- }
2548
-
2549
- const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
2550
- const modulePath = resolved?.modulePath ?? (canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined);
2551
- const api = serializeApi(decl, modulePath, prefix);
2552
- const snippet = generateSnippet(api, prefix);
2553
-
2554
- return {
2555
- content: [{ type: 'text', text: snippet }],
2556
- };
2557
- },
2558
- );
2559
-
2560
- // -----------------------------------------------------------------------
2561
- // Tool: get_install_recipe
2562
- // -----------------------------------------------------------------------
2563
- server.registerTool(
2564
- 'get_install_recipe',
2565
- {
2566
- description:
2567
- 'Get installation instructions and dependency tree for a component. When: setting up a component in a project. Returns: componentId, dependencies, import statements, and CLI command (wcf add).',
2568
- inputSchema: {
2569
- component: z.string(),
2570
- prefix: z.string().optional(),
2571
- },
2572
- },
2573
- async ({ component, prefix }) => {
2574
- const p = normalizePrefix(prefix);
2575
- const resolved = resolveDeclByComponent(indexes, component, p);
2576
- const decl = resolved?.decl;
2577
-
2578
- if (!decl) {
2579
- return {
2580
- content: [{ type: 'text', text: `Component not found: ${component}` }],
2581
- isError: true,
2582
- };
2583
- }
2584
-
2585
- const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
2586
- const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : resolved?.modulePath;
2587
- const api = serializeApi(decl, modulePath, p);
2588
- const usageSnippet = generateSnippet(api, p);
2589
-
2590
- const install = decl?.custom?.install;
2591
- if (!install || typeof install !== 'object') {
2592
- return {
2593
- content: [
2594
- {
2595
- type: 'text',
2596
- text: 'Install metadata not found in CEM.',
2597
- },
2598
- ],
2599
- isError: true,
2600
- };
2601
- }
2602
-
2603
- const componentId = String(install.id ?? '').trim() || api?.custom?.componentId;
2604
- const define = String(install.define ?? '').trim();
2605
- const deps = Array.isArray(install.deps) ? install.deps : [];
2606
- const tags = Array.isArray(install.tags) ? install.tags : [];
2607
-
2608
- // Resolve transitive dependencies via BFS
2609
- const transitiveDeps = componentId
2610
- ? resolveComponentClosure({ installRegistry }, [componentId]).filter((id) => id !== componentId)
2611
- : [];
2612
-
2613
- const tagNames =
2614
- tags.length > 0 ? tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : [api.tagName];
2615
-
2616
- const defineHint = define
2617
- ? [
2618
- modulePath ? `import { ${define} } from "${modulePath}";` : `import { ${define} } from "<modulePath>";`,
2619
- `${define}();`,
2620
- p !== CANONICAL_PREFIX ? `// If supported: ${define}("${p}");` : undefined,
2621
- ]
2622
- .filter(Boolean)
2623
- .join('\n')
2624
- : undefined;
2625
-
2626
- return {
2627
- content: [
2628
- {
2629
- type: 'text',
2630
- text: JSON.stringify(
2631
- {
2632
- componentId,
2633
- tagNames,
2634
- deps,
2635
- transitiveDeps,
2636
- define,
2637
- defineHint,
2638
- source: install.source,
2639
- usageSnippet,
2640
- usageContext: 'body-only',
2641
- installHint: componentId ? `wcf add ${componentId}` : undefined,
2642
- vendorHint: (() => {
2643
- const im = tagNames.length > 0
2644
- ? JSON.stringify({ imports: Object.fromEntries(tagNames.map((t) => [t, `./<dir>/components/${t.replace(/^[^-]+-/, '')}.js`])) })
2645
- : undefined;
2646
- return {
2647
- install: componentId ? `wcf add ${componentId} --prefix <prefix> --out <dir>` : undefined,
2648
- importMap: im,
2649
- importmap: im, // @deprecated — use importMap; will be removed in v1.0
2650
- boot: '<dir>/boot.js -- loads autoloader that registers components via import map',
2651
- };
2652
- })(),
2653
- },
2654
- null,
2655
- 2,
2656
- ),
2657
- },
2658
- ],
2659
- };
2660
- },
2661
- );
2662
-
2663
- // -----------------------------------------------------------------------
2664
- // Tool: validate_markup
2665
- // -----------------------------------------------------------------------
2666
- server.registerTool(
2667
- 'validate_markup',
2668
- {
2669
- description:
2670
- '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.',
2671
- inputSchema: {
2672
- html: z.string(),
2673
- prefix: z.string().optional(),
2674
- },
2675
- },
2676
- async ({ html, prefix }) => {
2677
- const p = normalizePrefix(prefix);
2678
- let cemIndex = canonicalCemIndex;
2679
- let enumMap = canonicalEnumMap;
2680
- let slotMap = canonicalSlotMap;
2681
- if (p !== CANONICAL_PREFIX) {
2682
- cemIndex = mergeWithPrefixed(canonicalCemIndex, p);
2683
- enumMap = mergeWithPrefixed(canonicalEnumMap, p);
2684
- slotMap = mergeWithPrefixed(canonicalSlotMap, p);
2685
- }
2686
-
2687
- const cemDiagnostics = validateTextAgainstCem({
2688
- filePath: '<markup>',
2689
- text: html,
2690
- cem: cemIndex,
2691
- severity: {
2692
- unknownElement: 'error',
2693
- unknownAttribute: 'warning',
2694
- },
2695
- });
2696
-
2697
- const enumDiagnostics = detectEnumValueMisuse({
2698
- filePath: '<markup>',
2699
- text: html,
2700
- enumMap,
2701
- severity: 'error',
2702
- });
2703
-
2704
- const tokenMisuseDiagnostics = detectTokenMisuseInInlineStyles({
2705
- filePath: '<markup>',
2706
- text: html,
2707
- valueToToken: tokenSuggestionMap,
2708
- severity: 'warning',
2709
- });
2710
-
2711
- const cemTagNames = new Set(cemIndex.keys());
2712
- const accessibilityDiagnostics = detectAccessibilityMisuseInMarkup({
2713
- filePath: '<markup>',
2714
- text: html,
2715
- severity: 'error',
2716
- cemTagNames,
2717
- });
2718
-
2719
- const slotDiagnostics = detectInvalidSlotName({
2720
- filePath: '<markup>',
2721
- text: html,
2722
- slotMap,
2723
- severity: 'error',
2724
- });
2725
-
2726
- const requiredAttrDiagnostics = detectMissingRequiredAttributes({
2727
- filePath: '<markup>',
2728
- text: html,
2729
- prefix: p,
2730
- severity: 'error',
2731
- });
2732
-
2733
- const orphanDiagnostics = detectOrphanedChildComponents({
2734
- filePath: '<markup>',
2735
- text: html,
2736
- prefix: p,
2737
- severity: 'warning',
2738
- });
2739
-
2740
- const emptyInteractiveDiagnostics = detectEmptyInteractiveElement({
2741
- filePath: '<markup>',
2742
- text: html,
2743
- prefix: p,
2744
- severity: 'warning',
2745
- });
2746
-
2747
- const lowercaseDiagnostics = detectNonLowercaseAttributes({
2748
- filePath: '<markup>',
2749
- text: html,
2750
- cem: cemIndex,
2751
- severity: 'warning',
2752
- });
2753
-
2754
- const cdnDiagnostics = detectCdnReferences({
2755
- filePath: '<markup>',
2756
- text: html,
2757
- severity: 'warning',
2758
- });
2759
-
2760
- const scaffoldDiagnostics = detectMissingRuntimeScaffold({
2761
- filePath: '<markup>',
2762
- text: html,
2763
- severity: 'warning',
2764
- });
2765
-
2766
- const allRawDiagnostics = [
2767
- ...cemDiagnostics,
2768
- ...enumDiagnostics,
2769
- ...slotDiagnostics,
2770
- ...requiredAttrDiagnostics,
2771
- ...orphanDiagnostics,
2772
- ...emptyInteractiveDiagnostics,
2773
- ...lowercaseDiagnostics,
2774
- ...tokenMisuseDiagnostics,
2775
- ...accessibilityDiagnostics,
2776
- ...cdnDiagnostics,
2777
- ...scaffoldDiagnostics,
2778
- ];
2779
- const diagnostics = allRawDiagnostics.map((d) => {
2780
- const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex, prefix: p });
2781
- return {
2782
- file: d.file,
2783
- range: d.range,
2784
- severity: d.severity,
2785
- code: d.code,
2786
- message: d.message,
2787
- tagName: d.tagName,
2788
- attrName: d.attrName,
2789
- hint: d.hint,
2790
- suggestion,
2791
- };
2792
- });
2793
-
2794
- return {
2795
- content: [{ type: 'text', text: JSON.stringify({ diagnostics }, null, 2) }],
2796
- };
2797
- },
2798
- );
2799
-
2800
- // -----------------------------------------------------------------------
2801
- // Tool: generate_full_page_html
2802
- // -----------------------------------------------------------------------
2803
- server.registerTool(
2804
- 'generate_full_page_html',
2805
- {
2806
- description:
2807
- 'Generate a complete, self-contained HTML page from a component HTML fragment. When: you need a preview-ready full page with <!DOCTYPE html>, importmap, and boot script. Returns: { fullHtml, componentCount, importMapEntries }. After: save to a .html file and open via a local HTTP server.',
2808
- inputSchema: {
2809
- html: z.string().describe('HTML fragment containing WCF custom elements'),
2810
- prefix: z.string().optional().describe('Component prefix (default: auto-detected)'),
2811
- },
2812
- },
2813
- async ({ html, prefix }) => {
2814
- const p = normalizePrefix(prefix);
2815
- let ci = canonicalCemIndex;
2816
- if (p !== CANONICAL_PREFIX) {
2817
- ci = mergeWithPrefixed(canonicalCemIndex, p);
2818
- }
2819
-
2820
- const { fullHtml, importEntries } = buildFullPageHtml({ html, prefix: p, cemIndex: ci });
2821
-
2822
- return {
2823
- content: [{
2824
- type: 'text',
2825
- text: JSON.stringify({
2826
- fullHtml,
2827
- componentCount: Object.keys(importEntries).length,
2828
- importMapEntries: importEntries,
2829
- }, null, 2),
2830
- }],
2831
- };
2832
- },
2833
- );
2834
-
2835
- // -----------------------------------------------------------------------
2836
- // Tool: get_component_selector_guide
2837
- // -----------------------------------------------------------------------
2838
186
  let selectorGuideData = null;
2839
187
  try {
2840
188
  selectorGuideData = await loadJson('component-selector-guide.json');
@@ -2842,596 +190,51 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2842
190
  // component-selector-guide.json may not exist yet
2843
191
  }
2844
192
 
2845
- server.registerTool(
2846
- 'get_component_selector_guide',
2847
- {
2848
- description:
2849
- 'Get a component selection guide organized by UI category and use case. When: deciding which component to use for a UI requirement. Returns: categories with recommended components and use cases. After: use get_component_api for the selected component details.',
2850
- inputSchema: {
2851
- category: z.string().optional().describe('Filter by category key (e.g., "Form", "Navigation", "Layout")'),
2852
- useCase: z.string().optional().describe('Search by use-case keyword (e.g., "date", "login", "upload")'),
2853
- },
2854
- },
2855
- async ({ category, useCase }) => {
2856
- if (!selectorGuideData || !Array.isArray(selectorGuideData.categories)) {
2857
- return {
2858
- content: [{ type: 'text', text: JSON.stringify({ error: 'Component selector guide not available.' }) }],
2859
- isError: true,
2860
- };
2861
- }
2862
-
2863
- let categories = selectorGuideData.categories;
2864
-
2865
- // Filter by category
2866
- if (typeof category === 'string' && category.trim()) {
2867
- const cat = category.trim().toLowerCase();
2868
- categories = categories.filter((c) => c.key.toLowerCase() === cat);
2869
- }
2870
-
2871
- // Filter by use case keyword
2872
- if (typeof useCase === 'string' && useCase.trim()) {
2873
- const kw = useCase.trim().toLowerCase();
2874
- categories = categories.map((c) => ({
2875
- ...c,
2876
- components: c.components.filter((comp) =>
2877
- comp.useCase.toLowerCase().includes(kw) ||
2878
- comp.id.toLowerCase().includes(kw) ||
2879
- comp.tagName.toLowerCase().includes(kw)
2880
- ),
2881
- })).filter((c) => c.components.length > 0);
2882
- }
2883
-
2884
- return buildJsonToolResponse({
2885
- totalCategories: categories.length,
2886
- categories: categories.map((c) => ({
2887
- key: c.key,
2888
- label: c.label,
2889
- description: c.description,
2890
- components: c.components,
2891
- })),
2892
- });
2893
- },
2894
- );
2895
-
2896
- // -----------------------------------------------------------------------
2897
- // Tool: list_patterns
2898
- // -----------------------------------------------------------------------
2899
- server.registerTool(
2900
- 'list_patterns',
2901
- {
2902
- description:
2903
- 'List available UI composition patterns (page recipes). When: looking for pre-built page layouts or UI compositions. Returns: array of {id, title, description, requires}. After: use get_pattern_recipe for full details including dependency resolution.',
2904
- inputSchema: {},
2905
- },
2906
- async () => {
2907
- const list = Object.values(patterns).map((p) => ({
2908
- id: p?.id,
2909
- title: p?.title,
2910
- description: p?.description,
2911
- requires: p?.requires,
2912
- }));
2913
-
2914
- return {
2915
- content: [{ type: 'text', text: JSON.stringify(list, null, 2) }],
2916
- };
2917
- },
2918
- );
2919
-
2920
- // -----------------------------------------------------------------------
2921
- // Helper: buildFullPageHtmlFromImportMap
2922
- // -----------------------------------------------------------------------
2923
- function escapeHtmlTitle(s) {
2924
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2925
- }
2926
-
2927
- /**
2928
- * Build import map entries from a component closure.
2929
- * @param {string[]} closure - Component IDs
2930
- * @param {Object} components - Component metadata (from install registry)
2931
- * @param {string} prefix - Tag name prefix
2932
- * @param {string} dir - Directory placeholder or concrete path
2933
- * @returns {Object} Import map entries { prefixedTag: path }
2934
- */
2935
- function buildImportMapEntries(closure, components, prefix, dir) {
2936
- return Object.fromEntries(
2937
- closure.flatMap((cid) => {
2938
- const meta = components[cid];
2939
- const tags = Array.isArray(meta?.tags) ? meta.tags : [cid];
2940
- return tags.map((t) => {
2941
- const lower = String(t).toLowerCase();
2942
- const suffix = lower.replace(PREFIX_STRIP_RE, '');
2943
- return [withPrefix(lower, prefix), `./${dir}/components/${suffix}.js`];
2944
- });
2945
- }),
2946
- );
2947
- }
2948
-
2949
- /**
2950
- * Build a complete HTML5 page from pattern data, resolving scaffoldHint
2951
- * placeholders into concrete paths.
2952
- * @param {Object} opts
2953
- * @param {string} opts.html - Pattern HTML body
2954
- * @param {string} opts.title - Page title
2955
- * @param {Object} opts.importMapEntries - Import map entries { tag: path }
2956
- * @param {string} [opts.dir] - Directory for JS assets
2957
- * @param {string} [opts.lang='ja'] - HTML lang attribute
2958
- * @returns {string} Complete HTML5 document
2959
- */
2960
- function buildFullPageHtmlFromImportMap({ html, title, importMapEntries, dir = VENDOR_DIR, lang = 'ja' }) {
2961
- const importMapJson = JSON.stringify({ imports: importMapEntries }, null, 2);
2962
- return [
2963
- '<!DOCTYPE html>',
2964
- `<html lang="${lang}">`,
2965
- '<head>',
2966
- ' <meta charset="UTF-8">',
2967
- ' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
2968
- ` <title>${escapeHtmlTitle(title)}</title>`,
2969
- ` <!-- distribution: selfHosted=true, strategy=vendor-importmap -->`,
2970
- ` <!-- Do NOT replace these local paths with CDN URLs. This design system is self-hosted. -->`,
2971
- ` <script type="importmap">`,
2972
- `${importMapJson}`,
2973
- ` </script>`,
2974
- ` <script type="module" src="./${dir}/boot.js"></script>`,
2975
- '</head>',
2976
- '<body>',
2977
- ` <noscript>このページの機能にはJavaScriptが必要です。</noscript>`,
2978
- ` ${html}`,
2979
- '</body>',
2980
- '</html>',
2981
- ].join('\n');
2982
- }
2983
-
2984
- // -----------------------------------------------------------------------
2985
- // Tool: get_pattern_recipe
2986
- // -----------------------------------------------------------------------
2987
- server.registerTool(
2988
- 'get_pattern_recipe',
2989
- {
2990
- description:
2991
- '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.',
2992
- inputSchema: {
2993
- patternId: z.string(),
2994
- prefix: z.string().optional(),
2995
- include: z.array(z.enum(['fullPage'])).optional(),
2996
- },
2997
- },
2998
- async ({ patternId, prefix, include }) => {
2999
- const id = String(patternId ?? '').trim();
3000
- const p = normalizePrefix(prefix);
3001
- const pat = patterns[id];
3002
- if (!pat) {
3003
- return {
3004
- content: [{ type: 'text', text: `Pattern not found: ${id}` }],
3005
- isError: true,
3006
- };
3007
- }
3008
-
3009
- const requires = Array.isArray(pat.requires) ? pat.requires : [];
3010
- const closure = resolveComponentClosure({ installRegistry }, requires);
3011
-
3012
- const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
3013
- const install = Object.fromEntries(
3014
- closure
3015
- .map((cid) => [cid, components[cid]])
3016
- .filter(([, meta]) => meta && typeof meta === 'object')
3017
- .map(([cid, meta]) => [
3018
- cid,
3019
- {
3020
- ...meta,
3021
- tags: Array.isArray(meta.tags) ? meta.tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : meta.tags,
3022
- },
3023
- ]),
3024
- );
3025
-
3026
- const canonicalHtml = String(pat.html ?? '');
3027
- const html = applyPrefixToHtml(canonicalHtml, p);
3028
-
3029
- const entryHints = Array.isArray(pat.entryHints) ? [...pat.entryHints] : ['boot'];
3030
-
3031
- const importMapEntries = buildImportMapEntries(closure, components, p, '<dir>');
3032
-
3033
- const scaffoldHint = {
3034
- doctype: '<!DOCTYPE html>',
3035
- importMap: `<script type="importmap">\n${JSON.stringify({ imports: importMapEntries }, null, 2)}\n</script>`,
3036
- bootScript: '<script type="module" src="./<dir>/boot.js"></script>',
3037
- noscript: '<noscript>このページの機能にはJavaScriptが必要です。</noscript>',
3038
- 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.',
3039
- };
3040
-
3041
- // Build fullPageHtml when requested via include: ['fullPage']
3042
- const includeArr = Array.isArray(include) ? include : [];
3043
- let fullPageHtml;
3044
- if (includeArr.includes('fullPage')) {
3045
- const resolvedImportMap = buildImportMapEntries(closure, components, p, VENDOR_DIR);
3046
- fullPageHtml = buildFullPageHtmlFromImportMap({
3047
- html,
3048
- title: pat.title ?? pat.id,
3049
- importMapEntries: resolvedImportMap,
3050
- });
3051
- }
3052
-
3053
- const result = {
3054
- pattern: {
3055
- id: pat.id,
3056
- title: pat.title,
3057
- description: pat.description,
3058
- },
3059
- prefix: p,
3060
- requires,
3061
- components: closure,
3062
- install,
3063
- html,
3064
- canonicalHtml,
3065
- installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
3066
- entryHints,
3067
- scaffoldHint,
3068
- behavior: typeof pat.behavior === 'string' ? pat.behavior : undefined,
3069
- };
3070
-
3071
- if (fullPageHtml !== undefined) {
3072
- result.fullPageHtml = fullPageHtml;
3073
- result.vendorSetup = {
3074
- command: `npx web-components-factory init --prefix ${p} --dir ${VENDOR_DIR} && npx web-components-factory add ${closure.join(' ')} --prefix ${p} --out ${VENDOR_DIR}`,
3075
- };
3076
- }
3077
-
3078
- return {
3079
- content: [
3080
- {
3081
- type: 'text',
3082
- text: JSON.stringify(result, null, 2),
3083
- },
3084
- ],
3085
- };
3086
- },
3087
- );
3088
-
3089
- // -----------------------------------------------------------------------
3090
- // Tool: generate_pattern_snippet
3091
- // -----------------------------------------------------------------------
3092
- server.registerTool(
3093
- 'generate_pattern_snippet',
3094
- {
3095
- description:
3096
- 'Generate just the HTML snippet for a pattern without dependency info. When: you only need the markup. Returns: HTML string with prefix applied. For full dependency resolution, use get_pattern_recipe instead.',
3097
- inputSchema: {
3098
- patternId: z.string(),
3099
- prefix: z.string().optional(),
3100
- },
3101
- },
3102
- async ({ patternId, prefix }) => {
3103
- const id = String(patternId ?? '').trim();
3104
- const p = normalizePrefix(prefix);
3105
- const pat = patterns[id];
3106
- if (!pat) {
3107
- return {
3108
- content: [{ type: 'text', text: `Pattern not found: ${id}` }],
3109
- isError: true,
3110
- };
3111
- }
3112
-
3113
- return {
3114
- content: [{ type: 'text', text: applyPrefixToHtml(String(pat.html ?? ''), p) }],
3115
- };
3116
- },
3117
- );
3118
-
3119
- // -----------------------------------------------------------------------
3120
- // Tool: get_design_tokens
3121
- // -----------------------------------------------------------------------
3122
- server.registerTool(
3123
- 'get_design_tokens',
3124
- {
3125
- description:
3126
- 'Get design tokens (colors, spacing, typography, etc.). ' +
3127
- 'When: building UI and need correct token values instead of hard-coded values. ' +
3128
- 'Returns: filtered list of tokens with CSS variable names and values. ' +
3129
- 'After: use token cssVariable values in your CSS.',
3130
- inputSchema: {
3131
- type: z.enum(['color', 'spacing', 'typography', 'radius', 'shadow']).optional()
3132
- .describe('Filter by token type'),
3133
- category: z.enum(['primitive', 'semantic', 'derived']).optional()
3134
- .describe('Filter by token category'),
3135
- query: z.string().optional()
3136
- .describe('Search token names (partial match)'),
3137
- theme: z.enum(['light', 'dark', 'all']).optional()
3138
- .describe('Theme filter (currently light only; dark/all return an error due to NG-06)'),
3139
- },
3140
- },
3141
- async ({ type, category, query, theme }) => {
3142
- const { isError, payload } = buildDesignTokensPayload(designTokensData, { type, category, query, theme });
3143
- if (isError) {
3144
- return {
3145
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
3146
- isError: true,
3147
- };
3148
- }
3149
- return buildJsonToolResponse(payload);
3150
- },
3151
- );
3152
-
3153
- // -----------------------------------------------------------------------
3154
- // Tool: get_design_token_detail
3155
- // -----------------------------------------------------------------------
3156
- server.registerTool(
3157
- 'get_design_token_detail',
3158
- {
3159
- description:
3160
- 'Get details for one design token. ' +
3161
- 'When: you already found a token and need its references, referencedBy, and usage examples. ' +
3162
- 'Returns: token detail object with relationships and example CSS snippets. ' +
3163
- 'After: apply the cssVariable in your implementation or validate related semantic aliases.',
3164
- inputSchema: {
3165
- name: z.string()
3166
- .describe('Token name or css variable (e.g. --color-primary or var(--color-primary))'),
3167
- theme: z.enum(['light', 'dark', 'all']).optional()
3168
- .describe('Theme selector (currently only light is supported due to NG-06)'),
3169
- },
3170
- },
3171
- async ({ name, theme }) => {
3172
- const { isError, payload } = buildDesignTokenDetailPayload(designTokensData, name, theme);
3173
- if (isError) {
3174
- return {
3175
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
3176
- isError: true,
3177
- };
3178
- }
3179
- // Enrich referencedBy with component tagNames from CEM cssProperties
3180
- const normalizedName = normalizeTokenIdentifier(name);
3181
- const componentRefs = componentTokenRefMap.get(normalizedName);
3182
- if (componentRefs && componentRefs.size > 0) {
3183
- payload.componentReferencedBy = [...componentRefs].sort();
3184
- }
3185
- return buildJsonToolResponse(payload);
3186
- },
3187
- );
3188
-
3189
- // -----------------------------------------------------------------------
3190
- // Tool: get_accessibility_docs
3191
- // -----------------------------------------------------------------------
3192
- server.registerTool(
3193
- 'get_accessibility_docs',
3194
- {
3195
- description:
3196
- 'Get accessibility guidance and component checklist entries. ' +
3197
- 'When: validating accessibility decisions, reviewing ARIA usage, or checking WCAG-focused implementation notes. ' +
3198
- 'Returns: filtered checklist entries from component a11y annotations and accessibility guidelines. ' +
3199
- 'After: apply the checks in your markup and run validate_markup.',
3200
- inputSchema: {
3201
- component: z.string().optional()
3202
- .describe('Filter by component tagName/className/componentId'),
3203
- topic: z.string().optional()
3204
- .describe('Filter by topic (e.g. semantics, keyboard, labels, states, zoom, motion, callouts, guideline)'),
3205
- wcagLevel: z.enum(['A', 'AA', 'AAA', 'all']).optional()
3206
- .describe('Filter by WCAG level (default: all)'),
3207
- maxResults: z.number().int().min(1).max(100).optional()
3208
- .describe('Maximum results to return (default: 20)'),
3209
- prefix: z.string().optional(),
3210
- },
3211
- },
3212
- async ({ component, topic, wcagLevel, maxResults, prefix }) => {
3213
- const p = normalizePrefix(prefix);
3214
- let componentTagName;
3215
-
3216
- if (typeof component === 'string' && component.trim() !== '') {
3217
- const decl = resolveDeclByComponent(indexes, component, p)?.decl;
3218
-
3219
- if (!decl || typeof decl?.tagName !== 'string') {
3220
- return {
3221
- content: [{
3222
- type: 'text',
3223
- text: `Component not found (component=${component})`,
3224
- }],
3225
- isError: true,
3226
- };
3227
- }
3228
-
3229
- componentTagName = withPrefix(decl.tagName.toLowerCase(), p);
3230
- }
3231
-
3232
- const entries = buildAccessibilityIndex(indexes, guidelinesIndexData, { prefix: p });
3233
- const result = queryAccessibilityIndex(entries, {
3234
- componentTagName,
3235
- topic,
3236
- wcagLevel,
3237
- maxResults,
3238
- });
3239
-
3240
- const payload = {
3241
- query: {
3242
- component: componentTagName ?? null,
3243
- topic: result.topic,
3244
- wcagLevel: result.wcagLevel,
3245
- },
3246
- totalHits: result.totalHits,
3247
- results: result.results,
3248
- };
3249
-
3250
- return buildJsonToolResponse(payload);
3251
- },
3252
- );
3253
-
3254
- // -----------------------------------------------------------------------
3255
- // Tool: search_guidelines
3256
- // -----------------------------------------------------------------------
3257
- server.registerTool(
3258
- 'search_guidelines',
3259
- {
3260
- description:
3261
- 'Search design system guidelines including accessibility, CSS patterns, and best practices. ' +
3262
- 'When: need to understand design system rules before implementing UI. ' +
3263
- 'Returns: relevant guideline sections with file paths and snippets. ' +
3264
- 'After: follow the guidelines in your implementation.',
3265
- inputSchema: {
3266
- query: z.string().describe('Search keywords'),
3267
- topic: z.enum(['accessibility', 'css', 'patterns', 'all']).optional()
3268
- .describe('Filter by topic area'),
3269
- maxResults: z.number().int().min(1).max(20).optional()
3270
- .describe('Maximum results to return (1-20, default: 5)'),
3271
- },
3272
- },
3273
- async ({ query, topic, maxResults }) => {
3274
- if (!guidelinesIndexData) {
3275
- return {
3276
- content: [{ type: 'text', text: 'Guidelines index not available. Run: npm run mcp:index-guidelines' }],
3277
- isError: true,
3278
- };
3279
- }
3280
-
3281
- const max = maxResults ?? 5;
3282
- const documents = Array.isArray(guidelinesIndexData.documents) ? guidelinesIndexData.documents : [];
3283
- const q = query.toLowerCase();
3284
- const expandedTerms = expandQueryWithSynonyms(q);
3285
-
3286
- // Score and rank sections
3287
- const results = [];
3288
-
3289
- for (const doc of documents) {
3290
- if (topic && topic !== 'all' && doc.topic !== topic) continue;
3291
-
3292
- const sections = Array.isArray(doc.sections) ? doc.sections : [];
3293
- for (const section of sections) {
3294
- let score = 0;
3295
- const heading = String(section.heading ?? '').toLowerCase();
3296
- const keywords = Array.isArray(section.keywords) ? section.keywords : [];
3297
- const snippet = String(section.snippet ?? '').toLowerCase();
3298
- const body = String(section.body ?? '').toLowerCase();
3299
-
3300
- // Heading match: weight 3
3301
- if (heading.includes(q)) score += 3;
3302
-
3303
- // Keyword match: weight 2
3304
- for (const kw of keywords) {
3305
- if (String(kw).toLowerCase().includes(q)) {
3306
- score += 2;
3307
- break;
3308
- }
3309
- }
3310
-
3311
- // Snippet match: weight 1
3312
- if (snippet.includes(q)) score += 1;
3313
-
3314
- // Body text match: weight 1, plus boost for multiple occurrences
3315
- if (body && body.includes(q)) {
3316
- score += 1;
3317
- // Count additional occurrences in body for boost (cap at +2)
3318
- let idx = body.indexOf(q);
3319
- let occurrences = 0;
3320
- while (idx !== -1 && occurrences < 3) {
3321
- occurrences++;
3322
- idx = body.indexOf(q, idx + q.length);
3323
- }
3324
- if (occurrences > 1) score += Math.min(occurrences - 1, 2);
3325
- }
3326
-
3327
- // Synonym expansion match: check all expanded terms, cap total synonym contribution at +2
3328
- if (expandedTerms.length > 1) {
3329
- let synScore = 0;
3330
- const lowerKeywords = keywords.map((kw) => String(kw).toLowerCase());
3331
- for (let i = 1; i < expandedTerms.length && synScore < 2; i++) {
3332
- const syn = expandedTerms[i];
3333
- if (heading.includes(syn)) { synScore += 1; continue; }
3334
- if (snippet.includes(syn) || body.includes(syn)) { synScore += 1; continue; }
3335
- for (const kw of lowerKeywords) {
3336
- if (kw.includes(syn)) {
3337
- synScore += 1;
3338
- break;
3339
- }
3340
- }
3341
- }
3342
- score += synScore;
3343
- }
3344
-
3345
- if (score > 0) {
3346
- results.push({
3347
- score,
3348
- documentId: doc.id,
3349
- title: doc.title,
3350
- topic: doc.topic,
3351
- heading: section.heading,
3352
- snippet: section.snippet,
3353
- startLine: section.startLine,
3354
- });
3355
- }
3356
- }
3357
- }
3358
-
3359
- // Sort by score descending, take top N
3360
- results.sort((a, b) => b.score - a.score);
3361
- const topResults = results.slice(0, max);
3362
-
3363
- const payload = {
3364
- query,
3365
- topic: topic ?? 'all',
3366
- totalHits: results.length,
3367
- results: topResults,
3368
- };
3369
-
3370
- // Zero-result fallback: suggest alternative queries and tools
3371
- if (results.length === 0) {
3372
- const synonymExpansions = expandedTerms.filter((t) => t !== q);
3373
- payload.suggestions = {
3374
- alternativeQueries: synonymExpansions.length > 0 ? synonymExpansions : [],
3375
- alternativeTools: [
3376
- { tool: 'get_accessibility_docs', hint: 'For component-specific a11y checks' },
3377
- { tool: 'get_component_api', hint: 'For component API details' },
3378
- ],
3379
- };
3380
- }
3381
-
3382
- return buildJsonToolResponse(payload);
3383
- },
3384
- );
193
+ const server = new McpServer({
194
+ name: 'web-components-factory-design-system',
195
+ version: PACKAGE_VERSION,
196
+ });
3385
197
 
3386
- for (const plugin of plugins) {
3387
- const pluginTools = Array.isArray(plugin.tools) ? plugin.tools : [];
3388
- for (const tool of pluginTools) {
3389
- server.registerTool(
3390
- tool.name,
3391
- {
3392
- description: tool.description,
3393
- inputSchema: toPassthroughSchema(tool.inputSchema),
3394
- },
3395
- async (args) => {
3396
- try {
3397
- if (typeof tool.handler === 'function') {
3398
- const result = await tool.handler(args, {
3399
- plugin: { name: plugin.name, version: plugin.version },
3400
- helpers: {
3401
- loadJsonData: loadJson,
3402
- loadTextData: loadText,
3403
- buildJsonToolResponse,
3404
- normalizePrefix,
3405
- withPrefix,
3406
- toCanonicalTagName,
3407
- },
3408
- });
3409
- if (isPlainObject(result) && Array.isArray(result.content)) {
3410
- return result;
3411
- }
3412
- return buildJsonToolResponse(result ?? {});
3413
- }
3414
- return buildJsonToolResponse(tool.staticPayload ?? {});
3415
- } catch (error) {
3416
- const message = error instanceof Error ? error.message : String(error);
3417
- return {
3418
- content: [{
3419
- type: 'text',
3420
- text: JSON.stringify({
3421
- error: {
3422
- code: 'PLUGIN_TOOL_RUNTIME_ERROR',
3423
- message: `Plugin tool failed (${tool.name}): ${message}`,
3424
- plugin: plugin.name,
3425
- },
3426
- }, null, 2),
3427
- }],
3428
- isError: true,
3429
- };
3430
- }
3431
- },
3432
- );
3433
- }
3434
- }
198
+ // Delegate all tool / resource / prompt registration to register.mjs (DD-08)
199
+ registerAll({
200
+ server,
201
+ indexes,
202
+ detectedPrefix,
203
+ canonicalCemIndex,
204
+ canonicalEnumMap,
205
+ canonicalSlotMap,
206
+ installRegistry,
207
+ patterns,
208
+ relatedComponentMap,
209
+ patternFrequency,
210
+ designTokensData,
211
+ guidelinesIndexData,
212
+ llmsFullText,
213
+ tokenSuggestionMap,
214
+ componentTokenRefMap,
215
+ plugins,
216
+ loadJsonData,
217
+ loadJson,
218
+ loadText,
219
+ loadValidator: async () => ({
220
+ collectCemCustomElements,
221
+ validateTextAgainstCem,
222
+ detectTokenMisuseInInlineStyles,
223
+ detectAccessibilityMisuseInMarkup,
224
+ buildEnumAttributeMap,
225
+ detectEnumValueMisuse,
226
+ buildSlotNameMap,
227
+ detectInvalidSlotName,
228
+ detectMissingRequiredAttributes,
229
+ detectOrphanedChildComponents,
230
+ detectEmptyInteractiveElement,
231
+ detectNonLowercaseAttributes,
232
+ detectCdnReferences,
233
+ detectMissingRuntimeScaffold,
234
+ }),
235
+ selectorGuideData,
236
+ maxToolResultBytes: MAX_TOOL_RESULT_BYTES,
237
+ });
3435
238
 
3436
239
  return {
3437
240
  server,