@monoharada/wcf-mcp 0.8.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/README.md +11 -34
- package/bin.mjs +160 -15
- package/core/cem.mjs +923 -0
- package/core/constants.mjs +143 -0
- package/core/plugins.mjs +158 -0
- package/core/prefix.mjs +165 -0
- package/core/register.mjs +1469 -0
- package/core/response.mjs +149 -0
- package/core/tokens.mjs +379 -0
- package/core.mjs +142 -3292
- package/data/custom-elements.json +0 -18
- package/data/design-tokens.json +1 -1
- package/data/guidelines-index.json +16 -12
- package/data/llms-full.txt +7 -7
- package/data/skills-registry.json +345 -0
- package/package.json +3 -2
- package/runtime-data.mjs +55 -0
- package/server.mjs +20 -43
- package/wcf-mcp.config.example.json +0 -3
- package/plugins/design-system-skills/check-drift.mjs +0 -685
- package/plugins/design-system-skills/get-skill-manifest.mjs +0 -193
- package/plugins/design-system-skills/index.mjs +0 -20
- package/plugins/design-system-skills/list-skills.mjs +0 -78
- package/plugins/design-system-skills/shared.mjs +0 -75
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/register.mjs — Tool / Resource / Prompt registration logic for the MCP server.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { CANONICAL_PREFIX, PACKAGE_VERSION, PLUGIN_TOOL_NOTICE, FIGMA_TO_WCF_PROMPT, WCF_RESOURCE_URIS, IDE_SETUP_TEMPLATES } from './constants.mjs';
|
|
8
|
+
import { normalizePrefix, withPrefix, toCanonicalTagName, getCategory, buildDiagnosticSuggestion, applyPrefixToHtml, applyPrefixToTagMap, mergeWithPrefixed } from './prefix.mjs';
|
|
9
|
+
import { buildJsonToolResponse, buildJsonToolErrorResponse, expandQueryWithSynonyms, finalizeToolResult } from './response.mjs';
|
|
10
|
+
import { normalizePlugins, buildPluginDataSourceMap, toPassthroughSchema } from './plugins.mjs';
|
|
11
|
+
import { normalizeTokenIdentifier, buildTokenSuggestionMap, buildDesignTokensPayload, buildDesignTokenDetailPayload, buildComponentTokenReferencedBy, buildTokensResourcePayload } from './tokens.mjs';
|
|
12
|
+
import {
|
|
13
|
+
buildIndexes,
|
|
14
|
+
extractPrefixFromIndexes,
|
|
15
|
+
buildFullPageHtml,
|
|
16
|
+
pickDecl,
|
|
17
|
+
serializeApi,
|
|
18
|
+
generateSnippet,
|
|
19
|
+
findDeclByComponentId,
|
|
20
|
+
loadPatternRegistryShape,
|
|
21
|
+
resolveComponentClosure,
|
|
22
|
+
buildPatternFrequencyMap,
|
|
23
|
+
buildComponentSummaries,
|
|
24
|
+
searchIconCatalog,
|
|
25
|
+
buildRelatedComponentMap,
|
|
26
|
+
getRelatedComponentsForTag,
|
|
27
|
+
extractAccessibilityChecklist,
|
|
28
|
+
buildAccessibilityIndex,
|
|
29
|
+
queryAccessibilityIndex,
|
|
30
|
+
INTERACTION_EXAMPLES_MAP,
|
|
31
|
+
LAYOUT_BEHAVIOR_MAP,
|
|
32
|
+
buildComponentsResourcePayload,
|
|
33
|
+
resolveDeclByComponent,
|
|
34
|
+
buildComponentNotFoundError,
|
|
35
|
+
} from './cem.mjs';
|
|
36
|
+
|
|
37
|
+
// Single-module constants (DD-14)
|
|
38
|
+
const GUIDELINE_TOPICS = Object.freeze(['accessibility', 'css', 'patterns', 'all']);
|
|
39
|
+
const GUIDELINE_TOPIC_SET = Object.freeze(new Set(GUIDELINE_TOPICS));
|
|
40
|
+
const ACCESSIBILITY_WARNING_CODES = Object.freeze(new Set([
|
|
41
|
+
'ariaLiveNotRecommended',
|
|
42
|
+
'roleAlertNotRecommended',
|
|
43
|
+
]));
|
|
44
|
+
|
|
45
|
+
/** Normalize a skill entry to summary fields (omit compat/manifest for wcf://skills). */
|
|
46
|
+
function normalizeSkillSummary(s) {
|
|
47
|
+
return {
|
|
48
|
+
name: s.name,
|
|
49
|
+
description: s.description ?? '',
|
|
50
|
+
status: s.status ?? 'active',
|
|
51
|
+
path: s.path ?? '',
|
|
52
|
+
entry: s.entry ?? 'SKILL.md',
|
|
53
|
+
clients: Array.isArray(s.clients) ? s.clients : [],
|
|
54
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
55
|
+
version: typeof s.version === 'string' ? s.version : '0.0.0',
|
|
56
|
+
dependencies: Array.isArray(s.dependencies) ? s.dependencies : [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildGuidelinesResourcePayload(guidelinesIndexData, rawTopic) {
|
|
61
|
+
const topic = String(rawTopic ?? '').trim().toLowerCase();
|
|
62
|
+
if (!GUIDELINE_TOPIC_SET.has(topic)) {
|
|
63
|
+
return {
|
|
64
|
+
isError: true,
|
|
65
|
+
error: {
|
|
66
|
+
code: 'INVALID_GUIDELINE_TOPIC',
|
|
67
|
+
message: `Unsupported topic: ${topic}. Allowed values are ${GUIDELINE_TOPICS.join(', ')}.`,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!Array.isArray(guidelinesIndexData?.documents)) {
|
|
73
|
+
return {
|
|
74
|
+
isError: true,
|
|
75
|
+
error: {
|
|
76
|
+
code: 'GUIDELINES_INDEX_UNAVAILABLE',
|
|
77
|
+
message: 'Guidelines index not available. Run: npm run mcp:index-guidelines',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const documents = guidelinesIndexData.documents
|
|
83
|
+
.filter((doc) => topic === 'all' || String(doc?.topic ?? '').toLowerCase() === topic)
|
|
84
|
+
.map((doc) => {
|
|
85
|
+
const sections = Array.isArray(doc?.sections) ? doc.sections : [];
|
|
86
|
+
return {
|
|
87
|
+
id: String(doc?.id ?? ''),
|
|
88
|
+
title: String(doc?.title ?? ''),
|
|
89
|
+
topic: String(doc?.topic ?? ''),
|
|
90
|
+
sectionCount: sections.length,
|
|
91
|
+
sections: sections.map((section) => ({
|
|
92
|
+
heading: String(section?.heading ?? ''),
|
|
93
|
+
startLine: Number.isInteger(section?.startLine) ? section.startLine : undefined,
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
isError: false,
|
|
100
|
+
payload: {
|
|
101
|
+
topic,
|
|
102
|
+
totalDocuments: documents.length,
|
|
103
|
+
topicCounts: guidelinesIndexData.topicCounts ?? {},
|
|
104
|
+
documents,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildFigmaToWcfPromptText({ figmaUrl, userIntent }) {
|
|
110
|
+
const url = String(figmaUrl ?? '').trim();
|
|
111
|
+
const intent = String(userIntent ?? '').trim();
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
`Figma URL: ${url}`,
|
|
115
|
+
intent ? `Implementation goal: ${intent}` : 'Implementation goal: (not specified)',
|
|
116
|
+
'',
|
|
117
|
+
'Use the workflow below in this exact order:',
|
|
118
|
+
'1. get_design_system_overview',
|
|
119
|
+
'2. get_design_tokens',
|
|
120
|
+
'3. get_component_api',
|
|
121
|
+
'4. generate_usage_snippet (or get_pattern_recipe)',
|
|
122
|
+
'5. validate_markup',
|
|
123
|
+
'',
|
|
124
|
+
'Output requirements:',
|
|
125
|
+
'- Split the UI into sections before writing code.',
|
|
126
|
+
'- For each section, name concrete components and token variables.',
|
|
127
|
+
'- Provide final validation notes and required fixes.',
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function escapeHtmlTitle(s) {
|
|
132
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildImportMapEntries(closure, components, prefix, dir, prefixStripRe) {
|
|
136
|
+
return Object.fromEntries(
|
|
137
|
+
closure.flatMap((cid) => {
|
|
138
|
+
const meta = components[cid];
|
|
139
|
+
const tags = Array.isArray(meta?.tags) ? meta.tags : [cid];
|
|
140
|
+
return tags.map((t) => {
|
|
141
|
+
const lower = String(t).toLowerCase();
|
|
142
|
+
const suffix = lower.replace(prefixStripRe, '');
|
|
143
|
+
return [withPrefix(lower, prefix), `./${dir}/components/${suffix}.js`];
|
|
144
|
+
});
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildFullPageHtmlFromImportMap({ html, title, importMapEntries, dir = 'vendor-runtime', lang = 'ja' }) {
|
|
150
|
+
const importMapJson = JSON.stringify({ imports: importMapEntries }, null, 2);
|
|
151
|
+
return [
|
|
152
|
+
'<!DOCTYPE html>',
|
|
153
|
+
`<html lang="${lang}">`,
|
|
154
|
+
'<head>',
|
|
155
|
+
' <meta charset="UTF-8">',
|
|
156
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
157
|
+
` <title>${escapeHtmlTitle(title)}</title>`,
|
|
158
|
+
` <!-- distribution: selfHosted=true, strategy=vendor-importmap -->`,
|
|
159
|
+
` <!-- Do NOT replace these local paths with CDN URLs. This design system is self-hosted. -->`,
|
|
160
|
+
` <script type="importmap">`,
|
|
161
|
+
`${importMapJson}`,
|
|
162
|
+
` </script>`,
|
|
163
|
+
` <script type="module" src="./${dir}/boot.js"></script>`,
|
|
164
|
+
'</head>',
|
|
165
|
+
'<body>',
|
|
166
|
+
` <noscript>このページの機能にはJavaScriptが必要です。</noscript>`,
|
|
167
|
+
` ${html}`,
|
|
168
|
+
'</body>',
|
|
169
|
+
'</html>',
|
|
170
|
+
].join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Register all built-in tools, resources, and prompts on the MCP server.
|
|
175
|
+
* @param {Object} context — shared data from createMcpServer
|
|
176
|
+
*/
|
|
177
|
+
export function registerAll(context) {
|
|
178
|
+
const {
|
|
179
|
+
server,
|
|
180
|
+
indexes,
|
|
181
|
+
detectedPrefix,
|
|
182
|
+
canonicalCemIndex,
|
|
183
|
+
canonicalEnumMap,
|
|
184
|
+
canonicalSlotMap,
|
|
185
|
+
installRegistry,
|
|
186
|
+
patterns,
|
|
187
|
+
relatedComponentMap,
|
|
188
|
+
patternFrequency,
|
|
189
|
+
designTokensData,
|
|
190
|
+
guidelinesIndexData,
|
|
191
|
+
llmsFullText,
|
|
192
|
+
tokenSuggestionMap,
|
|
193
|
+
componentTokenRefMap,
|
|
194
|
+
plugins,
|
|
195
|
+
loadJsonData,
|
|
196
|
+
loadJson,
|
|
197
|
+
loadText,
|
|
198
|
+
loadValidator,
|
|
199
|
+
} = context;
|
|
200
|
+
|
|
201
|
+
const VENDOR_DIR = 'vendor-runtime';
|
|
202
|
+
const PREFIX_STRIP_RE = /^[^-]+-/;
|
|
203
|
+
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
// Prompt: figma_to_wcf
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
server.registerPrompt(
|
|
208
|
+
FIGMA_TO_WCF_PROMPT,
|
|
209
|
+
{
|
|
210
|
+
title: 'Figma To WCF',
|
|
211
|
+
description:
|
|
212
|
+
'Guided prompt for converting a Figma URL into WCF implementation steps with a strict tool order.',
|
|
213
|
+
argsSchema: {
|
|
214
|
+
figmaUrl: z.string().trim().url().describe('Figma URL (design or board link)'),
|
|
215
|
+
userIntent: z.string().optional().describe('Optional implementation intent / screen purpose'),
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
async ({ figmaUrl, userIntent }) => ({
|
|
219
|
+
messages: [{
|
|
220
|
+
role: 'user',
|
|
221
|
+
content: {
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: buildFigmaToWcfPromptText({ figmaUrl, userIntent }),
|
|
224
|
+
},
|
|
225
|
+
}],
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// -----------------------------------------------------------------------
|
|
230
|
+
// Resource: wcf://components
|
|
231
|
+
// -----------------------------------------------------------------------
|
|
232
|
+
server.registerResource(
|
|
233
|
+
'wcf_components',
|
|
234
|
+
WCF_RESOURCE_URIS.components,
|
|
235
|
+
{
|
|
236
|
+
title: 'WCF Component Catalog',
|
|
237
|
+
description: 'Component catalog snapshot with categories and API entry points.',
|
|
238
|
+
mimeType: 'application/json',
|
|
239
|
+
},
|
|
240
|
+
async () => {
|
|
241
|
+
const payload = buildComponentsResourcePayload(indexes);
|
|
242
|
+
return {
|
|
243
|
+
contents: [{
|
|
244
|
+
uri: WCF_RESOURCE_URIS.components,
|
|
245
|
+
mimeType: 'application/json',
|
|
246
|
+
text: JSON.stringify(payload, null, 2),
|
|
247
|
+
}],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// -----------------------------------------------------------------------
|
|
253
|
+
// Resource: wcf://tokens
|
|
254
|
+
// -----------------------------------------------------------------------
|
|
255
|
+
server.registerResource(
|
|
256
|
+
'wcf_tokens',
|
|
257
|
+
WCF_RESOURCE_URIS.tokens,
|
|
258
|
+
{
|
|
259
|
+
title: 'WCF Design Tokens',
|
|
260
|
+
description: 'Token summary resource for colors, spacing, typography, radius, and shadows.',
|
|
261
|
+
mimeType: 'application/json',
|
|
262
|
+
},
|
|
263
|
+
async () => {
|
|
264
|
+
const result = buildTokensResourcePayload(designTokensData);
|
|
265
|
+
const payload = result.isError ? { error: result.error } : result.payload;
|
|
266
|
+
return {
|
|
267
|
+
contents: [{
|
|
268
|
+
uri: WCF_RESOURCE_URIS.tokens,
|
|
269
|
+
mimeType: 'application/json',
|
|
270
|
+
text: JSON.stringify(payload, null, 2),
|
|
271
|
+
}],
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
// Resource: wcf://guidelines/{topic}
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
server.registerResource(
|
|
280
|
+
'wcf_guidelines',
|
|
281
|
+
new ResourceTemplate(WCF_RESOURCE_URIS.guidelinesTemplate, {
|
|
282
|
+
list: async () => ({
|
|
283
|
+
resources: GUIDELINE_TOPICS.map((topic) => ({
|
|
284
|
+
uri: `wcf://guidelines/${topic}`,
|
|
285
|
+
name: `wcf guidelines (${topic})`,
|
|
286
|
+
description: `Guideline summary for topic=${topic}`,
|
|
287
|
+
})),
|
|
288
|
+
}),
|
|
289
|
+
complete: {
|
|
290
|
+
topic: async (value) => {
|
|
291
|
+
const query = String(value ?? '').trim().toLowerCase();
|
|
292
|
+
return GUIDELINE_TOPICS.filter((topic) => topic.startsWith(query));
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
{
|
|
297
|
+
title: 'WCF Guidelines',
|
|
298
|
+
description: 'Topic-scoped guideline resource (accessibility|css|patterns|all).',
|
|
299
|
+
mimeType: 'application/json',
|
|
300
|
+
},
|
|
301
|
+
async (_uri, variables) => {
|
|
302
|
+
const topic = String(variables?.topic ?? '').trim().toLowerCase();
|
|
303
|
+
const result = buildGuidelinesResourcePayload(guidelinesIndexData, topic);
|
|
304
|
+
if (result.isError) {
|
|
305
|
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
contents: [{
|
|
309
|
+
uri: `wcf://guidelines/${topic}`,
|
|
310
|
+
mimeType: 'application/json',
|
|
311
|
+
text: JSON.stringify(result.payload, null, 2),
|
|
312
|
+
}],
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// -----------------------------------------------------------------------
|
|
318
|
+
// Resource: wcf://llms-full
|
|
319
|
+
// -----------------------------------------------------------------------
|
|
320
|
+
server.registerResource(
|
|
321
|
+
'wcf_llms_full',
|
|
322
|
+
WCF_RESOURCE_URIS.llmsFull,
|
|
323
|
+
{
|
|
324
|
+
title: 'WCF llms-full',
|
|
325
|
+
description: 'LLM reference corpus for WCF usage, generated from repository docs.',
|
|
326
|
+
mimeType: 'text/plain',
|
|
327
|
+
},
|
|
328
|
+
async () => {
|
|
329
|
+
if (typeof llmsFullText !== 'string' || llmsFullText.length === 0) {
|
|
330
|
+
throw new Error('LLMS_FULL_UNAVAILABLE: llms-full.txt is not available.');
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
contents: [{
|
|
334
|
+
uri: WCF_RESOURCE_URIS.llmsFull,
|
|
335
|
+
mimeType: 'text/plain',
|
|
336
|
+
text: llmsFullText,
|
|
337
|
+
}],
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// -----------------------------------------------------------------------
|
|
343
|
+
// Resource: wcf://skills
|
|
344
|
+
// -----------------------------------------------------------------------
|
|
345
|
+
server.registerResource(
|
|
346
|
+
'wcf_skills',
|
|
347
|
+
WCF_RESOURCE_URIS.skills,
|
|
348
|
+
{
|
|
349
|
+
title: 'WCF Skills Catalog',
|
|
350
|
+
description: 'Registered Claude Code / Cursor / Codex skills from skills-registry.json.',
|
|
351
|
+
mimeType: 'application/json',
|
|
352
|
+
},
|
|
353
|
+
async () => {
|
|
354
|
+
const registry = await loadJsonData('skills-registry.json');
|
|
355
|
+
if (!registry || !Array.isArray(registry.skills)) {
|
|
356
|
+
throw new Error('SKILLS_REGISTRY_UNAVAILABLE: skills-registry.json is not available.');
|
|
357
|
+
}
|
|
358
|
+
const skills = registry.skills.map(normalizeSkillSummary);
|
|
359
|
+
return {
|
|
360
|
+
contents: [{
|
|
361
|
+
uri: WCF_RESOURCE_URIS.skills,
|
|
362
|
+
mimeType: 'application/json',
|
|
363
|
+
text: JSON.stringify({ schemaVersion: registry.schemaVersion ?? 2, total: skills.length, skills }, null, 2),
|
|
364
|
+
}],
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// -----------------------------------------------------------------------
|
|
370
|
+
// Tool: get_design_system_overview
|
|
371
|
+
// -----------------------------------------------------------------------
|
|
372
|
+
server.registerTool(
|
|
373
|
+
'get_design_system_overview',
|
|
374
|
+
{
|
|
375
|
+
description:
|
|
376
|
+
'**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.',
|
|
377
|
+
inputSchema: {},
|
|
378
|
+
},
|
|
379
|
+
async () => {
|
|
380
|
+
const categoryCount = {};
|
|
381
|
+
for (const { tagName } of indexes.decls) {
|
|
382
|
+
const cat = getCategory(tagName);
|
|
383
|
+
categoryCount[cat] = (categoryCount[cat] ?? 0) + 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const patternList = Object.values(patterns).map((p) => ({
|
|
387
|
+
id: p?.id,
|
|
388
|
+
title: p?.title,
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
const overview = {
|
|
392
|
+
name: 'DADS Web Components (wcf)',
|
|
393
|
+
version: PACKAGE_VERSION,
|
|
394
|
+
prefix: detectedPrefix,
|
|
395
|
+
totalComponents: indexes.decls.length,
|
|
396
|
+
componentsByCategory: categoryCount,
|
|
397
|
+
totalPatterns: patternList.length,
|
|
398
|
+
patterns: patternList,
|
|
399
|
+
setupInfo: {
|
|
400
|
+
npmPackage: 'web-components-factory',
|
|
401
|
+
installCommand: 'npm install web-components-factory',
|
|
402
|
+
vendorRuntimePath: '<dir>/',
|
|
403
|
+
htmlBoilerplate: [
|
|
404
|
+
'<script type="importmap">',
|
|
405
|
+
`{ "imports": { "${detectedPrefix}-button": "./<dir>/components/button.js" } }`,
|
|
406
|
+
'</script>',
|
|
407
|
+
'<script type="module" src="./<dir>/boot.js"></script>',
|
|
408
|
+
].join('\n'),
|
|
409
|
+
noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
|
|
410
|
+
noCDN: true,
|
|
411
|
+
deliveryModel: 'vendor-local',
|
|
412
|
+
distribution: {
|
|
413
|
+
selfHosted: true,
|
|
414
|
+
cdn: false,
|
|
415
|
+
strategy: 'vendor-importmap',
|
|
416
|
+
quickStart: 'npx web-components-factory init --prefix <prefix> --dir <dir>',
|
|
417
|
+
description:
|
|
418
|
+
'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.',
|
|
419
|
+
},
|
|
420
|
+
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\`.`,
|
|
421
|
+
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.',
|
|
422
|
+
detectedPrefix,
|
|
423
|
+
vendorSetup: {
|
|
424
|
+
init: `wcf init --prefix ${detectedPrefix} --dir <dir>`,
|
|
425
|
+
add: `wcf add <componentId> --prefix ${detectedPrefix} --out <dir>`,
|
|
426
|
+
workflow: '1. wcf init で初期化(boot.js, importmap.snippet.json, autoloader を生成) → 2. wcf add で各コンポーネントを追加 → import map と boot.js が自動生成される',
|
|
427
|
+
},
|
|
428
|
+
htmlSetup: [
|
|
429
|
+
'<script type="importmap">',
|
|
430
|
+
'{',
|
|
431
|
+
' "imports": {',
|
|
432
|
+
` "${detectedPrefix}-button": "./<dir>/components/button.js",`,
|
|
433
|
+
` "${detectedPrefix}-card": "./<dir>/components/card.js"`,
|
|
434
|
+
' }',
|
|
435
|
+
'}',
|
|
436
|
+
'</script>',
|
|
437
|
+
'<script type="module" src="./<dir>/boot.js"></script>',
|
|
438
|
+
].join('\n'),
|
|
439
|
+
},
|
|
440
|
+
ideSetupTemplates: IDE_SETUP_TEMPLATES,
|
|
441
|
+
availablePrompts: [
|
|
442
|
+
{
|
|
443
|
+
name: FIGMA_TO_WCF_PROMPT,
|
|
444
|
+
purpose: 'Figma-to-WCF conversion workflow prompt',
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
availableResources: [
|
|
448
|
+
{ uri: WCF_RESOURCE_URIS.components, purpose: 'Component catalog snapshot' },
|
|
449
|
+
{ uri: WCF_RESOURCE_URIS.tokens, purpose: 'Token summary snapshot' },
|
|
450
|
+
{ uri: WCF_RESOURCE_URIS.guidelinesTemplate, purpose: 'Topic-based guideline summaries' },
|
|
451
|
+
{ uri: WCF_RESOURCE_URIS.llmsFull, purpose: 'Full LLM reference text for WCF' },
|
|
452
|
+
{ uri: WCF_RESOURCE_URIS.skills, purpose: 'Skills catalog snapshot' },
|
|
453
|
+
],
|
|
454
|
+
availableTools: [
|
|
455
|
+
{ name: 'get_design_system_overview', purpose: 'This overview (start here)' },
|
|
456
|
+
{ name: 'list_components', purpose: 'Browse components with progressive disclosure and filters' },
|
|
457
|
+
{ name: 'search_icons', purpose: 'Search icon names and usage examples' },
|
|
458
|
+
{ name: 'get_component_api', purpose: 'Full API surface for a single component' },
|
|
459
|
+
{ name: 'generate_usage_snippet', purpose: 'Minimal HTML usage example' },
|
|
460
|
+
{ name: 'get_install_recipe', purpose: 'Installation instructions and dependency tree' },
|
|
461
|
+
{ name: 'validate_markup', purpose: 'Validate HTML against CEM schema' },
|
|
462
|
+
{ name: 'generate_full_page_html', purpose: 'Wrap HTML fragment into a complete page with importmap and boot script' },
|
|
463
|
+
{ name: 'list_patterns', purpose: 'Browse page-level UI composition patterns' },
|
|
464
|
+
{ name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
|
|
465
|
+
{ name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
|
|
466
|
+
{ name: 'get_design_tokens', purpose: 'Query design tokens (colors, spacing, typography, radius, shadows)' },
|
|
467
|
+
{ name: 'get_design_token_detail', purpose: 'Get details, relationships, and usage examples for one token' },
|
|
468
|
+
{ name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
|
|
469
|
+
{ name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
|
|
470
|
+
{ name: 'get_component_selector_guide', purpose: 'Component selection guide by category and use case' },
|
|
471
|
+
],
|
|
472
|
+
recommendedWorkflow: [
|
|
473
|
+
'1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
|
|
474
|
+
'2. figma_to_wcf (optional) → bootstrap the Figma-to-WCF tool sequence',
|
|
475
|
+
'3. wcf://components and wcf://tokens resources → preload catalog/token context',
|
|
476
|
+
'4. search_guidelines → find relevant guidelines',
|
|
477
|
+
'5. get_design_tokens → get correct token values',
|
|
478
|
+
'6. get_design_token_detail → inspect one token with references/referencedBy and usage examples',
|
|
479
|
+
'7. get_accessibility_docs → fetch component-level accessibility checklist',
|
|
480
|
+
'8. list_components (category/query + pagination) → shortlist components',
|
|
481
|
+
'9. search_icons (optional) → find icon names quickly',
|
|
482
|
+
'10. get_component_api → check attributes, slots, events, CSS parts',
|
|
483
|
+
'11. generate_usage_snippet or get_pattern_recipe → get code',
|
|
484
|
+
'12. validate_markup → verify your HTML and use suggestions to self-correct',
|
|
485
|
+
'13. generate_full_page_html → wrap fragment into a complete preview-ready page',
|
|
486
|
+
'14. get_install_recipe → get import/install instructions',
|
|
487
|
+
],
|
|
488
|
+
experimental: {
|
|
489
|
+
plugins: {
|
|
490
|
+
enabled: plugins.length > 0,
|
|
491
|
+
note: PLUGIN_TOOL_NOTICE,
|
|
492
|
+
pluginCount: plugins.length,
|
|
493
|
+
pluginToolCount: plugins.reduce((sum, plugin) => sum + (plugin.tools?.length ?? 0), 0),
|
|
494
|
+
plugins: plugins.map((plugin) => ({
|
|
495
|
+
name: plugin.name,
|
|
496
|
+
version: plugin.version,
|
|
497
|
+
toolCount: plugin.tools?.length ?? 0,
|
|
498
|
+
dataSourceOverrides: plugin.dataSources?.map((source) => source.fileName) ?? [],
|
|
499
|
+
})),
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
for (const plugin of plugins) {
|
|
505
|
+
const tools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
506
|
+
for (const tool of tools) {
|
|
507
|
+
overview.availableTools.push({
|
|
508
|
+
name: tool.name,
|
|
509
|
+
purpose: `${tool.description} (plugin: ${plugin.name})`,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return buildJsonToolResponse(overview);
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// -----------------------------------------------------------------------
|
|
519
|
+
// Tool: list_components
|
|
520
|
+
// -----------------------------------------------------------------------
|
|
521
|
+
server.registerTool(
|
|
522
|
+
'list_components',
|
|
523
|
+
{
|
|
524
|
+
description:
|
|
525
|
+
'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.',
|
|
526
|
+
inputSchema: {
|
|
527
|
+
category: z
|
|
528
|
+
.enum(['Form', 'Actions', 'Navigation', 'Content', 'Display', 'Layout', 'Other'])
|
|
529
|
+
.optional()
|
|
530
|
+
.describe('Filter by component category'),
|
|
531
|
+
query: z.string().optional().describe('Search by tagName/className/description/category/modulePath'),
|
|
532
|
+
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
|
|
533
|
+
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
534
|
+
prefix: z.string().optional(),
|
|
535
|
+
patternId: z.string().optional().describe('Filter to components required by this pattern'),
|
|
536
|
+
sort: z.enum(['default', 'frequency']).optional().describe('Sort order: "default" (CEM declaration order) or "frequency" (pattern usage count, descending)'),
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
async ({ category, query, limit, offset, prefix, patternId, sort }) => {
|
|
540
|
+
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency });
|
|
541
|
+
const payload = {
|
|
542
|
+
items: page.items,
|
|
543
|
+
total: page.total,
|
|
544
|
+
limit: page.limit,
|
|
545
|
+
offset: page.offset,
|
|
546
|
+
hasMore: page.hasMore,
|
|
547
|
+
};
|
|
548
|
+
if (page._notice) payload._notice = page._notice;
|
|
549
|
+
return buildJsonToolResponse(payload);
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// -----------------------------------------------------------------------
|
|
554
|
+
// Tool: search_icons
|
|
555
|
+
// -----------------------------------------------------------------------
|
|
556
|
+
server.registerTool(
|
|
557
|
+
'search_icons',
|
|
558
|
+
{
|
|
559
|
+
description:
|
|
560
|
+
'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.',
|
|
561
|
+
inputSchema: {
|
|
562
|
+
query: z.string().optional().describe('Search icon names (partial match)'),
|
|
563
|
+
limit: z.number().int().min(1).max(100).optional().describe('Maximum items to return (default: 20)'),
|
|
564
|
+
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
565
|
+
prefix: z.string().optional(),
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
async ({ query, limit, offset, prefix }) => {
|
|
569
|
+
const payload = searchIconCatalog(indexes, { query, limit, offset, prefix });
|
|
570
|
+
return buildJsonToolResponse(payload);
|
|
571
|
+
},
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
// -----------------------------------------------------------------------
|
|
575
|
+
// Tool: get_component_api
|
|
576
|
+
// -----------------------------------------------------------------------
|
|
577
|
+
server.registerTool(
|
|
578
|
+
'get_component_api',
|
|
579
|
+
{
|
|
580
|
+
description:
|
|
581
|
+
'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.',
|
|
582
|
+
inputSchema: {
|
|
583
|
+
tagName: z.string().optional().describe('Tag name (e.g., "dads-button")'),
|
|
584
|
+
className: z.string().optional().describe('Class name (e.g., "DadsButton")'),
|
|
585
|
+
component: z.string().optional().describe('Any identifier: tagName, className, or bare name (e.g., "button")'),
|
|
586
|
+
components: z.array(z.string()).max(10).optional().describe('Batch: array of component identifiers (max 10). When provided, component/tagName/className are ignored.'),
|
|
587
|
+
prefix: z.string().optional(),
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
async ({ tagName, className, component, components, prefix }) => {
|
|
591
|
+
const p = normalizePrefix(prefix);
|
|
592
|
+
|
|
593
|
+
// Batch mode: components array takes priority (DD-23)
|
|
594
|
+
if (Array.isArray(components) && components.length > 0) {
|
|
595
|
+
const results = [];
|
|
596
|
+
for (const comp of components) {
|
|
597
|
+
const resolved = resolveDeclByComponent(indexes, comp, p);
|
|
598
|
+
if (!resolved?.decl) {
|
|
599
|
+
results.push({ component: comp, error: `Component not found: ${comp}` });
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const { decl: d, modulePath: mp } = resolved;
|
|
603
|
+
const cTag = typeof d.tagName === 'string' ? d.tagName.toLowerCase() : undefined;
|
|
604
|
+
const mPath = mp ?? (cTag ? indexes.modulePathByTag.get(cTag) : undefined);
|
|
605
|
+
const api = serializeApi(d, mPath, prefix);
|
|
606
|
+
const related = getRelatedComponentsForTag({
|
|
607
|
+
canonicalTagName: cTag,
|
|
608
|
+
installRegistry,
|
|
609
|
+
relatedMap: relatedComponentMap,
|
|
610
|
+
prefix,
|
|
611
|
+
});
|
|
612
|
+
if (related.length > 0) api.relatedComponents = related;
|
|
613
|
+
const a11y = extractAccessibilityChecklist(d, { prefix });
|
|
614
|
+
if (a11y) api.accessibilityChecklist = a11y;
|
|
615
|
+
results.push(api);
|
|
616
|
+
}
|
|
617
|
+
return buildJsonToolResponse(results);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Single mode (existing behavior)
|
|
621
|
+
let decl;
|
|
622
|
+
let modulePath;
|
|
623
|
+
|
|
624
|
+
if (component) {
|
|
625
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
626
|
+
decl = resolved?.decl;
|
|
627
|
+
modulePath = resolved?.modulePath;
|
|
628
|
+
} else {
|
|
629
|
+
decl = pickDecl(indexes, { tagName, className, prefix: p });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!decl) {
|
|
633
|
+
const identifier = component || tagName || className || '';
|
|
634
|
+
return buildComponentNotFoundError(identifier, indexes, p);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
638
|
+
if (!modulePath) {
|
|
639
|
+
modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined;
|
|
640
|
+
}
|
|
641
|
+
const api = serializeApi(decl, modulePath, prefix);
|
|
642
|
+
const relatedComponents = getRelatedComponentsForTag({
|
|
643
|
+
canonicalTagName: canonicalTag,
|
|
644
|
+
installRegistry,
|
|
645
|
+
relatedMap: relatedComponentMap,
|
|
646
|
+
prefix,
|
|
647
|
+
});
|
|
648
|
+
if (relatedComponents.length > 0) {
|
|
649
|
+
api.relatedComponents = relatedComponents;
|
|
650
|
+
}
|
|
651
|
+
const accessibilityChecklist = extractAccessibilityChecklist(decl, { prefix });
|
|
652
|
+
if (accessibilityChecklist) {
|
|
653
|
+
api.accessibilityChecklist = accessibilityChecklist;
|
|
654
|
+
}
|
|
655
|
+
const interactionExamples = canonicalTag ? INTERACTION_EXAMPLES_MAP[canonicalTag] : undefined;
|
|
656
|
+
if (interactionExamples) {
|
|
657
|
+
api.interactionExamples = interactionExamples;
|
|
658
|
+
}
|
|
659
|
+
const layoutBehavior = canonicalTag ? LAYOUT_BEHAVIOR_MAP[canonicalTag] : undefined;
|
|
660
|
+
if (layoutBehavior) {
|
|
661
|
+
api.layoutBehavior = layoutBehavior;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return buildJsonToolResponse(api);
|
|
665
|
+
},
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
// -----------------------------------------------------------------------
|
|
669
|
+
// Tool: generate_usage_snippet
|
|
670
|
+
// -----------------------------------------------------------------------
|
|
671
|
+
server.registerTool(
|
|
672
|
+
'generate_usage_snippet',
|
|
673
|
+
{
|
|
674
|
+
description:
|
|
675
|
+
'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.',
|
|
676
|
+
inputSchema: {
|
|
677
|
+
component: z.string(),
|
|
678
|
+
prefix: z.string().optional(),
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
async ({ component, prefix }) => {
|
|
682
|
+
const p = normalizePrefix(prefix);
|
|
683
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
684
|
+
const decl = resolved?.decl;
|
|
685
|
+
|
|
686
|
+
if (!decl) {
|
|
687
|
+
return buildComponentNotFoundError(component, indexes, p);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
691
|
+
const modulePath = resolved?.modulePath ?? (canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : undefined);
|
|
692
|
+
const api = serializeApi(decl, modulePath, prefix);
|
|
693
|
+
const snippet = generateSnippet(api, prefix);
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
content: [{ type: 'text', text: snippet }],
|
|
697
|
+
};
|
|
698
|
+
},
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// -----------------------------------------------------------------------
|
|
702
|
+
// Tool: get_install_recipe
|
|
703
|
+
// -----------------------------------------------------------------------
|
|
704
|
+
server.registerTool(
|
|
705
|
+
'get_install_recipe',
|
|
706
|
+
{
|
|
707
|
+
description:
|
|
708
|
+
'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).',
|
|
709
|
+
inputSchema: {
|
|
710
|
+
component: z.string(),
|
|
711
|
+
prefix: z.string().optional(),
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
async ({ component, prefix }) => {
|
|
715
|
+
const p = normalizePrefix(prefix);
|
|
716
|
+
const resolved = resolveDeclByComponent(indexes, component, p);
|
|
717
|
+
const decl = resolved?.decl;
|
|
718
|
+
|
|
719
|
+
if (!decl) {
|
|
720
|
+
return {
|
|
721
|
+
content: [{ type: 'text', text: `Component not found: ${component}` }],
|
|
722
|
+
isError: true,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
|
|
727
|
+
const modulePath = canonicalTag ? indexes.modulePathByTag.get(canonicalTag) : resolved?.modulePath;
|
|
728
|
+
const api = serializeApi(decl, modulePath, p);
|
|
729
|
+
const usageSnippet = generateSnippet(api, p);
|
|
730
|
+
|
|
731
|
+
const install = decl?.custom?.install;
|
|
732
|
+
if (!install || typeof install !== 'object') {
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: 'text', text: 'Install metadata not found in CEM.' }],
|
|
735
|
+
isError: true,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const componentId = String(install.id ?? '').trim() || api?.custom?.componentId;
|
|
740
|
+
const define = String(install.define ?? '').trim();
|
|
741
|
+
const deps = Array.isArray(install.deps) ? install.deps : [];
|
|
742
|
+
const tags = Array.isArray(install.tags) ? install.tags : [];
|
|
743
|
+
|
|
744
|
+
const transitiveDeps = componentId
|
|
745
|
+
? resolveComponentClosure({ installRegistry }, [componentId]).filter((id) => id !== componentId)
|
|
746
|
+
: [];
|
|
747
|
+
|
|
748
|
+
const tagNames =
|
|
749
|
+
tags.length > 0 ? tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : [api.tagName];
|
|
750
|
+
|
|
751
|
+
const defineHint = define
|
|
752
|
+
? [
|
|
753
|
+
modulePath ? `import { ${define} } from "${modulePath}";` : `import { ${define} } from "<modulePath>";`,
|
|
754
|
+
`${define}();`,
|
|
755
|
+
p !== CANONICAL_PREFIX ? `// If supported: ${define}("${p}");` : undefined,
|
|
756
|
+
]
|
|
757
|
+
.filter(Boolean)
|
|
758
|
+
.join('\n')
|
|
759
|
+
: undefined;
|
|
760
|
+
|
|
761
|
+
return buildJsonToolResponse({
|
|
762
|
+
componentId,
|
|
763
|
+
tagNames,
|
|
764
|
+
deps,
|
|
765
|
+
transitiveDeps,
|
|
766
|
+
define,
|
|
767
|
+
defineHint,
|
|
768
|
+
source: install.source,
|
|
769
|
+
usageSnippet,
|
|
770
|
+
usageContext: 'body-only',
|
|
771
|
+
installHint: componentId ? `wcf add ${componentId}` : undefined,
|
|
772
|
+
vendorHint: (() => {
|
|
773
|
+
const im = tagNames.length > 0
|
|
774
|
+
? JSON.stringify({ imports: Object.fromEntries(tagNames.map((t) => [t, `./<dir>/components/${t.replace(PREFIX_STRIP_RE, '')}.js`])) })
|
|
775
|
+
: undefined;
|
|
776
|
+
return {
|
|
777
|
+
install: componentId ? `wcf add ${componentId} --prefix <prefix> --out <dir>` : undefined,
|
|
778
|
+
importMap: im,
|
|
779
|
+
importmap: im,
|
|
780
|
+
boot: '<dir>/boot.js -- loads autoloader that registers components via import map',
|
|
781
|
+
};
|
|
782
|
+
})(),
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
// -----------------------------------------------------------------------
|
|
788
|
+
// Tool: validate_markup
|
|
789
|
+
// -----------------------------------------------------------------------
|
|
790
|
+
server.registerTool(
|
|
791
|
+
'validate_markup',
|
|
792
|
+
{
|
|
793
|
+
description:
|
|
794
|
+
'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.',
|
|
795
|
+
inputSchema: {
|
|
796
|
+
html: z.string(),
|
|
797
|
+
prefix: z.string().optional(),
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
async ({ html, prefix }) => {
|
|
801
|
+
const {
|
|
802
|
+
collectCemCustomElements,
|
|
803
|
+
validateTextAgainstCem,
|
|
804
|
+
detectTokenMisuseInInlineStyles,
|
|
805
|
+
detectAccessibilityMisuseInMarkup,
|
|
806
|
+
buildEnumAttributeMap,
|
|
807
|
+
detectEnumValueMisuse,
|
|
808
|
+
buildSlotNameMap,
|
|
809
|
+
detectInvalidSlotName,
|
|
810
|
+
detectMissingRequiredAttributes,
|
|
811
|
+
detectOrphanedChildComponents,
|
|
812
|
+
detectEmptyInteractiveElement,
|
|
813
|
+
detectNonLowercaseAttributes,
|
|
814
|
+
detectCdnReferences,
|
|
815
|
+
detectMissingRuntimeScaffold,
|
|
816
|
+
} = await loadValidator();
|
|
817
|
+
|
|
818
|
+
const p = normalizePrefix(prefix);
|
|
819
|
+
let cemIndex = canonicalCemIndex;
|
|
820
|
+
let enumMap = canonicalEnumMap;
|
|
821
|
+
let slotMap = canonicalSlotMap;
|
|
822
|
+
if (p !== CANONICAL_PREFIX) {
|
|
823
|
+
cemIndex = mergeWithPrefixed(canonicalCemIndex, p);
|
|
824
|
+
enumMap = mergeWithPrefixed(canonicalEnumMap, p);
|
|
825
|
+
slotMap = mergeWithPrefixed(canonicalSlotMap, p);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const cemDiagnostics = validateTextAgainstCem({
|
|
829
|
+
filePath: '<markup>',
|
|
830
|
+
text: html,
|
|
831
|
+
cem: cemIndex,
|
|
832
|
+
severity: {
|
|
833
|
+
unknownElement: 'error',
|
|
834
|
+
unknownAttribute: 'warning',
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const enumDiagnostics = detectEnumValueMisuse({
|
|
839
|
+
filePath: '<markup>',
|
|
840
|
+
text: html,
|
|
841
|
+
enumMap,
|
|
842
|
+
severity: 'error',
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const tokenMisuseDiagnostics = detectTokenMisuseInInlineStyles({
|
|
846
|
+
filePath: '<markup>',
|
|
847
|
+
text: html,
|
|
848
|
+
valueToToken: tokenSuggestionMap,
|
|
849
|
+
severity: 'warning',
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
const cemTagNames = new Set(cemIndex.keys());
|
|
853
|
+
const accessibilityDiagnostics = detectAccessibilityMisuseInMarkup({
|
|
854
|
+
filePath: '<markup>',
|
|
855
|
+
text: html,
|
|
856
|
+
severity: 'error',
|
|
857
|
+
cemTagNames,
|
|
858
|
+
}).map((diagnostic) => ({
|
|
859
|
+
...diagnostic,
|
|
860
|
+
severity: ACCESSIBILITY_WARNING_CODES.has(diagnostic.code) ? 'warning' : diagnostic.severity,
|
|
861
|
+
}));
|
|
862
|
+
|
|
863
|
+
const slotDiagnostics = detectInvalidSlotName({
|
|
864
|
+
filePath: '<markup>',
|
|
865
|
+
text: html,
|
|
866
|
+
slotMap,
|
|
867
|
+
severity: 'error',
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const requiredAttrDiagnostics = detectMissingRequiredAttributes({
|
|
871
|
+
filePath: '<markup>',
|
|
872
|
+
text: html,
|
|
873
|
+
prefix: p,
|
|
874
|
+
severity: 'error',
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
const orphanDiagnostics = detectOrphanedChildComponents({
|
|
878
|
+
filePath: '<markup>',
|
|
879
|
+
text: html,
|
|
880
|
+
prefix: p,
|
|
881
|
+
severity: 'warning',
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const emptyInteractiveDiagnostics = detectEmptyInteractiveElement({
|
|
885
|
+
filePath: '<markup>',
|
|
886
|
+
text: html,
|
|
887
|
+
prefix: p,
|
|
888
|
+
severity: 'warning',
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const lowercaseDiagnostics = detectNonLowercaseAttributes({
|
|
892
|
+
filePath: '<markup>',
|
|
893
|
+
text: html,
|
|
894
|
+
cem: cemIndex,
|
|
895
|
+
severity: 'warning',
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const cdnDiagnostics = detectCdnReferences({
|
|
899
|
+
filePath: '<markup>',
|
|
900
|
+
text: html,
|
|
901
|
+
severity: 'warning',
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
const scaffoldDiagnostics = detectMissingRuntimeScaffold({
|
|
905
|
+
filePath: '<markup>',
|
|
906
|
+
text: html,
|
|
907
|
+
severity: 'warning',
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const allRawDiagnostics = [
|
|
911
|
+
...cemDiagnostics,
|
|
912
|
+
...enumDiagnostics,
|
|
913
|
+
...slotDiagnostics,
|
|
914
|
+
...requiredAttrDiagnostics,
|
|
915
|
+
...orphanDiagnostics,
|
|
916
|
+
...emptyInteractiveDiagnostics,
|
|
917
|
+
...lowercaseDiagnostics,
|
|
918
|
+
...tokenMisuseDiagnostics,
|
|
919
|
+
...accessibilityDiagnostics,
|
|
920
|
+
...cdnDiagnostics,
|
|
921
|
+
...scaffoldDiagnostics,
|
|
922
|
+
];
|
|
923
|
+
const diagnostics = allRawDiagnostics.map((d) => {
|
|
924
|
+
const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex, prefix: p });
|
|
925
|
+
return {
|
|
926
|
+
file: d.file,
|
|
927
|
+
range: d.range,
|
|
928
|
+
severity: d.severity,
|
|
929
|
+
code: d.code,
|
|
930
|
+
message: d.message,
|
|
931
|
+
tagName: d.tagName,
|
|
932
|
+
attrName: d.attrName,
|
|
933
|
+
hint: d.hint,
|
|
934
|
+
suggestion,
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
return buildJsonToolResponse({ diagnostics });
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
// -----------------------------------------------------------------------
|
|
943
|
+
// Tool: generate_full_page_html
|
|
944
|
+
// -----------------------------------------------------------------------
|
|
945
|
+
server.registerTool(
|
|
946
|
+
'generate_full_page_html',
|
|
947
|
+
{
|
|
948
|
+
description:
|
|
949
|
+
'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.',
|
|
950
|
+
inputSchema: {
|
|
951
|
+
html: z.string().describe('HTML fragment containing WCF custom elements'),
|
|
952
|
+
prefix: z.string().optional().describe('Component prefix (default: auto-detected)'),
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
async ({ html, prefix }) => {
|
|
956
|
+
const p = normalizePrefix(prefix);
|
|
957
|
+
let ci = canonicalCemIndex;
|
|
958
|
+
if (p !== CANONICAL_PREFIX) {
|
|
959
|
+
ci = mergeWithPrefixed(canonicalCemIndex, p);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const { fullHtml, importEntries } = buildFullPageHtml({ html, prefix: p, cemIndex: ci });
|
|
963
|
+
|
|
964
|
+
return buildJsonToolResponse({
|
|
965
|
+
fullHtml,
|
|
966
|
+
componentCount: Object.keys(importEntries).length,
|
|
967
|
+
importMapEntries: importEntries,
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
// -----------------------------------------------------------------------
|
|
973
|
+
// Tool: get_component_selector_guide
|
|
974
|
+
// -----------------------------------------------------------------------
|
|
975
|
+
server.registerTool(
|
|
976
|
+
'get_component_selector_guide',
|
|
977
|
+
{
|
|
978
|
+
description:
|
|
979
|
+
'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.',
|
|
980
|
+
inputSchema: {
|
|
981
|
+
category: z.string().optional().describe('Filter by category key (e.g., "Form", "Navigation", "Layout")'),
|
|
982
|
+
useCase: z.string().optional().describe('Search by use-case keyword (e.g., "date", "login", "upload")'),
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
async ({ category, useCase }) => {
|
|
986
|
+
if (!context.selectorGuideData || !Array.isArray(context.selectorGuideData.categories)) {
|
|
987
|
+
return buildJsonToolErrorResponse({
|
|
988
|
+
error: 'Component selector guide not available.',
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
let categories = context.selectorGuideData.categories;
|
|
993
|
+
|
|
994
|
+
if (typeof category === 'string' && category.trim()) {
|
|
995
|
+
const cat = category.trim().toLowerCase();
|
|
996
|
+
categories = categories.filter((c) => c.key.toLowerCase() === cat);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (typeof useCase === 'string' && useCase.trim()) {
|
|
1000
|
+
const kw = useCase.trim().toLowerCase();
|
|
1001
|
+
categories = categories.map((c) => ({
|
|
1002
|
+
...c,
|
|
1003
|
+
components: c.components.filter((comp) =>
|
|
1004
|
+
comp.useCase.toLowerCase().includes(kw) ||
|
|
1005
|
+
comp.id.toLowerCase().includes(kw) ||
|
|
1006
|
+
comp.tagName.toLowerCase().includes(kw)
|
|
1007
|
+
),
|
|
1008
|
+
})).filter((c) => c.components.length > 0);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return buildJsonToolResponse({
|
|
1012
|
+
totalCategories: categories.length,
|
|
1013
|
+
categories: categories.map((c) => ({
|
|
1014
|
+
key: c.key,
|
|
1015
|
+
label: c.label,
|
|
1016
|
+
description: c.description,
|
|
1017
|
+
components: c.components,
|
|
1018
|
+
})),
|
|
1019
|
+
});
|
|
1020
|
+
},
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
// -----------------------------------------------------------------------
|
|
1024
|
+
// Tool: list_patterns
|
|
1025
|
+
// -----------------------------------------------------------------------
|
|
1026
|
+
server.registerTool(
|
|
1027
|
+
'list_patterns',
|
|
1028
|
+
{
|
|
1029
|
+
description:
|
|
1030
|
+
'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.',
|
|
1031
|
+
inputSchema: {},
|
|
1032
|
+
},
|
|
1033
|
+
async () => {
|
|
1034
|
+
const list = Object.values(patterns).map((p) => ({
|
|
1035
|
+
id: p?.id,
|
|
1036
|
+
title: p?.title,
|
|
1037
|
+
description: p?.description,
|
|
1038
|
+
requires: p?.requires,
|
|
1039
|
+
}));
|
|
1040
|
+
|
|
1041
|
+
return buildJsonToolResponse(list);
|
|
1042
|
+
},
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
// -----------------------------------------------------------------------
|
|
1046
|
+
// Tool: get_pattern_recipe
|
|
1047
|
+
// -----------------------------------------------------------------------
|
|
1048
|
+
server.registerTool(
|
|
1049
|
+
'get_pattern_recipe',
|
|
1050
|
+
{
|
|
1051
|
+
description:
|
|
1052
|
+
'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.',
|
|
1053
|
+
inputSchema: {
|
|
1054
|
+
patternId: z.string(),
|
|
1055
|
+
prefix: z.string().optional(),
|
|
1056
|
+
include: z.array(z.enum(['fullPage'])).optional(),
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
async ({ patternId, prefix, include }) => {
|
|
1060
|
+
const id = String(patternId ?? '').trim();
|
|
1061
|
+
const p = normalizePrefix(prefix);
|
|
1062
|
+
const pat = patterns[id];
|
|
1063
|
+
if (!pat) {
|
|
1064
|
+
return {
|
|
1065
|
+
content: [{ type: 'text', text: `Pattern not found: ${id}` }],
|
|
1066
|
+
isError: true,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const requires = Array.isArray(pat.requires) ? pat.requires : [];
|
|
1071
|
+
const closure = resolveComponentClosure({ installRegistry }, requires);
|
|
1072
|
+
|
|
1073
|
+
const components = installRegistry?.components && typeof installRegistry.components === 'object' ? installRegistry.components : {};
|
|
1074
|
+
const install = Object.fromEntries(
|
|
1075
|
+
closure
|
|
1076
|
+
.map((cid) => [cid, components[cid]])
|
|
1077
|
+
.filter(([, meta]) => meta && typeof meta === 'object')
|
|
1078
|
+
.map(([cid, meta]) => [
|
|
1079
|
+
cid,
|
|
1080
|
+
{
|
|
1081
|
+
...meta,
|
|
1082
|
+
tags: Array.isArray(meta.tags) ? meta.tags.map((t) => withPrefix(String(t).toLowerCase(), p)) : meta.tags,
|
|
1083
|
+
},
|
|
1084
|
+
]),
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
const canonicalHtml = String(pat.html ?? '');
|
|
1088
|
+
const html = applyPrefixToHtml(canonicalHtml, p);
|
|
1089
|
+
|
|
1090
|
+
const entryHints = Array.isArray(pat.entryHints) ? [...pat.entryHints] : ['boot'];
|
|
1091
|
+
|
|
1092
|
+
const importMapEntries = buildImportMapEntries(closure, components, p, '<dir>', PREFIX_STRIP_RE);
|
|
1093
|
+
|
|
1094
|
+
const scaffoldHint = {
|
|
1095
|
+
doctype: '<!DOCTYPE html>',
|
|
1096
|
+
importMap: `<script type="importmap">\n${JSON.stringify({ imports: importMapEntries }, null, 2)}\n</script>`,
|
|
1097
|
+
bootScript: '<script type="module" src="./<dir>/boot.js"></script>',
|
|
1098
|
+
noscript: '<noscript>このページの機能にはJavaScriptが必要です。</noscript>',
|
|
1099
|
+
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.',
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
const includeArr = Array.isArray(include) ? include : [];
|
|
1103
|
+
let fullPageHtml;
|
|
1104
|
+
if (includeArr.includes('fullPage')) {
|
|
1105
|
+
const resolvedImportMap = buildImportMapEntries(closure, components, p, VENDOR_DIR, PREFIX_STRIP_RE);
|
|
1106
|
+
fullPageHtml = buildFullPageHtmlFromImportMap({
|
|
1107
|
+
html,
|
|
1108
|
+
title: pat.title ?? pat.id,
|
|
1109
|
+
importMapEntries: resolvedImportMap,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const result = {
|
|
1114
|
+
pattern: {
|
|
1115
|
+
id: pat.id,
|
|
1116
|
+
title: pat.title,
|
|
1117
|
+
description: pat.description,
|
|
1118
|
+
},
|
|
1119
|
+
prefix: p,
|
|
1120
|
+
requires,
|
|
1121
|
+
components: closure,
|
|
1122
|
+
install,
|
|
1123
|
+
html,
|
|
1124
|
+
canonicalHtml,
|
|
1125
|
+
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
1126
|
+
entryHints,
|
|
1127
|
+
scaffoldHint,
|
|
1128
|
+
behavior: typeof pat.behavior === 'string' ? pat.behavior : undefined,
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
if (fullPageHtml !== undefined) {
|
|
1132
|
+
result.fullPageHtml = fullPageHtml;
|
|
1133
|
+
result.vendorSetup = {
|
|
1134
|
+
command: `npx web-components-factory init --prefix ${p} --dir ${VENDOR_DIR} && npx web-components-factory add ${closure.join(' ')} --prefix ${p} --out ${VENDOR_DIR}`,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return buildJsonToolResponse(result);
|
|
1139
|
+
},
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1142
|
+
// -----------------------------------------------------------------------
|
|
1143
|
+
// Tool: generate_pattern_snippet
|
|
1144
|
+
// -----------------------------------------------------------------------
|
|
1145
|
+
server.registerTool(
|
|
1146
|
+
'generate_pattern_snippet',
|
|
1147
|
+
{
|
|
1148
|
+
description:
|
|
1149
|
+
'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.',
|
|
1150
|
+
inputSchema: {
|
|
1151
|
+
patternId: z.string(),
|
|
1152
|
+
prefix: z.string().optional(),
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
async ({ patternId, prefix }) => {
|
|
1156
|
+
const id = String(patternId ?? '').trim();
|
|
1157
|
+
const p = normalizePrefix(prefix);
|
|
1158
|
+
const pat = patterns[id];
|
|
1159
|
+
if (!pat) {
|
|
1160
|
+
return {
|
|
1161
|
+
content: [{ type: 'text', text: `Pattern not found: ${id}` }],
|
|
1162
|
+
isError: true,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
content: [{ type: 'text', text: applyPrefixToHtml(String(pat.html ?? ''), p) }],
|
|
1168
|
+
};
|
|
1169
|
+
},
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
// -----------------------------------------------------------------------
|
|
1173
|
+
// Tool: get_design_tokens
|
|
1174
|
+
// -----------------------------------------------------------------------
|
|
1175
|
+
server.registerTool(
|
|
1176
|
+
'get_design_tokens',
|
|
1177
|
+
{
|
|
1178
|
+
description:
|
|
1179
|
+
'Get design tokens (colors, spacing, typography, etc.). ' +
|
|
1180
|
+
'When: building UI and need correct token values instead of hard-coded values. ' +
|
|
1181
|
+
'Returns: filtered list of tokens with CSS variable names and values. ' +
|
|
1182
|
+
'After: use token cssVariable values in your CSS.',
|
|
1183
|
+
inputSchema: {
|
|
1184
|
+
type: z.enum(['color', 'spacing', 'typography', 'radius', 'shadow']).optional()
|
|
1185
|
+
.describe('Filter by token type'),
|
|
1186
|
+
category: z.enum(['primitive', 'semantic', 'derived']).optional()
|
|
1187
|
+
.describe('Filter by token category'),
|
|
1188
|
+
query: z.string().optional()
|
|
1189
|
+
.describe('Search token names (partial match)'),
|
|
1190
|
+
theme: z.enum(['light', 'dark', 'all']).optional()
|
|
1191
|
+
.describe('Theme filter (currently light only; dark/all return an error due to NG-06)'),
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
async ({ type, category, query, theme }) => {
|
|
1195
|
+
const { isError, payload } = buildDesignTokensPayload(designTokensData, { type, category, query, theme });
|
|
1196
|
+
if (isError) {
|
|
1197
|
+
return buildJsonToolErrorResponse(payload);
|
|
1198
|
+
}
|
|
1199
|
+
return buildJsonToolResponse(payload);
|
|
1200
|
+
},
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
// -----------------------------------------------------------------------
|
|
1204
|
+
// Tool: get_design_token_detail
|
|
1205
|
+
// -----------------------------------------------------------------------
|
|
1206
|
+
server.registerTool(
|
|
1207
|
+
'get_design_token_detail',
|
|
1208
|
+
{
|
|
1209
|
+
description:
|
|
1210
|
+
'Get details for one design token. ' +
|
|
1211
|
+
'When: you already found a token and need its references, referencedBy, and usage examples. ' +
|
|
1212
|
+
'Returns: token detail object with relationships and example CSS snippets. ' +
|
|
1213
|
+
'After: apply the cssVariable in your implementation or validate related semantic aliases.',
|
|
1214
|
+
inputSchema: {
|
|
1215
|
+
name: z.string()
|
|
1216
|
+
.describe('Token name or css variable (e.g. --color-primary or var(--color-primary))'),
|
|
1217
|
+
theme: z.enum(['light', 'dark', 'all']).optional()
|
|
1218
|
+
.describe('Theme selector (currently only light is supported due to NG-06)'),
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
async ({ name, theme }) => {
|
|
1222
|
+
const { isError, payload } = buildDesignTokenDetailPayload(designTokensData, name, theme);
|
|
1223
|
+
if (isError) {
|
|
1224
|
+
return buildJsonToolErrorResponse(payload);
|
|
1225
|
+
}
|
|
1226
|
+
const normalizedName = normalizeTokenIdentifier(name);
|
|
1227
|
+
const componentRefs = componentTokenRefMap.get(normalizedName);
|
|
1228
|
+
if (componentRefs && componentRefs.size > 0) {
|
|
1229
|
+
payload.componentReferencedBy = [...componentRefs].sort();
|
|
1230
|
+
}
|
|
1231
|
+
return buildJsonToolResponse(payload);
|
|
1232
|
+
},
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
// -----------------------------------------------------------------------
|
|
1236
|
+
// Tool: get_accessibility_docs
|
|
1237
|
+
// -----------------------------------------------------------------------
|
|
1238
|
+
server.registerTool(
|
|
1239
|
+
'get_accessibility_docs',
|
|
1240
|
+
{
|
|
1241
|
+
description:
|
|
1242
|
+
'Get accessibility guidance and component checklist entries. ' +
|
|
1243
|
+
'When: validating accessibility decisions, reviewing ARIA usage, or checking WCAG-focused implementation notes. ' +
|
|
1244
|
+
'Returns: filtered checklist entries from component a11y annotations and accessibility guidelines. ' +
|
|
1245
|
+
'After: apply the checks in your markup and run validate_markup.',
|
|
1246
|
+
inputSchema: {
|
|
1247
|
+
component: z.string().optional()
|
|
1248
|
+
.describe('Filter by component tagName/className/componentId'),
|
|
1249
|
+
topic: z.string().optional()
|
|
1250
|
+
.describe('Filter by topic (e.g. semantics, keyboard, labels, states, zoom, motion, callouts, guideline)'),
|
|
1251
|
+
wcagLevel: z.enum(['A', 'AA', 'AAA', 'all']).optional()
|
|
1252
|
+
.describe('Filter by WCAG level (default: all)'),
|
|
1253
|
+
maxResults: z.number().int().min(1).max(100).optional()
|
|
1254
|
+
.describe('Maximum results to return (default: 20)'),
|
|
1255
|
+
prefix: z.string().optional(),
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
async ({ component, topic, wcagLevel, maxResults, prefix }) => {
|
|
1259
|
+
const p = normalizePrefix(prefix);
|
|
1260
|
+
let componentTagName;
|
|
1261
|
+
|
|
1262
|
+
if (typeof component === 'string' && component.trim() !== '') {
|
|
1263
|
+
const decl = resolveDeclByComponent(indexes, component, p)?.decl;
|
|
1264
|
+
|
|
1265
|
+
if (!decl || typeof decl?.tagName !== 'string') {
|
|
1266
|
+
return {
|
|
1267
|
+
content: [{
|
|
1268
|
+
type: 'text',
|
|
1269
|
+
text: `Component not found (component=${component})`,
|
|
1270
|
+
}],
|
|
1271
|
+
isError: true,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
componentTagName = withPrefix(decl.tagName.toLowerCase(), p);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const entries = buildAccessibilityIndex(indexes, guidelinesIndexData, { prefix: p });
|
|
1279
|
+
const result = queryAccessibilityIndex(entries, {
|
|
1280
|
+
componentTagName,
|
|
1281
|
+
topic,
|
|
1282
|
+
wcagLevel,
|
|
1283
|
+
maxResults,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
const payload = {
|
|
1287
|
+
query: {
|
|
1288
|
+
component: componentTagName ?? null,
|
|
1289
|
+
topic: result.topic,
|
|
1290
|
+
wcagLevel: result.wcagLevel,
|
|
1291
|
+
},
|
|
1292
|
+
totalHits: result.totalHits,
|
|
1293
|
+
results: result.results,
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
return buildJsonToolResponse(payload);
|
|
1297
|
+
},
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
// -----------------------------------------------------------------------
|
|
1301
|
+
// Tool: search_guidelines
|
|
1302
|
+
// -----------------------------------------------------------------------
|
|
1303
|
+
server.registerTool(
|
|
1304
|
+
'search_guidelines',
|
|
1305
|
+
{
|
|
1306
|
+
description:
|
|
1307
|
+
'Search design system guidelines including accessibility, CSS patterns, and best practices. ' +
|
|
1308
|
+
'When: need to understand design system rules before implementing UI. ' +
|
|
1309
|
+
'Returns: relevant guideline sections with file paths and snippets. ' +
|
|
1310
|
+
'After: follow the guidelines in your implementation.',
|
|
1311
|
+
inputSchema: {
|
|
1312
|
+
query: z.string().describe('Search keywords'),
|
|
1313
|
+
topic: z.enum(['accessibility', 'css', 'patterns', 'all']).optional()
|
|
1314
|
+
.describe('Filter by topic area'),
|
|
1315
|
+
maxResults: z.number().int().min(1).max(20).optional()
|
|
1316
|
+
.describe('Maximum results to return (1-20, default: 5)'),
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
async ({ query, topic, maxResults }) => {
|
|
1320
|
+
if (!guidelinesIndexData) {
|
|
1321
|
+
return {
|
|
1322
|
+
content: [{ type: 'text', text: 'Guidelines index not available. Run: npm run mcp:index-guidelines' }],
|
|
1323
|
+
isError: true,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const max = maxResults ?? 5;
|
|
1328
|
+
const documents = Array.isArray(guidelinesIndexData.documents) ? guidelinesIndexData.documents : [];
|
|
1329
|
+
const q = query.toLowerCase();
|
|
1330
|
+
const expandedTerms = expandQueryWithSynonyms(q);
|
|
1331
|
+
|
|
1332
|
+
const results = [];
|
|
1333
|
+
|
|
1334
|
+
for (const doc of documents) {
|
|
1335
|
+
if (topic && topic !== 'all' && doc.topic !== topic) continue;
|
|
1336
|
+
|
|
1337
|
+
const sections = Array.isArray(doc.sections) ? doc.sections : [];
|
|
1338
|
+
for (const section of sections) {
|
|
1339
|
+
let score = 0;
|
|
1340
|
+
const heading = String(section.heading ?? '').toLowerCase();
|
|
1341
|
+
const keywords = Array.isArray(section.keywords) ? section.keywords : [];
|
|
1342
|
+
const snippet = String(section.snippet ?? '').toLowerCase();
|
|
1343
|
+
const body = String(section.body ?? '').toLowerCase();
|
|
1344
|
+
|
|
1345
|
+
if (heading.includes(q)) score += 3;
|
|
1346
|
+
|
|
1347
|
+
for (const kw of keywords) {
|
|
1348
|
+
if (String(kw).toLowerCase().includes(q)) {
|
|
1349
|
+
score += 2;
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (snippet.includes(q)) score += 1;
|
|
1355
|
+
|
|
1356
|
+
if (body && body.includes(q)) {
|
|
1357
|
+
score += 1;
|
|
1358
|
+
let idx = body.indexOf(q);
|
|
1359
|
+
let occurrences = 0;
|
|
1360
|
+
while (idx !== -1 && occurrences < 3) {
|
|
1361
|
+
occurrences++;
|
|
1362
|
+
idx = body.indexOf(q, idx + q.length);
|
|
1363
|
+
}
|
|
1364
|
+
if (occurrences > 1) score += Math.min(occurrences - 1, 2);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (expandedTerms.length > 1) {
|
|
1368
|
+
let synScore = 0;
|
|
1369
|
+
const lowerKeywords = keywords.map((kw) => String(kw).toLowerCase());
|
|
1370
|
+
for (let i = 1; i < expandedTerms.length && synScore < 2; i++) {
|
|
1371
|
+
const syn = expandedTerms[i];
|
|
1372
|
+
if (heading.includes(syn)) { synScore += 1; continue; }
|
|
1373
|
+
if (snippet.includes(syn) || body.includes(syn)) { synScore += 1; continue; }
|
|
1374
|
+
for (const kw of lowerKeywords) {
|
|
1375
|
+
if (kw.includes(syn)) {
|
|
1376
|
+
synScore += 1;
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
score += synScore;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (score > 0) {
|
|
1385
|
+
results.push({
|
|
1386
|
+
score,
|
|
1387
|
+
documentId: doc.id,
|
|
1388
|
+
title: doc.title,
|
|
1389
|
+
topic: doc.topic,
|
|
1390
|
+
heading: section.heading,
|
|
1391
|
+
snippet: section.snippet,
|
|
1392
|
+
startLine: section.startLine,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
results.sort((a, b) => b.score - a.score);
|
|
1399
|
+
const topResults = results.slice(0, max);
|
|
1400
|
+
|
|
1401
|
+
const payload = {
|
|
1402
|
+
query,
|
|
1403
|
+
topic: topic ?? 'all',
|
|
1404
|
+
totalHits: results.length,
|
|
1405
|
+
results: topResults,
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
if (results.length === 0) {
|
|
1409
|
+
const synonymExpansions = expandedTerms.filter((t) => t !== q);
|
|
1410
|
+
payload.suggestions = {
|
|
1411
|
+
alternativeQueries: synonymExpansions.length > 0 ? synonymExpansions : [],
|
|
1412
|
+
alternativeTools: [
|
|
1413
|
+
{ tool: 'get_accessibility_docs', hint: 'For component-specific a11y checks' },
|
|
1414
|
+
{ tool: 'get_component_api', hint: 'For component API details' },
|
|
1415
|
+
],
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return buildJsonToolResponse(payload);
|
|
1420
|
+
},
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
// -----------------------------------------------------------------------
|
|
1424
|
+
// Plugin tools registration
|
|
1425
|
+
// -----------------------------------------------------------------------
|
|
1426
|
+
for (const plugin of plugins) {
|
|
1427
|
+
const pluginTools = Array.isArray(plugin.tools) ? plugin.tools : [];
|
|
1428
|
+
for (const tool of pluginTools) {
|
|
1429
|
+
server.registerTool(
|
|
1430
|
+
tool.name,
|
|
1431
|
+
{
|
|
1432
|
+
description: tool.description,
|
|
1433
|
+
inputSchema: toPassthroughSchema(tool.inputSchema),
|
|
1434
|
+
},
|
|
1435
|
+
async (args) => {
|
|
1436
|
+
try {
|
|
1437
|
+
if (typeof tool.handler === 'function') {
|
|
1438
|
+
const result = await tool.handler(args, {
|
|
1439
|
+
plugin: { name: plugin.name, version: plugin.version },
|
|
1440
|
+
helpers: {
|
|
1441
|
+
loadJsonData: loadJson,
|
|
1442
|
+
loadTextData: loadText,
|
|
1443
|
+
buildJsonToolResponse,
|
|
1444
|
+
normalizePrefix,
|
|
1445
|
+
withPrefix,
|
|
1446
|
+
toCanonicalTagName,
|
|
1447
|
+
},
|
|
1448
|
+
});
|
|
1449
|
+
if (result !== null && typeof result === 'object' && !Array.isArray(result) && Array.isArray(result.content)) {
|
|
1450
|
+
return finalizeToolResult(result);
|
|
1451
|
+
}
|
|
1452
|
+
return buildJsonToolResponse(result ?? {});
|
|
1453
|
+
}
|
|
1454
|
+
return buildJsonToolResponse(tool.staticPayload ?? {});
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1457
|
+
return buildJsonToolErrorResponse({
|
|
1458
|
+
error: {
|
|
1459
|
+
code: 'PLUGIN_TOOL_RUNTIME_ERROR',
|
|
1460
|
+
message: `Plugin tool failed (${tool.name}): ${message}`,
|
|
1461
|
+
plugin: plugin.name,
|
|
1462
|
+
},
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|