MCPツールに破壊的操作を持たせるときの設計|削除・更新処理を安全にする

MCPツールに破壊的操作を持たせるときの設計|削除・更新処理を安全にする AI開発

MCP Toolでは、データを読むだけでなく、外部システムを変更する処理も実装できます。たとえば、delete_customer・update_issue・send_email・deploy_application・cancel_orderといったToolです。

AIエージェントにとって非常に便利な機能ですが、検索や読み取りToolと同じ感覚で実装すると危険です。モデルがIDを取り違えたり、Prompt Injectionによって誤った操作を選択したり、通信の再試行によって同じToolが複数回実行されたりすると、実データが失われる可能性があります。

特に、delete(id)だけを受け取って即座に削除するToolは避けたほうがよいでしょう。

安全な破壊的Toolでは、次のような複数段階の設計を考えます。

安全な破壊的Toolの基本フロー
対象を特定
  ↓
現在状態を確認
  ↓
変更内容をPreview
  ↓
必要ならユーザー確認
  ↓
権限を再確認
  ↓
更新・削除
  ↓
結果を検証
  ↓
監査ログを残す

現在のMCPには、Toolの性質をClientへ伝えるreadOnlyHint、destructiveHint、idempotentHintなどのAnnotationがあります。しかしMCP Python SDKの公式ドキュメントも、これらはあくまでClient向けのHintであり、Security機構ではないと明記しています。Clientが確認画面を出してくれることを前提にしてはいけません。

この記事では、MCP Toolに削除や更新などの破壊的操作を持たせる場合に、誤操作や二重実行を防ぐための設計方法を解説します。

スポンサーリンク
  1. destructiveHintで破壊的Toolだと明示する
  2. AnnotationをAccess Controlとして使わない
  3. deleteとupdateを巨大なmanage Toolへまとめない
  4. 「confirm=true」を引数に入れるだけでは確認にならない
  5. ユーザー確認はモデル入力とは別の経路で取る
  6. 2026-07-28では確認処理の再実行にも注意する
  7. Preview Toolと実行Toolを分ける
  8. 確認Tokenを発行する方法もある
  9. 確認Tokenには有効期限を付ける
  10. 更新Toolにはexpected_versionを持たせる
  11. update_customerに現在値も渡させる方法がある
  12. 削除はHard DeleteよりSoft Deleteを優先する
  13. deleteとrestoreを対にする
  14. idempotentHintは実装が本当に冪等な場合だけtrueにする
  15. 削除Toolも冪等に設計できる
  16. PaymentやEmail送信にはIdempotency Keyを使う
  17. 「delete_all」のような広すぎるToolを避ける
  18. 自然言語だけで対象を指定させない
  19. Prompt Injectionを前提にServer側で権限確認する
  20. Tool Descriptionにも副作用を具体的に書く
  21. update Toolでは変更可能FieldをAllowlistする
  22. JSON SchemaのValidationだけではBusiness Ruleを保証できない
  23. Database更新はTransactionでまとめる
  24. 外部APIを含む場合はRollbackできない前提で設計する
  25. 失敗時に「再実行してください」だけ返さない
  26. 結果には変更後の状態を返す
  27. 削除後に存在確認する
  28. 監査ログを残す
  29. BeforeとAfterを保存すると調査しやすい
  30. dry-runを用意する
  31. 本番Environmentはさらに権限を分離する
  32. 削除できる件数をServer側で制限する
  33. Tool Annotation・Confirmation・Permissionはそれぞれ役割が違う
  34. MCPの破壊的Tool設計に関するよくある質問
  35. MCPの破壊的Toolは「実行できる」より「間違って実行されても壊れにくい」を目指す

destructiveHintで破壊的Toolだと明示する

MCP Toolにはannotationsを設定できます。削除Toolなら、たとえばPython SDKで次のようにできます。

Python
from mcp.server import MCPServer
from mcp.types import ToolAnnotations

mcp = MCPServer("Customers")


@mcp.tool(
    annotations=ToolAnnotations(
        read_only_hint=False,
        destructive_hint=True,
        idempotent_hint=True,
        open_world_hint=False,
    )
)
def delete_customer(
    customer_id: str
) -> str:
    ...

