@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 CHANGED
@@ -78,7 +78,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
78
78
  }
79
79
  ```
80
80
 
81
- ## 提供機能(16 tools + 1 prompt + 4 resources)
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.0.0';
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
- const defaultVal = typeof a.default === 'string' ? a.default.replace(/^['"]|['"]$/g, '') : '';
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
- export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix } = {}) {
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.6.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.6.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.6.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": [