はじめに
前回は Claude Code Skill を整備し、ターミナルや VSCode 上の Claude Code からカレンダーに「打ち合わせを入れて」と話しかけるだけで予定が登録できるようになりました。
しかし Skill はローカル定義のため、Claude Code のセッション外では使えません。
- ブラウザの claude.ai (Web)
- iOS / Android の Claude アプリ
- Claude Desktop
今回はこれらから操作できるようにすることを目標に、前回整備した REST API を Model Context Protocol(MCP) のエンドポイントでラップしました。
計画と現実のギャップ
当初のゴールは「Web・スマホ・デスクトップすべてから自然言語でカレンダーを操作できる」でした。しかし実際に試したところ、次の壁にぶつかりました。
- claude.ai Web の「カスタムコネクタを追加」 — OAuth 専用で、静的 Bearer トークンを直接貼り付けることができない
- iOS / Android の Claude アプリ — Node.js が動かないため
mcp-remoteが使えず、claude.ai 経由でしか接続できない。上記と同じ制約
結果として、今回の実装で接続できたのは Claude Desktop のみです。
MCP(Model Context Protocol)とは
MCP は Anthropic が策定した、AI モデルと外部システムをつなぐ標準プロトコルです。Claude Desktop では claude_desktop_config.json に MCP サーバーを登録することで、チャット画面からそのシステムをツールとして呼び出せます。
Skill が「Claude Code 専用の取扱説明書」だとすれば、MCP は「Claude クライアントが使える標準インターフェース」です。
アーキテクチャ
既存の Next.js アプリに /api/mcp/[transport] ルートを追加し、Vercel に同居させました。Claude Desktop からは mcp-remote がブリッジとなって HTTPS 経由で接続します。
Claude Desktop
│
▼
npx mcp-remote(ブリッジ)
│ HTTPS + Authorization: Bearer
▼
/api/mcp/[transport] ← @vercel/mcp-adapter
│
▼
mcpAuth.ts (HMAC 検証 → {calendarId, role})
│
▼
mcpTools.ts (Zod 定義 7 ツール)
│
▼
src/lib/server/events.ts ← サービス層(新設)
│
▼
Supabase (service-role)
ポイントは サービス層の抽出 です。既存の REST Route Handler 内のロジックを src/lib/server/ 配下の純粋関数として切り出し、REST 経由でも MCP 経由でも同じ関数を呼ぶ構造にしました。curl から叩いても Claude Desktop から叩いても、完全に同じ挙動になります。
公開した MCP ツール(7つ)
| tool name | 必要ロール | 用途 |
|---|---|---|
| list_events | user / admin | 期間指定でイベント一覧取得 |
| get_event | user / admin | 単体取得 |
| create_event | admin | イベント登録 |
| update_event | admin | 部分更新 |
| delete_event | admin | 削除 |
| list_labels | user / admin | ラベル一覧 |
| get_calendar_info | user / admin | カレンダー情報確認 |
ツール引数に calendarId は含めていません。認証トークンから自動的に導出するため、利用者は意識する必要がありません。また、user ロールのトークンで接続した場合は書き込み系ツール(create / update / delete)がツール一覧から消え、誤操作を防げます。

認証の設計
新しい認証機構は作らず、前回から使っている HMAC Bearer トークン をそのまま流用しました。claude_desktop_config.json のヘッダオプションにトークンを記述するだけで完結します。
Claude Desktop への接続手順
claude_desktop_config.json に以下を追記します。
"wbcal": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://<your-domain>/api/mcp/mcp",
"--header",
"Authorization:Bearer <WBCAL_TOKEN>"
]
}
Claude Desktop を再起動すると MCP ツールが有効になります。あとはチャットで「来週月曜 14:00 にチーム定例を登録して」と話しかけるだけです。

実装上の注意点
Edge ランタイムは不可
Supabase のサービスロールキーを使うため、runtime = "nodejs" を明示します。
export const runtime = "nodejs";
export const maxDuration = 60; // Vercel Hobby の上限
CORS は許可リスト方式で
/api/mcp/* のみを対象に Access-Control-Allow-Origin: https://claude.ai を返します。ワイルドカード * は使わず、next.config.ts の headers() で制御します。
ルートハンドラのコード
import { createMcpHandler } from "@vercel/mcp-adapter";
import { registerTools } from "@/lib/server/mcpTools";
import { resolveContextFromAuthHeader } from "@/lib/server/mcpAuth";
export const runtime = "nodejs";
export const maxDuration = 60;
const handler = createMcpHandler(
async (server, { request }) => {
const ctx = await resolveContextFromAuthHeader(
request.headers.get("authorization")
); // 失敗すると adapter が 401 を返す
registerTools(server, ctx);
},
{ capabilities: { tools: {} } },
{ basePath: "/api/mcp", verboseLogs: false }
);
export { handler as GET, handler as POST, handler as DELETE };
前回(Skill)との使い分け
| Skill(前回) | MCP サーバー(今回) | |
|---|---|---|
| 使える場所 | Claude Code(ターミナル / VSCode) | Claude Desktop |
| 実装場所 | リポジトリ内 SKILL.md | Next.js API Route (/api/mcp/) |
| 認証 | .env.local から読む | claude_desktop_config.json に記述 |
| 呼び出し方 | curl をラップ | MCP プロトコル経由 |
両者は同じ REST API(→ 同じサービス層)を呼んでいるため、どちらから操作しても結果は同じです。Skill はそのまま残し、MCP を追加するだけで両立できます。
まとめ
今回の実装を経て、アプリの構造はこうなりました。
Web UI(ブラウザ)
│
├── REST API (/api/events, /api/calendars, ...)
│ │
│ ├── Claude Code Skill(前回)
│ │ → ターミナル / VSCode から自然言語で操作
│ │
│ └── MCP サーバー(今回)
│ → Claude Desktop から自然言語で操作
│
└── Supabase(サービス層経由)
当初は Web やスマホからも操作できる想定でしたが、claude.ai のカスタムコネクタが OAuth 専用であることが判明し、今回は Claude Desktop に絞った対応となりました。
claude.ai Web / iOS / Android への正式対応は、MCP の OAuth 認可フロー実装が必要です。それは次回以降の課題です。
