AIエージェント入門

MCP Tool Annotations実装|安全な権限設計5設定

MCP Tool AnnotationsでAIエージェントの権限を安全に設計するサムネイル

この記事の結論

MCP Tool AnnotationsのreadOnlyHintやdestructiveHintを使い、AIエージェントの危険操作を見分ける権限設計をコード付きで解説します。

AIエージェントにMCPサーバーをつなぐと、最初に困るのが「このツールは安全に自動実行してよいのか?」という判断です。

検索や一覧取得のような読み取り系ツールと、メール送信・DB更新・ファイル削除のような変更系ツールを同じ扱いにすると、ユーザー確認の設計が破綻します。逆に、すべてのツールで毎回承認を求めると、AIエージェントの良さである自律性が消えてしまいます。

この記事では、MCPのToolAnnotationsで定義されているreadOnlyHintdestructiveHintidempotentHintopenWorldHintを使い、AIエージェントの権限設計を安全に始める方法を整理します。公式仕様を確認しながら、コピペできる設定例・ポリシー表・CIチェックまでまとめました。

MCP Tool Annotationsとは何か

MCP Tool Annotationsは、MCPサーバーがクライアントに対して「このツールはどんな性質を持つか」を伝えるためのヒントです。MCPのSchema Referenceでは、Toolannotationsという任意フィールドがあり、そこにToolAnnotationsを指定できると説明されています。

重要なのは、これは「セキュリティ保証」ではなく「ヒント」だという点です。MCP仕様は、ToolAnnotationsの内容が常に正しいとは限らず、信頼できないサーバーから受け取ったアノテーションだけでツール使用判断をしてはいけない、と明記しています。つまり、AIエージェント側のUIや承認フローを良くする材料にはなりますが、最終的な認可・監査・実行制御はサーバー側で実装する必要があります。

現場では、この区別がかなり大事です。「readOnlyHintがtrueだから自由に実行していい」ではありません。「読み取り専用として設計されているはずなので、自動実行候補に入れやすい。ただし、サーバー側の認証・スコープ・ログで守る」という理解が安全です。

項目 意味 設計上の使いどころ
readOnlyHint 環境を変更しないツールであることを示す 検索、一覧取得、要約、計算など
destructiveHint 削除・上書きなど破壊的更新の可能性を示す 削除、キャンセル、上書き保存、送信取り消しなど
idempotentHint 同じ引数で繰り返し実行しても追加影響がないことを示す 冪等な更新、同一IDへの状態設定など
openWorldHint 外部世界とやり取りする可能性を示す Web検索、メール送信、外部API、公開投稿など

ざっくり言うと、Tool Annotationsは「AIに渡すツール説明書」です。ただし、説明書を信じるかどうかは別問題です。信頼できる社内MCPサーバーでは積極的に活用し、外部MCPサーバーではゼロトラストで扱うのが基本になります。

まず決めるべき5つの権限設定

最初に、ツールごとに次の5項目を棚卸ししてください。実装前にここを曖昧にすると、後から「このツールは自動実行してよかったのか?」という議論が必ず発生します。

  1. 読み取り専用か: データベース、SaaS、ファイル、外部サービスの状態を変更しないか。
  2. 破壊的操作を含むか: 削除、上書き、送信、公開、課金、キャンセルに該当するか。
  3. 冪等か: 同じ引数で複数回呼んでも結果が追加で悪化しないか。
  4. 外部世界へ出るか: 社内の閉じたデータだけで完結するか、外部API・公開Web・ユーザーへの通知が絡むか。
  5. 承認レベルはどこか: 自動実行、軽い確認、明示承認、管理者承認のどれにするか。

ここでのポイントは、ToolAnnotationsの4項目だけに閉じないことです。たとえばopenWorldHint: trueのツールでも、単なるWeb検索なら比較的リスクは低めです。一方で、同じ外部世界でも「顧客へメール送信」は重大な副作用があります。アノテーションは入口であり、最終的には業務リスクに合わせた承認レベルを持たせる必要があります。

