@monoharada/wcf-mcp 0.9.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
@@ -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
 
@@ -285,7 +285,9 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
285
285
  ```
286
286
 
287
287
  - bind: `127.0.0.1`
288
- - 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` が不正な場合は待受開始前に起動失敗します
289
291
 
290
292
  ## 設定ファイル
291
293
 
@@ -344,9 +346,11 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
344
346
 
345
347
  ## structuredContent rollback
346
348
 
347
- `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` を返します。
348
350
 
349
- - 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 を返す場合も、最終返却サイズには同じ上限が適用されます
350
354
  - 緊急切り戻し時は環境変数 `WCF_MCP_DISABLE_STRUCTURED_CONTENT=1` を設定してください
351
355
 
352
356
  例:
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
+ }