mcpField
T
typescript-mcp

typescript-mcp

github.com/jezweb開発1.1k335 回インストール3 週間前に更新
リポジトリ

typescript-mcp について

MCP SDK、Hono、Zod を使って Cloudflare Workers 上に MCP サーバーを構築する TypeScript 開発者向けスキルです。Streamable HTTP、認証、タスク、ツール付きサンプリング、運用上の問題対策を解説します。

主な機能

  1. 01Streamable HTTP サーバーの実装パターン
  2. 02API キーと Zero Trust 認証
  3. 03長時間タスクのライフサイクル
  4. 04ツール付きサンプリングリクエスト
  5. 05Cloudflare サービスツールの例
  6. 06運用上の問題への対策

活用シーン

  1. 01Workers への MCP サーバーのデプロイ
  2. 02MCP エンドポイントへの認証追加
  3. 03ポーリング可能な長時間処理の実装
  4. 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.json
commands/
init.md
references/
authentication-guide.md
cloudflare-agents-vs-standalone.md
cloudflare-integration.md
common-errors.md
deployment-guide.md
testing-guide.md
tool-patterns.md
rules/
typescript-mcp.md
scripts/
init-mcp-server.sh
test-mcp-connection.sh
templates/
authenticated-server.ts
basic-mcp-server.ts
full-server.ts
resource-server.ts
tasks-server.ts
tool-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 }
  }
}

タスクのライフサイクル:

  1. クライアントが task パラメーター付きでリクエストを送信し、taskId を受け取ります
  2. クライアントが taskId を指定して tasks/get でポーリングします
  3. ステータスが completed になったら、クライアントは tasks/result を呼び出して出力を取得します
  4. 必要に応じて、クライアントは 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 の汚染があります)

コメント