おすすめは、最初に以下のようなポリシーマトリクスを作ることです。大規模な権限管理基盤を入れる前でも、チーム内の判断基準が揃います。

ツール種別 readOnly destructive openWorld 推奨承認
社内FAQ検索 true false false 自動実行可
CRMの顧客情報取得 true false false ログ付き自動実行
Google Driveへの下書き作成 false false false 軽い確認
Slack投稿・メール送信 false false true 明示承認
DBレコード削除 false true false 管理者承認

この表を作るだけでも、MCPサーバーの設計レビューがかなり楽になります。特に「削除ではないから安全」と見なされがちな外部送信系ツールは、openWorldHintと承認フローで分離しておくと事故を減らせます。

実装例1:MCPサーバーでツールにアノテーションを付ける

まずは、MCPサーバー側でツール定義にアノテーションを付けます。以下はTypeScript風の例です。実際のSDKバージョンや関数名は利用しているMCP SDKに合わせて調整してください。

動作環境: Node.js 20以上、TypeScript 5系、MCP SDK系のサーバー実装を想定。注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

import { z } from "zod";

// 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
server.registerTool(
  "search_internal_faq",
  {
    title: "社内FAQ検索",
    description: "社内FAQを検索し、該当する回答候補を返します。データは変更しません。",
    inputSchema: {
      query: z.string().min(1).describe("検索キーワード")
    },
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: false
    }
  },
  async ({ query }) => {
    const results = await searchFaqIndex(query);
    return {
      structuredContent: { results },
      content: [{ type: "text", text: `${results.length}件見つかりました。` }]
    };
  }
);

server.registerTool(
  "send_customer_email_draft",
  {
    title: "顧客メール下書き作成",
    description: "顧客向けメールの下書きを作成します。送信は行いません。",
    inputSchema: {
      customerId: z.string(),
      subject: z.string(),
      body: z.string()
    },
    annotations: {
      readOnlyHint: false,
      destructiveHint: false,
      idempotentHint: false,
      openWorldHint: false
    }
  },
  async ({ customerId, subject, body }) => {
    const draft = await createEmailDraft({ customerId, subject, body });
    return {
      structuredContent: { draftId: draft.id },
      content: [{ type: "text", text: `下書きID ${draft.id} を作成しました。` }]
    };
  }
);

server.registerTool(
  "delete_crm_note",
  {
    title: "CRMメモ削除",
    description: "指定したCRMメモを削除します。復元できない場合があります。",
    inputSchema: {
      noteId: z.string().min(1),
      reason: z.string().min(5)
    },
    annotations: {
      readOnlyHint: false,
      destructiveHint: true,
      idempotentHint: true,
      openWorldHint: false
    }
  },
  async ({ noteId, reason }) => {
    await deleteCrmNote(noteId, reason);
    return {
      structuredContent: { noteId, deleted: true },
      content: [{ type: "text", text: "CRMメモを削除しました。" }]
    };
  }
);

この例では、FAQ検索は完全な読み取り専用として扱っています。メール下書き作成は外部送信こそしませんが、SaaS上に新しい下書きを作るためreadOnlyHintfalseです。CRMメモ削除は破壊的操作なので、destructiveHinttrueにしています。

ありがちなミスは、「送信しないから安全」「下書きだから読み取り」と雑に分類することです。下書きであっても顧客情報・社外秘・添付ファイルが含まれるなら、ログ・承認・削除ポリシーが必要です。ToolAnnotationsは、こうした議論をコード上に残すための実装ポイントになります。

実装例2:クライアント側で承認レベルを自動判定する

次に、クライアントやエージェントランタイム側で、ツール呼び出し前の承認レベルを判定します。繰り返しますが、これはサーバー側認可の代わりではありません。ユーザー体験と事故防止のための「事前ブレーキ」です。

