前回の記事で「Google KeepはMCPを出していなかったから逃げた」と書いた。 では、そのMCPとは何で、自分のサービスを対応させるには何をすればいいのか。
APIを普段から公開している人向けに、仕組み・公開手順・認証フローの3点を整理する。 結論を先に言うと、MCPは「LLMが呼ぶことを前提にAPIの上に被せる薄い標準層」であり、既存のAPIを捨てる必要はない。面倒なのは認証だけだ。
1. 全体像: 3つの役割
MCP(Model Context Protocol)には3つの登場人物がいる。
- ホスト: ユーザーが触るAIアプリ。Claude.ai、ChatGPT、Claude Codeなど
- クライアント: ホストの中で、サーバー1つにつき1本張られる接続
- サーバー: 自分のサービス側。ツール(操作)やリソース(データ)をLLMに公開する
ポイントは、MCPサーバーは既存APIの前に立つ「通訳」だという点だ。 中でやっているのは、LLMから来た「add_item」という呼び出しを、自分のAPIやDBへのクエリに変換しているだけ。
2. 通常のAPIと何が違うのか
APIを設計したことがある人なら、「JSONを返すエンドポイントと何が違うのか」と思うはずだ。違いは「誰が読むか」に集約される。
| 通常のREST API | MCPサーバー | |
|---|---|---|
| 読む相手 | プログラマー | LLM |
| 使い方の伝え方 | ドキュメントを人が読む | サーバー自身がツール一覧と説明を返す |
| エンドポイント | 用途ごとに複数 | 1つ。中でツール名で振り分け |
| 設計単位 | リソース(GET /tasks) | 意図(find_tasks) |
| 通信 | HTTP + JSON | JSON-RPC 2.0(HTTP上に載せる) |
| 認証 | 自由 | OAuth 2.1がほぼ標準 |
一番大きいのは「サーバー自身が使い方を説明する」ことだ。
クライアントが接続すると、まずtools/listでツールの一覧を取りにいく。返ってくるのはツール名・説明文・引数のJSON Schemaで、LLMはこれを読んで「今このユーザーの依頼に対して、どのツールをどんな引数で呼ぶか」を判断する。
つまりツールのdescriptionは事実上プロンプトになる。ここの書き方がそのまま使い勝手を決める。
3. 接続からツール実行までの流れ
initializeとtools/listは接続時に一度だけ。以降はユーザーの発言ごとにtools/callが飛ぶ。
戻り値もLLMが読むので、IDだけ返すより「追加しました: きゅうり」のように人が読める文で返したほうが、その後の会話が自然になる。
通信方式
- stdio: ローカルで動かすサーバー向け。Claude Codeやエディタが子プロセスとして起動する
- Streamable HTTP: リモート公開向け。1つのエンドポイント(例:
/mcp)にPOSTでJSON-RPCを送り、レスポンスは通常のJSONかSSEストリームで返る
Claude.aiやChatGPTから使わせたいなら後者一択。以前あったSSE専用方式は非推奨になっている。
ツール以外の要素
本記事ではツールだけ扱うが、他にもある。
- リソース: 読み取り専用のデータ。ファイルやレコードをURIで公開する
- プロンプト: 定型の指示テンプレート
- サンプリング/エリシテーション: サーバー側からLLMに生成を頼んだり、ユーザーに追加入力を求めたりする
個人サービスを繋ぐ範囲では、ツールだけで十分なことが多い。
4. 公開する: Cloudflare Workersに置く
Cloudflareスタックで運用しているなら、Workers上に建てるのが一番楽だ。CloudflareはMCP向けのライブラリとOAuthプロバイダーを用意していて、AnthropicのドキュメントでもリモートMCPのホスティング先として名前が挙がっている。
最小構成のサーバーはこの程度で書ける。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function buildServer(env: Env, userId: string) {
const server = new McpServer({ name: "my-list", version: "1.0.0" });
server.tool(
"add_item",
"指定したリストに項目を1件追加する。買い物リストへの追加は list に「買い物」を指定する。",
{ list: z.string(), text: z.string() },
async ({ list, text }) => {
await env.DB.prepare(
"INSERT INTO items (user_id, list, text) VALUES (?, ?, ?)"
).bind(userId, list, text).run();
return { content: [{ type: "text", text: `追加しました: ${text}(${list})` }] };
}
);
server.tool(
"list_items",
"指定したリストの未完了項目を一覧で返す。",
{ list: z.string() },
async ({ list }) => {
const { results } = await env.DB.prepare(
"SELECT text FROM items WHERE user_id = ? AND list = ? AND done = 0"
).bind(userId, list).all();
const body = results.map((r) => `- ${r.text}`).join("\n") || "(空です)";
return { content: [{ type: "text", text: body }] };
}
);
return server;
}
これをCloudflareのagentsパッケージが提供するMcpAgentに載せ、/mcpでStreamable HTTPとして待ち受けさせる。userIdは後述の認証で得たトークンから取り出す。
設計上の注意
- ツールは少なく、粗く。RESTのように細かく割ると、LLMが呼び分けを間違える
- 副作用の大きい操作は分ける。
delete_itemをupdate_itemのフラグにしない - 説明文には「いつ使うか」を書く。「項目を追加する」より「ユーザーが買うもの・やることを言ったときに使う」のほうが当たる
- 戻り値は人が読める文にする
5. 認証: なぜ普通のAPIキーでは駄目なのか
ここが一番引っかかるところだ。
自作APIならAPIキーを発行してAuthorization: Bearer xxxで済む。
MCPでも技術的には可能だが、Claude.aiやChatGPTの一般ユーザー向け設定には「固定トークンを入れる欄」が基本的にない。あるのはOAuthか認証なしだ。
理由は、MCPサーバーが「誰が繋ぎに来るか事前に分からない」前提で設計されているからだ。Claude、ChatGPT、Cursor、Claude Code、それ以外。相手ごとに手作業でキーを配るのでは、公開する意味がない。
そこでMCPの認証は、次の3つの標準の組み合わせになっている。
- OAuth 2.1: 認可コードフロー + PKCE必須
- 保護リソースメタデータ(RFC 9728): サーバーが「私の認可サーバーはここ」と名乗る
- 動的クライアント登録(RFC 7591): クライアントが接続時に自分で登録する
認証フロー全体
順に説明する。
(1) 401で場所を教える
最初の呼び出しは認証なしで来る。サーバーは401を返し、WWW-Authenticateヘッダーで「保護リソースメタデータはここ」と示す。クライアントはそのメタデータから認可サーバーのURLを知る。
(2) 認可サーバーのメタデータ
認可サーバーは/.well-known/oauth-authorization-serverで、認可・トークン・登録の各エンドポイントを公開する。クライアントはこれを読んで、あとは自動で進める。
(3) 動的クライアント登録
ここが通常のOAuthと違う。普通は開発者が管理画面でアプリを登録し、client_idを人が受け取る。MCPでは、クライアントが/registerにPOSTして、その場でclient_idを受け取る。人手の登録をAPI化しただけだ。
(4) 認可コード + PKCE あとは普通のOAuth。ユーザーのブラウザで自分のログイン画面が開き、許可すると認可コードが返り、クライアントがトークンと交換する。PKCEは必須で、client_secretは基本使わない(公開クライアント扱い)。
(5) resourceパラメータ
認可リクエストとトークンリクエストに、トークンの宛先(MCPサーバーのURL)をresourceとして付ける(RFC 8707)。発行されたトークンがそのサーバー専用であることを保証するためで、他のサーバーに使い回されるのを防ぐ。
自分で書く部分
多そうに見えるが、フレームワークを使えば自分で書くのはログイン画面と同意画面だけになる。Cloudflareのworkers-oauth-providerは、メタデータ・DCR・トークン発行・PKCE検証を全部持っている。
自分で気をつける点だけ挙げる。
- 同意画面は省略しない。DCRで来るクライアントは事前審査がないので、「Claudeがあなたの買い物リストにアクセスします」と明示するのがユーザーの安全装置になる
- redirect_uriは厳密に検証する。登録時に受け取ったものと完全一致で比較する
- トークンにはユーザーIDとスコープを紐づけ、
/mcp側で毎回検証する - リフレッシュトークンを出すなら、失効の仕組みも用意する
認証なしで公開する選択
読み取り専用のサービスなら、認証なしで公開してもいい。 たとえば辞書のようなデータを「○○で始まる4文字の単語」と聞ける形で出すだけなら、OAuthは要らない。認証なしのMCPサーバーはClaude.aiにURLを貼るだけで繋がる。
家族数人で使う書き込み系でも、推測不能なURL(パスにランダム文字列)で割り切る手はある。ただしURLが漏れたら全部書き換えられるので、それを許容できる範囲に限る。
6. まとめ
- MCPは既存APIの上に載せる通訳。中身は「ツール一覧を返す」「ツールを呼ばれたら実行する」の2つ
- 公開はStreamable HTTPで、Cloudflare Workersに置くのが楽
- 面倒なのは認証だけ。OAuth 2.1 + 保護リソースメタデータ + 動的クライアント登録で、「誰が繋ぎに来るか分からない」問題を解いている
- フレームワークを使えば、自分で書くのはログイン画面と同意画面
自分のサービスにMCPを付けると、ClaudeでもChatGPTでも、今後出てくるどのAIからでも同じように使える入口が一つ増える。 逆に付けなければ、そのサービスは「特定のAIからしか触れない」か「どのAIからも触れない」かのどちらかになる。 Keepが後者だったことで私が逃げたのは、前回書いたとおりだ。