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
副作用を発生させず「もし実行されたら何が起きるか」をシミュレートする。
動作
レスポンス例
{
"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
| tool | dry_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 エラー:
{
"error_code": "IDEMPOTENCY_CONFLICT",
"message": "同じ idempotency_key で異なるリクエストが送信されました",
"details": { "key": "KEY1" }
}検出方法: 1 回目のリクエストの input ハッシュを Redis に保存、2 回目で照合。
3.3 confirm_token(一括反映の意図確認)
5 件以上の一括反映、または影響範囲が大きい操作でユーザーの明示同意を強制する。
動作
必須となる条件
| 操作 | 条件 |
|---|---|
post.create | location_ids の長さ ≥ 5 |
post.delete | location_ids の長さ ≥ 3 |
media.upload | location_ids の長さ ≥ 5 OR media の合計サイズ ≥ 50 MB |
media.delete | media_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 レスポンス例
{
"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 ステータス遷移
ポーリング推奨間隔
| 状態 | 推奨間隔 |
|---|---|
queued | 5 秒 |
processing (target_count ≤ 5) | 5 秒 |
processing (target_count > 5) | 10 秒 |
done / failed / partial | ポーリング停止 |
クライアント側で指数バックオフを推奨。
失敗の部分許容
10 店舗中 1 店舗で GBP API エラーが起きた場合:
{
"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)からのキャンセル
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_key | 24 時間 1 回 |
| 同一 confirm_token | 5 分 1 回 |
レート制限ヒット時:
{
"error_code": "RATE_LIMITED",
"message": "書込操作のレート制限を超えました",
"details": {
"scope": "user_per_minute",
"limit": 30,
"retry_after_seconds": 45
}
}7. 確認トークンと idempotency key の関係
両者は役割が異なるため共存する。
| 観点 | confirm_token | idempotency_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 検証 OK | tool_name, expired/valid |
| confirm_token 検証 NG | reason (expired/conflict) |
| batch dispatch | batch_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. レビュー時の論点(未確定)
| # | 項目 | 候補 |
|---|---|---|
| 1 | confirm_token 必須となる件数の閾値 | 3 / 5 / 10 |
| 2 | dry_run の対応必須範囲 | 全書込 / 一括書込のみ |
| 3 | idempotency_key の TTL | 24h / 7d |
| 4 | レート制限の最終値 | 上記は暫定 |
| 5 | batch.cancel の許可範囲 | queued のみ / processing 途中も |
| 6 | GBP 失敗時の自動リトライ回数 | 3 / 5 |
| 7 | 部分成功の許容戦略 | partial 受容 / 全件成功必須 / 設定可 |
| 8 | クライアント側ポーリング vs Webhook | Phase 2 はポーリング、Phase 3 で Webhook 検討 |