@monoharada/wcf-mcp 0.6.0 → 0.8.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 +30 -2
- package/core.mjs +125 -10
- package/package.json +2 -1
- package/plugins/design-system-skills/check-drift.mjs +685 -0
- package/plugins/design-system-skills/get-skill-manifest.mjs +193 -0
- package/plugins/design-system-skills/index.mjs +20 -0
- package/plugins/design-system-skills/list-skills.mjs +78 -0
- package/plugins/design-system-skills/shared.mjs +75 -0
- package/validator.mjs +54 -25
- package/wcf-mcp.config.example.json +3 -0
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
|
|
|
78
78
|
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
## 提供機能(
|
|
81
|
+
## 提供機能(19 tools + 1 prompt + 4 resources)
|
|
82
82
|
|
|
83
83
|
### ガードレール
|
|
84
84
|
|
|
@@ -145,6 +145,17 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
|
|
|
145
145
|
| `get_accessibility_docs` | component/topic/wcagLevel で A11y チェックリストとガイドライン要点を検索(`topic=all` では両ソースを混在返却) |
|
|
146
146
|
| `search_guidelines` | ガイドライン(topic/query)をスコア付きで検索 |
|
|
147
147
|
|
|
148
|
+
### スキル管理(リポジトリ内専用)
|
|
149
|
+
|
|
150
|
+
| ツール | 説明 |
|
|
151
|
+
|--------|------|
|
|
152
|
+
| `list_skills` | 登録済みデザインシステムスキル一覧を取得 |
|
|
153
|
+
| `get_skill_manifest` | スキルの SKILL.md マニフェストを取得 |
|
|
154
|
+
| `check_drift` | スキルレジストリとリポジトリの整合性を検証 |
|
|
155
|
+
|
|
156
|
+
> **注意**: これらのツールはリポジトリ内でのみ有効です。npx 実行時は `SKILLS_REGISTRY_UNAVAILABLE` エラーを返します。
|
|
157
|
+
> 有効化には `wcf-mcp.config.json` で `design-system-skills` プラグインを設定してください(下記参照)。
|
|
158
|
+
|
|
148
159
|
#### `get_design_tokens` の使用例
|
|
149
160
|
|
|
150
161
|
**リクエスト:**
|
|
@@ -328,7 +339,7 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
|
|
|
328
339
|
※ `./plugins/custom-validation-plugin.mjs` は利用側プロジェクトに配置してください。
|
|
329
340
|
このリポジトリには参照用として `packages/mcp-server/examples/plugins/custom-validation-plugin.mjs` を同梱しています。
|
|
330
341
|
|
|
331
|
-
### plugin 契約(v1)
|
|
342
|
+
### plugin 契約(v1.1)
|
|
332
343
|
|
|
333
344
|
詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
|
|
334
345
|
|
|
@@ -381,6 +392,21 @@ Claude Desktop 設定例:
|
|
|
381
392
|
prefix: "myui" → dads-button → myui-button
|
|
382
393
|
```
|
|
383
394
|
|
|
395
|
+
## v0.8.0 新機能
|
|
396
|
+
|
|
397
|
+
### 新ツール(スキル管理)
|
|
398
|
+
- **`list_skills`** — 登録済みデザインシステムスキルの一覧を取得
|
|
399
|
+
- **`get_skill_manifest`** — スキルの SKILL.md マニフェスト内容を取得
|
|
400
|
+
- **`check_drift`** — スキルレジストリとリポジトリの整合性を検証(ドリフト検出)
|
|
401
|
+
|
|
402
|
+
### 改善
|
|
403
|
+
- **Plugin 契約 v1.1** — module plugin の `dataSources` パス解決をモジュールディレクトリ基準に統一
|
|
404
|
+
- **Skills Registry plugin** — `plugins/design-system-skills/` として同梱。`wcf-mcp.config.json` で有効化
|
|
405
|
+
|
|
406
|
+
> スキル管理ツールはリポジトリ内でのみ動作します。npx 実行時は graceful に `SKILLS_REGISTRY_UNAVAILABLE` を返します。
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
384
410
|
## v0.4.0 新機能
|
|
385
411
|
|
|
386
412
|
### 新ツール
|
|
@@ -642,6 +668,8 @@ packages/mcp-server/
|
|
|
642
668
|
├── server.mjs # MCP サーバー本体
|
|
643
669
|
├── validator.mjs # HTML バリデーター
|
|
644
670
|
├── package.json # npm パッケージ定義
|
|
671
|
+
├── plugins/ # 同梱プラグイン
|
|
672
|
+
│ └── design-system-skills/ # スキル管理ツール (list_skills, get_skill_manifest, check_drift)
|
|
645
673
|
└── data/ # バンドルデータ (npm run mcp:build で生成)
|
|
646
674
|
├── custom-elements.json
|
|
647
675
|
├── install-registry.json
|
package/core.mjs
CHANGED
|
@@ -17,8 +17,25 @@ export const CANONICAL_PREFIX = 'dads';
|
|
|
17
17
|
export const MAX_PREFIX_LENGTH = 64;
|
|
18
18
|
export const STRUCTURED_CONTENT_DISABLE_FLAG = 'WCF_MCP_DISABLE_STRUCTURED_CONTENT';
|
|
19
19
|
export const MAX_TOOL_RESULT_BYTES = 100 * 1024;
|
|
20
|
-
export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1).';
|
|
21
|
-
export const PLUGIN_CONTRACT_VERSION = '1.
|
|
20
|
+
export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1.1).';
|
|
21
|
+
export const PLUGIN_CONTRACT_VERSION = '1.1.0';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a plugin tool's inputSchema to a passthrough Zod schema.
|
|
25
|
+
* Handles three cases:
|
|
26
|
+
* - Already a Zod schema instance (has _def or _zod) → apply .passthrough()
|
|
27
|
+
* - Plain object (raw shape map) → wrap with z.object().passthrough()
|
|
28
|
+
* - Falsy / empty → z.object({}).passthrough()
|
|
29
|
+
*/
|
|
30
|
+
function toPassthroughSchema(schema) {
|
|
31
|
+
if (schema && (schema._def || schema._zod)) {
|
|
32
|
+
// Already a Zod schema — apply passthrough if it's an object type
|
|
33
|
+
return typeof schema.passthrough === 'function'
|
|
34
|
+
? schema.passthrough()
|
|
35
|
+
: schema;
|
|
36
|
+
}
|
|
37
|
+
return z.object(schema ?? {}).passthrough();
|
|
38
|
+
}
|
|
22
39
|
|
|
23
40
|
export const CATEGORY_MAP = {
|
|
24
41
|
'dads-input-text': 'Form',
|
|
@@ -722,7 +739,7 @@ function toPluginErrorMessage(name, reason) {
|
|
|
722
739
|
* name: string,
|
|
723
740
|
* description?: string,
|
|
724
741
|
* inputSchema?: Record<string, unknown>,
|
|
725
|
-
* handler?: (args: Record<string, unknown>, context: { plugin: { name: string, version: string }, helpers: { loadJsonData: Function } }) => unknown,
|
|
742
|
+
* handler?: (args: Record<string, unknown>, context: { plugin: { name: string, version: string }, helpers: { loadJsonData: Function, loadTextData: Function } }) => unknown,
|
|
726
743
|
* staticPayload?: unknown,
|
|
727
744
|
* }} WcfMcpPluginTool
|
|
728
745
|
*/
|
|
@@ -1132,6 +1149,20 @@ export function serializeApi(decl, modulePath, prefix) {
|
|
|
1132
1149
|
};
|
|
1133
1150
|
}
|
|
1134
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* Generic fallback values for common attributes when CEM default is missing.
|
|
1154
|
+
* Attribute-name-based (not component-specific). `type` is excluded to avoid
|
|
1155
|
+
* conflicts between button (type="button") and input (type="text").
|
|
1156
|
+
* `variant` is also excluded — its valid values differ per component,
|
|
1157
|
+
* so the first enum value is used instead (see generateSnippet).
|
|
1158
|
+
*/
|
|
1159
|
+
const SNIPPET_FALLBACK_VALUES = {
|
|
1160
|
+
label: 'ラベル',
|
|
1161
|
+
name: 'field1',
|
|
1162
|
+
value: 'サンプル値',
|
|
1163
|
+
'support-text': '説明テキスト',
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1135
1166
|
export function generateSnippet(api, prefix) {
|
|
1136
1167
|
// Use custom snippet if injected by CEM plugin (e.g. data-* driven components)
|
|
1137
1168
|
const customSnippet = api.custom?.usageSnippet;
|
|
@@ -1174,7 +1205,28 @@ export function generateSnippet(api, prefix) {
|
|
|
1174
1205
|
if (isBoolean) {
|
|
1175
1206
|
lines.push(` ${name}`);
|
|
1176
1207
|
} else {
|
|
1177
|
-
|
|
1208
|
+
let defaultVal;
|
|
1209
|
+
if (typeof a.default === 'string') {
|
|
1210
|
+
defaultVal = a.default.replace(/^['"]|['"]$/g, '');
|
|
1211
|
+
} else if (SNIPPET_FALLBACK_VALUES[name] !== undefined) {
|
|
1212
|
+
defaultVal = SNIPPET_FALLBACK_VALUES[name];
|
|
1213
|
+
} else {
|
|
1214
|
+
// For enum types, use the first enum value as fallback
|
|
1215
|
+
const enumMatch = t.match(/^'([^']+)'/);
|
|
1216
|
+
if (enumMatch) {
|
|
1217
|
+
defaultVal = enumMatch[1];
|
|
1218
|
+
} else {
|
|
1219
|
+
// Fallback: extract first value from description pattern like "(solid | outlined | text)"
|
|
1220
|
+
const desc = String(a.description ?? '');
|
|
1221
|
+
const descEnum = desc.match(/\(([^)]+)\)/);
|
|
1222
|
+
if (descEnum) {
|
|
1223
|
+
const first = descEnum[1].split(/\s*[||]\s*/)[0]?.trim();
|
|
1224
|
+
defaultVal = first || '';
|
|
1225
|
+
} else {
|
|
1226
|
+
defaultVal = '';
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1178
1230
|
lines.push(` ${name}="${defaultVal}"`);
|
|
1179
1231
|
}
|
|
1180
1232
|
if (lines.length >= 4) break;
|
|
@@ -1264,7 +1316,36 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
|
|
|
1264
1316
|
return [...out];
|
|
1265
1317
|
}
|
|
1266
1318
|
|
|
1267
|
-
|
|
1319
|
+
/**
|
|
1320
|
+
* Build a frequency map: componentId → count of patterns that require it.
|
|
1321
|
+
* Counts from pattern-registry.json `requires` arrays only.
|
|
1322
|
+
*/
|
|
1323
|
+
export function buildPatternFrequencyMap(patterns) {
|
|
1324
|
+
const freq = new Map();
|
|
1325
|
+
if (!patterns || typeof patterns !== 'object') return freq;
|
|
1326
|
+
for (const pat of Object.values(patterns)) {
|
|
1327
|
+
const requires = Array.isArray(pat?.requires) ? pat.requires : [];
|
|
1328
|
+
for (const id of requires) {
|
|
1329
|
+
const key = String(id ?? '').trim();
|
|
1330
|
+
if (key) freq.set(key, (freq.get(key) ?? 0) + 1);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return freq;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Convert a tag from the current prefix to canonical prefix using string ops
|
|
1338
|
+
* (no regex, safe for arbitrary prefix values).
|
|
1339
|
+
*/
|
|
1340
|
+
function toCanonicalTag(tag, currentPrefix) {
|
|
1341
|
+
const cp = `${currentPrefix}-`;
|
|
1342
|
+
if (tag.startsWith(cp)) {
|
|
1343
|
+
return `${CANONICAL_PREFIX}-${tag.slice(cp.length)}`;
|
|
1344
|
+
}
|
|
1345
|
+
return tag;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency } = {}) {
|
|
1268
1349
|
const p = normalizePrefix(prefix);
|
|
1269
1350
|
const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
1270
1351
|
const limitExplicit = Number.isInteger(limit);
|
|
@@ -1279,6 +1360,24 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
|
|
|
1279
1360
|
modulePath,
|
|
1280
1361
|
}));
|
|
1281
1362
|
|
|
1363
|
+
// patternId filter: restrict to components required by a specific pattern
|
|
1364
|
+
if (typeof patternId === 'string' && patternId.trim()) {
|
|
1365
|
+
const pats = patterns && typeof patterns === 'object' ? patterns : {};
|
|
1366
|
+
const pat = pats[patternId.trim()];
|
|
1367
|
+
if (pat && Array.isArray(pat.requires)) {
|
|
1368
|
+
const requiredIds = new Set(pat.requires.map((r) => String(r ?? '').trim()).filter(Boolean));
|
|
1369
|
+
const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
|
|
1370
|
+
items = items.filter((item) => {
|
|
1371
|
+
// Map tagName to componentId via install registry
|
|
1372
|
+
const canonicalTag = toCanonicalTag(item.tagName, p);
|
|
1373
|
+
const componentId = tags[canonicalTag];
|
|
1374
|
+
return componentId && requiredIds.has(componentId);
|
|
1375
|
+
});
|
|
1376
|
+
} else {
|
|
1377
|
+
items = [];
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1282
1381
|
if (category) {
|
|
1283
1382
|
items = items.filter((item) => item.category === category);
|
|
1284
1383
|
}
|
|
@@ -1296,6 +1395,18 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
|
|
|
1296
1395
|
});
|
|
1297
1396
|
}
|
|
1298
1397
|
|
|
1398
|
+
// frequency sort: order by pattern usage count descending
|
|
1399
|
+
if (sort === 'frequency') {
|
|
1400
|
+
const freq = patternFrequency instanceof Map ? patternFrequency : new Map();
|
|
1401
|
+
const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
|
|
1402
|
+
items = items.map((item) => {
|
|
1403
|
+
const canonicalTag = toCanonicalTag(item.tagName, p);
|
|
1404
|
+
const componentId = tags[canonicalTag] ?? '';
|
|
1405
|
+
return { ...item, frequency: freq.get(componentId) ?? 0 };
|
|
1406
|
+
});
|
|
1407
|
+
items.sort((a, b) => b.frequency - a.frequency);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1299
1410
|
const total = items.length;
|
|
1300
1411
|
const paged = items.slice(pageOffset, pageOffset + pageSize);
|
|
1301
1412
|
|
|
@@ -1886,6 +1997,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1886
1997
|
const patternRegistry = await loadJson('pattern-registry.json');
|
|
1887
1998
|
const { patterns } = loadPatternRegistryShape(patternRegistry);
|
|
1888
1999
|
const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
|
|
2000
|
+
const patternFrequency = buildPatternFrequencyMap(patterns);
|
|
1889
2001
|
|
|
1890
2002
|
// Load optional data files (design tokens, guidelines index)
|
|
1891
2003
|
let designTokensData = null;
|
|
@@ -1916,7 +2028,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1916
2028
|
|
|
1917
2029
|
const server = new McpServer({
|
|
1918
2030
|
name: 'web-components-factory-design-system',
|
|
1919
|
-
version: '0.
|
|
2031
|
+
version: '0.7.0',
|
|
1920
2032
|
});
|
|
1921
2033
|
|
|
1922
2034
|
server.registerPrompt(
|
|
@@ -2066,7 +2178,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2066
2178
|
|
|
2067
2179
|
const overview = {
|
|
2068
2180
|
name: 'DADS Web Components (wcf)',
|
|
2069
|
-
version: '0.
|
|
2181
|
+
version: '0.7.0',
|
|
2070
2182
|
prefix: detectedPrefix,
|
|
2071
2183
|
totalComponents: indexes.decls.length,
|
|
2072
2184
|
componentsByCategory: categoryCount,
|
|
@@ -2221,10 +2333,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2221
2333
|
limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
|
|
2222
2334
|
offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
|
|
2223
2335
|
prefix: z.string().optional(),
|
|
2336
|
+
patternId: z.string().optional().describe('Filter to components required by this pattern'),
|
|
2337
|
+
sort: z.enum(['default', 'frequency']).optional().describe('Sort order: "default" (CEM declaration order) or "frequency" (pattern usage count, descending)'),
|
|
2224
2338
|
},
|
|
2225
2339
|
},
|
|
2226
|
-
async ({ category, query, limit, offset, prefix }) => {
|
|
2227
|
-
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix });
|
|
2340
|
+
async ({ category, query, limit, offset, prefix, patternId, sort }) => {
|
|
2341
|
+
const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency });
|
|
2228
2342
|
const payload = {
|
|
2229
2343
|
items: page.items,
|
|
2230
2344
|
total: page.total,
|
|
@@ -3229,7 +3343,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
3229
3343
|
tool.name,
|
|
3230
3344
|
{
|
|
3231
3345
|
description: tool.description,
|
|
3232
|
-
inputSchema: tool.inputSchema
|
|
3346
|
+
inputSchema: toPassthroughSchema(tool.inputSchema),
|
|
3233
3347
|
},
|
|
3234
3348
|
async (args) => {
|
|
3235
3349
|
try {
|
|
@@ -3238,6 +3352,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
3238
3352
|
plugin: { name: plugin.name, version: plugin.version },
|
|
3239
3353
|
helpers: {
|
|
3240
3354
|
loadJsonData: loadJson,
|
|
3355
|
+
loadTextData: loadText,
|
|
3241
3356
|
buildJsonToolResponse,
|
|
3242
3357
|
normalizePrefix,
|
|
3243
3358
|
withPrefix,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoharada/wcf-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"validator.mjs",
|
|
14
14
|
"data/",
|
|
15
15
|
"examples/",
|
|
16
|
+
"plugins/",
|
|
16
17
|
"wcf-mcp.config.example.json"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|