Read Tool 最差通常只是查不到資料;Stateful Tool 會真的改變網站。當 Agent 可以 add_to_favorites、add_to_cart、update_profile,你要開始處理:重複呼叫、UI 同步、Session、競態條件。
今天用「收藏商品」做例子,讓 Tool 重複執行也不會產生錯亂。
很直覺的實作:
async function addFavorite(productId) {
favorites.push(productId);
}
呼叫兩次:
await addFavorite(123);
await addFavorite(123);
結果:
[123, 123]
這就是典型的重複副作用。
Browser Agent 可能因為:
再次執行同一個 Action。
收藏這種 Action 很適合設計成 idempotent:
async function addFavorite(productId) {
if (favorites.includes(productId)) {
return {
changed: false,
message: 'Product is already in favorites.'
};
}
favorites.push(productId);
return {
changed: true,
message: 'Product added to favorites.'
};
}
Tool:
await document.modelContext.registerTool({
name: 'add_to_favorites',
description: 'Add a product to the signed-in user favorites. Calling it for an existing favorite does not create a duplicate.',
inputSchema: {
type: 'object',
properties: {
productId: {
type: 'integer',
minimum: 1
}
},
required: ['productId']
},
annotations: {
readOnlyHint: false
},
execute: async ({ productId }) => {
return JSON.stringify(await addFavorite(productId));
}
});
用辦公室鍵盤(ID 123)來看這個流程。剛開啟 Demo 時,商品還沒被收藏,清單是空的,收藏數量為 0。
這個 Demo 先用本地記憶體保存收藏,重新整理就會清空,方便觀察每次操作帶來的變化;正式網站的資料保存與會員權限,仍要由後端處理。
📸 圖片 1|加入前,收藏數量為 0
Agent 呼叫 API 後:
Server:已收藏
Agent:已收藏
UI:♡ 還是空心
人類會以為沒成功,再點一次。
所以網站狀態要有單一來源,或至少在 Action 後同步:
async function addFavorite(productId) {
const result = await api.addFavorite(productId);
favoritesStore.replace(result.favorites);
renderFavorites();
return result;
}
以 Demo 的 add_to_favorites 為例,在 Inspector 傳入商品 ID:
{"productId":123}
執行後,鍵盤變成「已收藏」,清單多了一筆商品,收藏數量也變成 1。Tool 同時回傳 changed: true、isFavorite: true 與 favoritesCount: 1。
這樣使用者看到的畫面,才會和 Tool 回傳的結果一致。Demo 共用本地收藏狀態;上面的 API 範例則示範正式網站如何用伺服器結果更新畫面。
📸 圖片 2|加入後,收藏數量為 1
接著對同一個商品再呼叫一次 add_to_favorites。此時商品已經在收藏裡,所以不需要再次新增。
第二次結果會是 changed: false,但 isFavorite 仍為 true,favoritesCount 也維持 1,並回傳 Product is already in favorites.。
這裡的 changed: false 並不是失敗,而是「使用者要的狀態已經存在」。即使呼叫重做,清單也不會變成兩筆相同商品。這就是前面提到的 idempotency。
📸 圖片 3|重複加入,收藏仍只有一筆
反向操作也是同樣的道理。Demo 提供 remove_from_favorites,傳入相同的商品 ID:
{"productId":123}
移除後,鍵盤恢復「未收藏」,清單回到空白。Tool 回傳 changed: true、isFavorite: false 與 favoritesCount: 0,讓呼叫端知道商品確實已經不在收藏裡。
如果再移除一次,結果會是 changed: false,收藏數量仍為 0。加入與移除都以「達到指定狀態」為目標,重複執行也不會多做一次副作用。
📸 圖片 4|執行 remove_from_favorites,收藏數量回到 0
加入、重複加入與移除,都應該沿用同一套狀態更新邏輯。接上真正的後端後,也要把伺服器確認的結果同步到 UI,並回傳給 Agent,避免三邊各自保留不同的狀態。
WebMCP Tool 在目前頁面執行,通常使用的是目前 Web App 的登入/Session Context。
這表示:
Agent 代表的是「現在這個使用者」,不是一個後門管理員。
後端 Action 仍要檢查:
目前使用者是否登入?
是否擁有操作權限?
商品是否存在?
這個狀態是否允許修改?
不要因為 request 是從 WebMCP 進來就跳過權限。
兩個 Action 幾乎同時:
add_to_favorites(123)
remove_from_favorites(123)
最後狀態取決於 Server 的執行順序。
因此 Result 應該回「執行後的真實狀態」,而不是只回:
OK
例如:
{
"status": "success",
"productId": 123,
"isFavorite": false,
"favoritesCount": 8
}
Agent 才知道現在到底是什麼狀態。
Read Tool 可以很精簡;Write Tool 我通常希望回:
有沒有變更
變更後狀態
必要的識別 ID
下一步需要什麼
例如:
{
"status": "success",
"changed": true,
"productId": 123,
"isFavorite": true
}
「加入收藏」有副作用,但通常風險不高、可逆。
「刪除帳號」也是 Stateful,但風險完全不同。
所以不能只用:
Read / Write
兩類就結束。
OK。