WebMCP Tool 不一定要從頁面載入一路活到關閉。官方 registerTool() 可以搭配 AbortSignal 管理 Tool 生命週期;可用 Tools 改變時,document.modelContext 也會有 toolchange event。
今天模擬一個最常見的狀況:Guest 只看到 login;登入後移除 login,改提供 get_profile、logout。
假設你偷懶,一進頁面就註冊:
login
logout
get_profile
get_orders
delete_account
但訪客其實只能登入。
即使後端權限最後會擋住 get_orders,Agent 還是多看到一堆此刻不能用的 Tools,增加選錯與無效嘗試。
比較合理的做法是:
Guest
→ login
Member
→ get_profile
→ get_orders
→ logout
我們先寫一個 Helper:
const toolControllers = new Map();
async function registerManagedTool(tool) {
const controller = new AbortController();
await document.modelContext.registerTool(tool, {
signal: controller.signal
});
toolControllers.set(tool.name, controller);
}
function unregisterTool(name) {
const controller = toolControllers.get(name);
if (!controller) return;
controller.abort();
toolControllers.delete(name);
}
官方 Imperative API 目前支援把 AbortSignal 傳給 registerTool();signal 被 abort 時,Tool 就會被移除。
const loginTool = {
name: 'login',
description: 'Open the sign-in flow when the user wants to access member-only features.',
inputSchema: {
type: 'object',
properties: {}
},
annotations: {
readOnlyHint: false
},
execute: async () => {
openLoginDialog();
return 'Login dialog opened.';
}
};
載入:
await registerManagedTool(loginTool);
📸 圖片 1|未登入時只提供
loginTool
async function onLogin(user) {
unregisterTool('login');
await registerManagedTool({
name: 'get_profile',
description: 'Get the signed-in user profile.',
inputSchema: {
type: 'object',
properties: {}
},
annotations: {
readOnlyHint: true
},
execute: async () => {
return JSON.stringify({
id: user.id,
name: user.name
});
}
});
await registerManagedTool({
name: 'logout',
description: 'Sign out the current user.',
inputSchema: {
type: 'object',
properties: {}
},
execute: async () => {
await onLogout();
const response = JSON.stringify({
status: 'success',
message: 'Signed out.'
});
window.alert('登出完成!\n目前狀態:Guest(尚未登入)\n可用 Tool:login');
return response;
}
});
}
登出反過來:
async function onLogout() {
unregisterTool('get_profile');
unregisterTool('logout');
await registerManagedTool(loginTool);
}
📸 圖片 2|登入後的 Member Tools
在 Inspector 選擇 logout,Input Arguments 填入 {},再按 Execute Tool。頁面上的「登出」按鈕也會執行相同的登出流程。
移除會員 Tools 並重新註冊 login 後,Demo 會顯示提示視窗:
登出完成!
目前狀態:Guest(尚未登入)
可用 Tool:login
提示會停留到按下「確定」。此時登入狀態與 Tool Set 已切回 Guest;logout 的回傳則會在關閉提示後完成。本地 Demo 也會在提示出現前更新頁面上的最後一次執行結果。
📸 圖片 3|登出完成提示與 Guest 狀態
document.modelContext.addEventListener('toolchange', async () => {
const tools = await document.modelContext.getTools();
console.log(
'Tools changed:',
tools.map(tool => tool.name)
);
});
各階段完成後的預期 Tool 清單:
初始:
Tools changed: ["login"]
登入:
Tools changed: ["get_profile", "logout"]
登出:
Tools changed: ["login"]
事件發生時序不應被拿來當 Business Logic 的唯一依據。規格對 toolchange 有自己的 task timing,開發上把它當「通知」比較合理,不要靠事件先後做交易邏輯。
本地 Demo 的紀錄會分別標記瀏覽器的 toolchange 事件,以及額外讀取的初始/登入/登出完成快照。實際事件可能包含註冊或移除過程中的中間清單,不一定剛好只有上面的三行。關閉登出提示後,可查看完整事件紀錄。
📸 圖片 4|
toolchange記錄登入/登出時的 Tool List
假設一個 Product Component 每次切換商品都重新 mount:
registerTool({ name: 'add_current_product_to_cart', ... })
如果上一個 Component 沒清掉,就可能:
所以 Component mount / unmount 應和 Tool register / unregister 對齊。
這個原則同樣適用於 WordPress、Laravel Livewire:
UI State Lifecycle
≈ Tool Lifecycle
不要以為未登入時不註冊 delete_account 就安全了。
真正安全仍然要在 Server 驗證:
Authentication
Authorization
CSRF / Nonce
Business Rule
WebMCP Tool List 只是讓 Agent 少看到不該用的能力,不是後端 Access Control。
AbortSignal 可以管理 Tool 註冊生命週期。toolchange 可觀察可用能力變化。