動作環境: Node.js 20以上。注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

type ToolAnnotations = {
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;
};

type ApprovalLevel = "auto" | "confirm" | "explicit" | "admin";

export function decideApprovalLevel(
  annotations: ToolAnnotations,
  toolName: string
): ApprovalLevel {
  const readOnly = annotations.readOnlyHint === true;
  const destructive = annotations.destructiveHint === true;
  const openWorld = annotations.openWorldHint === true;

  // 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
  if (destructive) return "admin";

  if (openWorld && !readOnly) return "explicit";

  if (!readOnly) return "confirm";

  // 外部に出ない読み取り系だけを自動実行候補にする
  if (readOnly && !openWorld) return "auto";

  // Web検索などは読み取りでも確認を挟む運用にする
  return "confirm";
}

const examples = [
  ["search_internal_faq", { readOnlyHint: true, openWorldHint: false }],
  ["send_slack_message", { readOnlyHint: false, openWorldHint: true }],
  ["delete_crm_note", { readOnlyHint: false, destructiveHint: true }]
] as const;

for (const [name, annotations] of examples) {
  console.log(name, decideApprovalLevel(annotations, name));
}

このポリシーでは、読み取り専用かつ外部世界に出ないツールだけを自動実行候補にしています。Slack投稿やメール送信のように外部へ出るツールは、破壊的操作でなくても明示承認に寄せています。これは少し保守的ですが、社内導入の初期フェーズではこのくらいが扱いやすいです。

もし運用が進んだら、承認レベルをユーザー・チーム・データ分類ごとに分けるとよいでしょう。たとえば、社内のテストチャンネルへの投稿は軽い確認、本番の顧客向けメールは明示承認、契約データの削除は管理者承認という形です。

実装例3:CIでアノテーション漏れを検出する

MCPサーバーが増えてくると、ツール定義にアノテーションを付け忘れることがあります。レビューで毎回見るのはつらいので、CIで落とすのがおすすめです。

以下は、ツール定義をJSONとして出力できる前提の簡易チェックスクリプトです。実際にはtools/listのレスポンスや、ビルド時に生成したツールカタログに対して実行します。

動作環境: Python 3.11以上。注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

#!/usr/bin/env python3
import json
import sys
from pathlib import Path

REQUIRED = ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"]

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
def validate_tool(tool: dict) -> list[str]:
    errors = []
    name = tool.get("name", "<unknown>")
    annotations = tool.get("annotations") or {}

    for key in REQUIRED:
        if key not in annotations:
            errors.append(f"{name}: annotations.{key} が未設定です")

    if annotations.get("readOnlyHint") is True and annotations.get("destructiveHint") is True:
        errors.append(f"{name}: readOnlyHint と destructiveHint が同時に true です")

    if annotations.get("readOnlyHint") is True and name.startswith(("create_", "update_", "delete_", "send_")):
        errors.append(f"{name}: ツール名と readOnlyHint の整合性を確認してください")

    return errors


def main() -> int:
    path = Path(sys.argv[1])
    data = json.loads(path.read_text(encoding="utf-8"))
    tools = data.get("tools", data if isinstance(data, list) else [])

    errors = []
    for tool in tools:
        errors.extend(validate_tool(tool))

    if errors:
        print("Tool annotation validation failed:")
        for error in errors:
            print(f"- {error}")
        return 1

    print(f"OK: {len(tools)} tools checked")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

このチェックは完璧なセキュリティ監査ではありません。それでも、アノテーション漏れ・矛盾・命名との不整合を早期に見つけるには十分役立ちます。特に外部委託や複数チームでMCPサーバーを作る場合、CIで最低ラインを揃えておくとレビューの負担が下がります。

実装例4:サーバー側の認可で最終防衛線を作る

