MCPツールに「アノテーション」を付けて、AIクライアントの権限管理を読み書きで分類する

MCPサーバーのツール定義に readOnlyHint / destructiveHint といった「アノテーション」を付けると、Claudeなどのクライアント側で「読み取り専用ツール」「書き込み/削除ツール」として自動的にグループ分けされ、権限管理がぐっと扱いやすくなります。意外と知られていないこの仕組みのメリットと、実際の付け方を解説します。

広告ここから
広告ここまで

目次

    MCP(Model Context Protocol)サーバーを自作してClaudeなどのAIクライアントに接続したとき、ツールの権限管理画面の見え方がサーバーによって違う、と気づいたことはないでしょうか。あるサーバーは「読み取り専用ツール」「書き込み/削除ツール」ときれいにグループ分けされているのに、自作サーバーは全ツールが「その他のツール」としてフラットに並ぶだけ。

    この差を生んでいるのが、MCPの仕様にあるツールアノテーション(tool annotations)です。あまり知られていませんが、ツール定義にいくつかのヒントを添えるだけで、クライアント側の権限UIが読み書きを自動分類してくれるようになります。この記事では、アノテーションのメリットと具体的な付け方をまとめます。

    ツールアノテーションとは

    MCPの各ツール定義には annotations というメタデータを添付できます。これはツールの「振る舞いのヒント」をクライアントに伝えるためのもので、次の4つのフィールドがあります。

    アノテーション 意味
    readOnlyHint boolean true なら環境を変更しない読み取り専用ツール
    destructiveHint boolean true なら破壊的(削除・上書きなど)な操作。readOnlyHintfalse のときに意味を持つ
    idempotentHint boolean 同じ引数で繰り返し呼んでも結果(状態)が変わらないか
    openWorldHint boolean 外部エンティティ(外部API・Webなど)と相互作用するか

    重要なのは、これらはあくまでヒント(hint)であって保証ではない、という点です。クライアントはこの情報を使ってUIの出し分けや確認ダイアログの要否を判断しますが、サーバー側の実際の挙動を強制するものではありません。とはいえ、正しく付けておくことでユーザー体験が大きく変わります。

    アノテーションを付けるメリット

    • 権限UIがカテゴリ単位で扱える:Claudeの権限設定では、アノテーションがあると「読み取り専用ツール(◯個)」「書き込み/削除ツール(◯個)」のようにグループ化されます。「読み取り系はまとめて常に許可、書き込み系は都度承認」といった設定が1クリックで可能になります。
    • アノテーションが無いとフラットな一覧になる:ヒントを返さないサーバーのツールは、クライアントが読み書きを判別できず「その他のツール」として全部が1つの塊で並びます。ツール数が多いほど設定が面倒になります。
    • 破壊的操作の事故を減らせるdestructiveHint: true のツールに対してクライアントが追加確認を出すなど、削除系オペレーションのガードレールとして機能します。
    • 自律エージェントの安全性が上がる:エージェントが自動でツールを選ぶとき、「読み取り専用」を優先させる、破壊的ツールは人間の承認を挟む、といった制御の材料になります。

    付け方その1:registerTool のconfigに渡す

    TypeScript版MCP SDK(@modelcontextprotocol/sdk)の registerTool を使う場合、ツール定義のconfigオブジェクトに annotations を足すだけです。これが最も素直な書き方です。

    server.registerTool(
      "delete_post",
      {
        title: "Delete Post",
        description: "指定したIDの投稿を削除します。",
        inputSchema: { id: z.number() },
        annotations: {
          readOnlyHint: false,
          destructiveHint: true,
          idempotentHint: true,   // 同じIDを2回消しても最終状態は同じ
          openWorldHint: true,    // 外部API(WordPress)を叩く
        },
      },
      async ({ id }) => {
        // ...実際の削除処理
      },
    );

    読み取り系のツールなら次のようになります。

    server.registerTool(
      "list_posts",
      {
        title: "List Posts",
        description: "投稿の一覧を取得します。",
        inputSchema: {},
        annotations: {
          readOnlyHint: true,
          destructiveHint: false,
          openWorldHint: true,
        },
      },
      async () => {
        // ...一覧取得処理
      },
    );

    付け方その2:server.tool() のオーバーロードで渡す

    古めの server.tool() API を使っている場合は、引数の並びでアノテーションを渡せるオーバーロードがあります。name, description, paramsSchema, annotations, callback の順です。

    server.tool(
      tool.name,
      tool.description,
      tool.schema,
      tool.annotations,          // ← 4番目にアノテーション
      async (args) => handler(args),
    );

    ツールをレジストリ(配列)で一元管理しているプロジェクトなら、ツール定義の型に annotations フィールドを追加し、登録ループでそのまま渡すのがきれいです。「全ツールにアノテーション必須」を型レベルで強制したいなら、フィールドをオプショナル(?)ではなく必須にしておくと、新しいツールを追加するときに付け忘れがコンパイルエラーで弾けます。

    どう分類するか:判断のヒューリスティック

    個々のツールをどのカテゴリに割り当てるかは、操作のセマンティクスで決めます。ツール名の動詞から機械的に判断できることが多いです。

    操作の種類 readOnlyHint destructiveHint idempotentHint
    get / list / search / read(取得・検索) true false
    create / post / add(新規作成) false false
    update / patch / set(更新) false false true
    delete / clear / remove(削除) false true true

    • updateやdeleteは idempotentHint: true にしやすい:「同じ更新を2回適用しても最終状態は同じ」「同じものを2回消しても結果は同じ」だからです。一方、createは呼ぶたびに新しいリソースが増えるので冪等ではありません。
    • openWorldHint は外部依存の有無で決める:外部APIやWebを叩くツールは true。純粋にローカルで完結する計算ツール(足し算など)や、自前のストレージだけを触るツールは false にできます。
    • 「読み取りだが重い処理」も読み取り扱いでよい:データを取得してレポートを生成するだけのツールは、状態を変えないなら readOnlyHint: true です。処理の重さは関係ありません。

    テストで「付け忘れ」を防ぐ

    アノテーションは付け忘れても動いてしまうため、テストで担保しておくと安心です。ツールをレジストリで管理しているなら「全ツールに annotations が存在すること」を、代表的な読み取り/作成/削除ツールについては「期待するヒント値になっていること」をアサートするテストを書いておきます。

    import { tools } from "./tools";
    
    test("すべてのツールにアノテーションがある", () => {
      for (const tool of tools) {
        expect(tool.annotations).toBeDefined();
      }
    });
    
    test("削除ツールは破壊的としてマークされている", () => {
      const del = tools.find((t) => t.name === "delete_post")!;
      expect(del.annotations.readOnlyHint).toBe(false);
      expect(del.annotations.destructiveHint).toBe(true);
    });

    ツールを命令的に registerTool で登録しているプロジェクトなら、registerTool をモックして「記録された各ツールのconfigにアノテーションが含まれているか」を検証する形にすると、同じことができます。

    まとめ

    • 権限UIで読み書きがグループ化されるかどうかは、クライアントの挙動差ではなくサーバーがアノテーションを返しているかどうかの差。
    • readOnlyHint / destructiveHint を中心に、ツール定義へ annotations を足すだけで、Claudeなどのクライアントが「読み取り専用」「書き込み/削除」を自動分類してくれる。
    • 分類はツール名の動詞ベースのヒューリスティックでほぼ機械的に決められる。テストで付け忘れを防ぐとなお良い。

    自作MCPサーバーを公開・運用するなら、ぜひ全ツールにアノテーションを付けておきましょう。利用者の権限管理の手間と、破壊的操作の事故リスクを同時に下げられる、コストの低い改善です。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark