typescript-mcp について
MCP SDK、Hono、Zod を使って Cloudflare Workers 上に MCP サーバーを構築する TypeScript 開発者向けスキルです。Streamable HTTP、認証、タスク、ツール付きサンプリング、運用上の問題対策を解説します。
主な機能
- 01Streamable HTTP サーバーの実装パターン
- 02API キーと Zero Trust 認証
- 03長時間タスクのライフサイクル
- 04ツール付きサンプリングリクエスト
- 05Cloudflare サービスツールの例
- 06運用上の問題への対策
活用シーン
- 01Workers への MCP サーバーのデプロイ
- 02MCP エンドポイントへの認証追加
- 03ポーリング可能な長時間処理の実装
- 04D1、KV、R2、Vectorize との接続
MCP 対応
TypeScript MCP SDK を使い、Cloudflare Workers 上で Streamable HTTP 経由のツールをクライアントに提供する MCP サーバーの構築を解説します。タスクやサンプリングなどの MCP 機能も扱います。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill typescript-mcpインストール後、Claude Code で/typescript-mcpを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsoncommands/init.mdreferences/authentication-guide.mdcloudflare-agents-vs-standalone.mdcloudflare-integration.mdcommon-errors.mddeployment-guide.mdtesting-guide.mdtool-patterns.mdrules/typescript-mcp.mdscripts/init-mcp-server.shtest-mcp-connection.shtemplates/authenticated-server.tsbasic-mcp-server.tsfull-server.tsresource-server.tstasks-server.tstool-server.ts詳細
Cloudflare Workers 上の TypeScript MCP
最終更新日:2026-01-21 バージョン:@modelcontextprotocol/[email protected]、[email protected]、[email protected] 仕様バージョン:2025-11-25
クイックスタート
npm install @modelcontextprotocol/sdk@latest hono zod
npm install -D @cloudflare/workers-types wrangler typescript
トランスポートの推奨:本番環境では StreamableHTTPServerTransport を使用してください。SSE トランスポートは非推奨で、後方互換性のためだけに維持されています。Streamable HTTP は、より優れたエラー復旧、双方向通信、簡素化されたデプロイを実現します。
基本的な MCP サーバー:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { Hono } from 'hono';
import { z } from 'zod';
const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });
server.registerTool(
'echo',
{
description: 'Echoes back input',
inputSchema: z.object({ text: z.string() })
},
async ({ text }) => ({ content: [{ type: 'text', text }] })
);
const app = new Hono();
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
// CRITICAL: Set error handler to catch transport errors
transport.onerror = (error) => {
console.error('MCP transport error:', error);
};
// CRITICAL: Close transport to prevent memory leaks
c.res.raw.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c.req.json());
return c.body(null);
});
export default app; // CRITICAL: Direct export, not { fetch: app.fetch }
デプロイ:wrangler deploy
認証
API キー(KV ベース):
app.use('/mcp', async (c, next) => {
const apiKey = c.req.header('Authorization')?.replace('Bearer ', '');
const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);
if (!isValid) return c.json({ error: 'Unauthorized' }, 403);
await next();
});
Cloudflare Zero Trust:
const jwt = c.req.header('Cf-Access-Jwt-Assertion');
const payload = await verifyJWT(jwt, c.env.CF_ACCESS_TEAM_DOMAIN);
タスク(v1.24.0+)
タスクを使うと、長時間実行される操作でハンドルを返し、後から結果をポーリングできます。高コストな計算、バッチ処理、入力が必要になる場合がある操作に便利です。
タスクの状態:working → input_required → completed / failed / cancelled
サーバー機能の宣言:
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
capabilities: {
tasks: {
list: {},
cancel: {},
requests: {
tools: { call: {} }
}
}
}
});
タスクをサポートするツール:
server.registerTool(
'long-running-analysis',
{
description: 'Analyze large dataset',
inputSchema: z.object({ datasetId: z.string() }),
execution: { taskSupport: 'optional' } // 'forbidden' | 'optional' | 'required'
},
async ({ datasetId }, extra) => {
// If invoked as task, extra.task contains taskId
const result = await performAnalysis(datasetId);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
}
);
クライアントのタスクリクエスト:
{
"method": "tools/call",
"params": {
"name": "long-running-analysis",
"arguments": { "datasetId": "abc123" },
"task": { "ttl": 60000 }
}
}
タスクのライフサイクル:
- クライアントが
taskパラメーター付きでリクエストを送信し、taskIdを受け取ります - クライアントが
taskIdを指定してtasks/getでポーリングします - ステータスが
completedになったら、クライアントはtasks/resultを呼び出して出力を取得します - 必要に応じて、クライアントは
tasks/cancelを呼び出して中止できます
📚 仕様:https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
ツールを使ったサンプリング(v1.24.0+)
サーバーはサンプリングリクエストにツール定義を含められるようになり、サーバー側のエージェントループが可能になりました。
ユースケース:カスタムフレームワークを使わずに、LLM とツールを使用した複数ステップの推論をサーバーで調整する必要がある場合。
// Server initiates sampling with tools available
const result = await server.requestSampling({
messages: [{ role: 'user', content: 'Analyze this data and fetch more if needed' }],
maxTokens: 4096,
tools: [
{
name: 'fetch_data',
description: 'Fetch additional data from API',
inputSchema: { type: 'object', properties: { query: { type: 'string' } } }
}
]
});
// Handle tool calls in response
if (result.content[0].type === 'tool_use') {
const toolResult = await executeLocalTool(result.content[0]);
// Continue conversation with tool result...
}
ポイント:
- サーバー側のエージェント動作を、MCP の第一級機能として提供
- 標準の MCP プリミティブ(カスタムフレームワーク不要)
- ツール定義は
tools/listと同じスキーマに準拠
📚 仕様:SEP-1577
Cloudflare サービス用ツール
D1 データベース:
server.registerTool('query-db', {
inputSchema: z.object({ query: z.string(), params: z.array(z.union([z.string(), z.number()])).optional() })
}, async ({ query, params }, env) => {
const result = await env.DB.prepare(query).bind(...(params || [])).all();
return { content: [{ type: 'text', text: JSON.stringify(result.results) }] };
});
KV、R2、Vectorize:references/cloudflare-integration.md を参照してください
既知の問題の予防
このスキルでは、公式 MCP SDK および Cloudflare リポジトリに記載された、本番環境での 20 件の問題を防止します。
問題 #1:エクスポート構文の問題(重大)
エラー:"Cannot read properties of undefined (reading 'map')"
出典:honojs/hono#3955、honojs/vite-plugins#237
発生理由:Vite ビルドでエクスポート形式が正しくないと、原因の分かりにくいエラーが発生します
予防策:
// ❌ WRONG - Causes cryptic build errors
export default { fetch: app.fetch };
// ✅ CORRECT - Direct export
export default app;
問題 #2:トランスポート接続が閉じられない
エラー:メモリーリーク、接続のハング 出典:SDK メンテナーによるベストプラクティス 発生理由:リクエスト終了時に StreamableHTTPServerTransport を閉じていません 予防策:
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport(/*...*/);
// CRITICAL: Always close on response end
c.res.raw.on('close', () => transport.close());
// ... handle request
});
問題 #3:ツールスキーマの検証失敗
エラー:ListTools request handler fails to generate inputSchema
出典:GitHub modelcontextprotocol/typescript-sdk#1028
発生理由:Zod スキーマが JSON Schema に正しく変換されていません
予防策:
// ✅ CORRECT - SDK handles Zod schema conversion automatically
server.registerTool(
'tool-name',
{
inputSchema: z.object({ a: z.number() })
},
handler
);
// No need for manual zodToJsonSchema() unless custom validation
問題 #4:ツールの引数がハンドラーに渡されない
エラー:ハンドラーが undefined の引数を受け取ります
出典:GitHub modelcontextprotocol/typescript-sdk#1026
発生理由:登録時と呼び出し時でスキーマの型が一致していません
予防策:
const schema = z.object({ a: z.number(), b: z.number() });
type Input = z.infer<typeof schema>;
server.registerTool(
'add',
{ inputSchema: schema },
async (args: Input) => {
// args.a and args.b properly typed and passed
return { content: [{ type: 'text', text: String(args.a + args.b) }] };
}
);
問題 #5:CORS の設定ミス
エラー:ブラウザーのクライアントが MCP サーバーに接続できません 出典:本番環境でよくある問題 発生理由:HTTP トランスポートに必要な CORS ヘッダーがありません 予防策:
import { cors } from 'hono/cors';
app.use('/mcp', cors({
origin: ['http://localhost:3000', 'https://your-app.com'],
allowMethods: ['POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));
問題 #6:レート制限がない
エラー:API の不正利用、DDoS の脆弱性 出典:本番環境のセキュリティに関するベストプラクティス 発生理由:MCP エンドポイントにレート制限がありません 予防策:
app.post('/mcp', async (c) => {
const ip = c.req.header('CF-Connecting-IP');
const rateLimitKey = `ratelimit:${ip}`;
const count = await c.env.CACHE.get(rateLimitKey);
if (count && parseInt(count) > 100) {
return c.json({ error: 'Rate limit exceeded' }, 429);
}
await c.env.CACHE.put(
rateLimitKey,
String((parseInt(count || '0') + 1)),
{ expirationTtl: 60 }
);
// Continue...
});
問題 #7:TypeScript のコンパイル時に発生するメモリーの問題
エラー:tsc ビルド中の Out of memory
出典:GitHub modelcontextprotocol/typescript-sdk#985
発生理由:MCP SDK の依存関係ツリーが大きいためです
予防策:
# Add to package.json scripts
"build": "NODE_OPTIONS='--max-old-space-size=4096' tsc && vite build"
Issue #8: UriTemplate の ReDoS 脆弱性
エラー: 悪意のある URI パターンによってサーバーがハングする 出典: GitHub modelcontextprotocol/typescript-sdk#965(セキュリティ) 原因: URI テンプレートの解析における正規表現によるサービス拒否 防止策: SDK v1.20.2 以降に更新してください(修正が含まれています)
Issue #9: 認証バイパス
エラー: 認証されていない状態で MCP ツールにアクセスできる 出典: 本番環境におけるセキュリティのベストプラクティス 原因: 認証がない、または適切に実装されていない 防止策: 本番環境のサーバーでは必ず認証を実装してください(「認証パターン」セクションを参照)
Issue #10: 環境変数の漏えい
エラー: エラーメッセージやログにシークレットが露出する 出典: Cloudflare Workers のセキュリティに関するベストプラクティス 原因: 環境変数がログに記録されるか、レスポンスで返される 防止策:
// ❌ WRONG - Exposes secrets
console.log('Env:', JSON.stringify(env));
// ✅ CORRECT - Never log env objects
try {
// ... use env.SECRET_KEY
} catch (error) {
// Don't include env in error context
console.error('Operation failed:', error.message);
}
Issue #11: サーバーインスタンスの再利用によって同時 HTTP セッションが破損する(重大)
エラー: AbortError: This operation was aborted
出典: GitHub Issue #1405
原因: Server.connect(transport) を呼び出すと、警告なしに以前のトランスポートが上書きされ、すべての先行する接続が破損する
防止策:
// ✅ CORRECT - Create fresh McpServer per HTTP session
app.post('/mcp', async (c) => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// Register tools per request
server.registerTool('echo', { inputSchema: z.object({ text: z.string() }) },
async ({ text }) => ({ content: [{ type: 'text', text }] })
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
transport.onerror = (error) => console.error('Transport error:', error);
c.res.raw.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c.req.json());
return c.body(null);
});
// ❌ WRONG - Reusing server instance across sessions
const sharedServer = new McpServer({ name: 'my-server', version: '1.0.0' });
app.post('/mcp', async (c) => {
await sharedServer.connect(transport); // Breaks previous sessions!
});
Issue #12: TypeScript の厳格モードでの sessionIdGenerator 型エラー
エラー: Type 'undefined' is not assignable to type '() => string'
出典: GitHub Issue #1397
原因: tsconfig.json で exactOptionalPropertyTypes: true を使用しているプロジェクトでは、SDK 1.25.2 の型定義に問題がある
防止策:
// With exactOptionalPropertyTypes: true
// ✅ CORRECT - Omit the property instead of setting to undefined
const transport = new StreamableHTTPServerTransport({
enableJsonResponse: true
// sessionIdGenerator omitted entirely
});
// ❌ WRONG - Setting to undefined causes type error in SDK 1.25.2
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Type error!
enableJsonResponse: true
});
// Alternative: Provide a generator function
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true
});
Issue #13: Hono によるグローバル fetch の汚染(SDK 1.25.0-1.25.2)
エラー: SDK のインポート後にネイティブ Node.js の fetch の動作が変わる
出典: GitHub Issue #1376
原因: Hono のサーバーコードがグローバルの global.fetch を上書きし、ネイティブの動作を期待するライブラリを破損させる
防止策:
// FIXED in SDK v1.25.3 - Update to latest version
npm install @modelcontextprotocol/[email protected]
// Workaround for older versions (1.25.0-1.25.2):
const nativeFetch = global.fetch;
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
global.fetch = nativeFetch; // Restore if needed
Issue #14: タスクのエラーラッピングによって検証エラーが隠れる
エラー: 分かりにくいエラーメッセージによって実際の検証失敗が隠れる 出典: GitHub Issue #1385 原因: タスク作成前にタスク対応ツールの呼び出しが検証に失敗すると、SDK がエラーを誤ってラップする 防止策:
// Expected error for invalid input:
// "Invalid arguments: Too small: expected number to be >=500"
// Actual error (confusing):
// "Invalid task creation result: expected object, received undefined"
// WORKAROUND: Add explicit validation before task logic
server.experimental.tasks.registerToolTask(
'batch_process',
{
inputSchema: z.object({
itemCount: z.number().min(1).max(10),
processingTimeMs: z.number().min(500).max(5000).optional()
})
},
{
createTask: async (args, extra) => {
// SDK should fix this - currently no workaround
// Validation errors are masked by task wrapping
}
}
);
Issue #15: すべてのフィールドがオプションのツールスキーマによって InvalidParams が発生する
エラー: "expected": "object", "received": "undefined"
出典: GitHub Issue #400
原因: スキーマのすべてのプロパティがオプションの場合、一部の LLM クライアントは arguments フィールドを省略する
防止策:
// ❌ WRONG - All optional fields may cause issues
server.registerTool('fetch-records', {
inputSchema: z.object({
limit: z.number().optional()
})
}, handler);
// ✅ CORRECT - Always include at least one required field
server.registerTool('fetch-records', {
inputSchema: z.object({
action: z.literal('fetch').default('fetch'), // Required
limit: z.number().optional()
})
}, handler);
// Alternative: Use empty object schema
server.registerTool('fetch-records', {
inputSchema: z.object({}).passthrough()
}, handler);
Issue #16: ツールの一括登録によって EventEmitter のメモリリーク警告が発生する
エラー: MaxListenersExceededWarning: Possible EventEmitter memory leak detected
出典: GitHub Issue #842
原因: ループ内で 80 個以上のツールを登録すると、sendToolListChanged() 通知が急増して stdout バッファーを圧迫する
防止策:
// Workaround: Increase maxListeners before bulk registration
process.stdout.setMaxListeners(100);
const tools = [...]; // Array of 80+ tool definitions
for (const tool of tools) {
server.registerTool(tool.name, tool.schema, tool.handler);
}
// Future SDK may provide batch registration API
Issue #17: onerror ハンドラーがない場合、トランスポートエラーが通知されない
エラー: トランスポートエラーがログにも例外にも記録されず、消失する
出典: GitHub Issue #1395
原因: onerror コールバックが設定されていない場合、SDK はトランスポートエラーを通知せずに無視する
防止策:
// ✅ CORRECT - Always set onerror handler
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
transport.onerror = (error) => {
console.error('Transport error:', error);
// Handle error appropriately
};
await server.connect(transport);
Issue #18: クエリ文字列の配列上限を回避した DoS
エラー: 悪意のあるクエリパラメーターによるメモリ枯渇
出典: GitHub Issue #1368
原因: qs ライブラリの arrayLimit は、?foo[99999999]=bar のような角括弧表記を使って回避できる
防止策:
// Validate query parameters to prevent DoS
app.post('/mcp', async (c) => {
const queryParams = c.req.query();
// Reject malicious patterns
if (Object.keys(queryParams).some(key => /\[\d{6,}\]/.test(key))) {
return c.json({ error: 'Invalid query parameters' }, 400);
}
// ... handle request
});
Issue #19: トランスポートの切断時にリクエストハンドラーがキャンセルされない
エラー: クライアントの切断後も長時間実行されるハンドラーが処理を続け、リソースを浪費します 出典: GitHub Issue #611 発生する理由: トランスポート接続が閉じても、SDK はリクエストハンドラーを自動的にキャンセルしません 予防策:
// Workaround: Use AbortController pattern manually
server.registerTool(
'long-running-task',
{ inputSchema: z.object({ duration: z.number() }) },
async ({ duration }, extra) => {
const abortController = new AbortController();
// Listen for transport close
const transport = extra.transport;
if (transport) {
const originalOnClose = transport.onclose;
transport.onclose = () => {
abortController.abort();
if (originalOnClose) originalOnClose();
};
}
try {
await longRunningTask(duration, abortController.signal);
return { content: [{ type: 'text', text: 'Done' }] };
} catch (error) {
if (error.name === 'AbortError') {
return { content: [{ type: 'text', text: 'Cancelled' }], isError: true };
}
throw error;
}
}
);
Issue #20: SDK 1.22.0-1.22.x で $defs スキーマ参照に失敗する
エラー: can't resolve reference #/$defs/...
出典: GitHub Issue #1175
発生する理由: SDK 1.22.0 の cacheToolOutputSchemas におけるリグレッションにより、複雑な JSON Schema で listTools() が機能しなくなりました
予防策: SDK v1.23.0 以降に更新してください(修正済み)。1.22.x を使用している場合は、すぐにアップグレードしてください。
デプロイ
# Local
wrangler dev # http://localhost:8787/mcp
# Production
wrangler deploy
テスト: npx @modelcontextprotocol/inspector(http://localhost:8787/mcp に接続)
テンプレートとリファレンス
テンプレート: basic-mcp-server.ts、tool-server.ts、resource-server.ts、authenticated-server.ts、tasks-server.ts、wrangler.jsonc
リファレンス: tool-patterns.md、authentication-guide.md、testing-guide.md、cloudflare-integration.md、common-errors.md
重要なルール
必ず行うこと:
- ✅ HTTP リクエストごとに新しい
McpServerインスタンスを作成します(セッション間で再利用しないでください) - ✅
transport.onerrorハンドラーを設定して、表面化しないエラーを捕捉します - ✅ レスポンス終了時にトランスポートを閉じます(
c.res.raw.on('close', () => transport.close())) - ✅ 直接エクスポートを使用します(
export default app。{ fetch: app.fetch }は使用しないでください) - ✅ 本番環境では認証を実装します
- ✅ セキュリティ修正、Tasks のサポート、および fetch の汚染に関する修正のため、SDK v1.25.3+ に更新します
- ✅ ツールスキーマに必須フィールドを少なくとも 1 つ含めます(すべて任意のフィールドにするのは避けてください)
- ✅ 本番環境では
StreamableHTTPServerTransportを使用します(SSE は非推奨です)
決して行わないこと:
- ❌ 同時に実行される HTTP セッション間で
McpServerインスタンスを再利用する - ❌ オブジェクトラッパーを使ってエクスポートする
- ❌
StreamableHTTPServerTransportを閉じ忘れる - ❌
transport.onerrorハンドラーを省略する - ❌ 環境変数やシークレットをログに記録する
- ❌ 古い SDK バージョンを使用する(<1.23.0 にはスキーマの不具合があり、<1.25.3 には fetch の汚染があります)