公式仕様とOpenAI Apps SDKのドキュメントで共通している考え方は、「アノテーションはヒントであり、サーバー側の認可ロジックを置き換えない」ということです。クライアントが承認UIを出してくれても、悪意あるクライアントやバグったクライアントが直接ツールを呼ぶ可能性は残ります。

そのため、MCPサーバー側では少なくとも次の3つを実装します。

  • アクセストークンやセッションから、ユーザー・組織・ロールを特定する
  • ツールごとに必要スコープを定義し、実行前に必ず検証する
  • 変更系・外部送信系・削除系は監査ログを残す

以下は、ツール実行前にスコープを確認する擬似コードです。実際の認証方式はOAuth、社内ID基盤、API Gatewayなどに合わせてください。

動作環境: Python 3.11以上、FastAPI系のサーバー実装を想定。注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

TOOL_SCOPES = {
    "search_internal_faq": {"faq:read"},
    "send_customer_email": {"email:send", "customer:read"},
    "delete_crm_note": {"crm:note:delete"},
}

RISKY_TOOLS = {"send_customer_email", "delete_crm_note"}

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。
def authorize_tool_call(user, tool_name: str, approved: bool) -> None:
    required = TOOL_SCOPES.get(tool_name)
    if required is None:
        raise PermissionError(f"未知のツールです: {tool_name}")

    missing = required - set(user.scopes)
    if missing:
        raise PermissionError(f"必要スコープが不足しています: {sorted(missing)}")

    if tool_name in RISKY_TOOLS and not approved:
        raise PermissionError("このツールは明示承認なしでは実行できません")


def call_tool(user, tool_name: str, args: dict, approved: bool):
    authorize_tool_call(user, tool_name, approved)
    write_audit_log(user_id=user.id, tool_name=tool_name, args=args)
    return dispatch_tool(tool_name, args)

この実装では、annotationsとは別に、サーバー側でTOOL_SCOPESRISKY_TOOLSを持っています。冗長に見えますが、ここを分けるのが安全です。アノテーションはクライアント向けの説明、スコープはサーバー側の強制ルール、と役割を分離します。

さらに、MCPのセキュリティベストプラクティスでは、トークンのそのまま転送、混同代理問題、SSRF、セッションハイジャックなどがリスクとして整理されています。ToolAnnotationsだけでこれらのリスクは防げません。特に外部APIへのプロキシ型MCPサーバーでは、ユーザーごとの同意・クライアントごとの承認・スコープ最小化を別途設計してください。

【要注意】よくある失敗パターンと回避策

失敗1:readOnlyHintを認可の代わりにする

❌ よくある間違い: readOnlyHint: trueのツールなら、どのユーザーでも自由に呼べるようにする。

⭕ 正しいアプローチ: 読み取り専用でも、閲覧権限は別でチェックします。顧客情報、契約情報、社内ナレッジ、個人情報は「変更しない」だけでは安全とは言えません。

なぜ重要か: 情報漏えいは削除や更新よりも先に起きます。AIエージェントは大量のツールを横断できるため、読み取り権限の過剰付与がそのまま漏えい面積になります。

失敗2:destructiveHintを削除系だけに使う

❌ よくある間違い: delete_で始まるツールだけを破壊的操作として扱う。

⭕ 正しいアプローチ: 上書き保存、送信取り消し、ステータス変更、課金発生、外部通知なども業務上は破壊的になり得ます。ツール名だけでなく、戻せるか・影響範囲がどこまで広がるかで判断します。

なぜ重要か: 「削除していないから安全」という考え方は危険です。たとえば顧客への誤送信は、データを削除していなくても深刻な事故になります。

失敗3:openWorldHintを軽く見る

❌ よくある間違い: Web検索や外部API呼び出しを、読み取り系だから自動実行してよいと考える。

⭕ 正しいアプローチ: 外部世界に接続するツールは、プロンプトインジェクション、外部データの信頼性、意図しない情報送信を前提に設計します。読み取り専用でも、取得した外部コンテンツをそのまま次のツールに渡さないガードが必要です。

