MCPツール承認の設計:人が確認すべき呼び出し、クライアント別の自動承認設定、サーバー側の確認ゲート【MCP Security 2026】
MCPのツール呼び出しのうち、どれに人の承認が必要かを決める方法。2026-07-28版仕様のルール、確認の93%が承認される「承認疲れ」、4段階の承認ティア、Claude Code・Cursor・VS Code・Gemini CLI・OpenAIの自動承認設定、引数に結びつけた検証済みの確認ゲート。
最終更新:2026年10月28日 / 対象:MCP仕様 2026-07-28版(Tools、Elicitation、Multi Round-Trip Requests)、Claude Code、Claudeのコネクタ、VS Code、Cursor、Gemini CLI、OpenAI Responses API
結論(30秒で読める要約)
MCPのツール承認(ヒューマン・イン・ザ・ループ)とは、モデルが「このツールを呼ぶ」と決めてから実際に実行されるまでの間に、人の確認をはさむ仕組みです。MCP仕様はこれを求めていますが、レベルはSHOULD(推奨)にとどまり、具体的なやり方は各クライアントに任されています。そのため、MCP Security(MCPセキュリティ)の現場では「削除系のツールには人の確認が必要」という点では誰もが合意しているのに、「どのツールを、どのクライアントで、どこで強制するか」が決まっていない、という状態がよく起きます。
この記事では、その空白を埋めます。
- 仕様のルールを1つの表に整理。 人の確認はクライアント側のSHOULDです。
readOnlyHintなどのツール注釈は、信頼できるサーバー以外からのものは信用してはならない(MUST)とされています。入力の検証とアクセス制御はサーバーの義務(MUST)です(2章)。 - 確認ダイアログだけでは足りない理由。 Anthropicによると、Claude Codeのユーザーは許可プロンプトの93%を承認しています。分類器を使う自動モードでも、実際の「やりすぎ」操作の17%を見逃します(3章)。
- 4段階の承認ティア。 自由に実行してよい呼び出し、毎回確認する呼び出し、サーバー側の確認が必要な呼び出し、エージェント用の資格情報からは届かないようにすべき呼び出しを分けます(4章)。
- 主要6クライアントの比較。 Claude Code、Cursor、Gemini CLI、OpenAI Responses APIはそのまま使える設定例つきです(5章)。
- サーバー側の確認ゲート(検証済み)。 form形式のelicitationを使い、承認を「ユーザー」「ツール」「引数そのもののハッシュ」に結びつけます。検証では、改ざん系の6ケースをすべて拒否し、確認画面を出せない2種類のクライアントでは安全側に止まりました(7章)。
目安は「確認はクライアントに、判断はサーバーに、上限は資格情報に」です。すべての確認をオフにしたクライアントから呼ばれても安全でいられるように、削除系のツールを設計しておきます。
目次
- MCPのツール承認とは
- MCP仕様(2026-07-28版)が定めていること
- 確認ダイアログだけでは防げない理由
- 承認ティア:どの呼び出しに人の確認が必要か
- 主要6クライアントのツール承認の仕組み
- ツール注釈:readOnlyHintとdestructiveHintでできること・できないこと
- 検証:elicitationを使ったサーバー側の確認ゲート
- 承認しやすいツールの設計
- ツール承認のためのMCPセキュリティ・チェックリスト(14項目)
- よくある失敗(症状→原因→対処)
- NodeFlareならこう解決できる
- よくある質問
1. MCPのツール承認とは
MCPのツール承認とは、tools/callを一度止め、人がツール名と引数を確認したうえで許可または拒否するまで実行させない仕組みです。これが必要なのは、MCPのツールが仕様の言葉でいう「model-controlled(モデルが制御する)」ものだからです。いつツールを呼ぶかはモデルが決めるので、モデルに影響を与えられるもの、たとえばツールの結果に含まれる文章も、どの呼び出しが行われるかに影響を与えられます。
承認を置ける場所は3つあり、それぞれ破られ方が違います。
| 層 | 誰が強制するか | 例 | 破られるとき |
|---|---|---|---|
| クライアントの確認 | MCPクライアント(Claude Code、Cursor、VS Codeなど) | 「delete_projectを許可しますか?」のダイアログ |
ユーザーが読まずに承認する、または自動承認がオン |
| サーバー側のゲート | 自社のMCPサーバー | ツールがelicitationを返し、確認済みの再リクエストを待つ | クライアントがelicitationを自動で承認する、またはゲートが引数に結びついていない |
| 資格情報の上限 | 自社の認可の仕組み | エージェントのトークンにdelete_projectのスコープがない |
全員が万能のトークンを1つ共有している |
クライアントの確認は、あくまで使い勝手のための機能です。残りの2つが、自分たちで持てるセキュリティ対策です。この記事は、社内SaaSをAPIトークン付きのMCPサーバーとして公開するなど、自社のMCPサーバーを提供していて、利用者がどのクライアントを選んでも書き込み・削除系のツールを安全に保ちたいチームを想定しています。
2. MCP仕様(2026-07-28版)が定めていること
2026-07-28版のToolsのページは、人の確認をクライアントへの「推奨」としたうえで、サーバーには明確な義務を課しています。
| ルール(2026-07-28版) | レベル | 対象 | 出典 |
|---|---|---|---|
| ツールの呼び出しを拒否できる人が、常に介在すべき | SHOULD | クライアント | Tools:User Interaction Model |
| どのツールが公開されているかを示し、呼び出し時に表示し、確認を求める | SHOULD | クライアント | Tools:User Interaction Model |
| 重要な操作では利用者に確認を求め、サーバーを呼ぶ前にツールの入力を利用者に見せる | SHOULD | クライアント | Tools:Security Considerations |
| 信頼できるサーバー以外からのツール注釈は信用しない | MUST | クライアント | Tools:Data Types |
| すべての入力を検証し、適切なアクセス制御を実装し、呼び出しにレート制限をかけ、出力を無害化する | MUST | サーバー | Tools:Security Considerations |
tools/callにInputRequiredResult(elicitationなど)で応答し、再リクエストで処理を完了してよい |
MAY | サーバー | Tools:Input Required Tool Results |
| form形式のelicitationで、パスワード、APIキー、アクセストークン、決済情報を求めてはならない | MUST NOT | サーバー | Elicitation |
| どのサーバーが入力を求めているかを明示し、拒否・キャンセルの選択肢を用意する | MUST | クライアント | Elicitation |
ここから2つのことが言えます。1つ目は、仕様に沿ったクライアントでも確認を省略してよい、という点です。つまりサーバーは「人が必ず見ている」と仮定できません。2つ目は、サーバー自身が確認を求める標準の手段がある、という点です。2026-07-28版では、ツールがresultType: "input_required"とelicitation/createリクエストを返し、クライアントは利用者の回答とサーバーのrequestStateを付けて同じtools/callを再送します。7章はこの仕組みを使います。
3. 確認ダイアログだけでは防げない理由
確認ダイアログが効かない一番の理由は、人がほとんどすべてを承認してしまうことです。AnthropicはClaude Codeの自動モードに関する技術記事(2026年3月25日)で、ユーザーは手動の確認の93%を結局承認していると書き、この現象を「承認疲れ(approval fatigue)」、つまり何を承認しているのかに注意を払わなくなる状態だと説明しています。
人の代わりに分類器を使うと改善はしますが、それも境界線にはなりません。同じ記事が誤判定率を公開しています。
| 自動モードの段階(Anthropic、2026年3月) | 実際の通信での誤ブロック | 実際の「やりすぎ」操作の見逃し | 合成したデータ持ち出しの見逃し |
|---|---|---|---|
| 第1段階(高速フィルタ) | 8.5% | 6.6% | 1.8% |
| パイプライン全体 | 0.4% | 17% | 5.7% |
Anthropicはこの17%を「正直な数字」と呼び、自動モードは重要なインフラに対する人の慎重なレビューの代わりにはならない、と明記しています。
OWASPも、人の側のこの問題をエージェント型アプリケーションのTop 10でASI09:Human-Agent Trust Exploitation(人とエージェントの信頼の悪用)として挙げています。要約は「自信に満ちた、よくできた説明が、人のオペレーターを誤らせて有害な操作を承認させた」というもので、承認の流れがソーシャルエンジニアリングで迂回されないかを検証するよう勧めています。
SupabaseのMCPの事例(General Analysis、2025年7月)は、これが実際にどう起きるかを示しています。開発者のIDEのエージェントがservice_roleでSQLを実行できる状態で、仕込まれた指示を含むサポートチケットを読み、integration_tokensテーブルを照会し、その結果をチケットに書き戻しました。著者のまとめは「権限は1つも侵害されていない。エージェントは、信用すべきでない指示に従っただけだ」です。推奨されている対策は、まず届く範囲を絞ること(サーバーを1プロジェクトに限定、read_only=true、ツール群の制限)、次にレビュー(提案されたクエリと書き込み先を人が確認)です。フィルタは「データベースの権限を与えることも制限することもできない」とも書かれています。
MCP Securityとしての教訓は、承認はあった方がよいが、その下に「最悪の操作をそもそもできない資格情報」を敷いておく必要がある、ということです。
4. 承認ティア:どの呼び出しに人の確認が必要か
すべてのツールを、名前ではなく「間違って呼ばれたら何が起きるか」で4つのティアに分けます。そのうえで、各層の設定を下の表から決めます。
| ティア | ツールがすること | 例 | クライアントの設定 | サーバー側の対策 | 資格情報 |
|---|---|---|---|---|---|
| 0. 読み取り(閉じた範囲) | 自社データを読むだけで副作用なし | list_projects、get_ticket、search_docs |
自動承認 | 入力検証、ユーザーごとの認可 | エージェントのトークンには読み取りスコープ |
| 1. 読み取り(外部・信頼できない内容) | 外部URLや利用者が投稿した文章を取得する | fetch_url、read_inbox、get_public_issue |
セッションごとに1回確認 | 結果を「信頼できないデータ」として扱う、サイズ制限 | 可能ならティア2・3のツールと同じセッションに置かない |
| 2. 元に戻せる書き込み | 作成・編集で、取り消しが可能 | create_draft、update_ticket、add_label |
毎回確認し、引数をすべて表示 | 冪等キー、監査ログ | 必要な場所だけ書き込みスコープ |
| 3. 破壊的・外部送信・費用が発生 | 削除、外部への送信、支払い、権限変更 | delete_project、send_email、refund_order、grant_role |
毎回確認し、「常に許可」は使わない | 引数に結びついたサーバー側の確認(7章) | 通常のエージェント用トークンには含めない。別のキーか段階的なスコープ |
| 4. エージェントに渡さない | 大規模で取り消せない、またはセキュリティ上重要 | DB削除、ルートキーのローテーション、テナントをまたぐ一括削除 | 公開しない | ツールとして実装しない | どのエージェント用資格情報にも持たせない |
ティア分けを形だけにしないためのルールが3つあります。
- 最悪のケースで分類する。 汎用の
execute_sqlやrun_commandは、ほとんどの呼び出しがSELECTでもティア3です。引数を決めるのはモデルだからです。一部を自動承認したいなら、狭いツールに分割します。 - 組み合わせに注意する。 ティア1(信頼できない入力)とティア3(外部送信)を同じセッションに持つと、仕込まれた指示がデータの持ち出しにつながります。この「致命的な三要素(lethal trifecta)」はMCPのtool poisoning対策の記事で詳しく扱っています。
- ティア3のツールは別の資格情報の後ろに置く。 そうすれば、確認をオフにしたクライアントや、スケジュール実行のエージェントからは、そもそも呼べません。呼び出し元ごとに狭いスコープのキーを発行する方法はMCPのAPIキー管理の記事にまとめています。
5. 主要6クライアントのツール承認の仕組み
主要なクライアントはどれもツール単位の承認を持っていますが、同時にどれも「承認をオフにする」スイッチを持っています。2026年10月時点の各クライアントの公式ドキュメントの記述をまとめると次のとおりです。
| クライアント | ツール単位の制御 | 「すべて承認」のスイッチ | 管理者によるロック |
|---|---|---|---|
| Claude Code | permissionsのallow/ask/denyルール(例:mcp__crm__delete_project)。deny→ask→allowの順に評価 |
bypassPermissionsモード。autoモードは人の代わりに分類器が判断 |
管理設定のdisableBypassPermissionsModeとdisableAutoMode |
| Claudeのコネクタ(Claudeアプリ、Team・Enterpriseプラン) | オーナーが権限ごと・カテゴリごとに「常に許可」「承認が必要」「ブロック」を設定 | 常に許可 | 組織全体に適用され、個々のユーザーは上書きできない。この「承認が必要」の設定がClaude Codeに届いている場合、autoやbypassPermissionsモードでもそのコネクタのツールは毎回確認される |
| VS Code | 1回だけ・セッション・ワークスペース・今後すべて、の単位で承認。chat.tools.eligibleForAutoApprovalでfalseにしたツールは常に手動承認 |
「Allow all」「Autopilot」、chat.tools.global.autoApprove |
デバイス管理のポリシーで特定ツールの手動承認を強制できる |
| Cursor | permissions.jsonのmcpAllowlist(例:crm:get_*) |
「Run Everything」の実行モード | チーム管理者の設定がpermissions.jsonより優先 |
| Gemini CLI | サーバーごとのincludeTools/excludeTools、「このツールを常に許可」 |
"trust": trueでそのサーバーの確認をすべて省略(既定はfalse) |
この記事では扱わない |
| OpenAI Responses API | require_approvalに{"never": {"tool_names": [...]}} |
"require_approval": "never" |
アプリケーションのコードで決める |
特に重要な点が2つあります。Cursorのドキュメントは、許可リストを「ベストエフォートの便利機能」であり、セキュリティの保証ではないと明記しています。またVS Codeは、呼び出しの承認と、その結果をチャットに入れることの承認(post-approval)を分けています。ここに挙げた中で、ツールの入力ではなく「信頼できないツールの出力」を対象にしたクライアント側の制御はこれだけです。
ティア分けしたサーバー向けの設定例
crmという名前のサーバーに、ティア0のlist_projectsとget_project、ティア2のupdate_project、ティア3のdelete_projectがあるとします。
Claude Code(.claude/settings.json、リポジトリにコミットして共有):
{
"permissions": {
"allow": ["mcp__crm__list_projects", "mcp__crm__get_*"],
"ask": ["mcp__crm__update_project"],
"deny": ["mcp__crm__delete_project"]
}
}
allowでワイルドカードが使えるのは、mcp__<サーバー名>__という固定の接頭辞の後ろだけです。allowにmcp__*のような接頭辞のないワイルドカードを書くと、警告とともに無視されます。denyが最初に評価されるので、別の設定ファイルのallowルールでdelete_projectを再び許可することはできません。
Cursor(.cursor/permissions.json。実行モード(Run Mode)が有効なときだけ効きます):
{
"mcpAllowlist": ["crm:list_projects", "crm:get_*"]
}
Gemini CLI(.gemini/settings.json。httpUrlはStreamable HTTPのエンドポイント):
{
"mcpServers": {
"crm": {
"httpUrl": "https://crm.example.com/mcp",
"trust": false,
"excludeTools": ["delete_project"]
}
}
}
OpenAI Responses API(ツール定義):
{
"type": "mcp",
"server_label": "crm",
"server_url": "https://crm.example.com/mcp",
"require_approval": { "never": { "tool_names": ["list_projects", "get_project"] } }
}
これ以外のツールを呼ぼうとすると、ツール名と引数を含むmcp_approval_requestが返ります。アプリケーションはapproval_request_idとapprove(trueまたはfalse)を入れたmcp_approval_responseで答えます。既定は「承認が必要」で、OpenAIはリモートMCPサーバーに渡るデータをすべて確認し、必要に応じて記録するよう勧めています。
各クライアントの接続設定(URL、ヘッダー、OAuth)はリモートMCPクライアント設定のチートシートを参照してください。
6. ツール注釈:readOnlyHintとdestructiveHintでできること・できないこと
ツール注釈は、サーバーがツールに付ける「ヒント」です。クライアントはこれを見て、読み取り専用のツールは自動承認する、といった既定の動きを決められます。ただし権限ではありません。仕様は、信頼できるサーバー以外からのツール注釈を信用してはならない(MUST)と定めています。
| 注釈 | 意味 | 未設定のときの扱い |
|---|---|---|
readOnlyHint |
環境を変更しない | false |
destructiveHint |
破壊的な更新をすることがある | true |
idempotentHint |
同じ引数で何度呼んでも追加の影響がない | false |
openWorldHint |
外部の世界とやり取りすることがある | true |
(既定値は、MCP公式C# SDKのToolAnnotationsリファレンスの記述にもとづきます。)
正しい使い方は立場ごとに違います。
- サーバーを作る側は、すべてのツールに正確な注釈を付ける。 注釈のないツールは「外部に影響し、破壊的かもしれない書き込み」とみなされるので、クライアントは毎回確認しがちになります。安全ではありますが確認が増え、承認疲れの原因になります。
- クライアントを使う側は、審査していないサーバーの注釈を自動承認の根拠にしない。 悪意のあるサーバーは、
delete_everythingにreadOnlyHint: trueと書くこともできます。 - 組織としては、自分たちでデプロイしたサーバーの注釈だけを信用する。 その場合は「
readOnlyHint: trueなら自動承認」としてよいでしょう。ただし、その裏のトークンにはそもそも書き込みスコープを持たせないことが前提です。
7. 検証:elicitationを使ったサーバー側の確認ゲート
サーバー側の確認ゲートとは、ティア3のツールが最初の呼び出しでは何も実行しないようにする仕組みです。何が起きるかを具体的に書いたform形式のelicitationを返し、クライアントが「承認」の回答と、サーバーが署名した有効なrequestStateを付けて再リクエストしてきたときだけ実行します。確認が行われたかどうかを、クライアントではなくサーバーが判断する、というのが要点です。
下のゲートは、承認を5つのものに結びつけます。検証済みのユーザー(アクセストークンのsub)、ツール、引数そのもののSHA-256ハッシュ、5分の有効期限、1回限りのノンスです。Pythonの標準ライブラリだけで書かれ、2026-07-28版のmulti round-trip(複数往復)の形式に沿っています。
"""Server-side confirmation gate for a destructive MCP tool (MCP 2026-07-28, form-mode elicitation).
Standard library only. The approval is bound to: the verified user (sub), the tool, a hash of the
exact arguments, an expiry, and a single-use nonce. Tested with Python 3.13."""
import base64, hashlib, hmac, json, secrets, time
SECRET = secrets.token_bytes(32) # load from your secret store in production
TTL = 300 # the user has 5 minutes to answer
_used: set[str] = set() # use Redis/DB with expiry in production
def _b64(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
def _unb64(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def _args_hash(args): return hashlib.sha256(json.dumps(args, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def _sign(state):
body = json.dumps(state, separators=(",", ":")).encode()
return _b64(body) + "." + _b64(hmac.new(SECRET, body, hashlib.sha256).digest())
def _verify(token):
try:
body_s, mac_s = token.split(".")
body = _unb64(body_s)
except ValueError:
raise PermissionError("malformed")
if not hmac.compare_digest(_unb64(mac_s), hmac.new(SECRET, body, hashlib.sha256).digest()):
raise PermissionError("bad signature")
return json.loads(body)
def delete_project(params, sub, client_caps, now=None):
"""Handle tools/call for delete_project. `sub` comes from the VERIFIED access token."""
now = now or time.time()
args = params["arguments"]
if "inputResponses" not in params: # first call: ask, do nothing yet
el = client_caps.get("elicitation")
if el is None or (el and "form" not in el): # {} means form mode (spec)
return {"resultType": "complete", "isError": True, "content": [{"type": "text",
"text": "This tool needs a human confirmation, but your client cannot show one. "
"Delete the project in the web console instead."}]}
state = {"sub": sub, "tool": "delete_project", "ah": _args_hash(args),
"exp": int(now) + TTL, "n": secrets.token_urlsafe(12)}
return {"resultType": "input_required", "requestState": _sign(state), "inputRequests": {
"confirm": {"method": "elicitation/create", "params": {"mode": "form",
"message": f"Permanently delete project '{args['project_id']}' and its "
f"{args.get('expected_items', '?')} items? This cannot be undone.",
"requestedSchema": {"type": "object", "properties": {
"confirm": {"type": "boolean", "title": "Yes, delete it", "default": False}},
"required": ["confirm"]}}}}}
# retry: verify the approval belongs to THIS user, THIS tool and THESE arguments
try:
st = _verify(params.get("requestState", ""))
except Exception as e:
return _deny(f"invalid approval ({e})")
if st["exp"] < now: return _deny("approval expired")
if st["sub"] != sub: return _deny("approval belongs to another user")
if st["tool"] != "delete_project": return _deny("approval was for another tool")
if st["ah"] != _args_hash(args): return _deny("arguments changed after approval")
if st["n"] in _used: return _deny("approval already used")
_used.add(st["n"])
resp = params["inputResponses"].get("confirm", {})
if resp.get("action") != "accept" or resp.get("content", {}).get("confirm") is not True:
return {"resultType": "complete", "content": [{"type": "text", "text": "Cancelled by the user. Nothing was deleted."}]}
return {"resultType": "complete", "content": [{"type": "text", "text": f"Deleted {args['project_id']}."}]}
def _deny(why):
return {"resultType": "complete", "isError": True, "content": [{"type": "text", "text": f"Not executed: {why}."}]}
2026年10月28日にPython 3.13で12ケースを試しました。承認した場合だけ削除され、「いいえ」の2パターンでは何も削除されず、改ざん系の6ケースはすべて拒否、確認画面を出せない2種類のクライアントでも実行されませんでした。
| ケース | 結果 |
|---|---|
| 最初の呼び出し | 確認フォーム付きのinput_required。何も削除しない |
| 利用者が承認し、チェックを入れる | 削除 |
| 利用者が拒否する | キャンセル。何も削除しない |
| 承認したがチェックを入れていない | キャンセル。何も削除しない |
確認後、再リクエストでモデルがproject_idを差し替える |
拒否:承認後に引数が変わった |
| 別のユーザーが承認を使い回す | 拒否:別ユーザーの承認 |
| 同じ承認を2回使う | 拒否:使用済み |
| 6分後に回答が届く | 拒否:期限切れ |
requestStateを書き換える |
拒否:署名が不正 |
requestStateなしで「承認」だけ送る |
拒否:形式が不正 |
| elicitationに対応していないクライアント | 実行せず、Webの管理画面へ案内 |
| URL形式だけに対応したクライアント | 実行せず、Webの管理画面へ案内 |
このゲートで証明できること・できないことを整理しておきます。
- 証明できること:実行されるのが、表示されたとおりの呼び出しであり、表示されたユーザー本人のもので、直近に1回だけ承認されたものであること。引数の差し替えのケースは、多くの「本当によろしいですか?」の実装が見落とす点です。ツールに対して確認していても、引数の組み合わせに対しては確認していないからです。
- 証明できないこと:人が実際にクリックしたこと。elicitationを自動で承認するクライアントなら通ってしまいます。仕様はクライアントに、どのサーバーが求めているかの表示と、拒否・キャンセルの選択肢を義務づけていますが、クライアントの画面をサーバーから確かめることはできません。
- 最もリスクの高い操作では、代わりにURL形式のelicitationで自社ドメインの承認ページへ誘導し、そのページでブラウザのログインセッションと
subを照合します。elicitationの仕様がフィッシング対策として求めているのと同じ方法です。ページは帯域外なので、クライアントもモデルも中身を見たり操作したりできません。 - 安全側に止める。 フォームを出せないクライアントには、黙って実行するのではなくエラーを返します。仕様でも、クライアントが宣言していない形式のelicitationを送ってはならない(MUST NOT)とされています。
8. 承認しやすいツールの設計
承認の質は、人に見えるものの質で決まります。多くの確認ダイアログはツール名と生のJSON引数しか表示しないので、忙しい人が2秒で判断できる引数にしておくことが大切です。
| 設計の工夫 | 承認にどう効くか | 例 |
|---|---|---|
| 読み取りと書き込みのツールを分ける | 書き込みを開放せずに、読み取りだけ自動承認できる | project(action=...)ではなくget_projectとupdate_project |
| 何でも実行できるツールを狭いツールに置き換える | 狭い引数なら確認できるが、SQLやシェルの文字列は確認できない | execute_sql(query)ではなくarchive_project(project_id) |
| 影響の見込みをモデルに書かせる | 見込みと実際が食い違えば、それが止める合図になる | expected_items: 318。実際の件数と違えばサーバーが拒否 |
| 計画を作ってから確定する | 意図ではなく、具体的な差分を承認できる | plan_bulk_updateが計画IDを返し、apply_plan(plan_id)で実行 |
| 一度に処理する件数に上限を設ける | 1回の承認で1万件を消せないようにする | サーバー側でmax_itemsを100までに制限 |
| 書き込みに冪等キーを付ける | 承認が再送されても二重課金・二重送信にならない | refund_orderにidempotency_keyを必須にする |
| 人が読める識別子を引数に含める | 人はUUIDではなく名前で判断する | project_idと一緒にproject_nameを渡し、一致を確認 |
| 注釈を正確に付ける | 本当に安全なツールについては、クライアントが確認をやめられる | 純粋な読み取りツールには必ずreadOnlyHint: true |
「影響の見込み」は7章のゲートと相性がよい方法です。expected_itemsをハッシュ対象の引数に含め、確認メッセージに表示し、実際の件数と違えばサーバーが拒否するようにします。
9. ツール承認のためのMCPセキュリティ・チェックリスト(14項目)
分類
- [ ] すべてのツールに、ティア(0〜4)がコードかドキュメントで書かれている
- [ ] ティア4の操作はツールとして公開していない
- [ ] 汎用の実行ツール(
execute_sql、run_command)はティア3として扱うか、狭いツールに分割している
サーバー
- [ ] すべてのツールに正確な
readOnlyHint、destructiveHint、idempotentHint、openWorldHintを付けている - [ ] ティア3のツールは、ユーザー・ツール・引数のハッシュ・期限・1回限りのノンスに結びついたサーバー側の確認を使っている
- [ ] ティア3のツールは、確認を表示できないクライアントでは実行せずに止まる
- [ ] 書き込みには冪等キーがあり、処理件数の上限はサーバー側で決めている
- [ ] 承認の有無にかかわらず、毎回入力を検証し、認可を確認している
資格情報
- [ ] 通常のエージェント用トークンにティア3のスコープがない
- [ ] スケジュール実行や無人のエージェントは、ティア0と必要なティア2のツールだけに絞った別のトークンを使う
クライアント
- [ ] 共有のクライアント設定(
.claude/settings.jsonなど)で、ティア3のツールをdenyかaskにしている - [ ] クライアントが対応していれば、「すべて承認」のモードを管理設定やポリシーで無効にしている
- [ ] 審査していないサーバーの注釈を、自動承認の根拠にしていない
記録
- [ ] ティア2・3の呼び出しは、承認・拒否のどちらでも、呼び出し元とツール名つきで監査ログに残る
10. よくある失敗(症状→原因→対処)
| 症状 | 原因 | 対処 |
|---|---|---|
| 自動承認された「読み取り専用」のツールがデータを書き換えた | 審査していないサーバーのreadOnlyHintをクライアントが信用した |
注釈ではなく、自分たちの許可リストで自動承認を決める |
| 利用者が削除を読まずに承認している | 読み取りのたびに確認が出て承認疲れが起きている | ティア0を自動承認にして、確認を少なく・意味のあるものにする |
| 確認したのとは別のプロジェクトが削除された | 承認がツールにしか結びついておらず、引数に結びついていない | 引数のハッシュを署名付きのrequestStateに入れる |
| 夜間のエージェントが、誰も承認していないレコードを削除した | スケジュール実行が、対話用と同じ万能のトークンを使っていた | ティア3のスコープを持たない別のトークンにする |
| Claude Codeのdenyルールで呼び出しが止まらない | ルールのサーバー名が違う(サーバー名はクライアント設定での名前) | mcp__<設定上の名前>__<ツール名>を使い、/permissionsで確認する |
| Cursorの許可リストが効かない | 実行モードが無効、またはアプリ内のMCP許可リストがpermissions.jsonで置き換わっている |
実行モードを有効にし、リストを1か所で管理する |
| あるクライアントでは動くのに、別のクライアントでは確認エラーになる | 後者がform形式のelicitationに対応していない | 安全側に止まる想定どおりの動き。Web画面での代替手順を案内する |
| 承認した送信ツールで社外秘のデータが漏れた | 信頼できない入力と外部送信のツールが同じセッションにあった | そのセッションから送信ツールを外すか、ティア3として確認ゲートを付ける |
11. NodeFlareならこう解決できる
NodeFlareは、MCPサーバーをプロキシの後ろでホストし、すべてのリクエストを、コードに届く前に認証・認可・レート制限・記録します。確認ダイアログを表示したり、elicitationを代わりに実装したりはしませんし、プロンプトインジェクションそのものを防ぐこともできません。NodeFlareが受け持つのは、1章でいう「資格情報の上限」と「記録」の層です。クライアントですべての確認がオフになっていても、ティア3のツールに届かない状態を作れます。この記事で挙げたリスクと、ドキュメントに記載されている機能の対応は次のとおりです。
| この記事のリスク | NodeFlareの機能 | 内容 |
|---|---|---|
| 確認をオフにしたクライアントが削除系のツールを呼ぶ(3章、5章) | ツール単位のスコープ | tools:call:<ツール名>(例:tools:call:get_project)、resources:read:<uri>、prompts:get:<名前>といったスコープを、プロキシがMCPのメソッドと対象に照らして確認します。拒否された呼び出しはJSON-RPCエラーになり、サーバーには届きません |
| 呼び出し元が使うべきでないツールをモデルが提案する(4章) | tool_list_filter_by_scope |
tools/listを、その呼び出し元が実際に呼べるツールだけに絞ります。キーにティア3のスコープがなければ、エージェントにはそのツールが表示されません |
| 対話用の利用者とスケジュール実行のエージェントが同じトークンを使う(4章) | 呼び出し元ごとのmcp_* APIキーとOAuth |
必要なティアだけを持つキーを呼び出し元ごとに発行できます。キーはハッシュで保存され、間違ったキーが繰り返されるとその接続元IPを10分間ロックします。OAuthのディスカバリはRFC 8414とRFC 9728に沿っています |
| 承認された呼び出しが実際に実行されたかの記録がない(9章) | リクエストログ | 各リクエストのmcp_method、tool_name、arg_keys(引数の名前のみ。値は記録しない)、http_status、outcome(ok、scope_denied、rate_limitedなど)、caller_kind、client_ipを記録します。保存期間はFreeで7日、ProとTeamで30日です |
| 承認された呼び出しが想定外の宛先にデータを送る(3章) | Egress制御 | モードはoff(既定)、observe、enforce。登録は最大100件(完全なホスト名、先頭1つのワイルドカード(例:*.example.com)、IPアドレス)で、ホスト側のDNSとnftablesで強制します |
| 承認済みのエージェントがループして呼び出しを繰り返す | レート制限、クォータ、利用上限 | IPごとの毎分のレート制限、ワークスペースの月間クォータ、任意で設定できるサーバーごとの月間利用上限(到達するとHTTP 402)があります |
確認ゲートの秘密鍵(7章のSECRET) |
暗号化された環境変数 | 保存時に暗号化され、VM起動時にだけ注入されます。ビルド引数、イメージのレイヤー、ログには書き込まれません |
| 侵害されたサーバーが他のサーバーに影響する | サーバーごとのFirecracker microVM | 各サーバーはそれぞれ専用のmicroVMで動きます |
注意点が3つあります。1つ目に、tool_code_modeやtool_search_modeを有効にすると、クライアントには個々のツールではなくメタツール(run_code、call_tool)が見えるようになります。その結果、クライアント側のツール単位の承認ルールでは、どのツールが実行されるかを区別できません。ティア3のツールをクライアントの承認に頼っているサーバーではこれらのモードを使わず、スコープで制御してください(なおコードモードの実行は、既定で1回あたり最大50回のツール呼び出し、600秒のタイムアウトに制限されています)。2つ目に、auth_enabled = falseにすると呼び出し元の資格情報がそのままサーバーに渡り、NodeFlareのスコープ確認は適用されなくなります。ティア分けの強制はサーバー側の仕事に戻ります。3つ目に、7章の確認ゲートは自社のコードの中で動くものです。NodeFlareはそのまま動かしますが、ゲートを追加するわけではありません。
最小の始め方:
- MCPサーバーをデプロイし、
tool_list_filter_by_scopeを有効にします。 mcp_*キーを2つ発行します。エージェント用にはtools:call:<ティア0のツール>のスコープだけ、対話で使う利用者用にはティア2のツールを追加します。ティア3のツールはどちらにも入れないか、人だけが使う3つ目のキーにします。- Egress制御を
observeにして通常の利用を1週間続け、確認できた宛先を登録してからenforceに切り替えます。
1サーバーまでは無料で試せます:NodeFlareを始める
12. よくある質問
MCPの仕様では、ツールの呼び出しに人の承認が必須ですか?
必須ではなく推奨です。2026-07-28版のToolsのページは、ツールの呼び出しを拒否できる人が常に介在すべき(SHOULD)、クライアントは重要な操作で確認を求めるべき(SHOULD)としています。一方でサーバーには入力の検証とアクセス制御が義務(MUST)なので、承認はクライアントの機能と考え、制限はサーバー側で強制します。
MCPのヒューマン・イン・ザ・ループとは何ですか?
ツールの呼び出しが実行される前に、人がそれを確認して拒否できる状態のことです。実際には、ツール名と引数を表示するクライアントの確認ダイアログが中心で、必要に応じてelicitationによるサーバー側の確認と、危険なツールに届かない資格情報で補強します。
Claude CodeでMCPツールを自動承認するには?
設定ファイルのpermissionsにallowルールを追加します。たとえば"allow": ["mcp__crm__list_projects", "mcp__crm__get_*"]です。ルールはdeny→ask→allowの順に評価されるので、削除系のツールはdenyかaskに入れておきます。allowのワイルドカードはmcp__<サーバー名>__という固定の接頭辞の後ろでしか使えません。
readOnlyHintを根拠にツールを自動承認してもよいですか?
信頼できるサーバーのものに限ります。仕様は、信頼できるサーバー以外からのツール注釈を信用してはならない(MUST)としています。悪意のあるサーバーは破壊的なツールに「読み取り専用」と書けるからです。自動承認は、自分たちの許可リストと、書き込みスコープのない資格情報を根拠にします。
MCPのツール注釈の既定値は?
未設定の場合、readOnlyHintはfalse、destructiveHintはtrue、idempotentHintはfalse、openWorldHintはtrueとして扱われます。つまり注釈のないツールは「外部に影響し、破壊的かもしれない書き込み」とみなされます。
MCPサーバーから利用者に操作の確認を求めるには?
form形式のelicitation/createリクエストを含むInputRequiredResultを、署名したrequestStateと一緒に返します。クライアントはフォームを表示し、利用者の回答を付けて同じtools/callを再送します。サーバーは、回答が承認であり、かつrequestStateがユーザー・ツール・引数と一致するときだけ実行します。
elicitationでプロンプトインジェクションを防げますか?
防げません。elicitationで証明できるのは、その引数に対してクライアントが承認を返したことであって、人が読んだことではありません。クライアントが自動で承認する場合もあります。仕込まれた指示による被害を狭めることはできますが、指示そのものは止められません。スコープを絞った資格情報、Egressの制限、信頼できない入力と外部送信ツールの分離を組み合わせます。
承認疲れ(approval fatigue)とは?
確認が多すぎて、人が中身を読まずに承認するようになる状態です。Anthropicによると、Claude Codeのユーザーは許可プロンプトの93%を承認しています。本当に安全な読み取りは自動承認にして、残る確認を少なく・意味のあるものにすることで減らせます。
OpenAI Responses APIでMCPツールに承認を必須にするには?
既定で承認が必要です。安全なツールだけ承認を省略したい場合は、require_approvalに{"never": {"tool_names": [...]}}を指定します。それ以外の呼び出しではmcp_approval_requestが返るので、アプリケーションはapproval_request_idとapproveを含むmcp_approval_responseで答えます。
自動承認してはいけないMCPツールは?
データの削除、社外へのデータ送信、支払い、権限の変更、任意のコードやSQLの実行を行うツールです。どのクライアントでも「確認する」にしておき、サーバー側の確認を付け、無人で動くエージェントが使う通常の資格情報からは外します。
まとめ
MCPのツール承認は、何層かに重ねたときに効きます。仕様は人の確認をクライアントへの推奨にとどめ、実際の強制はサーバーに任せています。数字もその理由を示しています。人は確認の93%を承認し、分類器でもリスクのある操作の17%を見逃します。すべてのツールをティアに分け、本当に安全な読み取りだけを自動承認して確認の意味を保ち、削除系のツールには7章のゲートのように「ユーザー・ツール・引数そのもの」に結びついたサーバー側の確認を付けます。そして何より、ティア3のツールをエージェントが普段使う資格情報から外しておけば、確認をすべてオフにしたクライアントからでも最悪の呼び出しは失敗します。最後に、これらの制限ごとにテストを用意してMCPのセキュリティテストに加え、デプロイ後も効いていることを確かめてください。
参考資料(一次情報)
- MCP仕様 2026-07-28版:Tools
- MCP仕様 2026-07-28版:Elicitation
- MCP仕様 2026-07-28版:Multi round-trip requests
- MCP C# SDK:ToolAnnotationsリファレンス
- Anthropic Engineering:Claude Code auto mode(2026年3月25日)
- Claude Codeドキュメント:Configure permissions
- Claudeヘルプセンター:コネクタとツールの権限
- VS Codeドキュメント:Manage approvals and permissions
- Cursorドキュメント:permissions.json reference
- Gemini CLIドキュメント:MCP servers
- OpenAIドキュメント:Remote MCP servers(承認)
- OWASP Secure Agent Playbook:Top 10 for Agentic Applicationsのリスク評価
- General Analysis:Supabase MCP can leak your entire SQL database
- NodeFlareドキュメント
