Agent 有能力執行,不代表網站應該允許它直接執行。搜尋商品和刷卡付款只差幾個 Tool Calls,但風險完全不同。
今天建立一個很實用的模式:
Preview
→ Confirm
→ Execute
並搭配 WebMCP 的 consequentialHint,讓高風險操作有明確語意。
📸 圖片 1|Preview → Confirm → Execute 三階段流程
幫我找 2000 元以下的耳機。
失敗代價:很低。
幫我買第二個。
可能造成:
所以「Agent 已經知道使用者想買」還不夠。
await document.modelContext.registerTool({
name: 'buy_product',
description: 'Buy a product.',
// ...
execute: async ({ productId }) => {
return checkout(productId);
}
});
一個 Tool 同時:
選商品
確認數量
確認地址
確認付款方式
真的扣款
如果 Agent 理解錯 product ID,就直接產生真實後果。
本日 Demo 用本地記憶體模擬結帳,不會實際扣款或寄送商品,重新整理就會清空資料。商品 ID 為 123,單價 NT$1,680,運費 NT$60。
以下擷取 app.js 的 Tool 註冊與主要流程;prepareCheckout()、confirmCheckout() 與畫面更新由 Demo 實作,並不是 WebMCP 內建的方法。prepareCheckout() 檢查商品與數量後,產生有效 10 分鐘的 previewId,保存明細並更新預覽;建立新預覽會取代尚未完成的舊預覽。
await document.modelContext.registerTool({
name: 'prepare_checkout',
description: 'Prepare a checkout preview. This does not place an order or charge the user.',
inputSchema: {
type: 'object',
properties: {
productId: { type: 'integer', minimum: 1 },
quantity: { type: 'integer', minimum: 1, maximum: 10 }
},
required: ['productId', 'quantity']
},
annotations: {
readOnlyHint: false,
consequentialHint: false
},
execute: async ({ productId, quantity }) => {
const preview = prepareCheckout({ productId, quantity });
return JSON.stringify(preview);
}
});
以數量 1 為例,回傳的預覽包含以下欄位(previewId 與有效期限依實際執行產生):
{
"status": "preview_ready",
"simulated": true,
"previewId": "preview_abc123",
"product": "Wireless Headphones 無線耳機",
"quantity": 1,
"subtotal": 1680,
"shipping": 60,
"total": 1740,
"shippingAddress": "台北市中正區示範路 123 號(虛構地址)",
"payment": "Demo Visa **** 1234(不扣款)",
"expiresAt": "實際產生的 ISO 時間字串"
}
在 Inspector 呼叫 prepare_checkout,傳入 {"productId":123,"quantity":1},就會看到商品、數量、小計、運費、總額與收件資訊。此時總額為 NT$1,740,訂單數仍為 0。
這一步只建立「你即將做什麼」的可見結果,還沒有下單。
📸 圖片 2|Checkout Preview 顯示真正要確認的內容
接著把預覽回傳的 previewId 交給 confirm_checkout。Demo 的 confirmCheckout() 會先檢查預覽,再開啟網站自己的確認視窗,等待使用者選擇;呼叫 Tool 本身還不會建立訂單。
await document.modelContext.registerTool({
name: 'confirm_checkout',
description: 'Open the website confirmation dialog for a checkout preview. Wait for the user to confirm or cancel. Creates only a simulated order and never charges money.',
inputSchema: {
type: 'object',
properties: {
previewId: {
type: 'string',
description: 'Exact previewId returned by prepare_checkout.'
}
},
required: ['previewId']
},
annotations: {
readOnlyHint: false,
consequentialHint: true
},
execute: async ({ previewId }) => {
const result = await confirmCheckout({ previewId });
return JSON.stringify(result);
}
});
consequentialHint: true 用來標示可能造成重大或難以逆轉後果的操作,讓 Agent/Browser 可以據此要求確認。但它不保證瀏覽器一定顯示確認介面,也不會自動替網站產生商品明細。
本例雖然只模擬下單,仍用這個提示示範正式結帳 Tool 的語意。真正讓 Demo 暫停並等待選擇的,是網站實作的確認視窗。使用者可以核對商品、數量、NT$1,740 的總額、地址與付款摘要,再決定是否繼續。
但要注意:
Hint 是提示,不是你的唯一安全機制。
📸 圖片 3|網站顯示訂單明細,等待使用者確認
使用者按下「確認並模擬下單」後,Demo 會再次檢查預覽是否有效,再建立模擬訂單。頁面保留完成狀態,Tool 則回傳 status: success、simulated: true、orderId: DEMO-001 與 total: 1740;訂單數從 0 變成 1。
如果使用者按「取消」或 Escape,Tool 回傳 cancelled,不會新增訂單。等待選擇期間,Tool 尚未回傳最終結果是正常的。
📸 圖片 4|確認後完成模擬下單,回傳訂單結果
同一個已完成的 previewId 再次呼叫時,Demo 只回傳原訂單,並標示 alreadyExecuted: true,避免重試產生第二筆訂單。尚未完成的預覽若已過期,則回傳 expired,需要重新建立預覽。
這些紀錄目前只保存在本頁記憶體中。正式網站需要把預覽、確認與防止重複下單的紀錄交由後端保存;前端視窗與 Tool description 都不能作為付款授權的安全邊界。
後端仍要確認:
如果 Confirm Tool 還接受:
{
"productId": 999,
"quantity": 20,
"price": 1
}
那 Preview 就沒有意義。
我更喜歡:
prepare_checkout
→ Server 產生 previewId
→ User 看預覽
→ confirm_checkout(previewId)
→ Server 依 previewId 重新驗證
Confirm 階段只接受 previewId,就能避免重新傳入一組不同的商品或價格。後端仍須把這個 ID 綁定目前使用者、預覽明細與確認紀錄,拒絕未授權或已失效的請求;ID 本身不等於使用者已經同意付款。
至少:
而像:
通常不需要同等級確認。
好的確認應該包含使用者真正需要看的內容:
商品:Wireless Headphones
數量:1
總額:NT$1,740
地址:台北市…
付款:Visa **** 1234
確認下單?
不是只問:
Agent wants to use confirm_checkout. Allow?
因為使用者真正想確認的是結果,不是 Tool Name。
consequentialHint 用來標示重大/不可逆操作。