社内AIエージェントをMCPサーバーにつなぐ時、最初に詰まりやすいのはツール定義ではなく認可です。個人のAPIキーを環境変数に置くだけなら早い一方、営業情報、顧客DB、請求データ、社内ナレッジのような資産に接続する段階では、誰の権限で、どのMCPサーバーへ、どの範囲の操作を許すかを機械的に説明できる設計が必要になります。
本稿ではMCP 2025-06-18仕様のAuthorizationを前提に、OAuth 2.1系の考え方、Protected Resource Metadata、Resource Indicators、PKCE、audience検証を実装に落とします。ベンチマーク数字や未確認の料金比較は扱わず、公式仕様で確認できる要件だけを使います。
結論:MCPの認可はresourceで固定する
MCP OAuth認可の中心は、ログイン画面を出すことではありません。アクセストークンが特定のMCPサーバー向けに発行され、そのサーバーだけが受け付ける状態を作ることです。ここを曖昧にすると、別サービス向けのトークンを流用するConfused Deputy問題や、ツール越しの権限拡大が起きます。
HTTPベースのMCPでは、保護されたMCPサーバーはOAuthのresource serverとして振る舞い、MCPクライアントはOAuth clientとして動きます。認可サーバーはユーザー確認や同意を担当し、MCPサーバーは受け取ったBearerトークンを検証します。stdio transportではこの認可仕様ではなく環境から資格情報を取得する前提なので、同じ設計をそのまま当てはめません。
まず決めるべき設計単位はMCPサーバーのcanonical URIです。クライアントは認可リクエストとトークンリクエストの両方でresource parameterを渡し、そのトークンがどのMCPサーバー向けかを明示します。
一次情報で確認した仕様の要点
この記事で参照した一次情報は、MCP Authorization仕様、MCP Transports仕様、MCP Tools仕様、RFC 9728、RFC 8414、RFC 8707です。MCPのAuthorizationはHTTPベースtransport向けで、Protected Resource Metadata、Authorization Server Metadata、Resource Indicatorsを組み合わせます。
仕様上、Protected Resource Metadataにはauthorization_serversを少なくとも1つ含めます。401 UnauthorizedではWWW-AuthenticateでメタデータURLを示し、クライアントはそれを解析します。Bearer tokenは毎回Authorizationヘッダーで送り、URI query stringには入れません。
MCPサーバーはトークンの有効性とaudienceを検証します。無効または期限切れのtokenには401、scope不足には403を返すのが基本です。認可コードフローではPKCEを実装し、redirect URIは事前登録値と完全一致で検証します。
設計図:3つの役割を分ける
MCPクライアントは、ユーザーが使うAIエージェントやデスクトップアプリ、社内ポータル内のエージェントです。ツールを呼び出す前に保護されたMCPサーバーへ接続し、401を受けたらメタデータを取りに行き、認可サーバーのauthorization endpointへユーザーを誘導します。
MCPサーバーは、CRM検索、見積作成、契約書取得、社内Wiki検索などのツールを公開します。ここでは誰でもtools/listできるか、tools/callはどのscopeが必要か、読み取りと書き込みを分けるかを決めます。
認可サーバーは、ユーザー認証、同意、クライアント登録、token endpointを担当します。既存IdPを使う場合でも、resource parameterとaudienceをMCPサーバー単位で扱えるかを確認してください。
参照:MCP Authorization / MCP Transports / MCP Tools / RFC 9728 / RFC 8414 / RFC 8707
実装1:Protected Resource Metadataを返す
最初の実装ポイントは、MCPサーバー自身がどの認可サーバーを使うかを公開することです。以下は概念を示す最小例です。本番ではissuer、scope、TLS、キャッシュ、監査ログを整備します。
import express from "express";
const app = express();
app.get("/.well-known/oauth-protected-resource", (_req, res) => res.json({
resource: "https://mcp.example.com/mcp",
authorization_servers: ["https://idp.example.com"],
scopes_supported: ["crm.read", "crm.write", "wiki.search"],
bearer_methods_supported: ["header"]
}));
app.post("/mcp", (req, res, next) => {
if (!req.headers.authorization?.startsWith("Bearer ")) {
res.set("WWW-Authenticate", 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"');
return res.status(401).json({ error: "authorization_required" });
}
next();
});
実装2:401から認可サーバーを発見する
クライアント側は、MCPサーバーのURLだけを知っていれば接続を開始できる設計に近づけます。未認可でアクセスして401を受け、WWW-Authenticateからresource metadata URLを取り出し、authorization_serversを読みます。
import requests, re
mcp_url = "https://mcp.example.com/mcp"
r = requests.post(mcp_url, json={"jsonrpc":"2.0","id":1,"method":"tools/list"})
if r.status_code == 401:
www = r.headers.get("WWW-Authenticate", "")
meta_url = re.search(r'resource_metadata="([^"]+)"', www).group(1)
resource_meta = requests.get(meta_url, timeout=10).json()
issuer = resource_meta["authorization_servers"][0]
as_meta = requests.get(issuer + "/.well-known/oauth-authorization-server", timeout=10).json()
print(as_meta["authorization_endpoint"], as_meta["token_endpoint"])
実装3:resource parameterとPKCEを入れる
通常のOAuth実装に慣れているとscopeだけを見がちですが、MCPではこのMCPサーバー向けのトークンであることを認可リクエストとトークンリクエストの両方に含めます。PKCEは認可コード横取りへの対策として必須です。
from urllib.parse import urlencode
resource = "https://mcp.example.com/mcp"
params = {"response_type":"code","client_id":client_id,
"redirect_uri":"https://agent.example.com/callback",
"scope":"crm.read wiki.search","state":state,
"code_challenge":code_challenge,"code_challenge_method":"S256",
"resource":resource}
auth_url = authorization_endpoint + "?" + urlencode(params)
token_body = {"grant_type":"authorization_code","code":code,
"redirect_uri":"https://agent.example.com/callback",
"client_id":client_id,"code_verifier":code_verifier,
"resource":resource}
実装4:MCPサーバー側のaudience検証
サーバーはBearerトークンを受け取ったら、署名、期限、issuer、scopeだけでなくaudienceを見ます。audienceが自分のcanonical URIと一致しないトークンを受け付けると、別リソース向けのトークンでMCPツールを呼べてしまいます。
def authorize_mcp_request(claims, required_scope):
expected_aud = "https://mcp.example.com/mcp"
if claims.get("aud") != expected_aud: return (401, "invalid_audience")
if claims.get("exp", 0) < now(): return (401, "token_expired")
if required_scope not in set(claims.get("scope", "").split()):
return (403, "insufficient_scope")
return (200, "ok")
社内導入で決めるべきスコープ設計
scopeはツール名をそのまま並べるより、業務リスクに合わせて粗さを決めます。crm.read、crm.write、invoice.read、invoice.issue、wiki.searchのように読み取りと更新を分けると、AIエージェントに見せるだけの段階から安全に始められます。
tools/listを認可前に見せるかどうかも判断点です。社外公開MCPならツール名だけでも攻撃者にヒントを与えるため、認可後に限定する選択があります。社内MCPでも、部署ごとに見えるツールを分けると被害範囲を抑えられます。
scopeは人間の承認画面にも出るため、命名は運用文書と揃えます。顧客情報を読む、見積書を作成するのような説明を認可サーバー側に持たせると、PMや法務がレビューしやすくなります。
失敗パターン:APIキー共有から卒業できない設計
失敗しやすいのは、MCPサーバーに1つの管理者APIキーを置き、全ユーザーのツール呼び出しをそのキーで実行する設計です。PoCでは動きますが、監査ログ上は誰が顧客情報を読んだかが曖昧になり、退職者や権限変更にも弱くなります。
次に危ないのは、tokenをURLクエリに付ける実装です。MCP仕様はBearer tokenをAuthorizationヘッダーで送ることを前提にしており、URI query stringへ入れないと明記されています。アクセスログ、ブラウザ履歴、プロキシのログにtokenが残るリスクがあるためです。
三つ目は、audienceを検証しない実装です。issuerと署名だけを見て通すと、同じIdPが発行した別API向けtokenでも通ってしまう可能性があります。MCPサーバーを複数立てるほど、この差が事故につながります。
運用チェックリスト:公開前に見る10項目
- MCPサーバーのcanonical URIを文書化した
- Protected Resource Metadataにauthorization_serversを入れた
- 401でWWW-Authenticateを返す
- RFC 8414のmetadataを取得できる
- 認可リクエストとトークンリクエストにresourceを入れる
- PKCEとstateを実装した
- Bearer tokenをAuthorizationヘッダーで送る
- audience、issuer、期限、scopeを検証する
- 401と403の使い分けをログに残す
- 読み取りscopeから段階的に展開する
現場メモ:段階導入の進め方
営業CRMへMCPを接続する場合も、最初の一週間は読み取り専用scopeで始めるのが安全です。利用者、呼び出されたtool、resource、scope、応答ステータスを同じログに残すと、AIエージェントの失敗がプロンプト由来なのか、認可設定由来なのかを切り分けやすくなります。第1フェーズでは更新系toolをまだ出さず、承認画面の文言とログ粒度を固めることを優先します。
営業CRMの担当者に確認したいのは、AIに許可したい操作ではなく、AIに許可してはいけない操作です。削除、送信、発注、個人情報の一括取得などはwrite系scopeからさらに分け、MCPサーバー側で二段階承認や人間確認を挟める設計にします。OAuthのscopeだけに頼らず、tool実装の中でも入力値を検証してください。
社内ナレッジへMCPを接続する場合も、最初の一週間は読み取り専用scopeで始めるのが安全です。利用者、呼び出されたtool、resource、scope、応答ステータスを同じログに残すと、AIエージェントの失敗がプロンプト由来なのか、認可設定由来なのかを切り分けやすくなります。第2フェーズでは更新系toolをまだ出さず、承認画面の文言とログ粒度を固めることを優先します。
社内ナレッジの担当者に確認したいのは、AIに許可したい操作ではなく、AIに許可してはいけない操作です。削除、送信、発注、個人情報の一括取得などはwrite系scopeからさらに分け、MCPサーバー側で二段階承認や人間確認を挟める設計にします。OAuthのscopeだけに頼らず、tool実装の中でも入力値を検証してください。
請求・契約へMCPを接続する場合も、最初の一週間は読み取り専用scopeで始めるのが安全です。利用者、呼び出されたtool、resource、scope、応答ステータスを同じログに残すと、AIエージェントの失敗がプロンプト由来なのか、認可設定由来なのかを切り分けやすくなります。第3フェーズでは更新系toolをまだ出さず、承認画面の文言とログ粒度を固めることを優先します。
請求・契約の担当者に確認したいのは、AIに許可したい操作ではなく、AIに許可してはいけない操作です。削除、送信、発注、個人情報の一括取得などはwrite系scopeからさらに分け、MCPサーバー側で二段階承認や人間確認を挟める設計にします。OAuthのscopeだけに頼らず、tool実装の中でも入力値を検証してください。
開発支援へMCPを接続する場合も、最初の一週間は読み取り専用scopeで始めるのが安全です。利用者、呼び出されたtool、resource、scope、応答ステータスを同じログに残すと、AIエージェントの失敗がプロンプト由来なのか、認可設定由来なのかを切り分けやすくなります。第4フェーズでは更新系toolをまだ出さず、承認画面の文言とログ粒度を固めることを優先します。
開発支援の担当者に確認したいのは、AIに許可したい操作ではなく、AIに許可してはいけない操作です。削除、送信、発注、個人情報の一括取得などはwrite系scopeからさらに分け、MCPサーバー側で二段階承認や人間確認を挟める設計にします。OAuthのscopeだけに頼らず、tool実装の中でも入力値を検証してください。
人事FAQへMCPを接続する場合も、最初の一週間は読み取り専用scopeで始めるのが安全です。利用者、呼び出されたtool、resource、scope、応答ステータスを同じログに残すと、AIエージェントの失敗がプロンプト由来なのか、認可設定由来なのかを切り分けやすくなります。第5フェーズでは更新系toolをまだ出さず、承認画面の文言とログ粒度を固めることを優先します。
人事FAQの担当者に確認したいのは、AIに許可したい操作ではなく、AIに許可してはいけない操作です。削除、送信、発注、個人情報の一括取得などはwrite系scopeからさらに分け、MCPサーバー側で二段階承認や人間確認を挟める設計にします。OAuthのscopeだけに頼らず、tool実装の中でも入力値を検証してください。
カスタマーサポートへMCPを接続する場合も、最初の一週間は読み取り専用scopeで始めるのが安全です。利用者、呼び出されたtool、resource、scope、応答ステータスを同じログに残すと、AIエージェントの失敗がプロンプト由来なのか、認可設定由来なのかを切り分けやすくなります。第6フェーズでは更新系toolをまだ出さず、承認画面の文言とログ粒度を固めることを優先します。
カスタマーサポートの担当者に確認したいのは、AIに許可したい操作ではなく、AIに許可してはいけない操作です。削除、送信、発注、個人情報の一括取得などはwrite系scopeからさらに分け、MCPサーバー側で二段階承認や人間確認を挟める設計にします。OAuthのscopeだけに頼らず、tool実装の中でも入力値を検証してください。
監査ログ:あとから説明できる形に残す
本番導入で最後に効いてくるのは監査ログです。MCPサーバーのログには、ユーザーID、client_id、resource、scope、tool名、tool引数の要約、HTTPステータス、correlation idを残します。引数の全文を保存すると個人情報を抱え込みすぎる場合があるため、検索語やレコードIDだけを残す、本文はマスクする、保存期間を決めるといったルールも合わせて設計します。
AIエージェントの事故調査では、LLMがなぜそのtoolを選んだか、MCPサーバーがなぜ許可したか、業務システムがどの更新を受け付けたかを分けて見る必要があります。OAuth認可のログは二つ目のレイヤーを説明する材料です。プロンプトログだけを見ても、認可の設定ミスやscope過多は見つけにくいので、MCPサーバー側で独立して記録してください。
ログは検知にも使えます。短時間にtools/listを繰り返す、普段使わないwrite scopeを急に要求する、同じユーザーが複数のMCPサーバーへ横断的にアクセスする、といった兆候はアラート対象です。OAuthの設計と監査ログを結びつけると、AIエージェントの便利さを落とさずに運用リスクを下げられます。
PM向け判断基準:PoCから本番へ進める条件
PoCでは、MCPクライアントとMCPサーバーが接続できることに注目しがちです。しかし本番判断では、接続成功だけでは足りません。resource parameterを使ったtoken発行、audience検証、scope分離、PKCE、redirect URI検証、ログ保存、無効token時の401、scope不足時の403まで確認できて初めて、業務データに近づける準備が整ったと見なせます。
逆に、まだ管理者APIキーを共有している、ユーザー単位のログが残らない、scopeがread/writeで分かれていない、tokenをクエリ文字列で送っている、audienceを見ていない場合は、本番公開を止めるべきです。AIエージェントは自律的にtoolを選ぶため、人間が画面でボタンを押す通常アプリよりも、権限の境界が見えにくくなります。
最初の本番ユースケースは、社内Wiki検索、FAQ検索、CRMの参照など、読み取り中心で効果が出る領域を選ぶのが無難です。見積作成、メール送信、請求書発行のような外部影響があるtoolは、認可に加えて人間確認、承認キュー、取り消し手順を用意してから解放します。
小さく始める実装順序
実装順序は、認可なしのMCPサーバーを作ってから後でOAuthを足すより、最初から保護されたエンドポイントとして作る方が安全です。まずmetadata endpointと401応答を作り、次にクライアントのdiscoveryを通し、最後にtools/listとtools/callのscopeを分けます。この順序なら、toolが増えても認可境界を後から探す作業が減ります。
ローカル検証では、正しいtoken、期限切れtoken、別audienceのtoken、scope不足token、tokenなしの五種類を用意します。期待値はそれぞれ200、401、401、403、401です。このテストをCIに入れておくと、MCPサーバーの改修で認可チェックが抜けた時に早めに気づけます。
運用開始後は、scope追加の申請フローを決めておきます。AIエージェントの改善要望は、便利さを理由にwrite権限へ寄りがちです。要望を受けたら、既存read scopeで代替できないか、human-in-the-loopで足りないか、ログと承認画面の文言を更新する必要があるかを確認してから開放します。
なお、この記事の実装例は仕様理解のための最小構成です。本番では利用するIdPの仕様、鍵ローテーション、監査要件、個人情報の保存方針を合わせて確認し、定期的に棚卸ししてください。
最後に、認可設計は一度作って終わりではありません。MCPサーバー、利用者、scope、接続先の棚卸しを月次で行い、不要な権限を落とす運用まで含めて本番設計と考えます。
関連記事・次に読む
MCPの全体像から確認したい場合はClaude MCP入門、ブラウザや端末操作まで含むエージェント設計はComputer Use API実装ガイド、チェーン設計はLangChain LCEL完全ガイドが参考になります。
まとめ:認可を後付けにしない
MCP OAuth認可は、AIエージェントを業務システムにつなぐ時の安全装置です。ツール定義やプロンプトの前に、resource、audience、scope、PKCE、metadata discoveryを決めておくと、PoCから本番へ移る時の作り直しが減ります。
特に社内AIエージェントでは、最初から全権限を渡す必要はありません。検索系ツールだけをread scopeで公開し、ログと承認フローを見ながらwrite系を追加する方が、導入速度と安全性のバランスを取りやすいです。
MCPはツール連携を標準化しますが、標準化された接続口は攻撃者にも理解しやすい接続口です。だからこそ、公式仕様に沿ったOAuth認可を早い段階で入れておく価値があります。
この記事を読んで導入イメージが固まってきた方へ
UravationではAIエージェント導入の研修・コンサルを行っています。MCPサーバー設計、認可設計、社内データ連携のPoC設計までお気軽にご相談ください。
