Skip to content

MCP サーバー 書込安全装置

項目内容
ステータス🟡 設計中
関連案件#13 Mappy MCP サーバー新設
親ドキュメントMCP サーバー 全体像
関連設計ツールカタログ / 認証・テナンシ

1. 本ドキュメントの目的

MCP の 書込系 tool(Phase 2 以降) に必須の安全装置を定義する。

  • AI による誤実行のリスクを最小化
  • 同一操作の重複実行を防止
  • 大規模一括反映時のユーザー意図確認
  • 非同期処理の状態追跡
  • GBP API 制限への配慮
  • 監査可能性の確保

Phase 1 (MVP) ではすべて読取系

本ドキュメントの内容は Phase 2 以降 で実装する。MVP では batch.status を除き書込 tool を提供しない。

2. 設計原則

原則内容
明示同意5 件以上の一括反映は confirm_token 必須
再実行安全全書込 tool は idempotency_key 対応
状態可視化非同期処理は batch_id + ポーリングで完了確認
試行可能dry_run=true で副作用なしの確認モード
失敗の部分許容一括反映は per-location の成功/失敗を返す
監査全記録書込・dry_run・拒否を漏らさず Audit Log に記録
GBP API 配慮レート制限・トークン期限切れに対する保護

3. 4 つの安全装置

3.1 dry_run

副作用を発生させず「もし実行されたら何が起きるか」をシミュレートする。

動作

レスポンス例

json
{
  "data": {
    "would_execute": true,
    "operation": "post.create",
    "target_locations": [
      { "id": 8802, "title": "渋谷店", "store_code": "SHIBUYA001" },
      { "id": 8803, "title": "新宿店", "store_code": "SHINJUKU001" }
    ],
    "target_count": 2,
    "validation_warnings": [
      {
        "location_id": 8802,
        "code": "RECENT_POST_DETECTED",
        "message": "前回投稿から 1 時間未満です(重複の可能性)"
      }
    ],
    "estimated_gbp_api_calls": 2,
    "estimated_completion_seconds": 10
  }
}

必須対応 tool

tooldry_run 対応
post.create✅ 必須
post.delete✅ 必須
review.reply✅ 必須
media.upload✅ 必須
media.delete✅ 必須
location.update✅ 必須
report.create❌ 不要(生成自体に副作用なし、ただし課金あり)

dry_run の活用シーン

顧客 AI は「投稿しますか?」のような確認の前に dry_run を呼び、結果を要約して人間に提示する設計を推奨する。

3.2 idempotency_key

同一の意図の操作が複数回送信された場合に、1 回目だけ実行する。

仕様

項目内容
形式UUIDv4 推奨。任意の 8〜64 文字 ASCII
有効期間24 時間
スコープ(client_id, user_id, tool_name, idempotency_key) の組
衝突時動作1 回目のレスポンスをキャッシュから返却(status code 含む)
必須/任意書込系 tool では強く推奨、自動付与も可

動作

入力競合の検出

同じ idempotency_key異なる入力が来た場合は IDEMPOTENCY_CONFLICT エラー:

json
{
  "error_code": "IDEMPOTENCY_CONFLICT",
  "message": "同じ idempotency_key で異なるリクエストが送信されました",
  "details": { "key": "KEY1" }
}

検出方法: 1 回目のリクエストの input ハッシュを Redis に保存、2 回目で照合。

3.3 confirm_token(一括反映の意図確認)

5 件以上の一括反映、または影響範囲が大きい操作でユーザーの明示同意を強制する。

動作

必須となる条件

操作条件
post.createlocation_ids の長さ ≥ 5
post.deletelocation_ids の長さ ≥ 3
media.uploadlocation_ids の長さ ≥ 5 OR media の合計サイズ ≥ 50 MB
media.deletemedia_ids の長さ ≥ 5
location.update同時更新先 ≥ 3
review.reply通常不要(1 件単位)

confirm_token 詳細

項目内容
発行元MCP Server
形式32 文字以上のランダム ASCII(Redis に保存)
有効期間5 分
スコープ(client_id, user_id, tool_name, input_hash)
再利用不可(1 回消費)
入力変更input_hash が変わると無効

412 レスポンス例

json
{
  "error_code": "CONFIRMATION_REQUIRED",
  "message": "5 件以上の一括反映には confirm_token が必要です",
  "details": {
    "confirm_token": "abc123def456...",
    "expires_in": 300,
    "target_summary": {
      "operation": "post.create",
      "target_count": 10,
      "locations": [
        { "id": 8801, "title": "渋谷店" },
        { "id": 8802, "title": "新宿店" }
      ],
      "post_summary": "本日のキャンペーン情報..."
    },
    "human_readable_message": "10 店舗に同じ投稿を一括反映します。よろしいですか?"
  }
}

3.4 batch_id + ポーリング

GBP API への書込は時間がかかる(複数店舗一括で数十秒〜数分)ため、非同期実行 + 状態確認を必須とする。

動作

batch ステータス遷移

ポーリング推奨間隔

状態推奨間隔
queued5 秒
processing (target_count ≤ 5)5 秒
processing (target_count > 5)10 秒
done / failed / partialポーリング停止

クライアント側で指数バックオフを推奨。

失敗の部分許容

10 店舗中 1 店舗で GBP API エラーが起きた場合:

json
{
  "data": {
    "batch_id": "01HVABC...",
    "status": "partial",
    "target_count": 10,
    "success_count": 9,
    "failure_count": 1,
    "failures": [
      {
        "location_id": 8802,
        "error_code": "GBP_PERMISSION_DENIED",
        "message": "このロケーションへの投稿権限がありません",
        "retry_possible": false
      }
    ]
  }
}

