MENU

MCPサーバー権限スコープ設計|危険なツールをAIから封じる3層防御

クイックサマリー:自作MCPサーバーに read/write/admin の3層スコープを実装し、権限のないツールをAIから「見えなくする」設計パターンです。Model Context Protocol(MCP)に準拠したクライアント全般(Claude Desktop、Claude Code、Cursor、Antigravity CLI など)で利用できます。

導入:APIキーだけでは危険なツールを止められない

自作のMCPサーバーをチームに配ったら、AIが想定外のツールを勝手に実行してしまった——そんな不安を感じたことはないでしょうか。MCPサーバーはツールを登録した時点で「AIが呼べる」状態になります。APIキーを設定していても、それは外部サービスへの認証にすぎず、「AIにどのツールを許すか」という制御とは別問題です。

この記事でわかることは次のとおりです。

  • APIキーによる認証と、AIへのツール権限制御がなぜ別物なのか
  • read/write/admin の3層スコープをMCPサーバーに実装する具体的なコード
  • 登録時の除外と実行時の二重チェックによる多層防御の考え方
  • クライアント側の設定に権限判定を任せると起きる失敗パターン

ここで紹介するのは、公式SDKであるMCP Python SDK(modelcontextprotocol/python-sdk)に含まれる FastMCP をベースにした、個人開発者向けの実装パターンです。OSSのSDK上に自分で組み込むコードのため、特定のライセンス製品ではなく「設計と実装の型」として提供されています。

なお、Claude CodeやAntigravity CLIなどの開発支援ツールへ自作MCPを組み込む際も、事前にこの認証設計を組み込んでおくことで安全な運用が可能になります。

MCPサーバーの権限スコープで何ができるか

対応クライアントは、MCP仕様に準拠したツール全般です。Claude Desktop・Claude Code・Cursor・Antigravity CLI など、mcpServers 設定でMCPサーバーを起動できるクライアントであれば、どれでも同じ仕組みが機能します。MCPはAnthropicが公開したオープンな標準規格であり、クライアント側の実装に依存しない点が特徴です。

主な機能は次のとおりです。

  • 登録時のスコープ除外:環境変数で許可されていないスコープのツールは、そもそもMCPのツール一覧に登録しない
  • 実行時の二重チェック:登録済みツールであっても、呼び出し時に再度スコープを検証し監査ログへ記録する
  • 危険度による3分類:読み取り(read)/書き込み(write)/管理・削除(admin)の3層に分け、既定値を最小権限にする
  • 失敗の安全側への倒し込み:未知のスコープ値や空の設定は、自動的に最小権限へフォールバックする

元記事の筆者である葉山悠希氏は、Zennの記事「自作MCPサーバーに認証と権限スコープを入れる」の中で、この設計に至った経緯を「自作MCPサーバーをチームに配った翌日、Claudeが delete_note を呼んだ。消えたのはテスト用の1件だったから笑い話で済んだが、あれが本番のDBだったら終わっていた」と振り返っています。この体験から生まれた設計が、以下の3層スコープです。

インストール・有効化手順

ここではPython製MCPサーバーへの実装手順を、記事内のコードに沿って再現します。前提として、MCP Python SDK(from mcp.server.fastmcp import FastMCP が使えるパッケージ)が導入済みであることを確認してください。

実装1:スコープ未許可のツールを「登録しない」

ポイントは、権限のないツールを実行時に弾くのではなく、そもそも登録段階でMCPのツール一覧から除外することです。

# server.py
import os
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes-server")

# 起動時に一度だけ読む。未指定なら最小権限の "read" のみ
GRANTED = {s.strip() for s in os.environ.get("MCP_SCOPES", "read").split(",") if s.strip()}

def scoped(scope):
    """スコープが無ければツール自体を登録しない(AIから見えなくする)"""
    def decorator(fn):
        if scope in GRANTED:
            return mcp.tool()(fn)
        print(f"[skip] {fn.__name__}: '{scope}' スコープ未許可のため未登録")
        return fn
    return decorator

@scoped("read")
def search_notes(query: str) -> list[str]:
    """ノートを全文検索する(読み取り専用)"""
    return [f"{query} に一致するノート"]

@scoped("admin")
def delete_note(note_id: str) -> str:
    """ノートを完全に削除する(取り消し不可)"""
    return f"deleted: {note_id}"

if __name__ == "__main__":
    mcp.run()

起動時のログを見ると効果が分かります。MCP_SCOPES=read,write でサーバーを起動しても、ログには「[skip] delete_note: 'admin' スコープ未許可のため未登録」と出力され、delete_note はツール一覧に現れません。AIは存在しないツールを呼べないため、プロンプトインジェクションで削除を指示されても、原理的に実行手段そのものが無い状態になります。

