@monoharada/wcf-mcp 0.9.1 → 0.10.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 + 5 resources)
81
+ ## 提供機能(19 tools + 1 prompt + 5 resources)
82
82
 
83
83
  ### ガードレール
84
84
 
@@ -101,6 +101,11 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
101
101
  | ツール | 説明 |
102
102
  |--------|------|
103
103
  | `validate_markup` | HTML スニペットを検証し、セマンティック検証(下表)で `suggestion` 付き診断を返す |
104
+ | `validate_files` | 複数のマークアップファイルをまとめて検証し、ファイル別診断と集計を返す |
105
+ | `validate_project` | ディレクトリを走査し、include/exclude glob に一致する複数ファイルをまとめて検証する |
106
+
107
+ `validate_*` 系は common template syntax(`{{ }}`, `{% %}`, `<% %>`, `<? ?>`, script/style blocks)をそのまま HTML と誤認しないようにマスクして検証します。
108
+ `validate_project` の既定 include は `*.html`, `*.htm`, `*.njk`, `*.liquid`, `*.astro`, `*.twig`, `*.hbs` です。
104
109
 
105
110
  #### validate_markup 検出コード一覧
106
111
 
@@ -119,6 +124,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
119
124
  | `roleAlertNotRecommended` | warning | `role="alert"` の使用(DADS 非推奨) | `role="alert"` |
120
125
  | `emptyLabel` | error | 空の `label` 属性(アクセシビリティ違反) | `<dads-input-text label="">` |
121
126
  | `emptyAriaLabel` | error | 空の `aria-label` 属性(アクセシビリティ違反) | `<dads-button aria-label="">` |
127
+ | `duplicateId` | error | 同一ドキュメント内で `id` が重複している | `<div id="hero">...</div><section id="hero">...</section>` |
122
128
  | `forbiddenAttribute` | warning | 禁止属性 | `placeholder` |
123
129
 
124
130
  ### UI パターン
@@ -140,10 +146,11 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
140
146
 
141
147
  | ツール | 説明 |
142
148
  |--------|------|
143
- | `get_design_tokens` | デザイントークンを type/category/query/theme で検索(`theme=light` のみ。`dark/all` はエラー) |
149
+ | `get_design_tokens` | デザイントークンを type/category/query/theme で検索(現状データは `light` のみ。`all` は利用可能テーマ全体、`dark` は非サポート) |
144
150
  | `get_design_token_detail` | 単一トークンの詳細(references/referencedBy/relatedTokens/usageExamples)を取得 |
145
151
  | `get_accessibility_docs` | component/topic/wcagLevel で A11y チェックリストとガイドライン要点を検索(`topic=all` では両ソースを混在返却) |
146
152
  | `search_guidelines` | ガイドライン(topic/query)をスコア付きで検索 |
153
+ | `search_design_system_knowledge` | components/patterns/guidelines/tokens/skills を横断して検索し、exact/prefix/intent-aware ranking と source ごとの follow-up 導線を返す |
147
154
 
148
155
  #### `get_design_tokens` の使用例
149
156
 
@@ -226,7 +233,8 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
226
233
  }