4. GBP API レート制限への配慮

4.1 既存実装の活用

Mappy には既に GBP API のレート制限ハンドリング機構がある(spatie/guzzle-rate-limiter-middleware)。MCP は Laravel API 経由でアクセスするため、この機構が自動適用される。

4.2 アクセストークン管理

  • Mappy DB の google_access_tokens / mappy_google_access_tokens テーブルでトークン管理
  • 60 分の短期トークン + リフレッシュトークン方式
  • 並行リクエストでのトークン更新競合を Laravel 側で制御済み(SMARTMEO-47 対応で実施)

4.3 MCP 側の追加保護

保護内容
同時実行数制限1 ユーザー 5 並列まで
1 batch あたり最大 100 店舗DB 確認結果より、最大顧客の店舗数を考慮
pageSize=100 のループ制御Laravel 側既存ロジックを利用
Redis でのレート制限10 リクエスト/分(書込)

5. 失敗時の挙動とリトライ

5.1 失敗パターンと対処

失敗パターン対処
通信エラー(MCP ↔ Laravel)クライアント側で idempotency_key 再送可
GBP API 一時エラー(5xx)Worker が自動リトライ(最大 3 回、指数バックオフ)
GBP API 永続エラー(4xx)リトライせず failures に記録
GBP トークン期限切れ自動更新後リトライ
Worker クラッシュ既存 failed_jobs テーブル経由で再実行可
MCP Server 再起動batch_id は DB に永続化のため、状態追跡可能

5.2 ユーザー(顧客 AI)からのキャンセル

http
POST /tools/call
{
  "name": "batch.cancel",
  "arguments": { "batch_id": "..." }
}
  • queued 状態のみキャンセル可能(Job が pull される前)
  • processing に遷移した後はキャンセル不可(途中状態の不整合回避)

5.3 自動ロールバック

書込系の途中で重大エラー(DB 制約違反等)が起きた場合は Laravel 側のトランザクションでロールバック。MCP からはエラーレスポンスとして失敗を返却。

GBP API は外部のため、Mappy DB と GBP の整合性は完全保証不可

  • DB 側成功 + GBP 側失敗のケース:Laravel のリトライキューで GBP に再送
  • DB 側失敗 + GBP 側成功のケース:監査ログから手動修正が必要
  • 重要操作は dry_run + idempotency + 監査ログの 3 重で保護

6. レート制限(書込系)

スコープ制限
access_token 単位10 リクエスト/分
user_id 単位30 リクエスト/分
user_id 単位500 件/日(書込系 tool 呼び出し合計)
client_id 単位1000 リクエスト/時
同一 idempotency_key24 時間 1 回
同一 confirm_token5 分 1 回

レート制限ヒット時:

json
{
  "error_code": "RATE_LIMITED",
  "message": "書込操作のレート制限を超えました",
  "details": {
    "scope": "user_per_minute",
    "limit": 30,
    "retry_after_seconds": 45
  }
}

7. 確認トークンと idempotency key の関係

両者は役割が異なるため共存する。

観点confirm_tokenidempotency_key
目的ユーザー意図の確認重複実行の防止
発行元MCP Serverクライアント(顧客 AI)
有効期間5 分24 時間
必須条件5 件以上の一括等推奨(書込系すべて)
再使用不可(1 回消費)可(同一入力なら同一レスポンス)

同時使用フロー

8. 監査ログとの連携

書込系の全操作(dry_run・拒否含む)を Audit Log に記録する。

イベント記録項目
dry_run 実行tool_name, target_count, warnings
confirm_token 発行tool_name, target_summary, expires_at
confirm_token 検証 OKtool_name, expired/valid
confirm_token 検証 NGreason (expired/conflict)
batch dispatchbatch_id, target_locations
batch 完了success_count, failure_count, failures
レート制限ヒットscope, limit, retry_after
キャンセル要求batch_id, allowed/denied

詳細スキーマは DB 設計(監査ログ) を参照。

9. クライアント実装ガイド(顧客 AI 側)

9.1 推奨フロー

1. dry_run で結果プレビュー

2. 5 件以上または重要操作なら、人間に意図確認

3. confirm_token を含めて本実行

4. batch_id を受け取ったらポーリング

5. 完了状態を人間に報告

9.2 顧客 AI のシステムプロンプト推奨例

あなたが Mappy MCP の書込 tool を使う際は、以下を必ず守ってください:

1. dry_run=true で必ず事前確認を行う
2. dry_run 結果を要約して、ユーザーに「実行してよいか」を確認する
3. 5 店舗以上の一括反映では confirm_token が必須。サーバーから返されるトークンを必ず保持し再送に含める
4. idempotency_key は会話 ID ベースの UUID を毎回付与する
5. batch_id を受け取ったら、batch.status でポーリングし完了を待つ
6. 失敗時は failures 配列を読み取り、ユーザーに具体的に説明する

10. レビュー時の論点(未確定)

#項目候補
1confirm_token 必須となる件数の閾値3 / 5 / 10
2dry_run の対応必須範囲全書込 / 一括書込のみ
3idempotency_key の TTL24h / 7d
4レート制限の最終値上記は暫定
5batch.cancel の許可範囲queued のみ / processing 途中も
6GBP 失敗時の自動リトライ回数3 / 5
7部分成功の許容戦略partial 受容 / 全件成功必須 / 設定可
8クライアント側ポーリング vs WebhookPhase 2 はポーリング、Phase 3 で Webhook 検討

11. 関連ドキュメント