実装2:実行時の二重チェックと監査ログ

登録段階の除外だけでは、ツールを動的に追加する構成やadminを一時的に開ける運用に対応しきれません。呼び出し時にも検証し、拒否も含めて監査ログに残す実装を重ねます。

# guard.py
import os
import functools

def _granted() -> set[str]:
    return {s.strip() for s in os.environ.get("MCP_SCOPES", "read").split(",") if s.strip()}

AUDIT_LOG = "mcp_audit.log"

def guarded(scope: str):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            ok = scope in _granted()
            with open(AUDIT_LOG, "a", encoding="utf-8") as f:
                f.write(f"{'ALLOW' if ok else 'DENY'} {fn.__name__} scope={scope}\n")
            if not ok:
                raise PermissionError(f"{fn.__name__} には '{scope}' スコープが必要です")
            return fn(*args, **kwargs)
        return wrapper
    return decorator

記事内では「DENYログは攻撃の検知に効く。AIが拒否された操作を繰り返しているなら、そのプロンプトは何かに汚染されている」と説明されており、監査ログを異常検知の手がかりとして使う発想が示されています。

設定のカスタマイズ:未知の値を安全側に倒す

環境変数の打ち間違いや過剰な権限付与を防ぐため、許可リスト方式と二重の明示的許可を組み合わせます。

ALLOWED_SCOPES = {"read", "write", "admin"}

def granted() -> set[str]:
    raw = {s.strip() for s in os.environ.get("MCP_SCOPES", "read").split(",")}
    scopes = raw & ALLOWED_SCOPES  # 未知の値は黙って捨てる
    if "admin" in scopes and os.environ.get("MCP_ALLOW_ADMIN") != "yes":
        scopes.discard("admin")  # admin は二重の明示が要る
    return scopes or {"read"}  # 空なら最小権限へ落とす

動作確認は、MCP_SCOPES=read でサーバーを起動し、クライアント側のツール一覧が想定どおり減っているかを見るだけで完結します。

クライアント側から環境変数を渡す具体的な設定

サーバー側のコードだけを見て「では実際にどう起動すればいいのか」で手が止まる方のために、主要クライアントでの設定例を示します。いずれも、MCPサーバーを子プロセスとして起動する際の env フィールドに MCP_SCOPES や MCP_ALLOW_ADMIN を渡す点は共通です。

Claude Desktop(claude_desktop_config.json)の場合、設定ファイルの mcpServers にサーバーごとの起動コマンドと環境変数を記述します。

{
  "mcpServers": {
    "notes-server": {
      "command": "python",
      "args": ["/path/to/server.py"],
      "env": {
        "MCP_SCOPES": "read,write"
      }
    }
  }
}

ここでのポイントは、既定の設定では admin をスコープに含めないことです。チームメンバー全員に配布する設定ファイルには read,write までにとどめ、削除操作が必要な管理者だけが手元で MCP_ALLOW_ADMIN=yes を追加する運用にすると、誤ってadmin権限を配布してしまう事故を防げます。

Claude Codeの場合も同様に、プロジェクトの .mcp.json や claude mcp add コマンドで環境変数を指定できます。CLIから一時的に起動する場合は次のようにします。

# 通常運用:read/write のみ
MCP_SCOPES=read,write python server.py

# 一時的に管理者権限で動かす(自分のターミナルセッション限定)
MCP_SCOPES=read,write,admin MCP_ALLOW_ADMIN=yes python server.py

Windows PowerShellの場合は環境変数の書き方が異なるので注意してください。

# PowerShellでの一時的な管理者起動
$env:MCP_SCOPES = "read,write,admin"
$env:MCP_ALLOW_ADMIN = "yes"
python server.py

Cursorも設定ファイルの形式はClaude Desktopとほぼ共通で、プロジェクト直下の .cursor/mcp.json に同様の env フィールドを書きます。いずれのクライアントでも共通する注意点として、この env に書いた値がそのままログや設定ファイルの中に平文で残るため、MCP_ALLOW_ADMIN=yes のような強い権限を恒常的に設定ファイルへ書き込まず、必要なときだけターミナルから一時的に渡す運用を基本にすることをおすすめします。

実用例:実際の開発フローでの活用

この設計は、Claude Code や Claude Desktop のようなCLI/デスクトップクライアントから自作MCPサーバーを呼び出す場面でそのまま生きます。例えば社内向けのノート管理MCPサーバーをチームに配布する場合、開発者本人の環境では MCP_SCOPES=read,write,admin を設定してフル機能を使い、他メンバーの環境では既定値の read のみに絞るという運用が可能です。