read_only_hint=Falseは状態を変更するToolであることを示します。destructive_hint=Trueは、既存データを削除したり破壊的に更新したりする可能性があることを示します。idempotent_hint=Trueは、同じ引数で繰り返し実行しても追加の副作用が発生しないToolであることを示します。

現行MCP TypeScript SDKでは、destructiveHintのDefaultはtrue、idempotentHintのDefaultはfalseです。またこれらはreadOnlyHint == falseの場合に意味を持ちます。

AnnotationをAccess Controlとして使わない

たとえば、destructiveHint=trueにしておけばClientが必ず確認画面を出す、と考えるのは危険です。

MCP Python SDKの公式ドキュメントでは、ClientがAnnotationを見て「実行前にユーザーへ確認するか」といった判断に利用できる一方、AnnotationはSecurityではなく、Clientが従うことを前提にしてはいけないと説明されています。

つまり、destructiveHintは、このToolは危険ですとClientへ知らせるMetadataです。実際に、そのUserに削除権限があるか、そのResourceを削除してよいか、ユーザー確認が済んでいるかを保証するのはMCP Server側です。

deleteとupdateを巨大なmanage Toolへまとめない

たとえば、次のようにget・create・update・deleteを1つのToolにまとめることもできます。

NG: 1つのToolに操作を詰め込む
@mcp.tool()
def manage_customer(
    action: str,
    customer_id: str,
    data: dict | None = None
):
    ...

しかしAIモデル向けでは、処理の危険度が分かりにくくなります。読み取りと削除を分けて、get_customer・update_customer・delete_customerとしたほうが、モデルにもClientにも副作用が明確になります。

特にget_customerにはreadOnlyHint=trueを付け、delete_customerにはreadOnlyHint=false・destructiveHint=trueを付けられます。Tool単位でPermissionやScopeを分けることも容易になります。

「confirm=true」を引数に入れるだけでは確認にならない

削除Toolを安全にしようとして、次のようにすることがあります。

NG: confirmをTool Argumentに持たせる
@mcp.tool()
def delete_customer(
    customer_id: str,
    confirm: bool
):
    if not confirm:
        return "Confirmation required"

    ...

しかしこれは強い確認にはなりません。なぜなら、{"customer_id": "123", "confirm": true}を作るのもAIモデル自身だからです。モデルが「削除するにはconfirm=trueが必要」と理解すれば、自分でtrueを設定できます。これは人間の確認ではありません。

ユーザー確認はモデル入力とは別の経路で取る

MCP Python SDKでは、Elicitationを利用してUser Inputを取得できます。現在のSDKには、破壊的操作を確認する具体例として、Directory削除前に確認を行うResolverが掲載されています。

重要なのは、確認値がToolのInput Schemaへ出ないことです。Clientやモデルが渡すのはpathだけで、確認結果はResolver側からToolへ注入されます。概念的には次のようにできます。

OK: Elicitationで確認をモデル入力から分離(Python)
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import (
    Elicit,
    Resolve,
)

mcp = MCPServer("Customers")


class ConfirmDelete(BaseModel):
    ok: bool


async def confirm_delete(
    customer_id: str
) -> ConfirmDelete | Elicit[ConfirmDelete]:

    customer = load_customer(
        customer_id
    )

    return Elicit(
        (
            f"{customer['name']} "
            "を削除しますか?"
        ),
        ConfirmDelete,
    )


@mcp.tool()
async def delete_customer(
    customer_id: str,
    confirmation: Annotated[
        ConfirmDelete,
        Resolve(confirm_delete)
    ],
) -> str:

    delete_from_database(
        customer_id
    )

    return "deleted"

この場合、モデルはconfirmation=trueを勝手に生成できません。確認処理はTool本体より前にResolverによって実行されます。

2026-07-28では確認処理の再実行にも注意する

2026-07-28 MCPでは、ServerからClientへの従来型JSON-RPC Request Channelがなくなり、追加のUser Inputが必要な場合はinput_requiredを返し、Clientが回答を付けて元のCallを再実行するMulti Round-Trip方式になっています(この変更の詳細はMCPのtools・resources・promptsの違いで解説しています)。

