本文へスキップ
AI活用

MCPサーバーの認証ガイド|トークンの置き場所と漏れる場所の設計

Hirokuma
9分で読める
MCPサーバーの認証ガイド|トークンの置き場所と漏れる場所の設計

MCPサーバーの認証設計で最初に決めるべきは、BearerかOAuthかという方式ではありません。資格情報をどこに置き、どこに出さないかです。私は自作MCPサーバーを公開経路に置いて無人運用していますが、置き場所は1つに固定し、出さない場所を列挙して機械的に守る、という順番で設計しています。方式の選択はその後で十分でした。

理由は単純で、資格情報の漏れ口は自分のサーバーコードの外にもあるからです。2026年9月のClaude Code 2.1.274では「MCP設定の変数展開で解決された資格情報が、接続エラーの文面とログインツールの説明に出ていた」という不具合が修正されました。サーバー側をどれだけ固めても、値が通る経路の全部を数えていなければ守れない。この記事では、自作サーバーの実装を例に、その数え方を整理します。

対象読者は、自作MCPサーバーに認証を付けようとしているエンジニアと、チームのMCP設定で資格情報の扱いを決める立場の人です。MCPサーバーの構築自体は、以前の実装記事にまとめてあります。

MCPサーバーの自作入門|Cloudflare Workersで作って本番運用するまで自作MCPサーバーをCloudflare Workersで動かす手順を、実運用中のコードで解説します。公式TypeScript SDKでの最小構成、入力スキーマ・Bearer認証・confirm必須・KVの回数制限という「断る側」の設計、ツール表面を固定するテストまでまとめます。www.tentspace.net

資格情報が通る経路を先に数える

MCP接続で資格情報が現れる場所を、クライアントからサーバーまで順に並べます。

  1. クライアントの設定ファイル: .mcp.json などに書くトークン。チーム共有する設定では環境変数の展開(${VAR} 形式)を使い、ファイル自体に生の値を書かないのが基本です
  2. 展開後のメモリ上の値: 変数が解決された後の生トークン。ここからリクエストヘッダーに載ります
  3. 通信経路: Authorization: Bearer ... ヘッダーとしてHTTPSで運ばれます
  4. サーバー側の検証コード: 受け取った値を期待値と比較する場所
  5. サーバー側の保管場所: サーバー自身が外部APIを叩くために持つトークンの置き場所

設計で決めるのは、この5点それぞれで「値がどこに書き込まれうるか」です。設定ファイルの書き方は既存のセットアップ記事で扱ったので、この記事では3〜5のサーバー側と、後述する「予想外の面」に絞ります。

Claude CodeにMCPサーバーを追加する設定ガイド|3つのスコープと.mcp.jsonの使い分けClaude CodeへMCPサーバーを接続する設定方法を解説します。claude mcp addの基本、local・project・userの3スコープの使い分け、.mcp.jsonのチーム共有と環境変数展開、接続確認と権限設定、サーバーを足しすぎない判断軸をまとめます。www.tentspace.net

サーバー側の検証: 実運用中の実装

自作サーバー(Cloudflare Workers上で運用中)のBearer検証は、これだけの小さな関数です。

export async function authorizeMcpRequest(
  request: Request,
  expectedBearer: string,
  next: NextHandler,
): Promise<Response> {
  const authorization = request.headers.get("Authorization");
  const providedBearer = authorization?.startsWith("Bearer ")
    ? authorization.slice("Bearer ".length)
    : "";

  if (!(await secureEqual(providedBearer, expectedBearer))) {
    return Response.json(
      { error: "unauthorized" },
      { status: 401, headers: { "WWW-Authenticate": "Bearer" } },
    );
  }
  return next();
}

小さい割に、判断は3つ入っています。

  • 比較はタイミングセーフにする: secureEqual は両方の値をSHA-256でハッシュしてから crypto.subtle.timingSafeEqual で比較します。単純な === は一致した文字数で応答時間が変わるため、比較時間から値を推測される余地を作ります。ハッシュを挟むのは、長さの違う入力でも比較を定数時間にするためです
  • 失敗の理由を返さない: 401のボディは { error: "unauthorized" } だけです。「トークンが違う」のか「形式が違う」のかを外に説明しません。正しい呼び出し元は自分のトークンを知っているので、詳細な理由は攻撃側にしか役立ちません
  • 認可より前に何もしない: この関数はすべてのツール処理の前段に置き、通らなければ next() に進みません。認証前のリクエストがログや状態に痕跡を残す設計にしない、という後述の原則と対になります

置き場所は1つ: サーバーの外部トークンはKVだけに置く

