MCP Server — Cơ chế an toàn write
| Mục | Nội dung |
|---|---|
| Trạng thái | 🟡 Đang thiết kế |
| Case liên quan | #13 MCP Server (Thiết lập mới) |
| Tài liệu cha | MCP Server (Tổng thể) |
| Design liên quan | Tool Catalog / Auth & Tenancy |
1. Mục đích tài liệu
Định nghĩa các cơ chế an toàn bắt buộc cho tool write (từ Phase 2) của MCP.
- Giảm thiểu rủi ro AI thực thi sai
- Chống duplicate thực thi của cùng một thao tác
- Xác nhận ý định người dùng khi áp dụng hàng loạt quy mô lớn
- Theo dõi trạng thái xử lý bất đồng bộ
- Cân nhắc giới hạn GBP API
- Đảm bảo khả năng audit
Phase 1 (MVP) toàn bộ là read
Nội dung tài liệu này implement từ Phase 2 trở đi. MVP không cung cấp write tool ngoài batch.status.
2. Nguyên tắc thiết kế
| Nguyên tắc | Nội dung |
|---|---|
| Đồng ý rõ ràng | Áp dụng hàng loạt ≥ 5 cửa hàng bắt buộc confirm_token |
| An toàn khi thực thi lại | Tất cả write tool đều support idempotency_key |
| Trực quan hóa trạng thái | Xử lý bất đồng bộ check hoàn thành bằng batch_id + polling |
| Có thể thử nghiệm | Mode xác nhận không side effect bằng dry_run=true |
| Chấp nhận thất bại một phần | Áp dụng hàng loạt trả về thành công/thất bại theo từng location |
| Ghi toàn bộ audit | Write・dry_run・từ chối đều ghi vào Audit Log không sót |
| Cân nhắc GBP API | Bảo vệ chống rate limit・token hết hạn |
3. 4 cơ chế an toàn
3.1 dry_run
Mô phỏng "nếu thực thi thì sẽ xảy ra gì" mà không tạo side effect.
Hoạt động
Ví dụ response
{
"data": {
"would_execute": true,
"operation": "post.create",
"target_locations": [
{ "id": 8802, "title": "Shibuya", "store_code": "SHIBUYA001" },
{ "id": 8803, "title": "Shinjuku", "store_code": "SHINJUKU001" }
],
"target_count": 2,
"validation_warnings": [
{
"location_id": 8802,
"code": "RECENT_POST_DETECTED",
"message": "Chưa đến 1 giờ kể từ post trước (có thể duplicate)"
}
],
"estimated_gbp_api_calls": 2,
"estimated_completion_seconds": 10
}
}Tool bắt buộc support
| tool | Support dry_run |
|---|---|
post.create | ✅ Bắt buộc |
post.delete | ✅ Bắt buộc |
review.reply | ✅ Bắt buộc |
media.upload | ✅ Bắt buộc |
media.delete | ✅ Bắt buộc |
location.update | ✅ Bắt buộc |
report.create | ❌ Không cần (việc tạo không có side effect, nhưng có tính phí) |
Kịch bản sử dụng dry_run
Khuyến nghị design: AI khách gọi dry_run trước khi xác nhận "Bạn có muốn đăng không?", tóm tắt kết quả và trình bày cho người dùng.
3.2 idempotency_key
Khi cùng một ý định thao tác được gửi nhiều lần, chỉ thực thi lần đầu.
Spec
| Mục | Nội dung |
|---|---|
| Format | Khuyến nghị UUIDv4. Tùy ý 8〜64 ký tự ASCII |
| Thời hạn | 24 giờ |
| Scope | Tổ hợp (client_id, user_id, tool_name, idempotency_key) |
| Hành vi khi xung đột | Trả về response lần đầu từ cache (bao gồm status code) |
| Bắt buộc/tùy chọn | Khuyến nghị mạnh với write tool, có thể auto gán |
Hoạt động
Phát hiện xung đột input
Cùng idempotency_key nhưng input khác → lỗi IDEMPOTENCY_CONFLICT:
{
"error_code": "IDEMPOTENCY_CONFLICT",
"message": "Request khác đã được gửi với cùng idempotency_key",
"details": { "key": "KEY1" }
}Cách phát hiện: lưu hash input của request lần đầu vào Redis, đối chiếu lần 2.
3.3 confirm_token (xác nhận ý định khi áp dụng hàng loạt)
Khi áp dụng hàng loạt ≥ 5 cửa hàng hoặc thao tác có phạm vi ảnh hưởng lớn, bắt buộc đồng ý rõ ràng của người dùng.
Hoạt động
Điều kiện bắt buộc
| Thao tác | Điều kiện |
|---|---|
post.create | location_ids length ≥ 5 |
post.delete | location_ids length ≥ 3 |
media.upload | location_ids length ≥ 5 HOẶC tổng size media ≥ 50 MB |
media.delete | media_ids length ≥ 5 |
location.update | Số đích update đồng thời ≥ 3 |
review.reply | Thường không cần (1 đối tượng) |
Chi tiết confirm_token
| Mục | Nội dung |
|---|---|
| Nguồn phát | MCP Server |
| Format | ASCII random ≥ 32 ký tự (lưu Redis) |
| Thời hạn | 5 phút |
| Scope | (client_id, user_id, tool_name, input_hash) |
| Tái sử dụng | Không (tiêu thụ 1 lần) |
| Thay đổi input | input_hash thay đổi thì vô hiệu |
Ví dụ response 412
{
"error_code": "CONFIRMATION_REQUIRED",
"message": "Áp dụng hàng loạt ≥ 5 cửa hàng cần confirm_token",
"details": {
"confirm_token": "abc123def456...",
"expires_in": 300,
"target_summary": {
"operation": "post.create",
"target_count": 10,
"locations": [
{ "id": 8801, "title": "Shibuya" },
{ "id": 8802, "title": "Shinjuku" }
],
"post_summary": "Thông tin chiến dịch hôm nay..."
},
"human_readable_message": "Sẽ đăng cùng nội dung hàng loạt vào 10 cửa hàng. Bạn có chắc không?"
}
}3.4 batch_id + polling
Write tới GBP API tốn thời gian (hàng loạt nhiều cửa hàng = vài chục giây ~ vài phút), nên bắt buộc thực thi bất đồng bộ + check trạng thái.
Hoạt động
Chuyển trạng thái batch
Khoảng polling khuyến nghị
| Trạng thái | Khoảng khuyến nghị |
|---|---|
queued | 5 giây |
processing (target_count ≤ 5) | 5 giây |
processing (target_count > 5) | 10 giây |
done / failed / partial | Dừng polling |
Khuyến nghị exponential backoff phía client.
Chấp nhận thất bại một phần
Trường hợp 1 trong 10 cửa hàng bị lỗi 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": "Không có quyền đăng vào location này",
"retry_possible": false
}
]
}
}4. Cân nhắc rate limit của GBP API
4.1 Tận dụng implement hiện có
Mappy đã có cơ chế xử lý rate limit của GBP API (spatie/guzzle-rate-limiter-middleware). MCP access qua Laravel API, cơ chế này tự động áp dụng.
4.2 Quản lý access token
- Token quản lý ở bảng
google_access_tokens/mappy_google_access_tokenscủa Mappy DB - Phương thức token ngắn hạn 60 phút + refresh token
- Xung đột update token khi parallel đã được kiểm soát phía Laravel (SMARTMEO-47 đã xử lý)
4.3 Bảo vệ bổ sung phía MCP
| Bảo vệ | Nội dung |
|---|---|
| Giới hạn số thực thi đồng thời | 5 parallel / user |
| Tối đa 100 cửa hàng / batch | Theo kết quả check DB, cân nhắc số cửa hàng của khách lớn nhất |
| Kiểm soát loop pageSize=100 | Dùng logic hiện có của Laravel |
| Rate limit qua Redis | 10 request/phút (write) |
5. Hành vi khi thất bại và retry
5.1 Pattern thất bại và đối phó
| Pattern thất bại | Đối phó |
|---|---|
| Lỗi giao tiếp (MCP ↔ Laravel) | Client có thể resend bằng idempotency_key |
| GBP API lỗi tạm thời (5xx) | Worker auto retry (tối đa 3 lần, exponential backoff) |
| GBP API lỗi vĩnh viễn (4xx) | Không retry, ghi vào failures |
| GBP token hết hạn | Auto refresh rồi retry |
| Worker crash | Có thể re-run qua bảng failed_jobs hiện có |
| Restart MCP Server | batch_id lưu persistent trong DB, có thể tiếp tục theo dõi |
5.2 Cancel từ user (AI khách)
POST /tools/call
{
"name": "batch.cancel",
"arguments": { "batch_id": "..." }
}- Chỉ cancel được khi đang
queued(trước khi Job được pull) - Sau khi chuyển sang
processingthì không cancel được (tránh không nhất quán giữa chừng)
5.3 Auto rollback
Khi write gặp lỗi nghiêm trọng giữa chừng (vi phạm constraint DB v.v.), rollback bằng transaction phía Laravel. Trả về error response từ MCP.
GBP API là external, không thể đảm bảo tuyệt đối nhất quán Mappy DB - GBP
- Case DB thành công + GBP thất bại: Laravel retry queue gửi lại tới GBP
- Case DB thất bại + GBP thành công: cần sửa thủ công từ audit log
- Thao tác quan trọng bảo vệ 3 lớp: dry_run + idempotency + audit log
6. Rate limit (write)
| Scope | Giới hạn |
|---|---|
| Theo access_token | 10 request/phút |
| Theo user_id | 30 request/phút |
| Theo user_id | 500 lần/ngày (tổng gọi write tool) |
| Theo client_id | 1000 request/giờ |
| Cùng idempotency_key | 24 giờ 1 lần |
| Cùng confirm_token | 5 phút 1 lần |
Khi hit rate limit:
{
"error_code": "RATE_LIMITED",
"message": "Vượt rate limit thao tác write",
"details": {
"scope": "user_per_minute",
"limit": 30,
"retry_after_seconds": 45
}
}7. Quan hệ giữa confirm_token và idempotency_key
Hai cái vai trò khác nhau nên cùng tồn tại.
| Điểm | confirm_token | idempotency_key |
|---|---|---|
| Mục đích | Xác nhận ý định user | Chống duplicate thực thi |
| Nguồn phát | MCP Server | Client (AI khách) |
| Thời hạn | 5 phút | 24 giờ |
| Điều kiện bắt buộc | Hàng loạt ≥ 5 v.v. | Khuyến nghị (toàn bộ write) |
| Tái sử dụng | Không (tiêu thụ 1 lần) | Có (cùng input = cùng response) |
Flow sử dụng đồng thời
8. Liên kết với audit log
Ghi toàn bộ thao tác write (bao gồm dry_run・từ chối) vào Audit Log.
| Event | Mục ghi |
|---|---|
| Thực thi dry_run | tool_name, target_count, warnings |
| Phát confirm_token | tool_name, target_summary, expires_at |
| Verify confirm_token OK | tool_name, expired/valid |
| Verify confirm_token NG | reason (expired/conflict) |
| batch dispatch | batch_id, target_locations |
| batch hoàn thành | success_count, failure_count, failures |
| Hit rate limit | scope, limit, retry_after |
| Yêu cầu cancel | batch_id, allowed/denied |
Schema chi tiết xem DB (audit log).
9. Hướng dẫn implement client (phía AI khách)
9.1 Flow khuyến nghị
1. Preview kết quả bằng dry_run
↓
2. Nếu ≥ 5 đối tượng hoặc thao tác quan trọng, xác nhận ý định với người dùng
↓
3. Thực thi chính thức kèm confirm_token
↓
4. Nhận batch_id thì polling
↓
5. Báo cáo trạng thái hoàn thành cho người dùng9.2 Ví dụ system prompt khuyến nghị cho AI khách
Khi bạn dùng write tool của Mappy MCP, hãy tuân thủ các điều sau:
1. Luôn xác nhận trước với dry_run=true
2. Tóm tắt kết quả dry_run, hỏi user "có muốn thực thi không"
3. Áp dụng hàng loạt ≥ 5 cửa hàng bắt buộc confirm_token. Phải giữ token server trả về và gửi kèm khi resend
4. idempotency_key gán UUID dựa trên conversation ID mỗi lần
5. Khi nhận batch_id thì polling batch.status và chờ hoàn thành
6. Khi thất bại đọc mảng failures và giải thích cụ thể cho user10. Điểm tranh luận tại review (chưa chốt)
| # | Mục | Phương án |
|---|---|---|
| 1 | Ngưỡng số lượng bắt buộc confirm_token | 3 / 5 / 10 |
| 2 | Phạm vi bắt buộc support dry_run | Toàn bộ write / chỉ write hàng loạt |
| 3 | TTL của idempotency_key | 24h / 7d |
| 4 | Giá trị rate limit cuối cùng | Trên là tạm |
| 5 | Phạm vi cho phép batch.cancel | Chỉ queued / cả processing |
| 6 | Số lần auto retry khi GBP fail | 3 / 5 |
| 7 | Chiến lược chấp nhận thành công một phần | Chấp nhận partial / Bắt buộc tất cả / Có thể setting |
| 8 | Client polling vs Webhook | Phase 2 polling, Phase 3 xem xét Webhook |