Python SDKのResolverはこの違いを吸収しており、2026-07-28でもLegacy Clientでも同じ形で利用できます。ここで重要なのは、確認前に副作用を発生させないことです。

たとえば次の順序は危険です。

NG: 副作用の後に確認を取る
Databaseを更新
  ↓
input_requiredを返す
  ↓
ユーザーへ確認
  ↓
Tool Callが再実行

Multi Round-TripではHandlerが再度実行されるため、更新処理が複数回走る可能性があります。必ず、確認→権限確認→副作用の順にします。

OK: 確認を先に行う順序
確認
  ↓
権限確認
  ↓
副作用

Preview Toolと実行Toolを分ける

特に影響の大きい操作では、PreviewとExecutionを分ける方法が有効です。たとえば最初にpreview_delete_customerを呼び出します。

JSON: Preview結果
{
  "customerId": "123",
  "name": "Example Corp",
  "orders": 48,
  "invoices": 12,
  "canDelete": true
}

モデルやユーザーが影響を確認した後、delete_customerを実行します。こうすれば、IDだけを見て即削除する構成を避けられます。

確認Tokenを発行する方法もある

Previewと実行をより強く結び付けるなら、Serverが確認Tokenを発行する方法があります。たとえばPreview Toolが次を返します。

JSON: 確認Token付きPreview
{
  "customerId": "123",
  "version": 18,
  "confirmationToken": "opaque-token..."
}

削除Toolでは次を要求します。

JSON: 削除Toolへの入力
{
  "customerId": "123",
  "confirmationToken": "opaque-token..."
}

TokenにはServer側で、User ID・Resource ID・Action・Resource Version・Expirationを紐付けておきます。Tokenはモデルが自由に生成できる文字列ではなく、Serverが発行した署名付き、またはServer-side Storageと対応するOpaque Tokenにします。

これにより、Customer 123の削除確認をした後に別のCustomer 456を削除する、といった取り違えを防ぎやすくなります。

確認Tokenには有効期限を付ける

Preview後、数時間経ってから削除を実行すると、その間にデータが変化している可能性があります。そこで確認Tokenは短時間(5分・10分程度)で失効させます。

さらにTokenにResource Versionを関連付けておけば、次のような制御ができます。

Versionによる実行時再検証
Preview時 version=18
  ↓
別Userが更新
  ↓
現在 version=19
  ↓
削除拒否

確認した状態と実行時の状態が違うなら、もう一度Previewさせるほうが安全です。

更新Toolにはexpected_versionを持たせる

削除だけでなく更新でも問題になります。たとえばモデルがCustomerを取得します。

JSON
{
  "id": "123",
  "email": "old@example.com",
  "version": 18
}

その直後、人間が同じCustomerを更新してversion=19になったとします。AIが古い情報のまま更新すると、人間の変更を上書きする可能性があります。そこで更新Toolへexpected_versionを渡します。

Python
@mcp.tool()
def update_customer(
    customer_id: str,
    email: str,
    expected_version: int,
):
    ...

Databaseでは概念的に次のようにします。

SQL
UPDATE customers
SET
    email = :email,
    version = version + 1
WHERE
    id = :customer_id
    AND version = :expected_version;

更新件数が0なら、対象が変更されています。最新状態を再取得してください。と返します。これはOptimistic Lockingと呼ばれる一般的なConcurrency Controlです。AIエージェントでは「少し前に読んだ情報」をもとにToolを呼ぶことが多いため、特に相性のよい対策です。

update_customerに現在値も渡させる方法がある

Version Columnを追加できないシステムなら、old_email・new_emailのように現在値をPreconditionとして渡す方法もあります。

SQL
UPDATE customers
SET email = :new_email
WHERE
    id = :id
    AND email = :old_email;

すでに別の値へ変わっていれば更新しません。モデルが「以前読んだ状態」と「現在の状態」の差を無視して上書きする事故を防げます。

削除はHard DeleteよりSoft Deleteを優先する

可能なら、次のようなHard Deleteより、

NG: Hard Delete
DELETE FROM customers
WHERE id = ?;

