@monoharada/wcf-mcp 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -11
- package/core.mjs +765 -70
- package/data/custom-elements.json +18 -0
- package/data/design-tokens.json +1 -1
- package/data/guidelines-index.json +1 -1
- package/package.json +1 -1
- package/validator.mjs +174 -5
package/core.mjs
CHANGED
|
@@ -14,6 +14,9 @@ import { z } from 'zod';
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
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;
|
|
17
20
|
|
|
18
21
|
export const CATEGORY_MAP = {
|
|
19
22
|
'dads-input-text': 'Form',
|
|
@@ -79,6 +82,128 @@ export const CATEGORY_MAP = {
|
|
|
79
82
|
'dads-loading-icon': 'Display',
|
|
80
83
|
};
|
|
81
84
|
|
|
85
|
+
const TOKEN_MISUSE_ALLOWED_TYPES = Object.freeze(new Set(['color', 'spacing']));
|
|
86
|
+
const STRUCTURED_CONTENT_DISABLE_TRUE_VALUES = Object.freeze(new Set(['1', 'true', 'yes', 'on']));
|
|
87
|
+
const WCAG_LEVELS = Object.freeze(new Set(['A', 'AA', 'AAA', 'all']));
|
|
88
|
+
const A11Y_CATEGORY_LEVEL_MAP = Object.freeze({
|
|
89
|
+
semantics: 'A',
|
|
90
|
+
keyboard: 'A',
|
|
91
|
+
labels: 'A',
|
|
92
|
+
states: 'AA',
|
|
93
|
+
zoom: 'AA',
|
|
94
|
+
motion: 'AA',
|
|
95
|
+
callouts: 'AA',
|
|
96
|
+
guideline: 'A',
|
|
97
|
+
});
|
|
98
|
+
const NPX_TEMPLATE = Object.freeze({
|
|
99
|
+
command: 'npx',
|
|
100
|
+
args: ['@monoharada/wcf-mcp'],
|
|
101
|
+
});
|
|
102
|
+
export const IDE_SETUP_TEMPLATES = Object.freeze([
|
|
103
|
+
{
|
|
104
|
+
ide: 'Claude Desktop',
|
|
105
|
+
configPath: 'claude_desktop_config.json',
|
|
106
|
+
snippet: {
|
|
107
|
+
mcpServers: {
|
|
108
|
+
wcf: NPX_TEMPLATE,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
ide: 'Claude Code',
|
|
114
|
+
configPath: '.mcp.json',
|
|
115
|
+
snippet: {
|
|
116
|
+
mcpServers: {
|
|
117
|
+
wcf: NPX_TEMPLATE,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
ide: 'Cursor',
|
|
123
|
+
configPath: '.cursor/mcp.json',
|
|
124
|
+
snippet: {
|
|
125
|
+
mcpServers: {
|
|
126
|
+
wcf: NPX_TEMPLATE,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
export function isStructuredContentDisabled(env = process.env) {
|
|
133
|
+
const raw = String(env?.[STRUCTURED_CONTENT_DISABLE_FLAG] ?? '').trim().toLowerCase();
|
|
134
|
+
return STRUCTURED_CONTENT_DISABLE_TRUE_VALUES.has(raw);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function toStructuredContent(data) {
|
|
138
|
+
return {
|
|
139
|
+
type: 'application/json',
|
|
140
|
+
data,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function measureToolResultBytes(result) {
|
|
145
|
+
return Buffer.byteLength(JSON.stringify(result), 'utf8');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildJsonToolResponse(payload, { env = process.env } = {}) {
|
|
149
|
+
const content = [{
|
|
150
|
+
type: 'text',
|
|
151
|
+
text: JSON.stringify(payload, null, 2),
|
|
152
|
+
}];
|
|
153
|
+
|
|
154
|
+
if (isStructuredContentDisabled(env)) {
|
|
155
|
+
return { content };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const withStructuredContent = {
|
|
159
|
+
content,
|
|
160
|
+
structuredContent: toStructuredContent(payload),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Keep response size under the 100KB guardrail even when structuredContent is enabled.
|
|
164
|
+
if (measureToolResultBytes(withStructuredContent) > MAX_TOOL_RESULT_BYTES) {
|
|
165
|
+
return { content };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return withStructuredContent;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function normalizeTokenValue(value) {
|
|
172
|
+
if (typeof value === 'string') return value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
173
|
+
if (typeof value === 'number') return String(value);
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function normalizeCssVariable(value) {
|
|
178
|
+
if (typeof value !== 'string') return '';
|
|
179
|
+
|
|
180
|
+
const raw = value.trim();
|
|
181
|
+
if (!raw) return '';
|
|
182
|
+
if (raw.startsWith('--')) return raw;
|
|
183
|
+
|
|
184
|
+
const varMatch = /^var\(\s*(--[^,\s)]+)\s*(?:,\s*[^)]+)?\)$/.exec(raw);
|
|
185
|
+
if (varMatch) return varMatch[1];
|
|
186
|
+
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function buildTokenSuggestionMap(designTokensData) {
|
|
191
|
+
if (!Array.isArray(designTokensData?.tokens)) return new Map();
|
|
192
|
+
|
|
193
|
+
const out = new Map();
|
|
194
|
+
for (const token of designTokensData.tokens) {
|
|
195
|
+
const type = String(token?.type ?? '').toLowerCase();
|
|
196
|
+
if (!TOKEN_MISUSE_ALLOWED_TYPES.has(type)) continue;
|
|
197
|
+
|
|
198
|
+
const cssVariable = normalizeCssVariable(token?.cssVariable);
|
|
199
|
+
if (!cssVariable) continue;
|
|
200
|
+
|
|
201
|
+
const normalized = normalizeTokenValue(token?.value);
|
|
202
|
+
if (normalized && !out.has(normalized)) out.set(normalized, cssVariable);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
82
207
|
// ---------------------------------------------------------------------------
|
|
83
208
|
// Helpers (exported for testing)
|
|
84
209
|
// ---------------------------------------------------------------------------
|
|
@@ -87,11 +212,15 @@ export function getCategory(tagName) {
|
|
|
87
212
|
return CATEGORY_MAP[tagName] ?? 'Other';
|
|
88
213
|
}
|
|
89
214
|
|
|
90
|
-
|
|
215
|
+
function normalizePrefixRaw(prefix) {
|
|
91
216
|
if (typeof prefix !== 'string' || prefix.trim() === '') return CANONICAL_PREFIX;
|
|
92
217
|
return prefix.trim().toLowerCase();
|
|
93
218
|
}
|
|
94
219
|
|
|
220
|
+
export function normalizePrefix(prefix) {
|
|
221
|
+
return normalizePrefixRaw(prefix).slice(0, MAX_PREFIX_LENGTH);
|
|
222
|
+
}
|
|
223
|
+
|
|
95
224
|
export function withPrefix(tagName, prefix) {
|
|
96
225
|
if (typeof tagName !== 'string') return tagName;
|
|
97
226
|
const p = normalizePrefix(prefix);
|
|
@@ -107,14 +236,89 @@ export function toCanonicalTagName(tagName, prefix) {
|
|
|
107
236
|
if (!raw) return undefined;
|
|
108
237
|
if (raw.startsWith(`${CANONICAL_PREFIX}-`)) return raw;
|
|
109
238
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
239
|
+
const candidates = [...new Set([normalizePrefix(prefix), normalizePrefixRaw(prefix)])];
|
|
240
|
+
for (const p of candidates) {
|
|
241
|
+
if (p !== CANONICAL_PREFIX && raw.startsWith(`${p}-`)) {
|
|
242
|
+
return `${CANONICAL_PREFIX}-${raw.slice(p.length + 1)}`;
|
|
243
|
+
}
|
|
113
244
|
}
|
|
114
245
|
|
|
115
246
|
return raw;
|
|
116
247
|
}
|
|
117
248
|
|
|
249
|
+
export function levenshteinDistance(left, right) {
|
|
250
|
+
const a = String(left ?? '');
|
|
251
|
+
const b = String(right ?? '');
|
|
252
|
+
if (a === b) return 0;
|
|
253
|
+
if (!a.length) return b.length;
|
|
254
|
+
if (!b.length) return a.length;
|
|
255
|
+
|
|
256
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
257
|
+
const curr = Array.from({ length: b.length + 1 }, () => 0);
|
|
258
|
+
|
|
259
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
260
|
+
curr[0] = i;
|
|
261
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
262
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
263
|
+
curr[j] = Math.min(
|
|
264
|
+
curr[j - 1] + 1,
|
|
265
|
+
prev[j] + 1,
|
|
266
|
+
prev[j - 1] + cost,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = curr[j];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return prev[b.length];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
276
|
+
const target = String(tagName ?? '').trim().toLowerCase();
|
|
277
|
+
if (!target || !target.includes('-')) return undefined;
|
|
278
|
+
|
|
279
|
+
let bestTag;
|
|
280
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
281
|
+
const candidateSource = cemIndex instanceof Map ? cemIndex.keys() : [];
|
|
282
|
+
for (const rawCandidate of candidateSource) {
|
|
283
|
+
const candidate = String(rawCandidate ?? '').toLowerCase();
|
|
284
|
+
if (!candidate || !candidate.includes('-') || candidate === target) continue;
|
|
285
|
+
const distance = levenshteinDistance(target, candidate);
|
|
286
|
+
if (distance < bestDistance) {
|
|
287
|
+
bestDistance = distance;
|
|
288
|
+
bestTag = candidate;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!bestTag) return undefined;
|
|
293
|
+
const maxDistance = Math.max(1, Math.ceil(target.length * 0.3));
|
|
294
|
+
if (bestDistance > maxDistance) return undefined;
|
|
295
|
+
return bestTag;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function buildDiagnosticSuggestion({ diagnostic, cemIndex }) {
|
|
299
|
+
const code = String(diagnostic?.code ?? '');
|
|
300
|
+
if (!code) return undefined;
|
|
301
|
+
|
|
302
|
+
if (code === 'unknownElement') {
|
|
303
|
+
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex);
|
|
304
|
+
return tagName ? `Did you mean "${tagName}"?` : undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (code === 'forbiddenAttribute' && String(diagnostic?.attrName ?? '').toLowerCase() === 'placeholder') {
|
|
308
|
+
return 'Use aria-label or aria-describedby support text instead of placeholder.';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (code === 'ariaLiveNotRecommended') {
|
|
312
|
+
return 'Remove aria-live and connect support or error text via aria-describedby.';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (code === 'roleAlertNotRecommended') {
|
|
316
|
+
return 'Use role="alert" only for urgent live updates; otherwise use static text associated via aria-describedby.';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
118
322
|
export function findCustomElementDeclarations(manifest) {
|
|
119
323
|
const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
|
|
120
324
|
const decls = [];
|
|
@@ -327,6 +531,387 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
|
327
531
|
return [...out];
|
|
328
532
|
}
|
|
329
533
|
|
|
534
|
+
export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix } = {}) {
|
|
535
|
+
const p = normalizePrefix(prefix);
|
|
536
|
+
const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
537
|
+
const pageSize = Number.isInteger(limit) ? Math.max(1, Math.min(limit, 200)) : Number.MAX_SAFE_INTEGER;
|
|
538
|
+
const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
539
|
+
|
|
540
|
+
let items = indexes.decls.map(({ decl, tagName, modulePath }) => ({
|
|
541
|
+
tagName: withPrefix(tagName, p),
|
|
542
|
+
className: typeof decl?.name === 'string' ? decl.name : undefined,
|
|
543
|
+
description: typeof decl?.description === 'string' ? decl.description : undefined,
|
|
544
|
+
category: getCategory(tagName),
|
|
545
|
+
modulePath,
|
|
546
|
+
}));
|
|
547
|
+
|
|
548
|
+
if (category) {
|
|
549
|
+
items = items.filter((item) => item.category === category);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (q) {
|
|
553
|
+
items = items.filter((item) => {
|
|
554
|
+
const haystacks = [
|
|
555
|
+
item.tagName,
|
|
556
|
+
item.className,
|
|
557
|
+
item.description,
|
|
558
|
+
item.category,
|
|
559
|
+
item.modulePath,
|
|
560
|
+
];
|
|
561
|
+
return haystacks.some((value) => String(value ?? '').toLowerCase().includes(q));
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const total = items.length;
|
|
566
|
+
const paged = items.slice(pageOffset, pageOffset + pageSize);
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
total,
|
|
570
|
+
limit: pageSize,
|
|
571
|
+
offset: pageOffset,
|
|
572
|
+
hasMore: pageOffset + paged.length < total,
|
|
573
|
+
items: paged,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function parseIconNamesFromDescription(description) {
|
|
578
|
+
if (typeof description !== 'string' || description.trim() === '') return [];
|
|
579
|
+
|
|
580
|
+
const markerMatch = description.match(/iconPathsのキー[::]\s*([^))\n]+)/u);
|
|
581
|
+
if (!markerMatch) return [];
|
|
582
|
+
|
|
583
|
+
return [...new Set(
|
|
584
|
+
markerMatch[1]
|
|
585
|
+
.split(/[,、]/)
|
|
586
|
+
.map((name) => name.trim())
|
|
587
|
+
.map((name) => name.replace(/[`'"]/g, ''))
|
|
588
|
+
.filter(Boolean),
|
|
589
|
+
)];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function parseIconNamesFromType(typeText) {
|
|
593
|
+
if (typeof typeText !== 'string' || typeText.trim() === '') return [];
|
|
594
|
+
const out = [];
|
|
595
|
+
const regex = /'([^']+)'|"([^"]+)"|`([^`]+)`/g;
|
|
596
|
+
let match;
|
|
597
|
+
while ((match = regex.exec(typeText)) !== null) {
|
|
598
|
+
const value = match[1] ?? match[2] ?? match[3];
|
|
599
|
+
if (typeof value === 'string' && value.trim() !== '') out.push(value.trim());
|
|
600
|
+
}
|
|
601
|
+
return [...new Set(out)];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function extractIconNames(indexes) {
|
|
605
|
+
const decl = indexes.byTag.get('dads-icon');
|
|
606
|
+
if (!decl) return [];
|
|
607
|
+
|
|
608
|
+
const attributes = Array.isArray(decl?.attributes) ? decl.attributes : [];
|
|
609
|
+
const nameAttr = attributes.find((attr) => String(attr?.name ?? '') === 'name');
|
|
610
|
+
if (!nameAttr) return [];
|
|
611
|
+
|
|
612
|
+
const fromDescription = parseIconNamesFromDescription(nameAttr?.description);
|
|
613
|
+
const fromType = parseIconNamesFromType(nameAttr?.type?.text);
|
|
614
|
+
|
|
615
|
+
return [...new Set([...fromDescription, ...fromType])];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function buildIconCatalog(indexes, prefix) {
|
|
619
|
+
const p = normalizePrefix(prefix);
|
|
620
|
+
const tag = withPrefix('dads-icon', p);
|
|
621
|
+
const names = extractIconNames(indexes).sort((left, right) => left.localeCompare(right));
|
|
622
|
+
|
|
623
|
+
return names.map((name) => ({
|
|
624
|
+
name,
|
|
625
|
+
variants: ['default'],
|
|
626
|
+
usageExample: `<${tag} name="${name}" size="20"></${tag}>`,
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function searchIconCatalog(indexes, { query, limit, offset, prefix } = {}) {
|
|
631
|
+
const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
632
|
+
const pageSize = Number.isInteger(limit) ? Math.max(1, Math.min(limit, 100)) : 20;
|
|
633
|
+
const pageOffset = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
634
|
+
|
|
635
|
+
let icons = buildIconCatalog(indexes, prefix);
|
|
636
|
+
if (q) {
|
|
637
|
+
icons = icons.filter((icon) => icon.name.toLowerCase().includes(q));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const total = icons.length;
|
|
641
|
+
const paged = icons.slice(pageOffset, pageOffset + pageSize);
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
total,
|
|
645
|
+
limit: pageSize,
|
|
646
|
+
offset: pageOffset,
|
|
647
|
+
hasMore: pageOffset + paged.length < total,
|
|
648
|
+
icons: paged,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function buildRelatedComponentMap(installRegistry, patterns) {
|
|
653
|
+
const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
|
|
654
|
+
const patternList = Object.values(patterns ?? {});
|
|
655
|
+
const related = new Map();
|
|
656
|
+
|
|
657
|
+
const addRelation = (fromId, toId, via) => {
|
|
658
|
+
const from = String(fromId ?? '').trim();
|
|
659
|
+
const to = String(toId ?? '').trim();
|
|
660
|
+
if (!from || !to || from === to) return;
|
|
661
|
+
|
|
662
|
+
if (!related.has(from)) related.set(from, new Map());
|
|
663
|
+
const relMap = related.get(from);
|
|
664
|
+
if (!relMap.has(to)) relMap.set(to, new Set());
|
|
665
|
+
relMap.get(to).add(via);
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
for (const pattern of patternList) {
|
|
669
|
+
const patternId = String(pattern?.id ?? '').trim() || 'pattern';
|
|
670
|
+
const requires = [...new Set((Array.isArray(pattern?.requires) ? pattern.requires : []).map((id) => String(id ?? '').trim()).filter(Boolean))];
|
|
671
|
+
|
|
672
|
+
for (const fromId of requires) {
|
|
673
|
+
for (const toId of requires) {
|
|
674
|
+
addRelation(fromId, toId, patternId);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (const [componentId, meta] of Object.entries(components)) {
|
|
680
|
+
const deps = Array.isArray(meta?.deps) ? meta.deps : [];
|
|
681
|
+
for (const dep of deps) {
|
|
682
|
+
const depId = String(dep ?? '').trim();
|
|
683
|
+
addRelation(componentId, depId, 'dependency');
|
|
684
|
+
addRelation(depId, componentId, 'dependencyOf');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return related;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function getRelatedComponentsForTag({ canonicalTagName, installRegistry, relatedMap, prefix, maxResults = 12 }) {
|
|
692
|
+
const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
|
|
693
|
+
const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
|
|
694
|
+
const componentId = typeof canonicalTagName === 'string' ? tags[canonicalTagName] : undefined;
|
|
695
|
+
if (typeof componentId !== 'string' || componentId === '') return [];
|
|
696
|
+
|
|
697
|
+
const relMap = relatedMap?.get(componentId);
|
|
698
|
+
if (!relMap) return [];
|
|
699
|
+
|
|
700
|
+
const out = [];
|
|
701
|
+
for (const [relatedId, via] of relMap.entries()) {
|
|
702
|
+
const relatedMeta = components[relatedId];
|
|
703
|
+
if (!relatedMeta || typeof relatedMeta !== 'object') continue;
|
|
704
|
+
|
|
705
|
+
const canonicalTags = Array.isArray(relatedMeta.tags)
|
|
706
|
+
? relatedMeta.tags.map((tag) => String(tag ?? '').toLowerCase()).filter(Boolean)
|
|
707
|
+
: [];
|
|
708
|
+
|
|
709
|
+
out.push({
|
|
710
|
+
componentId: relatedId,
|
|
711
|
+
tagNames: canonicalTags.map((tag) => withPrefix(tag, prefix)),
|
|
712
|
+
via: [...via],
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
out.sort((left, right) => String(left.componentId).localeCompare(String(right.componentId)));
|
|
717
|
+
return out.slice(0, Math.max(1, maxResults));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function normalizeWcagLevel(level) {
|
|
721
|
+
const raw = typeof level === 'string' ? level.trim().toUpperCase() : '';
|
|
722
|
+
if (!raw || raw === 'ALL') return 'all';
|
|
723
|
+
return WCAG_LEVELS.has(raw) ? raw : 'all';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function getWcagLevelForA11yTopic(topic) {
|
|
727
|
+
const key = String(topic ?? '').trim().toLowerCase();
|
|
728
|
+
return A11Y_CATEGORY_LEVEL_MAP[key] ?? 'A';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function toChecklistItemsFromCategories(categories) {
|
|
732
|
+
if (!categories || typeof categories !== 'object') return [];
|
|
733
|
+
|
|
734
|
+
const out = [];
|
|
735
|
+
for (const [topic, checks] of Object.entries(categories)) {
|
|
736
|
+
if (!Array.isArray(checks)) continue;
|
|
737
|
+
const wcagLevel = getWcagLevelForA11yTopic(topic);
|
|
738
|
+
for (const check of checks) {
|
|
739
|
+
const text = String(check ?? '').trim();
|
|
740
|
+
if (!text) continue;
|
|
741
|
+
out.push({
|
|
742
|
+
topic: String(topic),
|
|
743
|
+
wcagLevel,
|
|
744
|
+
check: text,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function toChecklistItemsFromCallouts(callouts) {
|
|
752
|
+
if (!Array.isArray(callouts)) return [];
|
|
753
|
+
|
|
754
|
+
const out = [];
|
|
755
|
+
for (const callout of callouts) {
|
|
756
|
+
const parts = [
|
|
757
|
+
callout?.title,
|
|
758
|
+
callout?.label,
|
|
759
|
+
callout?.description,
|
|
760
|
+
...(Array.isArray(callout?.highlights) ? callout.highlights : []),
|
|
761
|
+
]
|
|
762
|
+
.map((value) => String(value ?? '').trim())
|
|
763
|
+
.filter(Boolean);
|
|
764
|
+
|
|
765
|
+
if (parts.length === 0) continue;
|
|
766
|
+
out.push({
|
|
767
|
+
topic: 'callouts',
|
|
768
|
+
wcagLevel: getWcagLevelForA11yTopic('callouts'),
|
|
769
|
+
check: parts.join(' — '),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function extractAccessibilityChecklist(decl, { prefix } = {}) {
|
|
776
|
+
const annotations = decl?.custom?.a11yAnnotations;
|
|
777
|
+
if (!annotations || typeof annotations !== 'object') return undefined;
|
|
778
|
+
|
|
779
|
+
const items = [
|
|
780
|
+
...toChecklistItemsFromCategories(annotations.categories),
|
|
781
|
+
...toChecklistItemsFromCallouts(annotations.callouts),
|
|
782
|
+
];
|
|
783
|
+
if (items.length === 0) return undefined;
|
|
784
|
+
|
|
785
|
+
const unique = new Map();
|
|
786
|
+
for (const item of items) {
|
|
787
|
+
const key = `${item.topic}|${item.wcagLevel}|${item.check}`;
|
|
788
|
+
if (!unique.has(key)) unique.set(key, item);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
summary: String(annotations.summary ?? '').trim() || 'Component accessibility checklist',
|
|
793
|
+
version: Number.isInteger(annotations.version) ? annotations.version : 1,
|
|
794
|
+
totalChecks: unique.size,
|
|
795
|
+
items: [...unique.values()],
|
|
796
|
+
componentTagName:
|
|
797
|
+
typeof decl?.tagName === 'string' ? withPrefix(decl.tagName.toLowerCase(), prefix) : undefined,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function buildAccessibilityIndex(indexes, guidelinesIndexData, { prefix } = {}) {
|
|
802
|
+
const out = [];
|
|
803
|
+
|
|
804
|
+
for (const { decl, tagName } of indexes.decls) {
|
|
805
|
+
const checklist = extractAccessibilityChecklist(decl, { prefix });
|
|
806
|
+
if (!checklist) continue;
|
|
807
|
+
const className = typeof decl?.name === 'string' ? decl.name : undefined;
|
|
808
|
+
|
|
809
|
+
for (const item of checklist.items) {
|
|
810
|
+
out.push({
|
|
811
|
+
source: 'component',
|
|
812
|
+
componentTagName: withPrefix(tagName, prefix),
|
|
813
|
+
componentClassName: className,
|
|
814
|
+
topic: item.topic,
|
|
815
|
+
wcagLevel: item.wcagLevel,
|
|
816
|
+
check: item.check,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const docs = Array.isArray(guidelinesIndexData?.documents)
|
|
822
|
+
? guidelinesIndexData.documents.filter((doc) => doc?.topic === 'accessibility')
|
|
823
|
+
: [];
|
|
824
|
+
|
|
825
|
+
for (const doc of docs) {
|
|
826
|
+
const sections = Array.isArray(doc?.sections) ? doc.sections : [];
|
|
827
|
+
for (const section of sections) {
|
|
828
|
+
const heading = String(section?.heading ?? '').trim();
|
|
829
|
+
const snippet = String(section?.snippet ?? '').trim();
|
|
830
|
+
if (!heading && !snippet) continue;
|
|
831
|
+
|
|
832
|
+
out.push({
|
|
833
|
+
source: 'guideline',
|
|
834
|
+
documentId: String(doc?.id ?? ''),
|
|
835
|
+
title: String(doc?.title ?? ''),
|
|
836
|
+
heading,
|
|
837
|
+
topic: 'guideline',
|
|
838
|
+
wcagLevel: getWcagLevelForA11yTopic('guideline'),
|
|
839
|
+
check: snippet || heading,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function queryAccessibilityIndex(
|
|
848
|
+
entries,
|
|
849
|
+
{ componentTagName, topic, wcagLevel, maxResults = 20 } = {},
|
|
850
|
+
) {
|
|
851
|
+
const normalizedTopic = String(topic ?? '').trim().toLowerCase() || 'all';
|
|
852
|
+
const normalizedWcagLevel = normalizeWcagLevel(wcagLevel);
|
|
853
|
+
const pageSize = Number.isInteger(maxResults) ? Math.max(1, Math.min(maxResults, 100)) : 20;
|
|
854
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
855
|
+
const results = [];
|
|
856
|
+
const shouldBalanceSources = !componentTagName && normalizedTopic === 'all';
|
|
857
|
+
const guidelineCandidates = [];
|
|
858
|
+
const componentCandidates = [];
|
|
859
|
+
const otherCandidates = [];
|
|
860
|
+
let totalHits = 0;
|
|
861
|
+
|
|
862
|
+
for (const entry of source) {
|
|
863
|
+
if (componentTagName && entry.componentTagName !== componentTagName) continue;
|
|
864
|
+
if (normalizedTopic !== 'all' && String(entry.topic ?? '').toLowerCase() !== normalizedTopic) continue;
|
|
865
|
+
if (normalizedWcagLevel !== 'all' && String(entry.wcagLevel ?? '').toUpperCase() !== normalizedWcagLevel) continue;
|
|
866
|
+
|
|
867
|
+
totalHits += 1;
|
|
868
|
+
if (!shouldBalanceSources) {
|
|
869
|
+
if (results.length < pageSize) results.push(entry);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (String(entry.source ?? '') === 'guideline') {
|
|
874
|
+
if (guidelineCandidates.length < pageSize) guidelineCandidates.push(entry);
|
|
875
|
+
} else if (String(entry.source ?? '') === 'component') {
|
|
876
|
+
if (componentCandidates.length < pageSize) componentCandidates.push(entry);
|
|
877
|
+
} else if (otherCandidates.length < pageSize) {
|
|
878
|
+
otherCandidates.push(entry);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (shouldBalanceSources) {
|
|
883
|
+
while (results.length < pageSize) {
|
|
884
|
+
const beforeLength = results.length;
|
|
885
|
+
if (guidelineCandidates.length > 0) results.push(guidelineCandidates.shift());
|
|
886
|
+
if (results.length < pageSize && componentCandidates.length > 0) results.push(componentCandidates.shift());
|
|
887
|
+
if (results.length < pageSize && otherCandidates.length > 0) results.push(otherCandidates.shift());
|
|
888
|
+
if (results.length === beforeLength) break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
topic: normalizedTopic,
|
|
894
|
+
wcagLevel: normalizedWcagLevel,
|
|
895
|
+
totalHits,
|
|
896
|
+
results,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function resolveDeclByComponent(indexes, component, prefix) {
|
|
901
|
+
const byTagOrClass =
|
|
902
|
+
pickDecl(indexes, { tagName: component, prefix }) ??
|
|
903
|
+
pickDecl(indexes, { className: component, prefix });
|
|
904
|
+
if (byTagOrClass) {
|
|
905
|
+
const canonicalTag = typeof byTagOrClass.tagName === 'string' ? byTagOrClass.tagName.toLowerCase() : undefined;
|
|
906
|
+
return {
|
|
907
|
+
decl: byTagOrClass,
|
|
908
|
+
modulePath: canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return findDeclByComponentId(indexes, component);
|
|
913
|
+
}
|
|
914
|
+
|
|
330
915
|
// ---------------------------------------------------------------------------
|
|
331
916
|
// createMcpServer — builds the McpServer with all tools registered, but does
|
|
332
917
|
// NOT connect a transport. Callers choose their own transport.
|
|
@@ -338,11 +923,17 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
|
338
923
|
export async function createMcpServer(loadJsonData, loadValidator) {
|
|
339
924
|
const manifest = await loadJsonData('custom-elements.json');
|
|
340
925
|
const indexes = buildIndexes(manifest);
|
|
341
|
-
const {
|
|
926
|
+
const {
|
|
927
|
+
collectCemCustomElements,
|
|
928
|
+
validateTextAgainstCem,
|
|
929
|
+
detectTokenMisuseInInlineStyles = () => [],
|
|
930
|
+
detectAccessibilityMisuseInMarkup = () => [],
|
|
931
|
+
} = await loadValidator();
|
|
342
932
|
const canonicalCemIndex = collectCemCustomElements(manifest);
|
|
343
933
|
const installRegistry = await loadJsonData('install-registry.json');
|
|
344
934
|
const patternRegistry = await loadJsonData('pattern-registry.json');
|
|
345
935
|
const { patterns } = loadPatternRegistryShape(patternRegistry);
|
|
936
|
+
const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
|
|
346
937
|
|
|
347
938
|
// Load optional data files (design tokens, guidelines index)
|
|
348
939
|
let designTokensData = null;
|
|
@@ -359,6 +950,8 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
359
950
|
// guidelines-index.json may not exist yet
|
|
360
951
|
}
|
|
361
952
|
|
|
953
|
+
const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
|
|
954
|
+
|
|
362
955
|
const server = new McpServer({
|
|
363
956
|
name: 'web-components-factory-design-system',
|
|
364
957
|
version: '0.1.1',
|
|
@@ -394,9 +987,11 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
394
987
|
componentsByCategory: categoryCount,
|
|
395
988
|
totalPatterns: patternList.length,
|
|
396
989
|
patterns: patternList,
|
|
990
|
+
ideSetupTemplates: IDE_SETUP_TEMPLATES,
|
|
397
991
|
availableTools: [
|
|
398
992
|
{ name: 'get_design_system_overview', purpose: 'This overview (start here)' },
|
|
399
|
-
{ name: 'list_components', purpose: 'Browse components
|
|
993
|
+
{ name: 'list_components', purpose: 'Browse components with progressive disclosure and filters' },
|
|
994
|
+
{ name: 'search_icons', purpose: 'Search icon names and usage examples' },
|
|
400
995
|
{ name: 'get_component_api', purpose: 'Full API surface for a single component' },
|
|
401
996
|
{ name: 'generate_usage_snippet', purpose: 'Minimal HTML usage example' },
|
|
402
997
|
{ name: 'get_install_recipe', purpose: 'Installation instructions and dependency tree' },
|
|
@@ -405,17 +1000,20 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
405
1000
|
{ name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
|
|
406
1001
|
{ name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
|
|
407
1002
|
{ name: 'get_design_tokens', purpose: 'Query design tokens (colors, spacing, typography, radius, shadows)' },
|
|
1003
|
+
{ name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
|
|
408
1004
|
{ name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
|
|
409
1005
|
],
|
|
410
1006
|
recommendedWorkflow: [
|
|
411
|
-
'1. get_design_system_overview → understand components, patterns
|
|
1007
|
+
'1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
|
|
412
1008
|
'2. search_guidelines → find relevant guidelines',
|
|
413
1009
|
'3. get_design_tokens → get correct token values',
|
|
414
|
-
'4.
|
|
415
|
-
'5.
|
|
416
|
-
'6.
|
|
417
|
-
'7.
|
|
418
|
-
'8.
|
|
1010
|
+
'4. get_accessibility_docs → fetch component-level accessibility checklist',
|
|
1011
|
+
'5. list_components (category/query + pagination) → shortlist components',
|
|
1012
|
+
'6. search_icons (optional) → find icon names quickly',
|
|
1013
|
+
'7. get_component_api → check attributes, slots, events, CSS parts',
|
|
1014
|
+
'8. generate_usage_snippet or get_pattern_recipe → get code',
|
|
1015
|
+
'9. validate_markup → verify your HTML and use suggestions to self-correct',
|
|
1016
|
+
'10. get_install_recipe → get import/install instructions',
|
|
419
1017
|
],
|
|
420
1018
|
};
|
|
421
1019
|
|
|
@@ -432,31 +1030,45 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
432
1030
|
'list_components',
|
|
433
1031
|
{
|
|
434
1032
|
description:
|
|
435
|
-
'List custom elements in the design system. When: exploring available components or
|
|
1033
|
+
'List custom elements in the design system. When: exploring available components, searching by keyword, or paging through results. Returns: array of {tagName, className, description, category}. After: use get_component_api for details on a specific component.',
|
|
436
1034
|
inputSchema: {
|
|
437
1035
|
category: z
|
|
438
1036
|
.enum(['Form', 'Actions', 'Navigation', 'Content', 'Display', 'Layout', 'Other'])
|
|
439
1037
|
.optional()
|
|
440
1038
|
.describe('Filter by component category'),
|
|
1039
|
+
query: z.string().optional().describe('Search by tagName/className/description/category/modulePath'),
|
|
1040
|
+
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (optional; omit for all results)'),
|
|
1041
|
+
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
441
1042
|
prefix: z.string().optional(),
|
|
442
1043
|
},
|
|
443
1044
|
},
|
|
444
|
-
async ({ category, prefix }) => {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
modulePath,
|
|
452
|
-
}));
|
|
453
|
-
|
|
454
|
-
if (category) {
|
|
455
|
-
list = list.filter((item) => item.category === category);
|
|
456
|
-
}
|
|
1045
|
+
async ({ category, query, limit, offset, prefix }) => {
|
|
1046
|
+
const { items } = buildComponentSummaries(indexes, { category, query, limit, offset, prefix });
|
|
1047
|
+
return {
|
|
1048
|
+
content: [{ type: 'text', text: JSON.stringify(items, null, 2) }],
|
|
1049
|
+
};
|
|
1050
|
+
},
|
|
1051
|
+
);
|
|
457
1052
|
|
|
1053
|
+
// -----------------------------------------------------------------------
|
|
1054
|
+
// Tool: search_icons
|
|
1055
|
+
// -----------------------------------------------------------------------
|
|
1056
|
+
server.registerTool(
|
|
1057
|
+
'search_icons',
|
|
1058
|
+
{
|
|
1059
|
+
description:
|
|
1060
|
+
'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.',
|
|
1061
|
+
inputSchema: {
|
|
1062
|
+
query: z.string().optional().describe('Search icon names (partial match)'),
|
|
1063
|
+
limit: z.number().int().min(1).max(100).optional().describe('Maximum items to return (default: 20)'),
|
|
1064
|
+
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
1065
|
+
prefix: z.string().optional(),
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
async ({ query, limit, offset, prefix }) => {
|
|
1069
|
+
const payload = searchIconCatalog(indexes, { query, limit, offset, prefix });
|
|
458
1070
|
return {
|
|
459
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1071
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
460
1072
|
};
|
|
461
1073
|
},
|
|
462
1074
|
);
|
|
@@ -492,10 +1104,21 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
492
1104
|
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
493
1105
|
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
494
1106
|
const api = serializeApi(decl, modulePath, prefix);
|
|
1107
|
+
const relatedComponents = getRelatedComponentsForTag({
|
|
1108
|
+
canonicalTagName: canonicalTag,
|
|
1109
|
+
installRegistry,
|
|
1110
|
+
relatedMap: relatedComponentMap,
|
|
1111
|
+
prefix,
|
|
1112
|
+
});
|
|
1113
|
+
if (relatedComponents.length > 0) {
|
|
1114
|
+
api.relatedComponents = relatedComponents;
|
|
1115
|
+
}
|
|
1116
|
+
const accessibilityChecklist = extractAccessibilityChecklist(decl, { prefix });
|
|
1117
|
+
if (accessibilityChecklist) {
|
|
1118
|
+
api.accessibilityChecklist = accessibilityChecklist;
|
|
1119
|
+
}
|
|
495
1120
|
|
|
496
|
-
return
|
|
497
|
-
content: [{ type: 'text', text: JSON.stringify(api, null, 2) }],
|
|
498
|
-
};
|
|
1121
|
+
return buildJsonToolResponse(api);
|
|
499
1122
|
},
|
|
500
1123
|
);
|
|
501
1124
|
|
|
@@ -550,13 +1173,8 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
550
1173
|
},
|
|
551
1174
|
async ({ component, prefix }) => {
|
|
552
1175
|
const p = normalizePrefix(prefix);
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
pickDecl(indexes, { tagName: component, prefix: p }) ??
|
|
556
|
-
pickDecl(indexes, { className: component, prefix: p });
|
|
557
|
-
|
|
558
|
-
const byComponentId = byTagOrClass ? undefined : findDeclByComponentId(indexes, component);
|
|
559
|
-
const decl = byTagOrClass ?? byComponentId?.decl;
|
|
1176
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
1177
|
+
const decl = resolved?.decl;
|
|
560
1178
|
|
|
561
1179
|
if (!decl) {
|
|
562
1180
|
return {
|
|
@@ -566,8 +1184,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
566
1184
|
}
|
|
567
1185
|
|
|
568
1186
|
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
569
|
-
const modulePath =
|
|
570
|
-
canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : byComponentId?.modulePath;
|
|
1187
|
+
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : resolved?.modulePath;
|
|
571
1188
|
const api = serializeApi(decl, modulePath, p);
|
|
572
1189
|
const usageSnippet = generateSnippet(api, p);
|
|
573
1190
|
|
|
@@ -633,7 +1250,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
633
1250
|
'validate_markup',
|
|
634
1251
|
{
|
|
635
1252
|
description:
|
|
636
|
-
'Validate HTML against the design system Custom Elements Manifest. When: checking generated or written HTML for correctness. Returns: diagnostics array with errors (unknown elements)
|
|
1253
|
+
'Validate HTML against the design system Custom Elements Manifest. When: checking generated or written HTML for correctness. Returns: diagnostics array with errors (unknown elements), warnings (unknown attributes/token misuse/accessibility misuse), and optional suggestion text for quick recovery. Use after generating HTML to catch mistakes.',
|
|
637
1254
|
inputSchema: {
|
|
638
1255
|
html: z.string(),
|
|
639
1256
|
prefix: z.string().optional(),
|
|
@@ -649,7 +1266,7 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
649
1266
|
cemIndex = combined;
|
|
650
1267
|
}
|
|
651
1268
|
|
|
652
|
-
const
|
|
1269
|
+
const cemDiagnostics = validateTextAgainstCem({
|
|
653
1270
|
filePath: '<markup>',
|
|
654
1271
|
text: html,
|
|
655
1272
|
cem: cemIndex,
|
|
@@ -657,16 +1274,35 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
657
1274
|
unknownElement: 'error',
|
|
658
1275
|
unknownAttribute: 'warning',
|
|
659
1276
|
},
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const tokenMisuseDiagnostics = detectTokenMisuseInInlineStyles({
|
|
1280
|
+
filePath: '<markup>',
|
|
1281
|
+
text: html,
|
|
1282
|
+
valueToToken: tokenSuggestionMap,
|
|
1283
|
+
severity: 'warning',
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
const accessibilityDiagnostics = detectAccessibilityMisuseInMarkup({
|
|
1287
|
+
filePath: '<markup>',
|
|
1288
|
+
text: html,
|
|
1289
|
+
severity: 'warning',
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const diagnostics = [...cemDiagnostics, ...tokenMisuseDiagnostics, ...accessibilityDiagnostics].map((d) => {
|
|
1293
|
+
const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex });
|
|
1294
|
+
return {
|
|
1295
|
+
file: d.file,
|
|
1296
|
+
range: d.range,
|
|
1297
|
+
severity: d.severity,
|
|
1298
|
+
code: d.code,
|
|
1299
|
+
message: d.message,
|
|
1300
|
+
tagName: d.tagName,
|
|
1301
|
+
attrName: d.attrName,
|
|
1302
|
+
hint: d.hint,
|
|
1303
|
+
suggestion,
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
670
1306
|
|
|
671
1307
|
return {
|
|
672
1308
|
content: [{ type: 'text', text: JSON.stringify({ diagnostics }, null, 2) }],
|
|
@@ -841,16 +1477,78 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
841
1477
|
tokens = tokens.filter((t) => t.name.toLowerCase().includes(q));
|
|
842
1478
|
}
|
|
843
1479
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
total: tokens.length,
|
|
849
|
-
tokens,
|
|
850
|
-
summary: designTokensData.summary,
|
|
851
|
-
}, null, 2),
|
|
852
|
-
}],
|
|
1480
|
+
const payload = {
|
|
1481
|
+
total: tokens.length,
|
|
1482
|
+
tokens,
|
|
1483
|
+
summary: designTokensData.summary,
|
|
853
1484
|
};
|
|
1485
|
+
|
|
1486
|
+
return buildJsonToolResponse(payload);
|
|
1487
|
+
},
|
|
1488
|
+
);
|
|
1489
|
+
|
|
1490
|
+
// -----------------------------------------------------------------------
|
|
1491
|
+
// Tool: get_accessibility_docs
|
|
1492
|
+
// -----------------------------------------------------------------------
|
|
1493
|
+
server.registerTool(
|
|
1494
|
+
'get_accessibility_docs',
|
|
1495
|
+
{
|
|
1496
|
+
description:
|
|
1497
|
+
'Get accessibility guidance and component checklist entries. ' +
|
|
1498
|
+
'When: validating accessibility decisions, reviewing ARIA usage, or checking WCAG-focused implementation notes. ' +
|
|
1499
|
+
'Returns: filtered checklist entries from component a11y annotations and accessibility guidelines. ' +
|
|
1500
|
+
'After: apply the checks in your markup and run validate_markup.',
|
|
1501
|
+
inputSchema: {
|
|
1502
|
+
component: z.string().optional()
|
|
1503
|
+
.describe('Filter by component tagName/className/componentId'),
|
|
1504
|
+
topic: z.string().optional()
|
|
1505
|
+
.describe('Filter by topic (e.g. semantics, keyboard, labels, states, zoom, motion, callouts, guideline)'),
|
|
1506
|
+
wcagLevel: z.enum(['A', 'AA', 'AAA', 'all']).optional()
|
|
1507
|
+
.describe('Filter by WCAG level (default: all)'),
|
|
1508
|
+
maxResults: z.number().int().min(1).max(100).optional()
|
|
1509
|
+
.describe('Maximum results to return (default: 20)'),
|
|
1510
|
+
prefix: z.string().optional(),
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
async ({ component, topic, wcagLevel, maxResults, prefix }) => {
|
|
1514
|
+
const p = normalizePrefix(prefix);
|
|
1515
|
+
let componentTagName;
|
|
1516
|
+
|
|
1517
|
+
if (typeof component === 'string' && component.trim() !== '') {
|
|
1518
|
+
const decl = resolveDeclByComponent(indexes, component, p)?.decl;
|
|
1519
|
+
|
|
1520
|
+
if (!decl || typeof decl?.tagName !== 'string') {
|
|
1521
|
+
return {
|
|
1522
|
+
content: [{
|
|
1523
|
+
type: 'text',
|
|
1524
|
+
text: `Component not found (component=${component})`,
|
|
1525
|
+
}],
|
|
1526
|
+
isError: true,
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
componentTagName = withPrefix(decl.tagName.toLowerCase(), p);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const entries = buildAccessibilityIndex(indexes, guidelinesIndexData, { prefix: p });
|
|
1534
|
+
const result = queryAccessibilityIndex(entries, {
|
|
1535
|
+
componentTagName,
|
|
1536
|
+
topic,
|
|
1537
|
+
wcagLevel,
|
|
1538
|
+
maxResults,
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
const payload = {
|
|
1542
|
+
query: {
|
|
1543
|
+
component: componentTagName ?? null,
|
|
1544
|
+
topic: result.topic,
|
|
1545
|
+
wcagLevel: result.wcagLevel,
|
|
1546
|
+
},
|
|
1547
|
+
totalHits: result.totalHits,
|
|
1548
|
+
results: result.results,
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
return buildJsonToolResponse(payload);
|
|
854
1552
|
},
|
|
855
1553
|
);
|
|
856
1554
|
|
|
@@ -930,17 +1628,14 @@ export async function createMcpServer(loadJsonData, loadValidator) {
|
|
|
930
1628
|
results.sort((a, b) => b.score - a.score);
|
|
931
1629
|
const topResults = results.slice(0, max);
|
|
932
1630
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
topic: topic ?? 'all',
|
|
939
|
-
totalHits: results.length,
|
|
940
|
-
results: topResults,
|
|
941
|
-
}, null, 2),
|
|
942
|
-
}],
|
|
1631
|
+
const payload = {
|
|
1632
|
+
query,
|
|
1633
|
+
topic: topic ?? 'all',
|
|
1634
|
+
totalHits: results.length,
|
|
1635
|
+
results: topResults,
|
|
943
1636
|
};
|
|
1637
|
+
|
|
1638
|
+
return buildJsonToolResponse(payload);
|
|
944
1639
|
},
|
|
945
1640
|
);
|
|
946
1641
|
|