227
234
  ```
228
235
 
229
- > **注意**: `theme` `"light"` のみサポート。`"dark"` `"all"` を指定すると `INVALID_THEME` エラーが返ります。
236
+ > **注意**: 現在の実データは `light` のみです。`theme="all"` は利用可能テーマ全体として `light` を返します。`theme="dark"` `INVALID_THEME` エラーです。
237
+ > `var(--token, #fff)` のような literal fallback は relationship graph には含めません。`var(--token-a, var(--token-b))` のような token fallback のみ参照関係として扱います。
230
238
 
231
239
  ### Prompt
232
240
 
@@ -336,13 +344,20 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
336
344
  詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
337
345
 
338
346
  - `plugins[].name` / `plugins[].version` は必須
347
+ - `plugins[].validators` を使うと `validate_markup` / `validate_files` / `validate_project` に独自診断を差し込めます
348
+ - `plugins[].prompts` / `plugins[].resources` を使うと MCP prompt / resource を追加できます
349
+ - `plugins[].resourceTemplates` を使うと MCP resource template も追加できます
339
350
  - tool 名は組み込みツール名と重複不可(例: `list_components` など)
351
+ - prompt の `argsSchema` は plain shape に加えて zod object shape も使えます
340
352
  - `dataSources` で差し替え可能な key は次のみ
341
353
  - `custom-elements.json`
342
354
  - `install-registry.json`
343
355
  - `pattern-registry.json`
356
+ - `component-selector-guide.json`
344
357
  - `design-tokens.json`
345
358
  - `guidelines-index.json`
359
+ - `skills-registry.json`
360
+ - `llms-full.txt`
346
361
 
347
362
  ## structuredContent rollback
348
363
 
@@ -636,8 +651,13 @@ CEM やレジストリを更新した後:
636
651
  ```bash
637
652
  npm run mcp:build # データファイルをパッケージにコピー
638
653
  npm run mcp:check # データが最新かチェック(CI用)
654
+ npm run mcp:summary # MCP inventory + quality summary(JSON)を生成
655
+ npm run mcp:summary:check # summary JSON の drift をチェック
639
656
  ```
640
657
 
658
+ - machine-readable summary: `packages/mcp-server/mcp-spec-test/summary/v3-final.json`
659
+ - `npm run mcp:check:response-size` は human-readable stdout、`node scripts/mcp/check-response-size.mjs --json` は JSON 出力に使えます
660
+
641
661
  ### パッケージ構成
642
662
 
643
663
  ```
@@ -13,7 +13,7 @@ export const CANONICAL_PREFIX = 'dads';
13
13
  export const MAX_PREFIX_LENGTH = 64;
14
14
  export const STRUCTURED_CONTENT_DISABLE_FLAG = 'WCF_MCP_DISABLE_STRUCTURED_CONTENT';
15
15
  export const MAX_TOOL_RESULT_BYTES = 100 * 1024;
16
- export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1.1).';
16
+ export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1.4).';
17
17
  export const PACKAGE_VERSION = String(packageMeta?.version ?? '0.0.0');
18
18
 
19
19
  export const CATEGORY_MAP = {
package/core/plugins.mjs CHANGED
@@ -5,17 +5,20 @@
5
5
  import { z } from 'zod';
6
6
  import { PLUGIN_TOOL_NOTICE } from './constants.mjs';
7
7
 
8
- export const PLUGIN_CONTRACT_VERSION = '1.1.0';
8
+ export const PLUGIN_CONTRACT_VERSION = '1.4.0';
9
9
 
10
10
  // Single-module constants (DD-14)
11
11
  const PLUGIN_DATA_SOURCE_KEYS = Object.freeze(new Set([
12
12
  'custom-elements.json',
13
13
  'install-registry.json',
14
14
  'pattern-registry.json',
15
+ 'component-selector-guide.json',
15
16
  'design-tokens.json',
16
17
  'guidelines-index.json',
18
+ 'skills-registry.json',
19
+ 'llms-full.txt',
17
20
  ]));
18
- const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
21
+ export const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
19
22
  'get_design_system_overview',
20
23
  'list_components',
21
24
  'search_icons',
@@ -23,6 +26,8 @@ const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
23
26
  'generate_usage_snippet',
24
27
  'get_install_recipe',
25
28
  'validate_markup',
29
+ 'validate_files',
30
+ 'validate_project',
26
31
  'list_patterns',
27
32
  'get_pattern_recipe',
28
33
  'generate_pattern_snippet',
@@ -30,9 +35,23 @@ const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
30
35
  'get_design_token_detail',
31
36
  'get_accessibility_docs',
32
37
  'search_guidelines',
38
+ 'search_design_system_knowledge',
33
39
  'generate_full_page_html',
34
40
  'get_component_selector_guide',
35
41
  ]));
42
+ const BUILTIN_PROMPT_NAMES = Object.freeze(new Set([
43
+ 'figma_to_wcf',
44
+ ]));
45
+ const BUILTIN_RESOURCE_URIS = Object.freeze(new Set([
46
+ 'wcf://components',
47
+ 'wcf://tokens',
48
+ 'wcf://guidelines/{topic}',
49
+ 'wcf://llms-full',
50
+ 'wcf://skills',
51
+ ]));
52
+ const BUILTIN_RESOURCE_TEMPLATE_URIS = Object.freeze(new Set([
53
+ 'wcf://guidelines/{topic}',
54
+ ]));
36
55
 
37
56
  function isPlainObject(value) {
38
57
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -109,11 +128,151 @@ function normalizePluginTools(pluginName, tools) {
109
128
  return out;
110
129
  }
111
130
 
131
+ function normalizePluginValidators(pluginName, validators) {
132
+ if (!Array.isArray(validators)) return [];
133
+ const out = [];
134
+ const seen = new Set();
135
+ for (const rawValidator of validators) {
136
+ if (!isPlainObject(rawValidator)) {
137
+ throw new Error(toPluginErrorMessage(pluginName, 'validators entries must be objects'));
138
+ }
139
+ const name = String(rawValidator.name ?? '').trim();
140
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'validator.name is required'));
141
+ if (seen.has(name)) {
142
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate validator name: ${name}`));
143
+ }
144
+ seen.add(name);
145
+ if (typeof rawValidator.handler !== 'function') {
146
+ throw new Error(toPluginErrorMessage(pluginName, `validator "${name}" needs handler`));
147
+ }
148
+ out.push({
149
+ name,
150
+ description: String(rawValidator.description ?? '').trim() || `Validator hook provided by ${pluginName}.`,
151
+ handler: rawValidator.handler,
152
+ });
153
+ }
154
+ return out;
155
+ }
156
+
157
+ function normalizePluginPrompts(pluginName, prompts) {
158
+ if (!Array.isArray(prompts)) return [];
159
+ const out = [];
160
+ const seen = new Set();
161
+ for (const rawPrompt of prompts) {
162
+ if (!isPlainObject(rawPrompt)) {
163
+ throw new Error(toPluginErrorMessage(pluginName, 'prompts entries must be objects'));
164
+ }
165
+ const name = String(rawPrompt.name ?? '').trim();
166
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'prompt.name is required'));
167
+ if (seen.has(name)) {
168
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate prompt name: ${name}`));
169
+ }
170
+ seen.add(name);
171
+ const hasHandler = typeof rawPrompt.handler === 'function';
172
+ const hasStaticText = typeof rawPrompt.text === 'string';
173
+ if (!hasHandler && !hasStaticText) {
174
+ throw new Error(toPluginErrorMessage(pluginName, `prompt "${name}" needs handler or text`));
175
+ }
176
+ out.push({
177
+ name,
178
+ title: String(rawPrompt.title ?? name).trim(),
179
+ description: String(rawPrompt.description ?? '').trim() || `Prompt provided by ${pluginName}.`,
180
+ argsSchema: isPlainObject(rawPrompt.argsSchema) ? rawPrompt.argsSchema : {},
181
+ handler: hasHandler ? rawPrompt.handler : undefined,
182
+ text: hasStaticText ? rawPrompt.text : undefined,
183
+ });
184
+ }
185
+ return out;
186
+ }
187
+
188
+ function normalizePluginResources(pluginName, resources) {
189
+ if (!Array.isArray(resources)) return [];
190
+ const out = [];
191
+ const seenNames = new Set();
192
+ const seenUris = new Set();
193
+ for (const rawResource of resources) {
194
+ if (!isPlainObject(rawResource)) {
195
+ throw new Error(toPluginErrorMessage(pluginName, 'resources entries must be objects'));
196
+ }
197
+ const name = String(rawResource.name ?? '').trim();
198
+ const uri = String(rawResource.uri ?? '').trim();
199
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'resource.name is required'));
200
+ if (!uri) throw new Error(toPluginErrorMessage(pluginName, `resource "${name}" needs uri`));
201
+ if (seenNames.has(name)) {
202
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate resource name: ${name}`));
203
+ }
204
+ if (seenUris.has(uri)) {
205
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate resource uri: ${uri}`));
206
+ }
207
+ seenNames.add(name);
208
+ seenUris.add(uri);
209
+ const hasHandler = typeof rawResource.handler === 'function';
210
+ const hasText = typeof rawResource.text === 'string';
211
+ const hasPayload = Object.prototype.hasOwnProperty.call(rawResource, 'payload');
212
+ if (!hasHandler && !hasText && !hasPayload) {
213
+ throw new Error(toPluginErrorMessage(pluginName, `resource "${name}" needs handler, text, or payload`));
214
+ }
215
+ out.push({
216
+ name,
217
+ uri,
218
+ title: String(rawResource.title ?? name).trim(),
219
+ description: String(rawResource.description ?? '').trim() || `Resource provided by ${pluginName}.`,
220
+ mimeType: String(rawResource.mimeType ?? '').trim() || undefined,
221
+ handler: hasHandler ? rawResource.handler : undefined,
222
+ text: hasText ? rawResource.text : undefined,
223
+ payload: hasPayload ? rawResource.payload : undefined,
224
+ });
225
+ }
226
+ return out;
227
+ }
228
+
229
+ function normalizePluginResourceTemplates(pluginName, resourceTemplates) {
230
+ if (!Array.isArray(resourceTemplates)) return [];
231
+ const out = [];
232
+ const seenNames = new Set();
233
+ const seenTemplates = new Set();
234
+ for (const rawTemplate of resourceTemplates) {
235
+ if (!isPlainObject(rawTemplate)) {
236
+ throw new Error(toPluginErrorMessage(pluginName, 'resourceTemplates entries must be objects'));
237
+ }
238
+ const name = String(rawTemplate.name ?? '').trim();
239
+ const uriTemplate = String(rawTemplate.uriTemplate ?? '').trim();
240
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'resourceTemplate.name is required'));
241
+ if (!uriTemplate) throw new Error(toPluginErrorMessage(pluginName, `resourceTemplate "${name}" needs uriTemplate`));
242
+ if (seenNames.has(name)) throw new Error(toPluginErrorMessage(pluginName, `duplicate resourceTemplate name: ${name}`));
243
+ if (seenTemplates.has(uriTemplate)) throw new Error(toPluginErrorMessage(pluginName, `duplicate resourceTemplate uriTemplate: ${uriTemplate}`));
244
+ seenNames.add(name);
245
+ seenTemplates.add(uriTemplate);
246
+ const hasHandler = typeof rawTemplate.handler === 'function';
247
+ const hasText = typeof rawTemplate.text === 'string';
248
+ const hasPayload = Object.prototype.hasOwnProperty.call(rawTemplate, 'payload');
249
+ if (!hasHandler && !hasText && !hasPayload) {
250
+ throw new Error(toPluginErrorMessage(pluginName, `resourceTemplate "${name}" needs handler, text, or payload`));
251
+ }
252
+ out.push({
253
+ name,
254
+ uriTemplate,
255
+ title: String(rawTemplate.title ?? name).trim(),
256
+ description: String(rawTemplate.description ?? '').trim() || `Resource template provided by ${pluginName}.`,
257
+ mimeType: String(rawTemplate.mimeType ?? '').trim() || undefined,
258
+ handler: hasHandler ? rawTemplate.handler : undefined,
259
+ text: hasText ? rawTemplate.text : undefined,
260
+ payload: hasPayload ? rawTemplate.payload : undefined,
261
+ list: Array.isArray(rawTemplate.list) ? rawTemplate.list : undefined,
262
+ complete: isPlainObject(rawTemplate.complete) ? rawTemplate.complete : undefined,
263
+ });
264
+ }
265
+ return out;
266
+ }
267
+
112
268
  export function normalizePlugins(plugins = []) {
113
269
  if (!Array.isArray(plugins)) throw new Error('Invalid plugin configuration: plugins must be an array');
114
270
  const normalized = [];
115
271
  const seenPluginNames = new Set();
116
272
  const seenToolNames = new Set(BUILTIN_TOOL_NAMES);
273
+ const seenPromptNames = new Set(BUILTIN_PROMPT_NAMES);
274
+ const seenResourceUris = new Set(BUILTIN_RESOURCE_URIS);
275
+ const seenResourceTemplateUris = new Set(BUILTIN_RESOURCE_TEMPLATE_URIS);
117
276
 
118
277
  for (const rawPlugin of plugins) {
119
278
  if (!isPlainObject(rawPlugin)) throw new Error('Invalid plugin configuration: each plugin must be an object');
@@ -124,15 +283,37 @@ export function normalizePlugins(plugins = []) {
124
283
  seenPluginNames.add(name);
125
284
 
126
285
  const tools = normalizePluginTools(name, rawPlugin.tools);
286
+ const validators = normalizePluginValidators(name, rawPlugin.validators);
287
+ const prompts = normalizePluginPrompts(name, rawPlugin.prompts);
288
+ const resources = normalizePluginResources(name, rawPlugin.resources);
289
+ const resourceTemplates = normalizePluginResourceTemplates(name, rawPlugin.resourceTemplates);
127
290
  for (const tool of tools) {
128
291
  if (seenToolNames.has(tool.name)) {
129
292
  throw new Error(toPluginErrorMessage(name, `tool name collision: ${tool.name}`));
130
293
  }
131
294
  seenToolNames.add(tool.name);
132
295
  }
296
+ for (const prompt of prompts) {
297
+ if (seenPromptNames.has(prompt.name)) {
298
+ throw new Error(toPluginErrorMessage(name, `prompt name collision: ${prompt.name}`));
299
+ }
300
+ seenPromptNames.add(prompt.name);
301
+ }
302
+ for (const resource of resources) {
303
+ if (seenResourceUris.has(resource.uri)) {
304
+ throw new Error(toPluginErrorMessage(name, `resource uri collision: ${resource.uri}`));
305
+ }
306
+ seenResourceUris.add(resource.uri);
307
+ }
308
+ for (const resourceTemplate of resourceTemplates) {
309
+ if (seenResourceTemplateUris.has(resourceTemplate.uriTemplate)) {
310
+ throw new Error(toPluginErrorMessage(name, `resourceTemplate uri collision: ${resourceTemplate.uriTemplate}`));
311
+ }
312
+ seenResourceTemplateUris.add(resourceTemplate.uriTemplate);
313
+ }
133
314
 
134
315
  const dataSources = normalizePluginDataSources(name, rawPlugin.dataSources);
135
- normalized.push({ name, version, tools, dataSources });
316
+ normalized.push({ name, version, tools, validators, prompts, resources, resourceTemplates, dataSources });
136
317
  }
137
318
 
138
319
  return normalized;