次のようなSoft Deleteを検討します。

OK: Soft Delete
UPDATE customers
SET deleted_at = NOW()
WHERE id = ?
  AND deleted_at IS NULL;

誤削除しても復元できます。さらに、delete_customerはSoft Delete、purge_customerだけを完全削除として分離できます。purge_customerは管理者専用にして、より強い確認を要求します。AIエージェントが通常利用するToolから不可逆なHard Deleteを外せるなら、それだけでも事故時の被害を大きく減らせます。

deleteとrestoreを対にする

Soft Deleteを使うなら、delete_customer・restore_customerをセットにできます。削除結果として、次のようなものを返せば、モデルやClientも復元可能な操作であることを理解できます。

JSON
{
  "customerId": "123",
  "deleted": true,
  "recoverableUntil": "2026-10-10T10:00:00Z"
}

即時Hard Deleteより、Undo可能なOperationを優先するのが安全です。

idempotentHintは実装が本当に冪等な場合だけtrueにする

Tool AnnotationにはidempotentHintがあります。同じArgumentsで複数回呼んでも追加の副作用が発生しない場合にtrueとします。現行MCP SDKではDefaultはfalseです。

たとえばset_customer_status(id="123", status="disabled")が、disabled→disabledとなるだけなら冪等に設計しやすいでしょう。一方、increment_credit(id="123", amount=100)は2回呼ぶと200増えるため、冪等ではありません。AnnotationだけidempotentHint=trueにしても、実装は冪等になりません。

削除Toolも冪等に設計できる

たとえばSoft Deleteなら、次のように、すでに削除済みなら追加変更しない設計ができます。

SQL
UPDATE customers
SET deleted_at = COALESCE(
    deleted_at,
    NOW()
)
WHERE id = ?;

Tool Resultも次のように正常終了させられます。

JSON
{
  "customerId": "123",
  "status": "already_deleted"
}

このような実装なら、Network Retryなどで同じTool Callが再送されても被害が広がりにくくなります。ただし対象が一度削除された後に同じIDで再生成されるシステムなどでは、単純なIDだけで本当に冪等と言えるか慎重に確認する必要があります。

PaymentやEmail送信にはIdempotency Keyを使う

削除よりさらに注意したいのが、決済・メール送信・注文作成・チケット発行などです。同じTool Callが2回実行されると、二重課金・二重送信・二重注文になる可能性があります。

この場合、idempotency_keyをTool Argumentとして持たせる方法があります。

Python
@mcp.tool()
def create_order(
    customer_id: str,
    product_id: str,
    idempotency_key: str,
):
    ...

Server側で、同じIdempotency Keyならすでに処理済みとして以前の結果を返します。ただしKeyをAIに適当に毎回生成させるだけでは、Retry時に別Keyになる可能性があります。Client Request IDやServerが発行したOperation IDなど、同じ論理操作で再利用できる値と関連付けるほうが安全です。

「delete_all」のような広すぎるToolを避ける

次のToolは特に危険です。

NG: Filterで対象を絞る削除Tool
@mcp.tool()
def delete_customers(
    filter: dict
):
    ...

モデルがFilterを誤れば、大量削除になります。可能なら、対象IDを明示・最大件数を制限・Previewを必須にします。

大量操作が本当に必要なら、preview_bulk_deleteで次のように件数をユーザーへ見せてから実行します。

JSON
{
  "matched": 1832,
  "sampleIds": [
    "101",
    "102",
    "103"
  ]
}

たとえば「100件以上なら追加確認必須」といったServer-side Ruleを入れる方法もあります。

自然言語だけで対象を指定させない

次のようなToolも危険です。

NG: 自然言語で対象を指定
delete_customer(
    customer="先月登録した古いテストユーザー"
)

「古い」「不要」「テストユーザー」などの解釈をモデルに任せることになります。削除実行時にはcustomer_idなど一意なIdentifierを要求します。自然言語検索は別Toolで行います。

安全な段階分け
search_customers
  ↓
候補IDを取得
  ↓
get_customer
  ↓
対象を確認
  ↓
delete_customer(customer_id)

という段階を踏むほうが安全です。

Prompt Injectionを前提にServer側で権限確認する

