@monoharada/wcf-mcp 0.6.0 → 0.7.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/core.mjs +103 -6
- package/package.json +1 -1
- package/validator.mjs +54 -25
package/core.mjs
CHANGED
|
@@ -1132,6 +1132,20 @@ export function serializeApi(decl, modulePath, prefix) {
|
|
|
1132
1132
|
};
|
|
1133
1133
|
}
|
|
1134
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Generic fallback values for common attributes when CEM default is missing.
|
|
1137
|
+
* Attribute-name-based (not component-specific). `type` is excluded to avoid
|
|
1138
|
+
* conflicts between button (type="button") and input (type="text").
|
|
1139
|
+
* `variant` is also excluded — its valid values differ per component,
|
|
1140
|
+
* so the first enum value is used instead (see generateSnippet).
|
|
1141
|
+
*/
|
|
1142
|
+
const SNIPPET_FALLBACK_VALUES = {
|
|
1143
|
+
label: 'ラベル',
|
|
1144
|
+
name: 'field1',
|
|
1145
|
+
value: 'サンプル値',
|
|
1146
|
+
'support-text': '説明テキスト',
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1135
1149
|
export function generateSnippet(api, prefix) {
|
|
1136
1150
|
// Use custom snippet if injected by CEM plugin (e.g. data-* driven components)
|
|
1137
1151
|
const customSnippet = api.custom?.usageSnippet;
|
|
@@ -1174,7 +1188,28 @@ export function generateSnippet(api, prefix) {
|
|
|
1174
1188
|
if (isBoolean) {
|
|
1175
1189
|
lines.push(` ${name}`);
|
|
1176
1190
|
} else {
|
|
1177
|
-
|
|
1191
|
+
let defaultVal;
|
|
1192
|
+
if (typeof a.default === 'string') {
|
|
1193
|
+
defaultVal = a.default.replace(/^['"]|['"]$/g, '');
|
|
1194
|
+
} else if (SNIPPET_FALLBACK_VALUES[name] !== undefined) {
|
|
1195
|
+
defaultVal = SNIPPET_FALLBACK_VALUES[name];
|
|
1196
|
+
} else {
|
|
1197
|
+
// For enum types, use the first enum value as fallback
|
|
1198
|
+
const enumMatch = t.match(/^'([^']+)'/);
|
|
1199
|
+
if (enumMatch) {
|
|
1200
|
+
defaultVal = enumMatch[1];
|
|
1201
|
+
} else {
|
|
1202
|
+
// Fallback: extract first value from description pattern like "(solid | outlined | text)"
|
|
1203
|
+
const desc = String(a.description ?? '');
|
|
1204
|
+
const descEnum = desc.match(/\(([^)]+)\)/);
|
|
1205
|
+
if (descEnum) {
|
|
1206
|
+
const first = descEnum[1].split(/\s*[||]\s*/)[0]?.trim();
|
|
1207
|
+
defaultVal = first || '';
|
|
1208
|
+
} else {
|
|
1209
|
+
defaultVal = '';
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1178
1213
|
lines.push(` ${name}="${defaultVal}"`);
|
|
1179
1214
|
}
|
|
1180
1215
|
if (lines.length >= 4) break;
|
|
@@ -1264,7 +1299,36 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
|
1264
1299
|
return [...out];
|
|
1265
1300
|
}
|
|
1266
1301
|
|
|
1267
|
-
|
|
1302
|
+
/**
|
|
1303
|
+
* Build a frequency map: componentId → count of patterns that require it.
|
|
1304
|
+
* Counts from pattern-registry.json `requires` arrays only.
|
|
1305
|
+
*/
|
|
1306
|
+
export function buildPatternFrequencyMap(patterns) {
|
|
1307
|
+
const freq = new Map();
|
|
1308
|
+
if (!patterns || typeof patterns !== 'object') return freq;
|
|
1309
|
+
for (const pat of Object.values(patterns)) {
|
|
1310
|
+
const requires = Array.isArray(pat?.requires) ? pat.requires : [];
|
|
1311
|
+
for (const id of requires) {
|
|
1312
|
+
const key = String(id ?? '').trim();
|
|
1313
|
+
if (key) freq.set(key, (freq.get(key) ?? 0) + 1);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return freq;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Convert a tag from the current prefix to canonical prefix using string ops
|
|
1321
|
+
* (no regex, safe for arbitrary prefix values).
|
|
1322
|
+
*/
|
|
1323
|
+
function toCanonicalTag(tag, currentPrefix) {
|
|
1324
|
+
const cp = `${currentPrefix}-`;
|
|
1325
|
+
if (tag.startsWith(cp)) {
|
|
1326
|
+
return `${CANONICAL_PREFIX}-${tag.slice(cp.length)}`;
|
|
1327
|
+
}
|
|
1328
|
+
return tag;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency } = {}) {
|
|
1268
1332
|
const p = normalizePrefix(prefix);
|
|
1269
1333
|
const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
1270
1334
|
const limitExplicit = Number.isInteger(limit);
|
|
@@ -1279,6 +1343,24 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
|
|
|
1279
1343
|
modulePath,
|
|
1280
1344
|
}));
|
|
1281
1345
|
|
|
1346
|
+
// patternId filter: restrict to components required by a specific pattern
|
|
1347
|
+
if (typeof patternId === 'string' && patternId.trim()) {
|
|
1348
|
+
const pats = patterns && typeof patterns === 'object' ? patterns : {};
|
|
1349
|
+
const pat = pats[patternId.trim()];
|
|
1350
|
+
if (pat && Array.isArray(pat.requires)) {
|
|
1351
|
+
const requiredIds = new Set(pat.requires.map((r) => String(r ?? '').trim()).filter(Boolean));
|
|
1352
|
+
const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
|
|
1353
|
+
items = items.filter((item) => {
|
|
1354
|
+
// Map tagName to componentId via install registry
|
|
1355
|
+
const canonicalTag = toCanonicalTag(item.tagName, p);
|
|
1356
|
+
const componentId = tags[canonicalTag];
|
|
1357
|
+
return componentId && requiredIds.has(componentId);
|
|
1358
|
+
});
|
|
1359
|
+
} else {
|
|
1360
|
+
items = [];
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1282
1364
|
if (category) {
|
|
1283
1365
|
items = items.filter((item) => item.category === category);
|
|
1284
1366
|
}
|
|
@@ -1296,6 +1378,18 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
|
|
|
1296
1378
|
});
|
|
1297
1379
|
}
|
|
1298
1380
|
|
|
1381
|
+
// frequency sort: order by pattern usage count descending
|
|
1382
|
+
if (sort === 'frequency') {
|
|
1383
|
+
const freq = patternFrequency instanceof Map ? patternFrequency : new Map();
|
|
1384
|
+
const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
|
|
1385
|
+
items = items.map((item) => {
|
|
1386
|
+
const canonicalTag = toCanonicalTag(item.tagName, p);
|
|
1387
|
+
const componentId = tags[canonicalTag] ?? '';
|
|
1388
|
+
return { ...item, frequency: freq.get(componentId) ?? 0 };
|
|
1389
|
+
});
|
|
1390
|
+
items.sort((a, b) => b.frequency - a.frequency);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1299
1393
|
const total = items.length;
|
|
1300
1394
|
const paged = items.slice(pageOffset, pageOffset + pageSize);
|
|
1301
1395
|
|
|
@@ -1886,6 +1980,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1886
1980
|
const patternRegistry = await loadJson('pattern-registry.json');
|
|
1887
1981
|
const { patterns } = loadPatternRegistryShape(patternRegistry);
|
|
1888
1982
|
const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
|
|
1983
|
+
const patternFrequency = buildPatternFrequencyMap(patterns);
|
|
1889
1984
|
|
|
1890
1985
|
// Load optional data files (design tokens, guidelines index)
|
|
1891
1986
|
let designTokensData = null;
|
|
@@ -1916,7 +2011,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1916
2011
|
|
|
1917
2012
|
const server = new McpServer({
|
|
1918
2013
|
name: 'web-components-factory-design-system',
|
|
1919
|
-
version: '0.
|
|
2014
|
+
version: '0.7.0',
|
|
1920
2015
|
});
|
|
1921
2016
|
|
|
1922
2017
|
server.registerPrompt(
|
|
@@ -2066,7 +2161,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2066
2161
|
|
|
2067
2162
|
const overview = {
|
|
2068
2163
|
name: 'DADS Web Components (wcf)',
|
|
2069
|
-
version: '0.
|
|
2164
|
+
version: '0.7.0',
|
|
2070
2165
|
prefix: detectedPrefix,
|
|
2071
2166
|
totalComponents: indexes.decls.length,
|
|
2072
2167
|
componentsByCategory: categoryCount,
|
|
@@ -2221,10 +2316,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2221
2316
|
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
|
|
2222
2317
|
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
2223
2318
|
prefix: z.string().optional(),
|
|
2319
|
+
patternId: z.string().optional().describe('Filter to components required by this pattern'),
|
|
2320
|
+
sort: z.enum(['default', 'frequency']).optional().describe('Sort order: "default" (CEM declaration order) or "frequency" (pattern usage count, descending)'),
|
|
2224
2321
|
},
|
|
2225
2322
|
},
|
|
2226
|
-
async ({ category, query, limit, offset, prefix }) => {
|
|
2227
|
-
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix });
|
|
2323
|
+
async ({ category, query, limit, offset, prefix, patternId, sort }) => {
|
|
2324
|
+
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency });
|
|
2228
2325
|
const payload = {
|
|
2229
2326
|
items: page.items,
|
|
2230
2327
|
total: page.total,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoharada/wcf-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for the web-components-factory design system. Provides component discovery, validation, and pattern-based UI composition without cloning the repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/validator.mjs
CHANGED
|
@@ -676,9 +676,18 @@ const PARENT_CHILD_CONSTRAINTS = new Map([
|
|
|
676
676
|
['dads-menu-list-item', 'dads-menu-list'],
|
|
677
677
|
]);
|
|
678
678
|
|
|
679
|
+
/**
|
|
680
|
+
* HTML void elements that never have a closing tag.
|
|
681
|
+
*/
|
|
682
|
+
const HTML_VOID_ELEMENTS = new Set([
|
|
683
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
684
|
+
'link', 'meta', 'source', 'track', 'wbr',
|
|
685
|
+
]);
|
|
686
|
+
|
|
679
687
|
/**
|
|
680
688
|
* Detect orphaned child components (child appears without expected parent).
|
|
681
|
-
* Uses
|
|
689
|
+
* Uses a lightweight tag-stack approach: only prefix-matching tags (e.g. dads-*)
|
|
690
|
+
* are tracked on the stack; all other HTML elements are ignored.
|
|
682
691
|
* @param {{
|
|
683
692
|
* filePath?: string;
|
|
684
693
|
* text: string;
|
|
@@ -697,7 +706,7 @@ export function detectOrphanedChildComponents({
|
|
|
697
706
|
const p = prefix.toLowerCase();
|
|
698
707
|
const canonicalPrefix = 'dads';
|
|
699
708
|
|
|
700
|
-
// Build prefix-aware constraint map
|
|
709
|
+
// Build prefix-aware constraint map (child → parent)
|
|
701
710
|
const constraints = new Map();
|
|
702
711
|
for (const [child, parent] of PARENT_CHILD_CONSTRAINTS.entries()) {
|
|
703
712
|
const mappedChild = p !== canonicalPrefix ? child.replace(canonicalPrefix, p) : child;
|
|
@@ -705,36 +714,56 @@ export function detectOrphanedChildComponents({
|
|
|
705
714
|
constraints.set(mappedChild, mappedParent);
|
|
706
715
|
}
|
|
707
716
|
|
|
708
|
-
const
|
|
717
|
+
const prefixDash = `${p}-`;
|
|
718
|
+
// Stack of currently open prefix-matching tags
|
|
719
|
+
const stack = [];
|
|
709
720
|
|
|
710
|
-
|
|
721
|
+
// Regex matches opening tags, closing tags, and self-closing tags
|
|
722
|
+
const tagRe = /<\/?([a-z][a-z0-9-]*)\b[^<>]*?\/?>/gi;
|
|
711
723
|
let m;
|
|
712
724
|
|
|
713
725
|
while ((m = tagRe.exec(text))) {
|
|
726
|
+
const fullMatch = m[0];
|
|
714
727
|
const tag = String(m[1] ?? '').toLowerCase();
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
728
|
+
const isClosing = fullMatch.startsWith('</');
|
|
729
|
+
const isSelfClosing = fullMatch.endsWith('/>');
|
|
730
|
+
|
|
731
|
+
// Only track prefix-matching tags on the stack
|
|
732
|
+
if (!tag.startsWith(prefixDash)) continue;
|
|
733
|
+
|
|
734
|
+
if (isClosing) {
|
|
735
|
+
// Pop the matching opening tag from the stack (search from top)
|
|
736
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
737
|
+
if (stack[i] === tag) {
|
|
738
|
+
stack.splice(i, 1);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
722
744
|
|
|
723
|
-
|
|
724
|
-
const
|
|
745
|
+
// Check if this is a child that needs a parent
|
|
746
|
+
const expectedParent = constraints.get(tag);
|
|
747
|
+
if (expectedParent) {
|
|
748
|
+
const hasParent = stack.includes(expectedParent);
|
|
749
|
+
if (!hasParent) {
|
|
750
|
+
const tagOffset = m.index + 1;
|
|
751
|
+
const range = makeRange(lineStarts, tagOffset, tagOffset + tag.length);
|
|
752
|
+
diagnostics.push({
|
|
753
|
+
file: filePath,
|
|
754
|
+
range,
|
|
755
|
+
severity,
|
|
756
|
+
code: 'orphanedChildComponent',
|
|
757
|
+
message: `<${tag}> should be a child of <${expectedParent}>.`,
|
|
758
|
+
tagName: tag,
|
|
759
|
+
hint: `Wrap <${tag}> inside <${expectedParent}>...</${expectedParent}>.`,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
725
763
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
diagnostics.push({
|
|
730
|
-
file: filePath,
|
|
731
|
-
range,
|
|
732
|
-
severity,
|
|
733
|
-
code: 'orphanedChildComponent',
|
|
734
|
-
message: `<${tag}> should be a child of <${expectedParent}>.`,
|
|
735
|
-
tagName: tag,
|
|
736
|
-
hint: `Wrap <${tag}> inside <${expectedParent}>...</${expectedParent}>.`,
|
|
737
|
-
});
|
|
764
|
+
// Push opening tag to stack (skip void elements and self-closing)
|
|
765
|
+
if (!isSelfClosing && !HTML_VOID_ELEMENTS.has(tag)) {
|
|
766
|
+
stack.push(tag);
|
|
738
767
|
}
|
|
739
768
|
}
|
|
740
769
|
|