@monoharada/wcf-mcp 0.9.0 → 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
 
@@ -92,7 +92,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
92
92
  |--------|------|
93
93
  | `list_components` | カテゴリ/クエリ/limit/offset でコンポーネントを段階的に取得(デフォルト20件。全件取得は `limit: 200`) |
94
94
  | `search_icons` | アイコン名をキーワード検索し、usage example を返す |
95
- | `get_component_api` | tagName or className で属性・スロット・イベント・CSS Parts・CSS Custom Properties を取得。`components` 配列でバッチ取得可能(最大10件) |
95
+ | `get_component_api` | tagName or className で属性・スロット・イベント・CSS Parts・CSS Custom Properties を取得。`components` 配列でバッチ取得可能(最大10件。レスポンス予算次第で `structuredContent` は省略される) |
96
96
  | `generate_usage_snippet` | コンポーネントの最小限 HTML スニペットを生成 |
97
97
  | `get_install_recipe` | componentId・依存関係・define関数・インストールコマンドを取得 |
98
98
 
@@ -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
 
@@ -285,7 +293,9 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
285
293
  ```
286
294
 
287
295
  - bind: `127.0.0.1`
288
- - endpoint: `http://127.0.0.1:3100/mcp`
296
+ - endpoint: `http://127.0.0.1:3100/mcp` のみ
297
+ - Host / Origin validation を有効化して DNS rebinding を抑止します
298
+ - `--config` が不正な場合は待受開始前に起動失敗します
289
299
 
290
300
  ## 設定ファイル
291
301
 
@@ -334,19 +344,28 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
334
344
  詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
335
345
 
336
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 も追加できます
337
350
  - tool 名は組み込みツール名と重複不可(例: `list_components` など)
351
+ - prompt の `argsSchema` は plain shape に加えて zod object shape も使えます
338
352
  - `dataSources` で差し替え可能な key は次のみ
339
353
  - `custom-elements.json`
340
354
  - `install-registry.json`
341
355
  - `pattern-registry.json`
356
+ - `component-selector-guide.json`
342
357
  - `design-tokens.json`
343
358
  - `guidelines-index.json`
359
+ - `skills-registry.json`
360
+ - `llms-full.txt`
344
361
 
345
362
  ## structuredContent rollback
346
363
 
347
- `get_component_api` / `get_design_tokens` / `get_design_token_detail` / `get_accessibility_docs` / `search_guidelines` は通常 `structuredContent` を返します。
364
+ `get_component_api` / `get_design_tokens` / `get_design_token_detail` / `get_accessibility_docs` / `search_guidelines` のうち、object payload を返すケースでは通常 `structuredContent` を返します。
348
365
 
349
- - 100KB 制限を超える場合は自動的に `structuredContent` を省略し、`content` のみ返します
366
+ - `structuredContent` MCP 仕様どおり JSON object を直接返します
367
+ - 100KB 制限を超える場合は自動的に `structuredContent` を省略し、必要に応じて `content` を compact JSON に切り替え、それでも収まらなければ `TOOL_RESULT_TOO_LARGE` warning payload にフォールバックします
368
+ - plugin handler が raw MCP result を返す場合も、最終返却サイズには同じ上限が適用されます
350
369
  - 緊急切り戻し時は環境変数 `WCF_MCP_DISABLE_STRUCTURED_CONTENT=1` を設定してください
351
370
 
352
371
  例:
@@ -632,8 +651,13 @@ CEM やレジストリを更新した後:
632
651
  ```bash
633
652
  npm run mcp:build # データファイルをパッケージにコピー
634
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 をチェック
635
656
  ```
636
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
+
637
661
  ### パッケージ構成
638
662
 
