workers-mcp-starter
Cloudflare Workers 上で動くリモート MCP サーバーの最小スターター。SDK に頼らず、Streamable HTTP の JSON-RPC を素の fetch ハンドラで直接処理する stateless 構成です。
サンプルとして D1 の notes テーブルに対する CRUD ツール 5 つを載せています。ツール定義とハンドラを差し替えれば、自分のデータを Claude に繋ぐサーバーになります。
特徴
- 依存が薄い —
@modelcontextprotocol/sdkも Agents SDK(Durable Objects 前提)も使わない。プロトコル処理は src/mcp-server.ts の素の JSON-RPC ディスパッチのみ。セッション ID を発行しない stateless な Streamable HTTP JSON-RPC 実装(protocolVersion2025-06-18を返す。単一バージョンのみ対応で、複数版ネゴシエーションは未実装) - 2系統の認証
- Bearer 共有シークレット(
MCP_API_TOKEN)— Claude Code 等ヘッダを設定できるクライアント向け。SHA-256 ハッシュ化+定数時間比較でタイミング攻撃を避ける - OAuth 2.1(src/oauth.ts)— claude.ai のカスタムコネクタ向け。claude.ai は任意ヘッダを設定できず、MCP 仕様の Authorization フロー(RFC9728/RFC8414/RFC7591 DCR/RFC7636 PKCE)を話せるサーバーにしか繋がらない。必要なのは KV だけで、Durable Objects 不要
- Bearer 共有シークレット(
- 統合テスト — 実 D1(miniflare)相手に
SELF.fetchで認証・プロトコル・ツール実行を一気通貫で検証(src/mcp-server.workers.test.ts)
セットアップ
npm install
npx wrangler d1 create workers-mcp-starter # 出力されたdatabase_idをwrangler.jsoncに反映
npm run db:migrate:remote # notesテーブル作成
npx wrangler secret put MCP_API_TOKEN # 十分に長いランダム値(例: openssl rand -hex 32)
npm run deploy
Claude Code から接続する(.mcp.json):
{
"mcpServers": {
"my-notes": {
"type": "http",
"url": "https://<your-worker>.workers.dev/mcp",
"headers": { "Authorization": "Bearer <MCP_API_TOKEN>" }
}
}
}
claude.ai カスタムコネクタから接続する(OAuth)
npx wrangler kv namespace create MCP_OAUTH_KV→ 出力された id を wrangler.jsonc に反映して再デプロイ- claude.ai の設定 → コネクタ → カスタムコネクタ追加で
https://<your-worker>.workers.dev/mcpを指定 - 同意画面が開くので
MCP_API_TOKENを入力して許可する
OAuth を使わないなら KV バインディングごと削除してよい(関連エンドポイントは静かに 404 になる)。
設計メモ(なぜこの形か)
- クエリパラメータでのトークン受け渡しはしない。 URL はシェル履歴・アクセスログ・Referer に残る。Bearer ヘッダのみ
- トークンは平文で保存しない。 OAuth のアクセス/リフレッシュトークンは SHA-256 ハッシュを KV キーにして保存し、KV の内容が漏れても原文を復元できない
- DCR は開放、認可はシークレットで防衛。 クライアント登録(RFC7591)は未認証で誰でもできるが、登録しただけでは何のデータにも触れない。実際の認可は同意画面での
MCP_API_TOKEN入力が必要。登録レコードには TTL を付け、bot の大量登録で KV が膨らみ続けるのを防ぐ - redirect_uri は登録値との完全一致のみ(オープンリダイレクト対策)。https 以外は
http://localhost/http://127.0.0.1(ローカル開発用)を除き登録段階で拒否、フラグメント付き URI も拒否(OAuth 2.1) - PKCE(S256)必須、plain 不可。
code_verifierは RFC7636 の形式(43〜128 文字の unreserved)を検証。resource(RFC8707)は指定されるなら自サーバーの正準 URI と完全一致を要求し、認可時に束縛された値はトークン交換でも同じ値を必須にする(confused deputy 対策) - 認可コード・リフレッシュトークンはワンタイム。 既知の限界: Workers KV は get/delete がアトミックでないため、理論上ごく短い時間窓の同時リクエストが両方通過しうる。真の単一利用保証には Durable Objects が要るが、シングルユーザー用途では許容している(コード内コメント参照)
- 同意画面はクリックジャッキング禁止(
X-Frame-Options: DENY/ CSPframe-ancestors 'none')。/mcpはトークン認証のみで Cookie を使わないため、ブラウザ系クライアント向けに CORS(*)と OPTIONS プリフライトを許可している - ドメインエラーは JSON-RPC エラーではなく
isError付き正常応答で返す。 MCP クライアント(LLM)がエラーメッセージを読んで次の手を選べるようにするため
自分のツールに差し替える
- schema.sql を自分のテーブル定義に変更
- src/mcp-server.ts の
TOOLS(ツール定義)とcallTool()(ディスパッチ)、各tool*()ハンドラを差し替え - テストを合わせて更新して
npm test
開発
npm run dev # ローカル起動(D1はローカルシミュレーション)
npm test # 統合テスト(実D1/miniflare)
npm run typecheck
License
MIT