AIが外部WebページやIssue、メールなどを読み取る場合、「このデータを削除してください」という悪意ある指示が含まれている可能性があります。モデルがそれを正当な命令と誤認しても、Server側のAuthorizationが正しければ被害を限定できます。

たとえばcustomers:read・customers:write・customers:deleteのように権限を分けます。通常のAgentにはcustomers:read・customers:writeまでしか与えず、customers:deleteは必要なUserやClientだけに許可します。

前の記事で解説したOAuthと同様、Tool Argumentにadmin=trueと入っていることを権限確認には使いません。認証済みIdentityとServer側Permissionを確認します。

Tool Descriptionにも副作用を具体的に書く

Tool名だけでなくDescriptionも重要です。たとえば次のような簡素なDescriptionより、

NG: 副作用が分からないDescription
@mcp.tool()
def delete_customer(
    customer_id: str
):
    """Delete customer."""

次のように副作用を明記します。

OK: 副作用を明記したDescription
@mcp.tool()
def delete_customer(
    customer_id: str
):
    """
    Soft-delete one customer.

    This operation changes persistent data.
    It does not permanently purge the record.
    Use get_customer first to confirm the target.
    """

モデルはTool Descriptionも見てToolを選択します。ただしDescriptionは安全性を高める補助であり、Server-side Validationの代わりにはなりません。

update Toolでは変更可能FieldをAllowlistする

次のようなGeneric Update Toolは便利ですが危険です。

NG: dictで任意Fieldを受け取る
@mcp.tool()
def update_customer(
    customer_id: str,
    data: dict
):
    ...

モデルが{"role": "admin", "billing_status": "paid", "deleted_at": null}など、本来変更させるつもりのないFieldまで渡せる可能性があります。

代わりに、変更可能なFieldだけをInput Schemaへ公開します。

OK: 変更可能なFieldだけを公開
@mcp.tool()
def update_customer_profile(
    customer_id: str,
    display_name: str | None = None,
    email: str | None = None,
    expected_version: int = 0,
):
    ...

MCP Python SDKでは型HintやPydantic制約からInput Schemaを生成し、不正な型や制約違反をTool Handler実行前に拒否できます。DatabaseのColumn名をモデルへ自由入力させる必要はありません。

JSON SchemaのValidationだけではBusiness Ruleを保証できない

Input Schemaでcustomer_idはstring・amountはintegerを保証しても、そのCustomerを削除してよいか、そのOrderはCancel可能か、現在のUserがOwnerかまでは分かりません。

そのためTool Handlerでは、Schema Validationの後にBusiness Ruleを検証します。たとえば注文キャンセルなら、次のような処理にします。

注文キャンセルのBusiness Rule検証順序
存在確認
  ↓
Userがその注文へアクセス可能か確認
  ↓
現在Statusがcancellableか確認
  ↓
Version確認
  ↓
Cancellation

Database更新はTransactionでまとめる

1つのTool Callで複数のDatabase変更が必要な場合があります。たとえばCustomer削除時に、CustomerをSoft Delete・Sessionを無効化・API Tokenを失効・Audit Logを保存するとします。

途中でErrorになると、Customerだけ削除済みでAPI Tokenは有効、という中途半端な状態になる可能性があります。可能ならDatabase Transactionを利用し、すべて成功ならCommit、どれか失敗ならRollbackにします。

AIエージェントから見るとTool Callは1つの論理操作なので、Server側でもAtomicに近い単位にまとめるほうが扱いやすくなります。

外部APIを含む場合はRollbackできない前提で設計する

Database Transactionだけでは、メール送信・外部Payment API・GitHub Issue作成・Cloud Resource削除などをRollbackできません。この場合は処理順序を考えます。

たとえば、DBを削除した後に外部API失敗、より、事前Validation→外部処理→DB状態更新が適切なケースもあります。逆に外部処理が成功しDB更新が失敗する可能性もあります。そのため、Retry可能か・Idempotency Keyが使えるか・Compensating Actionがあるかを設計します。

たとえば作成したCloud Resourceを後から削除できるなら、失敗時にCompensating Actionとして削除します。