なぜ重要か: 外部Webページ、チケット本文、メール本文には、AIへの悪意ある指示が混ざる可能性があります。MCPツールは便利ですが、外部入力を「命令」ではなく「データ」として扱うルールが欠かせません。

失敗4:CIチェックなしで人間レビューに頼る

❌ よくある間違い: PRレビューで「危なそうなツールだけ確認する」運用にする。

⭕ 正しいアプローチ: すべてのツールに4つのアノテーションを明示させ、矛盾があればCIで落とします。さらに、変更系ツールには監査ログの実装を必須にします。

なぜ重要か: AIエージェント基盤は、ツールが増えるほどレビュー漏れが起きます。人間レビューは設計判断に集中し、機械で検出できる漏れはCIへ寄せるのが現実的です。

導入時のレビュー観点

社内でMCPサーバーを増やすなら、次のチェックリストを設計レビューに入れておくと便利です。

  • ツール名と実際の副作用が一致しているか
  • 読み取り専用ツールでも、ユーザーごとの閲覧権限を確認しているか
  • 変更系ツールに、明示承認と監査ログがあるか
  • 外部世界に出るツールで、送信内容の確認画面を出しているか
  • アノテーションの値がCIで検証されているか
  • 信頼できないMCPサーバーのアノテーションを、そのまま信じない設計になっているか

ここまで整えると、MCPサーバーの追加スピードを落とさずに、最低限の安全基準を保てます。特にPMや事業側と話すときは、「自動実行できるツール」と「承認が必要なツール」を表にして見せると合意形成が早いです。

もう一つ大事なのは、ツールのリスク評価を一度決めて終わりにしないことです。最初は検索だけだったツールに、後から更新機能が追加されることがあります。その瞬間にreadOnlyHintや承認レベルも見直さないと、古い安全評価のまま危険なツールを運用することになります。

関連記事・次に読む

MCPの認可全体を確認したい場合は、MCP認可の実装ガイド|OAuth 2.1対応を先に読むと、OAuthやProtected Resource Metadataとの関係が整理しやすくなります。

MCPサーバーの脆弱性対策を広く押さえたい場合は、MCPサーバーの脆弱性と防御7選も参考になります。ToolAnnotationsは便利ですが、SSRF・トークン管理・セッション保護とは別レイヤーなので、合わせて確認してください。

ChatGPT上でMCP互換アプリを作る場合は、OpenAI Apps SDK入門|5ステップで作るも関連します。Apps SDKでもreadOnlyHintなどの考え方が出てくるため、UI側の承認体験を設計する参考になります。

参考・出典

まとめ:今日から始める3つのアクション

  1. 今日やること: 既存MCPツールを一覧化し、読み取り・変更・外部送信・削除の4分類を付ける。
  2. 今週中: すべてのツールにreadOnlyHintdestructiveHintidempotentHintopenWorldHintを明示し、CIで漏れを検出する。
  3. 今月中: アノテーションとは別に、サーバー側のスコープ検証・明示承認・監査ログを実装する。

ToolAnnotationsは小さな設定に見えますが、AIエージェントの自動実行範囲を決めるうえでかなり重要です。まずは「ヒント」と「強制ルール」を分け、UI・承認・サーバー認可の3層で設計してみてください。

この記事を読んで導入イメージが固まってきた方へ

UravationではAIエージェント導入の研修・コンサルを行っています。

この記事はAIgent Lab編集部がお届けしました。

Need help moving from reading to rollout?

この記事を読んで導入イメージが固まってきた方へ

Uravationでは、AIエージェントの要件整理、PoC設計、社内導入、研修まで一気通貫で支援しています。

この記事をシェア

X Facebook LINE

※ 本記事の情報は2026年5月時点のものです。サービスの料金・仕様は変更される可能性があります。最新情報は各サービスの公式サイトをご確認ください。

関連記事