Claude Code のCLAUDE.mdやプロジェクト設定に「このMCPサーバーは read スコープのみで動作する」という前提を明記しておけば、AIエージェント自身がツール一覧を見て安全に動作範囲を把握できます。さらに、.claude/settings.json のようなクライアント側の設定ファイルに権限判定を書き込む方式は後述のとおり脆弱なため、あくまでサーバー起動時の環境変数でスコープを渡す構成を基本にするのが安全です。

削除・一括更新のような取り消し不可能な操作を持つMCPサーバーを開発する際は、まず全ツールを read/write/admin の3列に振り分け、admin系ツールにだけ MCP_ALLOW_ADMIN=yes の二重ガードを追加する、という手順がそのまま開発フローに組み込めます。

この設計が前提とする範囲:ローカル起動(stdio)向けであること

ここまで紹介したコードは、os.environ を起動時に一度だけ読み込む設計です。これはMCPサーバーがクライアントの子プロセスとしてローカルに1つだけ起動される「stdio方式」を前提にしています。個人利用やチームメンバーがそれぞれ自分の手元でサーバーを起動する運用であれば、この前提のままで問題なく機能します。

一方で、1台のサーバープロセスをHTTP/SSE(Streamable HTTP)経由で複数人・複数チームが共有する「リモートMCPサーバー」として運用する場合は、事情が変わります。プロセス起動時の環境変数はプロセス全体で1つしかないため、リクエストを送ってきたユーザーごとに異なるスコープを割り当てることができません。全員が同じ MCP_SCOPES の設定を共有してしまう形になり、本記事の設計をそのまま持ち込むと「誰か1人がadminを必要とするから全員がadmin」という状態になりかねません。

社内共有サーバーのようなマルチユーザー環境への拡張を検討する場合は、環境変数によるプロセス起動時の判定から、リクエストごとの判定へ設計を切り替える必要があります。具体的には、リクエストに付与されたBearerトークンをユーザーごとのスコープと突き合わせる、あるいはMCP Python SDKが各ツール呼び出し時に渡してくる ctx.request_context(リクエストコンテキスト)からユーザー識別情報を取り出し、その場でスコープを動的に判定するアプローチが必要になります。この記事の3層スコープの考え方(登録時除外・実行時二重チェック・監査ログ)自体はリモート運用でも有効ですが、「どこからスコープの値を読むか」の部分だけを環境変数からリクエストベースの判定に置き換える追加設計が要る、と理解しておくとよいでしょう。

類似アプローチ・代替手段との比較

アプローチ対応クライアント権限判定の場所特徴
3層スコープ設計(本記事)MCP準拠クライアント全般サーバー側(環境変数+登録時除外)登録時除外により未許可ツールがAIから完全に見えなくなる。監査ログで拒否も記録
クライアント側フラグのみで制御Claude Desktop等の設定ファイル依存クライアント側(claude_desktop_config.json等)設定ファイルが人間に書き換えられる、環境変数がシェルから継承されるなどの理由で防御が破られやすい
vscode-as-mcp-serverVS Code経由のMCPクライアントVS Code拡張側の権限モデルファイル編集の差分確認・承認フローをエディタ側で提供。汎用の自作サーバーへの応用は別途設計が必要

比較から分かるとおり、権限判定をクライアント側の設定だけに委ねる方式は、設定ファイルの書き換えや環境変数の継承によって簡単に崩れます。判定は必ずサーバー側で完結させることが、この3層スコープ設計の核心です。

注意点・制約・セキュリティ

実装にあたっては、次の点に注意してください。

  • クライアント側の設定を信用しない:元記事の筆者は、Claude Desktopの設定に「読み取り専用」と書きつつサーバー側は全ツールを登録したままにする方式で、実際に運用が壊れた経験を共有しています。理由は「設定ファイルは人間が書き換える」「環境変数はシェルから継承される」「AIはスコープの概念自体を知らないため拒否されると別ツールで迂回を試みる」の3点です
  • 外部通信・データ送信先:本実装自体は外部にデータを送信するものではなく、ローカルのMCPサーバープロセス内でスコープ判定と監査ログ出力を行うだけです。ただしサーバーが実際に叩く外部APIのAPIキー管理は別途セキュアに行う必要があります
  • 監査ログの保管場所:mcp_audit.log はサンプル実装であり、本番運用ではログの保存先・ローテーション・アクセス権限を別途設計してください
  • ローカル起動(stdio)前提の設計であること:前述のとおり、この記事のコードはクライアントの子プロセスとして1ユーザー1プロセスで動くことを前提にしています。複数人で1つのサーバープロセスを共有するリモート運用に持ち込む場合は、環境変数ベースの判定をリクエストベースの判定に置き換える追加設計が必要です
  • バージョン依存:MCP Python SDKのAPI(FastMCP、mcp.tool())は今後のバージョンアップで変更される可能性があります。導入前に公式リポジトリで最新のAPIを確認してください