639
663
  ```
package/bin.mjs CHANGED
@@ -9,9 +9,12 @@
9
9
  */
10
10
 
11
11
  import { createServer } from './server.mjs';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
12
15
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
16
 
14
- const USAGE = [
17
+ export const USAGE = [
15
18
  'Usage:',
16
19
  ' wcf-mcp',
17
20
  ' wcf-mcp --transport=stdio',
@@ -19,8 +22,9 @@ const USAGE = [
19
22
  ' wcf-mcp --config=./wcf-mcp.config.json',
20
23
  ' wcf-mcp --help',
21
24
  ].join('\n');
25
+ export const HTTP_ENDPOINT_PATH = '/mcp';
22
26
 
23
- function parseArgs(argv) {
27
+ export function parseArgs(argv) {
24
28
  let transport = 'stdio';
25
29
  let port = 3100;
26
30
  let configPath;
@@ -90,6 +94,144 @@ function parseArgs(argv) {
90
94
  return { help: false, transport, port, configPath };
91
95
  }
92
96
 
97
+ export function buildHttpTransportOptions({ host = '127.0.0.1', port }) {
98
+ const allowedHostnames = new Set([host]);
99
+ if (host === '127.0.0.1') {
100
+ allowedHostnames.add('localhost');
101
+ }
102
+ if (host === 'localhost') {
103
+ allowedHostnames.add('127.0.0.1');
104
+ }
105
+
106
+ const defaultHttpPort = port === 80;
107
+ const allowedHosts = new Set();
108
+ const allowedOrigins = new Set();
109
+ for (const allowedHostname of allowedHostnames) {
110
+ allowedHosts.add(`${allowedHostname}:${port}`);
111
+ allowedOrigins.add(`http://${allowedHostname}:${port}`);
112
+ if (defaultHttpPort) {
113
+ allowedHosts.add(allowedHostname);
114
+ allowedOrigins.add(`http://${allowedHostname}`);
115
+ }
116
+ }
117
+
118
+ return {
119
+ sessionIdGenerator: undefined,
120
+ allowedHosts: [...allowedHosts],
121
+ allowedOrigins: [...allowedOrigins],
122
+ enableDnsRebindingProtection: true,
123
+ };
124
+ }
125
+
126
+ function sendJsonRpcError(res, status, message) {
127
+ if (res.headersSent) return;
128
+ res.statusCode = status;
129
+ res.setHeader('content-type', 'application/json');
130
+ res.end(JSON.stringify({
131
+ jsonrpc: '2.0',
132
+ error: {
133
+ code: -32603,
134
+ message,
135
+ },
136
+ id: null,
137
+ }));
138
+ }
139
+
140
+ function sendNotFound(res) {
141
+ if (res.headersSent) return;
142
+ res.statusCode = 404;
143
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
144
+ res.end('Not Found');
145
+ }
146
+
147
+ function getRequestPath(req) {
148
+ const rawUrl = req.url ?? '/';
149
+ try {
150
+ return new URL(rawUrl, 'http://localhost').pathname;
151
+ } catch {
152
+ return rawUrl;
153
+ }
154
+ }
155
+
156
+ export async function validateHttpStartup({
157
+ configPath,
158
+ createServerImpl = createServer,
159
+ } = {}) {
160
+ const bootstrap = await createServerImpl({ configPath });
161
+ await Promise.allSettled([
162
+ bootstrap?.server?.close?.(),
163
+ ]);
164
+ }
165
+
166
+ export function createHttpRequestHandler({
167
+ port,
168
+ host = '127.0.0.1',
169
+ configPath,
170
+ createServerImpl = createServer,
171
+ } = {}) {
172
+ const transportOptions = buildHttpTransportOptions({ host, port });
173
+
174
+ return async function handleHttpRequest(req, res) {
175
+ if (getRequestPath(req) !== HTTP_ENDPOINT_PATH) {
176
+ sendNotFound(res);
177
+ return;
178
+ }
179
+
180
+ let server;
181
+ let transport;
182
+ let cleanedUp = false;
183
+ const cleanup = async () => {
184
+ if (cleanedUp) return;
185
+ cleanedUp = true;
186
+ await Promise.allSettled([
187
+ transport?.close?.(),
188
+ server?.close?.(),
189
+ ]);
190
+ };
191
+
192
+ res.on('close', () => {
193
+ void cleanup();
194
+ });
195
+
196
+ try {
197
+ ({ server } = await createServerImpl({ configPath }));
198
+ const { StreamableHTTPServerTransport } = await import(
199
+ '@modelcontextprotocol/sdk/server/streamableHttp.js'
200
+ );
201
+ transport = new StreamableHTTPServerTransport(transportOptions);
202
+ await server.connect(transport);
203
+ await transport.handleRequest(req, res);
204
+ if (res.writableEnded) {
205
+ await cleanup();
206
+ }
207
+ } catch (error) {
208
+ await cleanup();
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ if (res.headersSent) {
211
+ res.destroy(error instanceof Error ? error : new Error(message));
212
+ return;
213
+ }
214
+ sendJsonRpcError(res, 500, message);
215
+ }
216
+ };
217
+ }
218
+
219
+ function isDirectRun(metaUrl = import.meta.url, argv = process.argv) {
220
+ const entryPath = argv[1];
221
+ if (!entryPath) return false;
222
+
223
+ const resolveFilePath = (value) => {
224
+ const resolvedPath = path.resolve(value);
225
+ try {
226
+ return fs.realpathSync.native?.(resolvedPath) ?? fs.realpathSync(resolvedPath);
227
+ } catch {
228
+ return resolvedPath;
229
+ }
230
+ };
231
+
232
+ return resolveFilePath(entryPath) === resolveFilePath(fileURLToPath(metaUrl));
233
+ }
234
+
93
235
  async function main() {
94
236
  let parsed;
95
237
  try {
@@ -106,25 +248,28 @@ async function main() {
106
248
  return;
107
249
  }
108
250
 
109
- const { server } = await createServer({ configPath: parsed.configPath });
110
-
111
251
  if (parsed.transport === 'http') {
112
- const { StreamableHTTPServerTransport } = await import(
113
- '@modelcontextprotocol/sdk/server/streamableHttp.js'
114
- );
252
+ await validateHttpStartup({ configPath: parsed.configPath });
115
253
  const { createServer: createHttpServer } = await import('node:http');
116
- const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
117
- await server.connect(httpTransport);
118
- const httpServer = createHttpServer((req, res) => httpTransport.handleRequest(req, res));
254
+ const handleHttpRequest = createHttpRequestHandler({
255
+ port: parsed.port,
256
+ configPath: parsed.configPath,
257
+ });
258
+ const httpServer = createHttpServer((req, res) => {
259
+ void handleHttpRequest(req, res);
260
+ });
119
261
  httpServer.listen(parsed.port, '127.0.0.1', () => {
120
- console.error(`MCP HTTP server listening on http://127.0.0.1:${parsed.port}/mcp`);
262
+ console.error(`MCP HTTP server listening on http://127.0.0.1:${parsed.port}${HTTP_ENDPOINT_PATH}`);
121
263
  });
122
264
  } else {
265
+ const { server } = await createServer({ configPath: parsed.configPath });
123
266
  await server.connect(new StdioServerTransport());
124
267
  }
125
268
  }
126
269
 
127
- main().catch((err) => {
128
- console.error(err);
129
- process.exit(1);
130
- });
270
+ if (isDirectRun()) {
271
+ main().catch((err) => {
272
+ console.error(err);
273
+ process.exit(1);
274
+ });
275
+ }