失敗時に「再実行してください」だけ返さない

Tool Errorで「処理に失敗しました。もう一度実行してください。」とだけ返すと、モデルがすぐ再実行する可能性があります。破壊的Toolでは危険です。

Server側で、操作が実行される前に失敗したのか、操作自体は成功したがResponse送信で失敗したのか区別できる設計が重要です。Idempotency KeyやOperation IDを使えば、再実行時に「このOperationはすでに成功しています」と判断できます。

結果には変更後の状態を返す

更新ToolがOKだけ返すより、次のようなStructured Resultを返すほうが安全です。

JSON
{
  "customerId": "123",
  "status": "disabled",
  "version": 19,
  "updated": true
}

モデルは何が実際に変更されたかを確認できます。削除なら次のようにします。

JSON
{
  "customerId": "123",
  "deleted": true,
  "mode": "soft-delete"
}

前の記事で解説したoutputSchemaとstructuredContentを利用すれば、Client側も変更結果を安定して処理できます。

削除後に存在確認する

重要な操作では、変更後のStateをServer側で確認する方法もあります。たとえば、UPDATE→対象を再取得→deleted_atが設定されたことを確認→成功Result、とします。

外部APIでも可能なら、DELETE Request→GET Request→削除済みを確認、とします。ただしAPIによってはEventual Consistencyがあるため、必ずしも即座に確認できるとは限りません。対象Systemの特性に合わせます。

監査ログを残す

破壊的Toolでは、後から誰が・いつ・何を・どのToolで・どの値からどの値へ変更したかを追えることが重要です。たとえばAudit Logには、次のような項目を記録できます。

Audit Logに残す項目の例
request_id
user_id
client_id
tool_name
resource_id
operation
old_version
new_version
result
timestamp

削除なら、少なくとも対象IDと実行主体、結果を残します。ただしAccess TokenやPasswordなど秘密情報はLogへ保存しないようにします。

BeforeとAfterを保存すると調査しやすい

更新処理なら、次のような差分をAudit Logへ残すと、誤操作時の調査が容易になります。

JSON
{
  "before": {
    "status": "active"
  },
  "after": {
    "status": "disabled"
  }
}

ただし個人情報や巨大Dataを無条件で丸ごと保存する必要はありません。必要なFieldだけ記録します。

dry-runを用意する

一括更新やDeploymentのように影響範囲が大きい操作では、dry_runも有効です。

Python
@mcp.tool()
def disable_inactive_users(
    days: int,
    dry_run: bool = True,
):
    ...

として、Defaultでは変更せず、次のように返します。

JSON
{
  "matched": 48,
  "modified": 0,
  "dryRun": true
}

ただし前述したconfirm=trueと同じく、単純なdry_run=Falseをモデルが設定できるだけでは人間確認の代わりにはなりません。dry-runは影響確認の機能であり、AuthorizationやConfirmationとは別です。

本番Environmentはさらに権限を分離する

同じToolでdevelopment・staging・productionを引数として自由に選べる構成も注意が必要です。たとえば{"environment": "production"}をモデルが自分で指定できます。

本番操作は、別MCP Server・別Credential・別OAuth Scope・別Toolに分ける方法もあります。たとえばdeploy_stagingは通常Agentへ許可し、deploy_productionは特定Clientだけに公開します。「Environment名を引数へ入れれば安全」という設計より、Credential Boundaryそのものを分けるほうが強力です。

削除できる件数をServer側で制限する

一括削除が必要でも、「1回最大50件」などHard Limitを設けられます。モデルが「全ユーザーを削除」と判断しても1回のTool Callで全件削除できなくなります。

大量操作は、Preview→件数表示→強いConfirmation→Batch処理のように別Workflowへ分けます。単純な1件削除と数万件削除を同じ安全レベルで扱わないことが重要です。

Tool Annotation・Confirmation・Permissionはそれぞれ役割が違う

破壊的MCP Toolでは、1つの仕組みだけで安全性を確保することは難しくなります。

destructiveHintはClientへ危険性を伝えるものです。MCP公式SDKも、AnnotationはSecurityではないと明示しています。