まとめ

  • APIキーは「サーバーが外部を叩く資格情報」であり、「AIにどのツールを渡すか」の制御とは別問題です
  • read/write/admin の3層スコープを設け、危険なツールは登録段階でAIから見えなくすることが最も効きます
  • 権限判定は必ずサーバー側で完結させ、クライアント側の設定ファイルに委ねないことが、実際に踏まれた失敗から得られた教訓です
  • この設計はローカル起動(stdio)を前提としており、複数人で共有するリモートMCPサーバーに拡張する場合はリクエストベースの動的判定への置き換えが必要です

より体系的にMCPサーバー設計・ツール設計を学びたい場合は、元記事の著者・葉山悠希氏が執筆する技術書や、Model Context Protocol公式サイトのドキュメントも参考になります。Claude Code や Antigravity CLI でMCPサーバーを運用する場合も、まずはサーバー側のスコープ設計から着手することをおすすめします。

目次

よくある質問(FAQ)

MCPサーバーの権限スコープはどのクライアントで使えますか?

MCPはAnthropicが公開したオープンな標準規格のため、Claude Desktop・Claude Code・Cursor・Antigravity CLIなど、MCP準拠のクライアントであれば基本的にどれでも利用できます。

APIキーを設定していれば権限管理は不要ですか?

不要ではありません。APIキーはサーバーが外部サービスを呼び出す際の資格情報であり、AIがどのツールを呼べるかという制御とは別の仕組みです。両方を別々に設計する必要があります。

スコープをクライアント側の設定ファイルだけで制御してもよいですか?

推奨されません。設定ファイルは人間が書き換えられ、環境変数はシェルから継承されるため、クライアント側だけの制御は容易に破られます。判定は必ずサーバー側で完結させてください。

既存のMCPサーバーに後から権限スコープを追加できますか?

可能です。既存のツール関数をスコープ判定デコレータでラップし、環境変数で許可スコープを制御する形に段階的に移行できます。まず全ツールをread/write/adminに分類することから始めます。

ローカルLLM経由でも同じ仕組みは動きますか?

MCPサーバー自体はローカルプロセスとして動作するため、MCPプロトコルに対応したクライアントであればローカルLLMベースのツールからでも同様の仕組みが機能すると考えられます。ただし個別の対応状況は各クライアントの実装に依存するため公式ドキュメントで確認してください。

監査ログにはどのような情報が記録されますか?

紹介した実装例では、ツール名・要求スコープ・許可(ALLOW)か拒否(DENY)かをテキストファイルに追記します。DENYが繰り返されている場合はプロンプトインジェクションなどの異常を疑う手がかりになります。

adminスコープはどのように保護すべきですか?

記事の実装例では、環境変数でadminを含めるだけでなく、別途MCP_ALLOW_ADMINのような二重の明示的な環境変数が設定されていない限りadminスコープを付与しない、という二段階の確認を行っています。

クライアント側の設定ファイルにはどう書けばよいですか?

Claude Desktopのclaude_desktop_config.jsonやCursorの.cursor/mcp.jsonでは、mcpServers配下のサーバー定義にenvフィールドを追加し、MCP_SCOPESの値を指定します。管理者権限が必要な場合のみ、恒常的な設定ファイルではなくターミナルから一時的にMCP_ALLOW_ADMIN=yesを渡す運用が安全です。

複数人で1つのMCPサーバーを共有する場合も同じ設計で大丈夫ですか?

この記事の設計はローカル起動(stdio)で1ユーザー1プロセストを前提にしています。HTTP/SSEなどでリモートに1つのサーバーを複数人が共有する場合は、環境変数ではなくリクエストごとのBearerトークンやリクエストコンテキストからスコープを動的に判定する設計への置き換えが必要です。

この設計を導入しても動かない場合はどうすればよいですか?

まず環境変数MCP_SCOPESが起動プロセスに正しく渡っているか、シェルの継承設定で意図しないスコープが混入していないかを確認してください。次にツール一覧が想定どおり減っているかをクライアント側で確認します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次