このサーバーはSNSの公開APIを叩くので、自分自身も外部サービスのトークンを持ちます。その置き場所はWorkerのKVただ1箇所に固定し、サンドボックス、ローカルの環境変数、コード、会話、ログのどこにも置かない・表示しないルールを運用の憲法にしています。

置き場所を1つにすると、更新と失敗検知もその1箇所に付けられます。実装では、KVに保存したトークンを45日経過で自動更新し、更新失敗が3日分たまったら通知を送る形にしました。ここで効いてくるのがエラーメッセージの情報設計です。トークンが未初期化のときのエラーは次の1行だけです。

Threads token is not initialized in THREADS_TOKEN_KV

場所の名前は出すが、値は出さない。 エラー文は障害対応で人が読み、ログに残り、ときに通知やチケットへコピーされます。つまりエラー文に値を入れた瞬間、置き場所が1つではなくなります。「どこを見ればよいか」だけを言うエラー文は、切り分けの速さと境界の維持を両立させます。

それでも漏れた場所は、クライアント側のエラー表示だった

ここまでがサーバー側の設計です。ところが、この記事で一番伝えたい発見は逆側にありました。資格情報が実際に露出していた場所は、自分のサーバーではなくクライアントのエラー経路だったのです。

Claude Code 2.1.274(2026-09-17公開)の修正にこうあります。設定の変数展開で解決された資格情報(シークレット)が、MCPの接続エラーの文面と、ログインツールの説明文に出ていた。つまり .mcp.json に生の値を書かず ${VAR} で参照するという「正しい」運用をしていても、展開後の値が接続失敗時の表示という予想外の面へ回り込んでいたことになります。

Claude Code Changelog(2.1.274ほか)Release notes for Claude Code, including new features, improvements, and bug fixes by version.code.claude.com

同じリリースには、エラー表示の情報設計に関わる修正がもう1件あります。403 insufficient_scope で拒否されたMCPツール呼び出しが「サインインの期限切れ」と表示されていた不具合です。変更履歴では、修正後はエラーが不足している権限を名指しし、再認証へ導くと説明されています。権限不足を期限切れと表示されると、対応する側は再ログインを繰り返して原因へ辿り着けません。エラー文が原因を丸めると、切り分けの順番ごと狂う。自分のサーバーの401設計で考えたことと同じ論点が、クライアント側でも起きていたわけです。

この2件から持ち帰った設計の含意は2つです。

  • 「出さない場所」のリストに、自分のコードの外を含める: 資格情報は展開された瞬間から、クライアントのエラー文・ツール説明・ログという自分の管理外の面に現れうる。サーバー側の設計だけで完結したと考えない
  • クライアントの更新を認証設計の一部として扱う: 漏れ口の修正はクライアントのリリースで届きます。MCPを無人運用に組み込んでいるなら、クライアントのバージョンを固定しつつ、セキュリティ関連の修正は追随する運用が要ります

認証前に副作用を残さない

もう1つ、クライアント側の変更で設計原則として持ち帰れるものがあります。2.1.265(2026-09-08公開)で、サインインが必要なリモートMCPサーバーに対し、実際に認証するまでOAuthクライアントを登録しない挙動へ変わりました。

それまでは、接続を試しただけで相手側にクライアント登録という痕跡が残る形でした。サーバーを設計する側から見ると、これは鏡写しの原則になります。認証が通る前のリクエストで、状態を作らない・記録を増やさない。 未認証のリクエストがDBに行を作ったり、外部通知を飛ばしたりする設計は、それ自体が攻撃面になります。私のサーバーで認可チェックをすべての処理の前段に置いているのは、この原則の実装です。

資格情報設計のチェックリスト

最後に、この記事の判断をチェックリストにまとめます。

  1. 資格情報が通る経路を、設定ファイルから保管場所まで列挙したか
  2. 設定ファイルには生の値を書かず、変数参照で渡しているか
  3. サーバーの検証はタイミングセーフな比較か。失敗応答は理由を説明していないか
  4. サーバー自身が持つ外部トークンの置き場所は1つか。エラー文は場所の名前だけを出しているか
  5. 認証前のリクエストが、ログ・状態・外部通知に副作用を残していないか
  6. クライアント側の漏れ口修正(エラー文・ツール説明への露出など)に追随する更新運用があるか

方式の議論より先にこの6点を埋めると、BearerでもOAuthでも「どこまで守れているか」を同じ物差しで言えるようになります。認証の強度は方式が決めますが、漏れるかどうかは経路の数え漏れが決める。運用して分かったのは、その順番でした。