Skip to main content
Glama
novice1248

workers-mcp-starter

by novice1248
README.md
# 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](src/mcp-server.ts) の素の JSON-RPC ディスパッチのみ。セッション ID を発行しない stateless な Streamable HTTP JSON-RPC 実装(protocolVersion `2025-06-18` を返す。単一バージョンのみ対応で、複数版ネゴシエーションは未実装)
- **2系統の認証**
  - Bearer 共有シークレット(`MCP_API_TOKEN`)— Claude Code 等ヘッダを設定できるクライアント向け。SHA-256 ハッシュ化+定数時間比較でタイミング攻撃を避ける
  - **OAuth 2.1**([src/oauth.ts](src/oauth.ts))— claude.ai のカスタムコネクタ向け。claude.ai は任意ヘッダを設定できず、MCP 仕様の Authorization フロー(RFC9728/RFC8414/RFC7591 DCR/RFC7636 PKCE)を話せるサーバーにしか繋がらない。必要なのは KV だけで、Durable Objects 不要
- **統合テスト** — 実 D1(miniflare)相手に `SELF.fetch` で認証・プロトコル・ツール実行を一気通貫で検証([src/mcp-server.workers.test.ts](src/mcp-server.workers.test.ts))

## セットアップ

```bash
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`):

```json
{
  "mcpServers": {
    "my-notes": {
      "type": "http",
      "url": "https://<your-worker>.workers.dev/mcp",
      "headers": { "Authorization": "Bearer <MCP_API_TOKEN>" }
    }
  }
}
```

### claude.ai カスタムコネクタから接続する(OAuth)

1. `npx wrangler kv namespace create MCP_OAUTH_KV` → 出力された id を wrangler.jsonc に反映して再デプロイ
2. claude.ai の設定 → コネクタ → カスタムコネクタ追加で `https://<your-worker>.workers.dev/mcp` を指定
3. 同意画面が開くので `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` / CSP `frame-ancestors 'none'`)。`/mcp` はトークン認証のみで Cookie を使わないため、ブラウザ系クライアント向けに CORS(`*`)と OPTIONS プリフライトを許可している
- **ドメインエラーは JSON-RPC エラーではなく `isError` 付き正常応答で返す。** MCP クライアント(LLM)がエラーメッセージを読んで次の手を選べるようにするため

## 自分のツールに差し替える

1. [schema.sql](schema.sql) を自分のテーブル定義に変更
2. [src/mcp-server.ts](src/mcp-server.ts) の `TOOLS`(ツール定義)と `callTool()`(ディスパッチ)、各 `tool*()` ハンドラを差し替え
3. テストを合わせて更新して `npm test`

## 開発

```bash
npm run dev        # ローカル起動(D1はローカルシミュレーション)
npm test           # 統合テスト(実D1/miniflare)
npm run typecheck
```

## License

MIT

Maintenance

ActivitySlowing
ResponsivenessNo issues