@monoharada/wcf-mcp 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -3
- package/core.mjs +649 -51
- package/data/component-selector-guide.json +102 -0
- package/data/design-tokens.json +1 -1
- package/data/guidelines-index.json +1 -1
- package/data/pattern-registry.json +155 -36
- package/package.json +1 -1
- package/server.mjs +1 -0
- package/validator.mjs +183 -8
package/core.mjs
CHANGED
|
@@ -112,6 +112,8 @@ const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
|
|
|
112
112
|
'get_design_token_detail',
|
|
113
113
|
'get_accessibility_docs',
|
|
114
114
|
'search_guidelines',
|
|
115
|
+
'generate_full_page_html',
|
|
116
|
+
'get_component_selector_guide',
|
|
115
117
|
]));
|
|
116
118
|
const A11Y_CATEGORY_LEVEL_MAP = Object.freeze({
|
|
117
119
|
semantics: 'A',
|
|
@@ -141,12 +143,125 @@ const SYNONYM_TABLE = new Map([
|
|
|
141
143
|
['aria-live', ['role=alert', 'aria-describedby', 'live region', 'error text']],
|
|
142
144
|
['keyboard', ['focus', 'tab', 'tabindex', 'key event', 'focus trap']],
|
|
143
145
|
['contrast', ['color', 'wcag', 'color contrast']],
|
|
144
|
-
['spacing', ['margin', 'padding', 'gap', 'spacing token']],
|
|
146
|
+
['spacing', ['margin', 'padding', 'gap', 'spacing token', '--spacing']],
|
|
145
147
|
['skip-navigation', ['skip-link', 'landmark', 'skip nav']],
|
|
146
148
|
['heading', ['heading hierarchy', 'h1', 'heading level']],
|
|
147
149
|
['form', ['input', 'validation', 'required', 'label']],
|
|
150
|
+
['part', ['::part', 'css part', 'shadow dom styling']],
|
|
151
|
+
['layout', ['grid', 'flexbox', 'layout-shell', 'responsive', 'breakpoint']],
|
|
152
|
+
['responsive', ['media query', 'breakpoint', 'viewport', 'mobile']],
|
|
153
|
+
['error', ['validation', 'aria-invalid', 'aria-describedby', 'error text']],
|
|
154
|
+
['focus', ['focus-visible', 'focus ring', 'outline', 'tabindex', 'keyboard']],
|
|
155
|
+
['token', ['design token', 'css variable', 'custom property', 'spacing token']],
|
|
156
|
+
['div-soup', ['wrapper', 'unnecessary div', 'minimal dom']],
|
|
148
157
|
]);
|
|
149
158
|
|
|
159
|
+
// Icon alias table: common alias → canonical icon names (DD-18)
|
|
160
|
+
// Maps user-friendly search terms to actual icon names in the CEM catalog.
|
|
161
|
+
const ICON_ALIAS_TABLE = new Map([
|
|
162
|
+
['x', ['close', 'cancel']],
|
|
163
|
+
['trash', ['delete']],
|
|
164
|
+
['pencil', ['edit']],
|
|
165
|
+
['magnifying', ['search']],
|
|
166
|
+
['gear', ['settings']],
|
|
167
|
+
['plus', ['add']],
|
|
168
|
+
['minus', ['subtract']],
|
|
169
|
+
['tick', ['check', 'checkmark']],
|
|
170
|
+
['alert', ['warning', 'attention']],
|
|
171
|
+
['info', ['information', 'help']],
|
|
172
|
+
['hamburger', ['menu']],
|
|
173
|
+
['back', ['arrowBack', 'arrowLeft']],
|
|
174
|
+
['forward', ['arrowForward', 'arrowRight']],
|
|
175
|
+
['eye', ['visibility']],
|
|
176
|
+
['user', ['person']],
|
|
177
|
+
['file', ['document']],
|
|
178
|
+
['bell', ['notification']],
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// Interaction examples for form components (P-04 / #206)
|
|
182
|
+
const INTERACTION_EXAMPLES_MAP = Object.freeze({
|
|
183
|
+
'dads-input-text': [
|
|
184
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "hello";' },
|
|
185
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
186
|
+
{ scenario: 'Clear validation error', trigger: 'attribute', code: 'el.error = false; el.errorText = "";' },
|
|
187
|
+
{ scenario: 'Listen to value change', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
188
|
+
],
|
|
189
|
+
'dads-textarea': [
|
|
190
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "long text...";' },
|
|
191
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "入力できる文字数を超えています";' },
|
|
192
|
+
{ scenario: 'Listen to input event', trigger: 'event', code: "el.addEventListener('input', (e) => { console.log(e.target.value); });" },
|
|
193
|
+
],
|
|
194
|
+
'dads-select': [
|
|
195
|
+
{ scenario: 'Set selected value', trigger: 'property', code: 'el.value = "option1";' },
|
|
196
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
197
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
198
|
+
],
|
|
199
|
+
'dads-checkbox': [
|
|
200
|
+
{ scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
|
|
201
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
202
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.checked); });" },
|
|
203
|
+
],
|
|
204
|
+
'dads-radio': [
|
|
205
|
+
{ scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
|
|
206
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
207
|
+
],
|
|
208
|
+
'dads-combobox': [
|
|
209
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "selected-option";' },
|
|
210
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
211
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
212
|
+
],
|
|
213
|
+
'dads-date-picker': [
|
|
214
|
+
{ scenario: 'Set date value', trigger: 'property', code: 'el.value = "2024-01-15";' },
|
|
215
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
216
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
217
|
+
],
|
|
218
|
+
'dads-file-upload': [
|
|
219
|
+
{ scenario: 'Listen to file selection', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.files); });" },
|
|
220
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Layout behavior metadata for layout/display components (P-05 / #207)
|
|
225
|
+
const LAYOUT_BEHAVIOR_MAP = Object.freeze({
|
|
226
|
+
'dads-layout-shell': {
|
|
227
|
+
responsive: {
|
|
228
|
+
breakpoints: { desktop: '80rem', tablet: '48rem' },
|
|
229
|
+
modes: ['auto', 'desktop', 'tablet', 'mobile'],
|
|
230
|
+
defaultMode: 'auto',
|
|
231
|
+
description: 'Automatically switches between desktop/tablet/mobile layouts based on viewport width when mode="auto".',
|
|
232
|
+
},
|
|
233
|
+
overflow: {
|
|
234
|
+
strategy: 'slot-driven',
|
|
235
|
+
description: 'Slots (header, sidebar, aside, footer) are auto-hidden when empty. Sidebar collapses to rail on tablet.',
|
|
236
|
+
},
|
|
237
|
+
constraints: {
|
|
238
|
+
patterns: ['website', 'app-shell', 'master-detail', 'left-header-pane', 'three-pane', 'three-pane-shell'],
|
|
239
|
+
defaultPattern: 'app-shell',
|
|
240
|
+
mobileSidebarOptions: ['hidden', 'top', 'bottom'],
|
|
241
|
+
description: 'Choose a pattern attribute to control layout structure. Pair with mode and mobile-sidebar for full control.',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
'dads-layout-sidebar': {
|
|
245
|
+
responsive: {
|
|
246
|
+
description: 'Designed to be placed inside dads-layout-shell sidebar slot. Width is controlled by the parent shell.',
|
|
247
|
+
},
|
|
248
|
+
constraints: {
|
|
249
|
+
description: 'Simple container for sidebar content. Use inside dads-layout-shell for responsive behavior.',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
'dads-device-mock': {
|
|
253
|
+
responsive: {
|
|
254
|
+
devices: ['desktop', 'tablet', 'mobile'],
|
|
255
|
+
defaultDevice: 'mobile',
|
|
256
|
+
description: 'Renders a device frame (desktop/tablet/mobile) around slotted content. Set device attribute to switch.',
|
|
257
|
+
},
|
|
258
|
+
constraints: {
|
|
259
|
+
visibleHeight: 'Use visible-height attribute to clip the mock to a specific height (e.g. "220px").',
|
|
260
|
+
description: 'Display-only component for previewing content in device frames. Not a layout container.',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
150
265
|
export function expandQueryWithSynonyms(query) {
|
|
151
266
|
const q = String(query ?? '').toLowerCase().trim();
|
|
152
267
|
if (!q) return [q];
|
|
@@ -377,6 +492,42 @@ export function buildTokenRelationshipIndex(designTokensData) {
|
|
|
377
492
|
return { byToken };
|
|
378
493
|
}
|
|
379
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Extract which components reference which design tokens via var() in CEM cssProperties.
|
|
497
|
+
* Returns Map<tokenName, Set<componentTagName>>.
|
|
498
|
+
* DD-25: var(--token, fallback) fallback values are not extracted (known limitation).
|
|
499
|
+
*/
|
|
500
|
+
export function buildComponentTokenReferencedBy(manifest) {
|
|
501
|
+
const result = new Map();
|
|
502
|
+
const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
|
|
503
|
+
const varRe = /var\((--[\w-]+)/g;
|
|
504
|
+
for (const mod of modules) {
|
|
505
|
+
const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
|
|
506
|
+
for (const decl of declarations) {
|
|
507
|
+
const tag = decl?.tagName;
|
|
508
|
+
if (typeof tag !== 'string') continue;
|
|
509
|
+
const cssProps = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
|
|
510
|
+
for (const prop of cssProps) {
|
|
511
|
+
const defaultVal = typeof prop?.default === 'string' ? prop.default : '';
|
|
512
|
+
let m;
|
|
513
|
+
while ((m = varRe.exec(defaultVal)) !== null) {
|
|
514
|
+
const tokenName = normalizeTokenIdentifier(m[1]);
|
|
515
|
+
if (!tokenName) continue;
|
|
516
|
+
if (!result.has(tokenName)) result.set(tokenName, new Set());
|
|
517
|
+
result.get(tokenName).add(tag);
|
|
518
|
+
}
|
|
519
|
+
// Also index the css property name itself as a token → component mapping
|
|
520
|
+
const propName = normalizeTokenIdentifier(prop?.name);
|
|
521
|
+
if (propName) {
|
|
522
|
+
if (!result.has(propName)) result.set(propName, new Set());
|
|
523
|
+
result.get(propName).add(tag);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
|
|
380
531
|
function toTokenSummary(token) {
|
|
381
532
|
return {
|
|
382
533
|
name: String(token?.name ?? ''),
|
|
@@ -752,10 +903,16 @@ export function levenshteinDistance(left, right) {
|
|
|
752
903
|
return prev[b.length];
|
|
753
904
|
}
|
|
754
905
|
|
|
755
|
-
export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
906
|
+
export function suggestUnknownElementTagName(tagName, cemIndex, prefix) {
|
|
756
907
|
const target = String(tagName ?? '').trim().toLowerCase();
|
|
757
908
|
if (!target || !target.includes('-')) return undefined;
|
|
758
909
|
|
|
910
|
+
// Try prefix-prepend before Levenshtein (e.g. input-text → dads-input-text)
|
|
911
|
+
if (prefix && cemIndex instanceof Map) {
|
|
912
|
+
const prefixed = `${String(prefix).toLowerCase()}-${target}`;
|
|
913
|
+
if (cemIndex.has(prefixed)) return prefixed;
|
|
914
|
+
}
|
|
915
|
+
|
|
759
916
|
let bestTag;
|
|
760
917
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
761
918
|
const candidateSource = cemIndex instanceof Map ? cemIndex.keys() : [];
|
|
@@ -775,15 +932,19 @@ export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
|
775
932
|
return bestTag;
|
|
776
933
|
}
|
|
777
934
|
|
|
778
|
-
export function buildDiagnosticSuggestion({ diagnostic, cemIndex }) {
|
|
935
|
+
export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
|
|
779
936
|
const code = String(diagnostic?.code ?? '');
|
|
780
937
|
if (!code) return undefined;
|
|
781
938
|
|
|
782
939
|
if (code === 'unknownElement') {
|
|
783
|
-
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex);
|
|
940
|
+
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex, prefix);
|
|
784
941
|
return tagName ? `Did you mean "${tagName}"?` : undefined;
|
|
785
942
|
}
|
|
786
943
|
|
|
944
|
+
if (code === 'canonicalLowercaseRecommendation') {
|
|
945
|
+
return diagnostic?.hint ?? undefined;
|
|
946
|
+
}
|
|
947
|
+
|
|
787
948
|
if (code === 'forbiddenAttribute' && String(diagnostic?.attrName ?? '').toLowerCase() === 'placeholder') {
|
|
788
949
|
return 'Use aria-label or aria-describedby support text instead of placeholder.';
|
|
789
950
|
}
|
|
@@ -796,6 +957,14 @@ export function buildDiagnosticSuggestion({ diagnostic, cemIndex }) {
|
|
|
796
957
|
return 'Use role="alert" only for urgent live updates; otherwise use static text associated via aria-describedby.';
|
|
797
958
|
}
|
|
798
959
|
|
|
960
|
+
if (code === 'emptyLabel') {
|
|
961
|
+
return diagnostic?.hint ?? 'Provide a meaningful label value for accessibility.';
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (code === 'emptyAriaLabel') {
|
|
965
|
+
return diagnostic?.hint ?? 'Provide a meaningful aria-label value or use a visible <label> element.';
|
|
966
|
+
}
|
|
967
|
+
|
|
799
968
|
return undefined;
|
|
800
969
|
}
|
|
801
970
|
|
|
@@ -835,6 +1004,74 @@ export function buildIndexes(manifest) {
|
|
|
835
1004
|
return { byTag, byClass, modulePathByTag, decls };
|
|
836
1005
|
}
|
|
837
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Extracts the primary component prefix from CEM indexes.
|
|
1009
|
+
* Returns the most common prefix among all tagNames (e.g. 'dads' from 'dads-button').
|
|
1010
|
+
* Falls back to CANONICAL_PREFIX if no tagNames are found.
|
|
1011
|
+
*/
|
|
1012
|
+
export function extractPrefixFromIndexes(indexes) {
|
|
1013
|
+
const counts = new Map();
|
|
1014
|
+
for (const { tagName } of indexes.decls) {
|
|
1015
|
+
const i = tagName.indexOf('-');
|
|
1016
|
+
if (i > 0) {
|
|
1017
|
+
const p = tagName.slice(0, i);
|
|
1018
|
+
counts.set(p, (counts.get(p) ?? 0) + 1);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
let best = CANONICAL_PREFIX;
|
|
1022
|
+
let bestCount = 0;
|
|
1023
|
+
for (const [p, c] of counts) {
|
|
1024
|
+
if (c > bestCount) { best = p; bestCount = c; }
|
|
1025
|
+
}
|
|
1026
|
+
return best;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Build a full HTML page from a fragment.
|
|
1031
|
+
* @param {{ html: string; prefix: string; cemIndex: Map }} params
|
|
1032
|
+
*/
|
|
1033
|
+
export function buildFullPageHtml({ html, prefix, cemIndex }) {
|
|
1034
|
+
// Extract custom element tags from the HTML fragment
|
|
1035
|
+
const tagRe = /<([a-z][a-z0-9]*-[a-z0-9-]*)\b/gi;
|
|
1036
|
+
const tags = new Set();
|
|
1037
|
+
let m;
|
|
1038
|
+
while ((m = tagRe.exec(html))) {
|
|
1039
|
+
tags.add(String(m[1]).toLowerCase());
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Build import map entries for recognized components
|
|
1043
|
+
const importEntries = {};
|
|
1044
|
+
for (const tag of tags) {
|
|
1045
|
+
if (cemIndex.has(tag)) {
|
|
1046
|
+
const suffix = tag.replace(/^[^-]+-/, '');
|
|
1047
|
+
importEntries[tag] = `./<dir>/components/${suffix}.js`;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const importMapJson = JSON.stringify({ imports: importEntries }, null, 2);
|
|
1052
|
+
|
|
1053
|
+
const lines = [
|
|
1054
|
+
'<!DOCTYPE html>',
|
|
1055
|
+
`<html lang="ja">`,
|
|
1056
|
+
'<head>',
|
|
1057
|
+
' <meta charset="utf-8">',
|
|
1058
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
1059
|
+
` <title>WCF Preview</title>`,
|
|
1060
|
+
` <link rel="stylesheet" href="./<dir>/styles/tokens.css">`,
|
|
1061
|
+
` <script type="importmap">`,
|
|
1062
|
+
importMapJson,
|
|
1063
|
+
` </script>`,
|
|
1064
|
+
'</head>',
|
|
1065
|
+
'<body>',
|
|
1066
|
+
html,
|
|
1067
|
+
` <script type="module" src="./<dir>/boot.js"></script>`,
|
|
1068
|
+
'</body>',
|
|
1069
|
+
'</html>',
|
|
1070
|
+
];
|
|
1071
|
+
|
|
1072
|
+
return { fullHtml: lines.join('\n'), importEntries };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
838
1075
|
export function pickDecl({ byTag, byClass }, { tagName, className, prefix }) {
|
|
839
1076
|
if (typeof tagName === 'string' && tagName.trim() !== '') {
|
|
840
1077
|
const canonical = toCanonicalTagName(tagName, prefix);
|
|
@@ -867,6 +1104,7 @@ export function serializeApi(decl, modulePath, prefix) {
|
|
|
867
1104
|
attributes: attributes.map((a) => ({
|
|
868
1105
|
name: a?.name,
|
|
869
1106
|
type: a?.type?.text,
|
|
1107
|
+
default: a?.default ?? null,
|
|
870
1108
|
description: a?.description,
|
|
871
1109
|
inheritedFrom: a?.inheritedFrom,
|
|
872
1110
|
deprecated: a?.deprecated,
|
|
@@ -933,8 +1171,12 @@ export function generateSnippet(api, prefix) {
|
|
|
933
1171
|
if (!a) continue;
|
|
934
1172
|
const t = String(a.type ?? '').toLowerCase();
|
|
935
1173
|
const isBoolean = t.includes('boolean');
|
|
936
|
-
if (isBoolean)
|
|
937
|
-
|
|
1174
|
+
if (isBoolean) {
|
|
1175
|
+
lines.push(` ${name}`);
|
|
1176
|
+
} else {
|
|
1177
|
+
const defaultVal = typeof a.default === 'string' ? a.default.replace(/^['"]|['"]$/g, '') : '';
|
|
1178
|
+
lines.push(` ${name}="${defaultVal}"`);
|
|
1179
|
+
}
|
|
938
1180
|
if (lines.length >= 4) break;
|
|
939
1181
|
}
|
|
940
1182
|
|
|
@@ -1133,7 +1375,18 @@ export function searchIconCatalog(indexes, { query, limit, offset, prefix } = {}
|
|
|
1133
1375
|
|
|
1134
1376
|
let icons = buildIconCatalog(indexes, prefix);
|
|
1135
1377
|
if (q) {
|
|
1136
|
-
|
|
1378
|
+
// Expand query with icon aliases (DD-18)
|
|
1379
|
+
const searchTerms = [q];
|
|
1380
|
+
const aliases = ICON_ALIAS_TABLE.get(q);
|
|
1381
|
+
if (aliases) {
|
|
1382
|
+
for (const alias of aliases) {
|
|
1383
|
+
if (!searchTerms.includes(alias)) searchTerms.push(alias);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
icons = icons.filter((icon) => {
|
|
1387
|
+
const name = icon.name.toLowerCase();
|
|
1388
|
+
return searchTerms.some((term) => name.includes(term));
|
|
1389
|
+
});
|
|
1137
1390
|
}
|
|
1138
1391
|
|
|
1139
1392
|
const total = icons.length;
|
|
@@ -1609,6 +1862,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1609
1862
|
|
|
1610
1863
|
const manifest = await loadJson('custom-elements.json');
|
|
1611
1864
|
const indexes = buildIndexes(manifest);
|
|
1865
|
+
const detectedPrefix = extractPrefixFromIndexes(indexes);
|
|
1612
1866
|
const {
|
|
1613
1867
|
collectCemCustomElements,
|
|
1614
1868
|
validateTextAgainstCem,
|
|
@@ -1621,6 +1875,9 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1621
1875
|
detectMissingRequiredAttributes = () => [],
|
|
1622
1876
|
detectOrphanedChildComponents = () => [],
|
|
1623
1877
|
detectEmptyInteractiveElement = () => [],
|
|
1878
|
+
detectNonLowercaseAttributes = () => [],
|
|
1879
|
+
detectCdnReferences = () => [],
|
|
1880
|
+
detectMissingRuntimeScaffold = () => [],
|
|
1624
1881
|
} = await loadValidator();
|
|
1625
1882
|
const canonicalCemIndex = collectCemCustomElements(manifest);
|
|
1626
1883
|
const canonicalEnumMap = buildEnumAttributeMap(manifest);
|
|
@@ -1652,10 +1909,14 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1652
1909
|
}
|
|
1653
1910
|
|
|
1654
1911
|
const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
|
|
1912
|
+
const componentTokenRefMap = buildComponentTokenReferencedBy(manifest);
|
|
1913
|
+
|
|
1914
|
+
const VENDOR_DIR = 'vendor-runtime';
|
|
1915
|
+
const PREFIX_STRIP_RE = /^[^-]+-/;
|
|
1655
1916
|
|
|
1656
1917
|
const server = new McpServer({
|
|
1657
1918
|
name: 'web-components-factory-design-system',
|
|
1658
|
-
version: '0.
|
|
1919
|
+
version: '0.6.0',
|
|
1659
1920
|
});
|
|
1660
1921
|
|
|
1661
1922
|
server.registerPrompt(
|
|
@@ -1805,8 +2066,8 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1805
2066
|
|
|
1806
2067
|
const overview = {
|
|
1807
2068
|
name: 'DADS Web Components (wcf)',
|
|
1808
|
-
version: '0.
|
|
1809
|
-
prefix:
|
|
2069
|
+
version: '0.6.0',
|
|
2070
|
+
prefix: detectedPrefix,
|
|
1810
2071
|
totalComponents: indexes.decls.length,
|
|
1811
2072
|
componentsByCategory: categoryCount,
|
|
1812
2073
|
totalPatterns: patternList.length,
|
|
@@ -1814,9 +2075,43 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1814
2075
|
setupInfo: {
|
|
1815
2076
|
npmPackage: 'web-components-factory',
|
|
1816
2077
|
installCommand: 'npm install web-components-factory',
|
|
1817
|
-
vendorRuntimePath: '
|
|
1818
|
-
htmlBoilerplate:
|
|
2078
|
+
vendorRuntimePath: '<dir>/',
|
|
2079
|
+
htmlBoilerplate: [
|
|
2080
|
+
'<script type="importmap">',
|
|
2081
|
+
`{ "imports": { "${detectedPrefix}-button": "./<dir>/components/button.js" } }`,
|
|
2082
|
+
'</script>',
|
|
2083
|
+
'<script type="module" src="./<dir>/boot.js"></script>',
|
|
2084
|
+
].join('\n'),
|
|
1819
2085
|
noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
|
|
2086
|
+
noCDN: true,
|
|
2087
|
+
deliveryModel: 'vendor-local',
|
|
2088
|
+
distribution: {
|
|
2089
|
+
selfHosted: true,
|
|
2090
|
+
cdn: false,
|
|
2091
|
+
strategy: 'vendor-importmap',
|
|
2092
|
+
quickStart: 'npx web-components-factory init --prefix <prefix> --dir <dir>',
|
|
2093
|
+
description:
|
|
2094
|
+
'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.',
|
|
2095
|
+
},
|
|
2096
|
+
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\`.`,
|
|
2097
|
+
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.',
|
|
2098
|
+
detectedPrefix,
|
|
2099
|
+
vendorSetup: {
|
|
2100
|
+
init: `wcf init --prefix ${detectedPrefix} --dir <dir>`,
|
|
2101
|
+
add: `wcf add <componentId> --prefix ${detectedPrefix} --out <dir>`,
|
|
2102
|
+
workflow: '1. wcf init で初期化(boot.js, importmap.snippet.json, autoloader を生成) → 2. wcf add で各コンポーネントを追加 → import map と boot.js が自動生成される',
|
|
2103
|
+
},
|
|
2104
|
+
htmlSetup: [
|
|
2105
|
+
'<script type="importmap">',
|
|
2106
|
+
'{',
|
|
2107
|
+
' "imports": {',
|
|
2108
|
+
` "${detectedPrefix}-button": "./<dir>/components/button.js",`,
|
|
2109
|
+
` "${detectedPrefix}-card": "./<dir>/components/card.js"`,
|
|
2110
|
+
' }',
|
|
2111
|
+
'}',
|
|
2112
|
+
'</script>',
|
|
2113
|
+
'<script type="module" src="./<dir>/boot.js"></script>',
|
|
2114
|
+
].join('\n'),
|
|
1820
2115
|
},
|
|
1821
2116
|
ideSetupTemplates: IDE_SETUP_TEMPLATES,
|
|
1822
2117
|
availablePrompts: [
|
|
@@ -1851,6 +2146,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1851
2146
|
{ name: 'generate_usage_snippet', purpose: 'Minimal HTML usage example' },
|
|
1852
2147
|
{ name: 'get_install_recipe', purpose: 'Installation instructions and dependency tree' },
|
|
1853
2148
|
{ name: 'validate_markup', purpose: 'Validate HTML against CEM schema' },
|
|
2149
|
+
{ name: 'generate_full_page_html', purpose: 'Wrap HTML fragment into a complete page with importmap and boot script' },
|
|
1854
2150
|
{ name: 'list_patterns', purpose: 'Browse page-level UI composition patterns' },
|
|
1855
2151
|
{ name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
|
|
1856
2152
|
{ name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
|
|
@@ -1858,6 +2154,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1858
2154
|
{ name: 'get_design_token_detail', purpose: 'Get details, relationships, and usage examples for one token' },
|
|
1859
2155
|
{ name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
|
|
1860
2156
|
{ name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
|
|
2157
|
+
{ name: 'get_component_selector_guide', purpose: 'Component selection guide by category and use case' },
|
|
1861
2158
|
],
|
|
1862
2159
|
recommendedWorkflow: [
|
|
1863
2160
|
'1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
|
|
@@ -1872,7 +2169,8 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1872
2169
|
'10. get_component_api → check attributes, slots, events, CSS parts',
|
|
1873
2170
|
'11. generate_usage_snippet or get_pattern_recipe → get code',
|
|
1874
2171
|
'12. validate_markup → verify your HTML and use suggestions to self-correct',
|
|
1875
|
-
'13.
|
|
2172
|
+
'13. generate_full_page_html → wrap fragment into a complete preview-ready page',
|
|
2173
|
+
'14. get_install_recipe → get import/install instructions',
|
|
1876
2174
|
],
|
|
1877
2175
|
experimental: {
|
|
1878
2176
|
plugins: {
|
|
@@ -1971,16 +2269,53 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1971
2269
|
'get_component_api',
|
|
1972
2270
|
{
|
|
1973
2271
|
description:
|
|
1974
|
-
'Get the full API surface of
|
|
2272
|
+
'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.',
|
|
1975
2273
|
inputSchema: {
|
|
1976
2274
|
tagName: z.string().optional().describe('Tag name (e.g., "dads-button")'),
|
|
1977
2275
|
className: z.string().optional().describe('Class name (e.g., "DadsButton")'),
|
|
1978
2276
|
component: z.string().optional().describe('Any identifier: tagName, className, or bare name (e.g., "button")'),
|
|
2277
|
+
components: z.array(z.string()).max(10).optional().describe('Batch: array of component identifiers (max 10). When provided, component/tagName/className are ignored.'),
|
|
1979
2278
|
prefix: z.string().optional(),
|
|
1980
2279
|
},
|
|
1981
2280
|
},
|
|
1982
|
-
async ({ tagName, className, component, prefix }) => {
|
|
2281
|
+
async ({ tagName, className, component, components, prefix }) => {
|
|
1983
2282
|
const p = normalizePrefix(prefix);
|
|
2283
|
+
|
|
2284
|
+
// Batch mode: components array takes priority (DD-23)
|
|
2285
|
+
if (Array.isArray(components) && components.length > 0) {
|
|
2286
|
+
const results = [];
|
|
2287
|
+
for (const comp of components) {
|
|
2288
|
+
const resolved = resolveDeclByComponent(indexes, comp, p);
|
|
2289
|
+
if (!resolved?.decl) {
|
|
2290
|
+
results.push({ component: comp, error: `Component not found: ${comp}` });
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
const { decl: d, modulePath: mp } = resolved;
|
|
2294
|
+
const cTag = typeof d.tagName === 'string' ? d.tagName.toLowerCase() : undefined;
|
|
2295
|
+
const mPath = mp ?? (cTag ? indexes.modulePathByTag.get(cTag) : undefined);
|
|
2296
|
+
const api = serializeApi(d, mPath, prefix);
|
|
2297
|
+
const related = getRelatedComponentsForTag({
|
|
2298
|
+
canonicalTagName: cTag,
|
|
2299
|
+
installRegistry,
|
|
2300
|
+
relatedMap: relatedComponentMap,
|
|
2301
|
+
prefix,
|
|
2302
|
+
});
|
|
2303
|
+
if (related.length > 0) api.relatedComponents = related;
|
|
2304
|
+
const a11y = extractAccessibilityChecklist(d, { prefix });
|
|
2305
|
+
if (a11y) api.accessibilityChecklist = a11y;
|
|
2306
|
+
results.push(api);
|
|
2307
|
+
}
|
|
2308
|
+
const resultJson = JSON.stringify(results, null, 2);
|
|
2309
|
+
if (measureToolResultBytes(resultJson) > MAX_TOOL_RESULT_BYTES) {
|
|
2310
|
+
return {
|
|
2311
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'Batch result exceeds size limit. Reduce the number of components.' }) }],
|
|
2312
|
+
isError: true,
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
return buildJsonToolResponse(results);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// Single mode (existing behavior)
|
|
1984
2319
|
let decl;
|
|
1985
2320
|
let modulePath;
|
|
1986
2321
|
|
|
@@ -2015,6 +2350,14 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2015
2350
|
if (accessibilityChecklist) {
|
|
2016
2351
|
api.accessibilityChecklist = accessibilityChecklist;
|
|
2017
2352
|
}
|
|
2353
|
+
const interactionExamples = canonicalTag ? INTERACTION_EXAMPLES_MAP[canonicalTag] : undefined;
|
|
2354
|
+
if (interactionExamples) {
|
|
2355
|
+
api.interactionExamples = interactionExamples;
|
|
2356
|
+
}
|
|
2357
|
+
const layoutBehavior = canonicalTag ? LAYOUT_BEHAVIOR_MAP[canonicalTag] : undefined;
|
|
2358
|
+
if (layoutBehavior) {
|
|
2359
|
+
api.layoutBehavior = layoutBehavior;
|
|
2360
|
+
}
|
|
2018
2361
|
|
|
2019
2362
|
return buildJsonToolResponse(api);
|
|
2020
2363
|
},
|
|
@@ -2133,7 +2476,19 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2133
2476
|
defineHint,
|
|
2134
2477
|
source: install.source,
|
|
2135
2478
|
usageSnippet,
|
|
2479
|
+
usageContext: 'body-only',
|
|
2136
2480
|
installHint: componentId ? `wcf add ${componentId}` : undefined,
|
|
2481
|
+
vendorHint: (() => {
|
|
2482
|
+
const im = tagNames.length > 0
|
|
2483
|
+
? JSON.stringify({ imports: Object.fromEntries(tagNames.map((t) => [t, `./<dir>/components/${t.replace(/^[^-]+-/, '')}.js`])) })
|
|
2484
|
+
: undefined;
|
|
2485
|
+
return {
|
|
2486
|
+
install: componentId ? `wcf add ${componentId} --prefix <prefix> --out <dir>` : undefined,
|
|
2487
|
+
importMap: im,
|
|
2488
|
+
importmap: im, // @deprecated — use importMap; will be removed in v1.0
|
|
2489
|
+
boot: '<dir>/boot.js -- loads autoloader that registers components via import map',
|
|
2490
|
+
};
|
|
2491
|
+
})(),
|
|
2137
2492
|
},
|
|
2138
2493
|
null,
|
|
2139
2494
|
2,
|
|
@@ -2192,10 +2547,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2192
2547
|
severity: 'warning',
|
|
2193
2548
|
});
|
|
2194
2549
|
|
|
2550
|
+
const cemTagNames = new Set(cemIndex.keys());
|
|
2195
2551
|
const accessibilityDiagnostics = detectAccessibilityMisuseInMarkup({
|
|
2196
2552
|
filePath: '<markup>',
|
|
2197
2553
|
text: html,
|
|
2198
|
-
severity: '
|
|
2554
|
+
severity: 'error',
|
|
2555
|
+
cemTagNames,
|
|
2199
2556
|
});
|
|
2200
2557
|
|
|
2201
2558
|
const slotDiagnostics = detectInvalidSlotName({
|
|
@@ -2226,8 +2583,40 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2226
2583
|
severity: 'warning',
|
|
2227
2584
|
});
|
|
2228
2585
|
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2586
|
+
const lowercaseDiagnostics = detectNonLowercaseAttributes({
|
|
2587
|
+
filePath: '<markup>',
|
|
2588
|
+
text: html,
|
|
2589
|
+
cem: cemIndex,
|
|
2590
|
+
severity: 'warning',
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
const cdnDiagnostics = detectCdnReferences({
|
|
2594
|
+
filePath: '<markup>',
|
|
2595
|
+
text: html,
|
|
2596
|
+
severity: 'warning',
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
const scaffoldDiagnostics = detectMissingRuntimeScaffold({
|
|
2600
|
+
filePath: '<markup>',
|
|
2601
|
+
text: html,
|
|
2602
|
+
severity: 'warning',
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
const allRawDiagnostics = [
|
|
2606
|
+
...cemDiagnostics,
|
|
2607
|
+
...enumDiagnostics,
|
|
2608
|
+
...slotDiagnostics,
|
|
2609
|
+
...requiredAttrDiagnostics,
|
|
2610
|
+
...orphanDiagnostics,
|
|
2611
|
+
...emptyInteractiveDiagnostics,
|
|
2612
|
+
...lowercaseDiagnostics,
|
|
2613
|
+
...tokenMisuseDiagnostics,
|
|
2614
|
+
...accessibilityDiagnostics,
|
|
2615
|
+
...cdnDiagnostics,
|
|
2616
|
+
...scaffoldDiagnostics,
|
|
2617
|
+
];
|
|
2618
|
+
const diagnostics = allRawDiagnostics.map((d) => {
|
|
2619
|
+
const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex, prefix: p });
|
|
2231
2620
|
return {
|
|
2232
2621
|
file: d.file,
|
|
2233
2622
|
range: d.range,
|
|
@@ -2247,6 +2636,102 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2247
2636
|
},
|
|
2248
2637
|
);
|
|
2249
2638
|
|
|
2639
|
+
// -----------------------------------------------------------------------
|
|
2640
|
+
// Tool: generate_full_page_html
|
|
2641
|
+
// -----------------------------------------------------------------------
|
|
2642
|
+
server.registerTool(
|
|
2643
|
+
'generate_full_page_html',
|
|
2644
|
+
{
|
|
2645
|
+
description:
|
|
2646
|
+
'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.',
|
|
2647
|
+
inputSchema: {
|
|
2648
|
+
html: z.string().describe('HTML fragment containing WCF custom elements'),
|
|
2649
|
+
prefix: z.string().optional().describe('Component prefix (default: auto-detected)'),
|
|
2650
|
+
},
|
|
2651
|
+
},
|
|
2652
|
+
async ({ html, prefix }) => {
|
|
2653
|
+
const p = normalizePrefix(prefix);
|
|
2654
|
+
let ci = canonicalCemIndex;
|
|
2655
|
+
if (p !== CANONICAL_PREFIX) {
|
|
2656
|
+
ci = mergeWithPrefixed(canonicalCemIndex, p);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
const { fullHtml, importEntries } = buildFullPageHtml({ html, prefix: p, cemIndex: ci });
|
|
2660
|
+
|
|
2661
|
+
return {
|
|
2662
|
+
content: [{
|
|
2663
|
+
type: 'text',
|
|
2664
|
+
text: JSON.stringify({
|
|
2665
|
+
fullHtml,
|
|
2666
|
+
componentCount: Object.keys(importEntries).length,
|
|
2667
|
+
importMapEntries: importEntries,
|
|
2668
|
+
}, null, 2),
|
|
2669
|
+
}],
|
|
2670
|
+
};
|
|
2671
|
+
},
|
|
2672
|
+
);
|
|
2673
|
+
|
|
2674
|
+
// -----------------------------------------------------------------------
|
|
2675
|
+
// Tool: get_component_selector_guide
|
|
2676
|
+
// -----------------------------------------------------------------------
|
|
2677
|
+
let selectorGuideData = null;
|
|
2678
|
+
try {
|
|
2679
|
+
selectorGuideData = await loadJson('component-selector-guide.json');
|
|
2680
|
+
} catch {
|
|
2681
|
+
// component-selector-guide.json may not exist yet
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
server.registerTool(
|
|
2685
|
+
'get_component_selector_guide',
|
|
2686
|
+
{
|
|
2687
|
+
description:
|
|
2688
|
+
'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.',
|
|
2689
|
+
inputSchema: {
|
|
2690
|
+
category: z.string().optional().describe('Filter by category key (e.g., "Form", "Navigation", "Layout")'),
|
|
2691
|
+
useCase: z.string().optional().describe('Search by use-case keyword (e.g., "date", "login", "upload")'),
|
|
2692
|
+
},
|
|
2693
|
+
},
|
|
2694
|
+
async ({ category, useCase }) => {
|
|
2695
|
+
if (!selectorGuideData || !Array.isArray(selectorGuideData.categories)) {
|
|
2696
|
+
return {
|
|
2697
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'Component selector guide not available.' }) }],
|
|
2698
|
+
isError: true,
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
let categories = selectorGuideData.categories;
|
|
2703
|
+
|
|
2704
|
+
// Filter by category
|
|
2705
|
+
if (typeof category === 'string' && category.trim()) {
|
|
2706
|
+
const cat = category.trim().toLowerCase();
|
|
2707
|
+
categories = categories.filter((c) => c.key.toLowerCase() === cat);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// Filter by use case keyword
|
|
2711
|
+
if (typeof useCase === 'string' && useCase.trim()) {
|
|
2712
|
+
const kw = useCase.trim().toLowerCase();
|
|
2713
|
+
categories = categories.map((c) => ({
|
|
2714
|
+
...c,
|
|
2715
|
+
components: c.components.filter((comp) =>
|
|
2716
|
+
comp.useCase.toLowerCase().includes(kw) ||
|
|
2717
|
+
comp.id.toLowerCase().includes(kw) ||
|
|
2718
|
+
comp.tagName.toLowerCase().includes(kw)
|
|
2719
|
+
),
|
|
2720
|
+
})).filter((c) => c.components.length > 0);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
return buildJsonToolResponse({
|
|
2724
|
+
totalCategories: categories.length,
|
|
2725
|
+
categories: categories.map((c) => ({
|
|
2726
|
+
key: c.key,
|
|
2727
|
+
label: c.label,
|
|
2728
|
+
description: c.description,
|
|
2729
|
+
components: c.components,
|
|
2730
|
+
})),
|
|
2731
|
+
});
|
|
2732
|
+
},
|
|
2733
|
+
);
|
|
2734
|
+
|
|
2250
2735
|
// -----------------------------------------------------------------------
|
|
2251
2736
|
// Tool: list_patterns
|
|
2252
2737
|
// -----------------------------------------------------------------------
|
|
@@ -2271,6 +2756,70 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2271
2756
|
},
|
|
2272
2757
|
);
|
|
2273
2758
|
|
|
2759
|
+
// -----------------------------------------------------------------------
|
|
2760
|
+
// Helper: buildFullPageHtmlFromImportMap
|
|
2761
|
+
// -----------------------------------------------------------------------
|
|
2762
|
+
function escapeHtmlTitle(s) {
|
|
2763
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* Build import map entries from a component closure.
|
|
2768
|
+
* @param {string[]} closure - Component IDs
|
|
2769
|
+
* @param {Object} components - Component metadata (from install registry)
|
|
2770
|
+
* @param {string} prefix - Tag name prefix
|
|
2771
|
+
* @param {string} dir - Directory placeholder or concrete path
|
|
2772
|
+
* @returns {Object} Import map entries { prefixedTag: path }
|
|
2773
|
+
*/
|
|
2774
|
+
function buildImportMapEntries(closure, components, prefix, dir) {
|
|
2775
|
+
return Object.fromEntries(
|
|
2776
|
+
closure.flatMap((cid) => {
|
|
2777
|
+
const meta = components[cid];
|
|
2778
|
+
const tags = Array.isArray(meta?.tags) ? meta.tags : [cid];
|
|
2779
|
+
return tags.map((t) => {
|
|
2780
|
+
const lower = String(t).toLowerCase();
|
|
2781
|
+
const suffix = lower.replace(PREFIX_STRIP_RE, '');
|
|
2782
|
+
return [withPrefix(lower, prefix), `./${dir}/components/${suffix}.js`];
|
|
2783
|
+
});
|
|
2784
|
+
}),
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
/**
|
|
2789
|
+
* Build a complete HTML5 page from pattern data, resolving scaffoldHint
|
|
2790
|
+
* placeholders into concrete paths.
|
|
2791
|
+
* @param {Object} opts
|
|
2792
|
+
* @param {string} opts.html - Pattern HTML body
|
|
2793
|
+
* @param {string} opts.title - Page title
|
|
2794
|
+
* @param {Object} opts.importMapEntries - Import map entries { tag: path }
|
|
2795
|
+
* @param {string} [opts.dir] - Directory for JS assets
|
|
2796
|
+
* @param {string} [opts.lang='ja'] - HTML lang attribute
|
|
2797
|
+
* @returns {string} Complete HTML5 document
|
|
2798
|
+
*/
|
|
2799
|
+
function buildFullPageHtmlFromImportMap({ html, title, importMapEntries, dir = VENDOR_DIR, lang = 'ja' }) {
|
|
2800
|
+
const importMapJson = JSON.stringify({ imports: importMapEntries }, null, 2);
|
|
2801
|
+
return [
|
|
2802
|
+
'<!DOCTYPE html>',
|
|
2803
|
+
`<html lang="${lang}">`,
|
|
2804
|
+
'<head>',
|
|
2805
|
+
' <meta charset="UTF-8">',
|
|
2806
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
2807
|
+
` <title>${escapeHtmlTitle(title)}</title>`,
|
|
2808
|
+
` <!-- distribution: selfHosted=true, strategy=vendor-importmap -->`,
|
|
2809
|
+
` <!-- Do NOT replace these local paths with CDN URLs. This design system is self-hosted. -->`,
|
|
2810
|
+
` <script type="importmap">`,
|
|
2811
|
+
`${importMapJson}`,
|
|
2812
|
+
` </script>`,
|
|
2813
|
+
` <script type="module" src="./${dir}/boot.js"></script>`,
|
|
2814
|
+
'</head>',
|
|
2815
|
+
'<body>',
|
|
2816
|
+
` <noscript>このページの機能にはJavaScriptが必要です。</noscript>`,
|
|
2817
|
+
` ${html}`,
|
|
2818
|
+
'</body>',
|
|
2819
|
+
'</html>',
|
|
2820
|
+
].join('\n');
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2274
2823
|
// -----------------------------------------------------------------------
|
|
2275
2824
|
// Tool: get_pattern_recipe
|
|
2276
2825
|
// -----------------------------------------------------------------------
|
|
@@ -2278,13 +2827,14 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2278
2827
|
'get_pattern_recipe',
|
|
2279
2828
|
{
|
|
2280
2829
|
description:
|
|
2281
|
-
'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.',
|
|
2830
|
+
'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.',
|
|
2282
2831
|
inputSchema: {
|
|
2283
2832
|
patternId: z.string(),
|
|
2284
2833
|
prefix: z.string().optional(),
|
|
2834
|
+
include: z.array(z.enum(['fullPage'])).optional(),
|
|
2285
2835
|
},
|
|
2286
2836
|
},
|
|
2287
|
-
async ({ patternId, prefix }) => {
|
|
2837
|
+
async ({ patternId, prefix, include }) => {
|
|
2288
2838
|
const id = String(patternId ?? '').trim();
|
|
2289
2839
|
const p = normalizePrefix(prefix);
|
|
2290
2840
|
const pat = patterns[id];
|
|
@@ -2315,28 +2865,60 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2315
2865
|
const canonicalHtml = String(pat.html ?? '');
|
|
2316
2866
|
const html = applyPrefixToHtml(canonicalHtml, p);
|
|
2317
2867
|
|
|
2868
|
+
const entryHints = Array.isArray(pat.entryHints) ? [...pat.entryHints] : ['boot'];
|
|
2869
|
+
|
|
2870
|
+
const importMapEntries = buildImportMapEntries(closure, components, p, '<dir>');
|
|
2871
|
+
|
|
2872
|
+
const scaffoldHint = {
|
|
2873
|
+
doctype: '<!DOCTYPE html>',
|
|
2874
|
+
importMap: `<script type="importmap">\n${JSON.stringify({ imports: importMapEntries }, null, 2)}\n</script>`,
|
|
2875
|
+
bootScript: '<script type="module" src="./<dir>/boot.js"></script>',
|
|
2876
|
+
noscript: '<noscript>このページの機能にはJavaScriptが必要です。</noscript>',
|
|
2877
|
+
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.',
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
// Build fullPageHtml when requested via include: ['fullPage']
|
|
2881
|
+
const includeArr = Array.isArray(include) ? include : [];
|
|
2882
|
+
let fullPageHtml;
|
|
2883
|
+
if (includeArr.includes('fullPage')) {
|
|
2884
|
+
const resolvedImportMap = buildImportMapEntries(closure, components, p, VENDOR_DIR);
|
|
2885
|
+
fullPageHtml = buildFullPageHtmlFromImportMap({
|
|
2886
|
+
html,
|
|
2887
|
+
title: pat.title ?? pat.id,
|
|
2888
|
+
importMapEntries: resolvedImportMap,
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
const result = {
|
|
2893
|
+
pattern: {
|
|
2894
|
+
id: pat.id,
|
|
2895
|
+
title: pat.title,
|
|
2896
|
+
description: pat.description,
|
|
2897
|
+
},
|
|
2898
|
+
prefix: p,
|
|
2899
|
+
requires,
|
|
2900
|
+
components: closure,
|
|
2901
|
+
install,
|
|
2902
|
+
html,
|
|
2903
|
+
canonicalHtml,
|
|
2904
|
+
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
2905
|
+
entryHints,
|
|
2906
|
+
scaffoldHint,
|
|
2907
|
+
behavior: typeof pat.behavior === 'string' ? pat.behavior : undefined,
|
|
2908
|
+
};
|
|
2909
|
+
|
|
2910
|
+
if (fullPageHtml !== undefined) {
|
|
2911
|
+
result.fullPageHtml = fullPageHtml;
|
|
2912
|
+
result.vendorSetup = {
|
|
2913
|
+
command: `npx web-components-factory init --prefix ${p} --dir ${VENDOR_DIR} && npx web-components-factory add ${closure.join(' ')} --prefix ${p} --out ${VENDOR_DIR}`,
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2318
2917
|
return {
|
|
2319
2918
|
content: [
|
|
2320
2919
|
{
|
|
2321
2920
|
type: 'text',
|
|
2322
|
-
text: JSON.stringify(
|
|
2323
|
-
{
|
|
2324
|
-
pattern: {
|
|
2325
|
-
id: pat.id,
|
|
2326
|
-
title: pat.title,
|
|
2327
|
-
description: pat.description,
|
|
2328
|
-
},
|
|
2329
|
-
prefix: p,
|
|
2330
|
-
requires,
|
|
2331
|
-
components: closure,
|
|
2332
|
-
install,
|
|
2333
|
-
html,
|
|
2334
|
-
canonicalHtml,
|
|
2335
|
-
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
2336
|
-
},
|
|
2337
|
-
null,
|
|
2338
|
-
2,
|
|
2339
|
-
),
|
|
2921
|
+
text: JSON.stringify(result, null, 2),
|
|
2340
2922
|
},
|
|
2341
2923
|
],
|
|
2342
2924
|
};
|
|
@@ -2433,6 +3015,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2433
3015
|
isError: true,
|
|
2434
3016
|
};
|
|
2435
3017
|
}
|
|
3018
|
+
// Enrich referencedBy with component tagNames from CEM cssProperties
|
|
3019
|
+
const normalizedName = normalizeTokenIdentifier(name);
|
|
3020
|
+
const componentRefs = componentTokenRefMap.get(normalizedName);
|
|
3021
|
+
if (componentRefs && componentRefs.size > 0) {
|
|
3022
|
+
payload.componentReferencedBy = [...componentRefs].sort();
|
|
3023
|
+
}
|
|
2436
3024
|
return buildJsonToolResponse(payload);
|
|
2437
3025
|
},
|
|
2438
3026
|
);
|
|
@@ -2562,25 +3150,35 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2562
3150
|
// Snippet match: weight 1
|
|
2563
3151
|
if (snippet.includes(q)) score += 1;
|
|
2564
3152
|
|
|
2565
|
-
// Body text match: weight 1
|
|
2566
|
-
if (body && body.includes(q))
|
|
3153
|
+
// Body text match: weight 1, plus boost for multiple occurrences
|
|
3154
|
+
if (body && body.includes(q)) {
|
|
3155
|
+
score += 1;
|
|
3156
|
+
// Count additional occurrences in body for boost (cap at +2)
|
|
3157
|
+
let idx = body.indexOf(q);
|
|
3158
|
+
let occurrences = 0;
|
|
3159
|
+
while (idx !== -1 && occurrences < 3) {
|
|
3160
|
+
occurrences++;
|
|
3161
|
+
idx = body.indexOf(q, idx + q.length);
|
|
3162
|
+
}
|
|
3163
|
+
if (occurrences > 1) score += Math.min(occurrences - 1, 2);
|
|
3164
|
+
}
|
|
2567
3165
|
|
|
2568
|
-
// Synonym expansion match:
|
|
2569
|
-
if (
|
|
2570
|
-
|
|
3166
|
+
// Synonym expansion match: check all expanded terms, cap total synonym contribution at +2
|
|
3167
|
+
if (expandedTerms.length > 1) {
|
|
3168
|
+
let synScore = 0;
|
|
3169
|
+
const lowerKeywords = keywords.map((kw) => String(kw).toLowerCase());
|
|
3170
|
+
for (let i = 1; i < expandedTerms.length && synScore < 2; i++) {
|
|
2571
3171
|
const syn = expandedTerms[i];
|
|
2572
|
-
if (heading.includes(syn)
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if (String(kw).toLowerCase().includes(syn)) {
|
|
2578
|
-
score += 1;
|
|
3172
|
+
if (heading.includes(syn)) { synScore += 1; continue; }
|
|
3173
|
+
if (snippet.includes(syn) || body.includes(syn)) { synScore += 1; continue; }
|
|
3174
|
+
for (const kw of lowerKeywords) {
|
|
3175
|
+
if (kw.includes(syn)) {
|
|
3176
|
+
synScore += 1;
|
|
2579
3177
|
break;
|
|
2580
3178
|
}
|
|
2581
3179
|
}
|
|
2582
|
-
if (score > 0) break;
|
|
2583
3180
|
}
|
|
3181
|
+
score += synScore;
|
|
2584
3182
|
}
|
|
2585
3183
|
|
|
2586
3184
|
if (score > 0) {
|