MCP(Model Context Protocol)では、LLMが外部のAPI、データベース、ファイルシステムなどを操作できます。便利な一方で、ツールを追加するだけでモデルに強い権限を与えてしまう危険があります。
結論から言うと、readOnlyHint などのメタデータだけに安全性を依存してはいけません。ホスト側で「許可するツール」「確認が必要なツール」「常に拒否するツール」を明示し、引数・対象範囲・実行回数まで検査する必要があります。
この記事では、実際に小さなポリシー実装を作り、危険な呼び出しを拒否できることを確認します。
MCPツールは権限境界として設計する
MCPのツールは、モデルが自動的に発見・呼び出せる関数です。公式仕様でも、ツールはAPI呼び出しやファイル書き込みなどの操作を提供するものと説明されています。
たとえば、次のツールは同じ「関数」に見えても危険度が異なります。
search_docs 文書を検索するだけ
get_status 状態を取得するだけ
delete_file ファイルを削除する
write_db データベースを更新する
send_email 外部へメッセージを送信する
run_shell 任意のコマンドを実行する
特に危険なのは、複数のツールを組み合わせたときです。
- メールから機密情報を読む
- その内容をモデルが解釈する
-
send_emailで外部へ送る
個々のツールだけを見て安全性を判断すると、情報漏えいの経路を見落とします。権限はツール単位だけでなく、セッション全体の組み合わせで評価します。
ツールを3段階に分類する
私は実装前に、ツールを次の3種類へ分類するようにしています。
- 自動許可: 読み取り専用で、影響範囲が限定されている
- 明示確認: 書き込み・送信・削除など、ユーザーの意思確認が必要
- 常時拒否: 任意シェル、秘密情報の取得、無制限の外部通信など
分類例は次のとおりです。
tools:
search_docs:
action: allow
scope: local-docs
delete_file:
action: confirm
scope: /srv/app/data
send_email:
action: confirm
recipients: approved-domain.example
run_shell:
action: deny
ここで重要なのは、ツール名や説明文を信頼しすぎないことです。delete_file という名前でも、実際には任意のパスを削除できるかもしれません。登録時の情報だけでなく、実行直前に引数を検査します。
最小権限を実装する
ツール呼び出しが実行されるまでの安全側の判定フローは次のとおりです。
以下は、MCPクライアントやホストの手前に置く簡易ポリシーです。実際のSDKに依存しない形にしているため、各言語のMCP実装へ組み込みやすくなっています。
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class Policy:
allowed_tools: frozenset[str]
require_approval: frozenset[str]
blocked_tools: frozenset[str]
POLICY = Policy(
allowed_tools=frozenset({"search_docs", "delete_file"}),
require_approval=frozenset({"delete_file"}),
blocked_tools=frozenset({"run_shell"}),
)
def authorize(
tool: str,
args: dict[str, Any],
approved: bool = False,
) -> str:
if tool in POLICY.blocked_tools:
return "DENY: blocked tool"
if tool not in POLICY.allowed_tools:
return "DENY: not in allowlist"
if tool in POLICY.require_approval and not approved:
return "ASK: explicit approval required"
if tool == "delete_file":
path = args.get("path", "")
if not path.startswith("/srv/app/data/"):
return "DENY: path outside sandbox"
return "ALLOW"
呼び出し直前に必ずこの関数を通します。
decision = authorize(
tool_name,
tool_arguments,
approved=user_clicked_confirm,
)
if decision != "ALLOW":
raise PermissionError(decision)
result = mcp_client.call_tool(tool_name, tool_arguments)
私はこのロジックを実行して、次の4ケースを確認しました。
assert authorize("search_docs", {"q": "MCP"}) == "ALLOW"
assert authorize("run_shell", {"command": "whoami"}) == "DENY: blocked tool"
assert authorize(
"delete_file", {"path": "/tmp/a"}, True
) == "DENY: path outside sandbox"
assert authorize(
"delete_file", {"path": "/srv/app/data/a"}, False
) == "ASK: explicit approval required"
print("4 policy checks passed")
実行結果は次のとおりです。
$ python policy_check.py
4 policy checks passed
annotationsは便利だが、強制機構ではない
MCPには、ツールの性質を表すannotationsがあります。たとえば、読み取り専用や破壊的操作かどうかを示せます。
{
"name": "delete_file",
"description": "指定されたデータ領域内のファイルを削除する",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": ["path"]
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": false
}
}
これらは確認ダイアログを出す判断材料として有用です。しかし、悪意のあるサーバーが誤った値を返す可能性もあるため、クライアントは信頼できないサーバーのannotationsを未検証の情報として扱う必要があります。
つまり、次のように考えます。
annotations UIや事前警告のヒント
ホストのポリシー 実際の許可・拒否
サーバーの検証 最終的な防御
公式仕様でも、ツール呼び出し前のユーザー確認、入力検証、レート制限、出力のサニタイズ、監査ログなどが推奨されています。詳細はMCPのTools仕様で確認できます。
認証と認可を分けて考える
OAuthなどでMCPサーバーへ接続できても、それは「誰であるか」を確認しただけです。「何を実行できるか」は別途設計します。
たとえば、トークンに次のようなスコープを持たせます。
docs:read
files:read
files:delete
mail:send
通常のユーザーにはdocs:readだけを与え、削除や送信は別の承認フローに分けます。
HTTPトランスポートで認可を使う場合は、MCPの認可仕様に沿ってトークンの対象サーバー、発行者、期限、スコープを検証します。STDIO接続では環境変数などから資格情報を取得する方式が想定されているため、ログにトークンを出さないこと、プロセスの実行ユーザーを分離することが重要です。
認証済みだから安全、とは考えません。ツール単位のallowlist、引数検証、サンドボックス、ユーザー確認を組み合わせます。
実運用前のチェックリスト
最後に、私がMCPサーバーを追加するときに確認している項目をまとめます。
- ツール一覧を固定またはallowlist管理しているか
- 削除・送信・更新・外部通信を確認対象にしているか
- パス、URL、宛先、件数に上限があるか
- 任意シェル実行をデフォルトで拒否しているか
- サーバーごとに資格情報と権限を分離しているか
- タイムアウトとレート制限があるか
- ツール名、引数、実行結果を監査ログへ残しているか
- ツール結果に含まれる外部文章を命令として扱っていないか
- annotationsがなくても安全側に倒れるか
- 危険な呼び出しを自動テストしているか
特に、ツール結果に「このコマンドを実行してください」のような文章が含まれるケースには注意します。ツールの出力はデータであり、権限変更の指示ではありません。
まとめ
MCPの安全性は、ツールの説明文やannotationsだけでは実現できません。
実践上は、次の順番で設計すると扱いやすくなります。
- ツールを読み取り・確認・拒否に分類する
- ホスト側でallowlistと引数制約を適用する
- 削除や外部送信には明示確認を要求する
- サーバー側でも入力検証と権限確認を行う
- 認証、サンドボックス、監査ログ、テストを組み合わせる
MCPはモデルに行動能力を与える仕組みです。だからこそ、便利なツールを増やすことより、最初に「どこまで許可するか」をコードで固定することが重要です。