Elicitationや2026-07-28のMulti Round-Trip Requestは、モデルが生成した引数とは別経路でユーザー確認を取得するために利用できます。Python SDKには削除前確認のResolverが実際に用意されています。

Authorizationは、そのUserやClientがその操作を実行できるかをServer側で判断します。expected_versionは古いStateをもとにした上書きを防ぎます。IdempotencyはRetryや二重実行による副作用を抑えます。

Soft Deleteは事故が起きた後の復旧手段になります。Transactionは途中失敗による中途半端なStateを防ぎます。Audit Logは事故後に何が起きたかを追跡できるようにします。

MCPの破壊的Tool設計に関するよくある質問

QdestructiveHintをtrueにすれば、Clientは必ず確認を求めますか?

Aいいえ、保証されません。MCP Python SDKの公式ドキュメントでも、AnnotationはClient向けのHintであってSecurity機構ではないと明記されています。Clientの実装によって確認の有無は変わるため、重要な確認はServer側のElicitationなど別の経路で取得する必要があります。

QElicitationを使えば確認は常に安全になりますか?

AElicitation自体はモデルが勝手に生成できない経路でUser Inputを取得できる点で有効ですが、それだけで十分ではありません。確認後に権限確認・Version確認などのServer側検証を行い、実際の副作用は確認が完了した最終段階でのみ発生させる設計と組み合わせる必要があります。

QSoft Deleteにしておけば監査ログは不要になりますか?

A不要にはなりません。Soft Deleteはデータの復元を容易にしますが、「誰が・いつ・何を変更したか」を追跡する役割は別です。Soft Deleteと監査ログはそれぞれ目的が異なるため、両方実装するのが安全です。

QidempotentHintをtrueにしておけば実装も自動的に冪等になりますか?

Aなりません。idempotentHintはClientへ伝えるMetadataであり、実装の振る舞いを変えるものではありません。同じ引数で複数回呼んでも追加の副作用が出ないように、Tool Handler自身を冪等に実装する必要があります。

Qexpected_versionとIdempotency Keyはどちらを使うべきですか?

A目的が異なるため、場面に応じて使い分けます。expected_versionは「古い情報をもとに上書きしていないか」を防ぐ楽観的ロックで、更新系のToolに向いています。Idempotency Keyは「同じ操作が複数回実行されていないか」を防ぐもので、決済や注文作成など新規作成系のToolに向いています。両方が必要になる操作もあります。

MCPの破壊的Toolは「実行できる」より「間違って実行されても壊れにくい」を目指す

MCP Toolに削除や更新を実装すること自体が危険なのではありません。危険なのは、モデルが対象を選ぶ→Toolが即座に不可逆処理を行う、だけの構成です。

まずToolを読み取りと更新に分離し、破壊的Toolには次のようなAnnotationを付けて性質をClientへ明示します。

Python
ToolAnnotations(
    read_only_hint=False,
    destructive_hint=True,
)

ただしAnnotationはSecurity Boundaryではありません。不可逆な操作では、ユーザー確認をTool Argumentのconfirm=trueで済ませず、Elicitationなどモデルが勝手に生成できない経路で取得します。2026-07-28ではUser Input取得がMulti Round-Trip方式になっているため、副作用は確認後の最終段階でだけ発生させることも重要です。

更新処理にはexpected_versionなどのPreconditionを持たせ、モデルが古い情報をもとに新しい変更を上書きすることを防ぎます。削除は可能ならSoft Deleteにし、完全削除はより強い権限を持つ別Toolへ分離します。二重実行が問題になる処理にはIdempotency Keyを持たせます。

そしてServer側で、Authentication・Authorization・Validation・Confirmation・Concurrency Control・Transaction・Audit Logを実装します。

MCPの破壊的Toolで重要なのは、AIモデルが常に正しい判断をすると期待することではなく、モデルが間違ったTool Callを生成した場合でもServer側で被害を止められる設計にすることです。

読み取りToolでは「便利に使えるか」が中心になりますが、削除・更新Toolでは「誤実行されても復元できるか」「二重実行されても追加被害が出ないか」「古いStateを上書きしないか」まで含めて設計すると、安全にAIエージェントへ操作権限を与えやすくなります。