前回の記事で「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サーバー: 自作サービス

MCPサーバー: Todoist

ホスト: Claude.ai など

LLM

MCPクライアント

MCPクライアント

add_task / find_tasks

add_item / list_items

既存のREST API / DB

ポイントは、MCPサーバーは既存APIの前に立つ「通訳」だという点だ。 中でやっているのは、LLMから来た「add_item」という呼び出しを、自分のAPIやDBへのクエリに変換しているだけ。

2. 通常のAPIと何が違うのか

APIを設計したことがある人なら、「JSONを返すエンドポイントと何が違うのか」と思うはずだ。違いは「誰が読むか」に集約される。

通常のREST APIMCPサーバー
読む相手プログラマーLLM
使い方の伝え方ドキュメントを人が読むサーバー自身がツール一覧と説明を返す
エンドポイント用途ごとに複数1つ。中でツール名で振り分け
設計単位リソース(GET /tasks)意図(find_tasks)
通信HTTP + JSONJSON-RPC 2.0(HTTP上に載せる)
認証自由OAuth 2.1がほぼ標準

一番大きいのは「サーバー自身が使い方を説明する」ことだ。 クライアントが接続すると、まずtools/listでツールの一覧を取りにいく。返ってくるのはツール名・説明文・引数のJSON Schemaで、LLMはこれを読んで「今このユーザーの依頼に対して、どのツールをどんな引数で呼ぶか」を判断する。

つまりツールのdescriptionは事実上プロンプトになる。ここの書き方がそのまま使い勝手を決める。

3. 接続からツール実行までの流れ

自分のAPI / DBMCPサーバーホスト(Claude)ユーザー自分のAPI / DBMCPサーバーホスト(Claude)ユーザーLLMがツール一覧を読み add_item を選ぶinitialize(プロトコルバージョン, 対応機能)サーバー情報, 対応機能notifications/initializedtools/listツール一覧(名前, 説明, 引数スキーマ)「買い物リストにきゅうり追加して」tools/call add_itemINSERT / POSTOK「追加しました: きゅうり」「きゅうりを買い物リストに追加しました」

initializetools/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のホスティング先として名前が挙がっている。

Cloudflare

tools/call

認可

Worker: /mcp

Worker: OAuth(/authorize, /token, /register)

KV: トークン・クライアント登録

DB: サービスのデータ

Claude.ai / ChatGPT

最小構成のサーバーはこの程度で書ける。

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_itemupdate_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): クライアントが接続時に自分で登録する

認証フロー全体

ユーザー(ブラウザ)認可サーバーMCPサーバー(/mcp)Claude.ai(クライアント)ユーザー(ブラウザ)認可サーバーMCPサーバー(/mcp)Claude.ai(クライアント)POST /mcp(トークンなし)401 + WWW-AuthenticateGET /.well-known/oauth-protected-resource認可サーバーのURLGET /.well-known/oauth-authorization-server認可・トークン・登録の各エンドポイントPOST /register(client_name, redirect_uris)client_idブラウザで /authorize を開くログイン、「Claudeにアクセスを許可」redirect_uri へ認可コード認可コードを渡すPOST /token(code, code_verifier)access_token, refresh_tokenPOST /mcp + Bearer access_token200(以降は通常のツール呼び出し)

順に説明する。

(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が後者だったことで私が逃げたのは、前回書いたとおりだ。