@monoharada/wcf-mcp 0.8.0 → 0.9.1

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
- ## 提供機能(19 tools + 1 prompt + 4 resources)
81
+ ## 提供機能(16 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
 
@@ -145,17 +145,6 @@ 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
-
159
148
  #### `get_design_tokens` の使用例
160
149
 
161
150
  **リクエスト:**
@@ -253,6 +242,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
253
242
  | `wcf://tokens` | トークン summary(type/category/themes/sample) |
254
243
  | `wcf://guidelines/{topic}` | topic 別ガイドライン要約(`accessibility`,`css`,`patterns`,`all`) |
255
244
  | `wcf://llms-full` | `llms-full.txt` の全文 |
245
+ | `wcf://skills` | 登録済み Claude Code / Cursor / Codex スキルカタログ(skills-registry.json ベース) |
256
246
 
257
247
  ### Figma MCP との併用
258
248
 
@@ -295,7 +285,9 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
295
285
  ```
296
286
 
297
287
  - bind: `127.0.0.1`
298
- - endpoint: `http://127.0.0.1:3100/mcp`
288
+ - endpoint: `http://127.0.0.1:3100/mcp` のみ
289
+ - Host / Origin validation を有効化して DNS rebinding を抑止します
290
+ - `--config` が不正な場合は待受開始前に起動失敗します
299
291
 
300
292
  ## 設定ファイル
301
293
 
@@ -339,7 +331,7 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
339
331
  ※ `./plugins/custom-validation-plugin.mjs` は利用側プロジェクトに配置してください。
340
332
  このリポジトリには参照用として `packages/mcp-server/examples/plugins/custom-validation-plugin.mjs` を同梱しています。
341
333
 
342
- ### plugin 契約(v1.1
334
+ ### plugin 契約(v1)
343
335
 
344
336
  詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
345
337
 
@@ -354,9 +346,11 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
354
346
 
355
347
  ## structuredContent rollback
356
348
 
357
- `get_component_api` / `get_design_tokens` / `get_design_token_detail` / `get_accessibility_docs` / `search_guidelines` は通常 `structuredContent` を返します。
349
+ `get_component_api` / `get_design_tokens` / `get_design_token_detail` / `get_accessibility_docs` / `search_guidelines` のうち、object payload を返すケースでは通常 `structuredContent` を返します。
358
350
 
359
- - 100KB 制限を超える場合は自動的に `structuredContent` を省略し、`content` のみ返します
351
+ - `structuredContent` MCP 仕様どおり JSON object を直接返します
352
+ - 100KB 制限を超える場合は自動的に `structuredContent` を省略し、必要に応じて `content` を compact JSON に切り替え、それでも収まらなければ `TOOL_RESULT_TOO_LARGE` warning payload にフォールバックします
353
+ - plugin handler が raw MCP result を返す場合も、最終返却サイズには同じ上限が適用されます
360
354
  - 緊急切り戻し時は環境変数 `WCF_MCP_DISABLE_STRUCTURED_CONTENT=1` を設定してください
361
355
 
362
356
  例:
@@ -392,21 +386,6 @@ Claude Desktop 設定例:
392
386
  prefix: "myui" → dads-button → myui-button
393
387
  ```
394
388
 
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
-
410
389
  ## v0.4.0 新機能
411
390
 
412
391
  ### 新ツール
@@ -668,8 +647,6 @@ packages/mcp-server/
668
647
  ├── server.mjs # MCP サーバー本体
669
648
  ├── validator.mjs # HTML バリデーター
670
649
  ├── package.json # npm パッケージ定義
671
- ├── plugins/ # 同梱プラグイン
672
- │ └── design-system-skills/ # スキル管理ツール (list_skills, get_skill_manifest, check_drift)
673
650
  └── data/ # バンドルデータ (npm run mcp:build で生成)
674
651
  ├── custom-elements.json
675
652
  ├── install-registry.json
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
+ }