前端開發 別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站 · 第 12 / 30 篇

Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來?

2026 iThome 鐵人賽 WebMCP Day 12:安安~我是ChiYu~ 昨天的 Declarative API 很適合表單:HTML 還在,Tool 就跟著存在;表單離開頁面,能力也一起 消失。兩邊的生命週期幾乎黏在一起。 活動詳情就沒這麼省事了。它要讀取目前活動、跟著 route 切換情境,離…

快速導覽 查看本文目錄

本文同步自 2026 iThome 鐵人賽原文 。完整系列可見 iThome 系列頁 。

安安~我是ChiYu~

昨天的 Declarative API 很適合表單:HTML 還在,Tool 就跟著存在;表單離開頁面,能力也一起 消失。兩邊的生命週期幾乎黏在一起。

活動詳情就沒這麼省事了。它要讀取目前活動、跟著 route 切換情境,離開頁面後還得把舊能力 收回來。

我第一次實作時只記得:

await document.modelContext.registerTool(detailsTool);

Tool 的確註冊成功,但我完全沒處理解除。結果畫面早已切到別頁,舊 handler 還握著上一場 活動的 ID,像一位已經換班、鑰匙卻還放在口袋裡的值班人員。

所以今天真正要處理的不是「Imperative API 怎麼多註冊一支 Tool」,而是完整生命週期:它在 什麼情境出現、離開時怎麼失效、同一個 route 重跑時如何避免重複,以及第一次註冊失敗後 能不能恢復。

先切到專門隔離生命週期問題的版本

今天使用 v3-day-12:

git switch --detach v3-day-12
npm ci
npm run dev

接著開啟:

http://127.0.0.1:5173/labs/day-11-tool-lifecycle/index.html?route=list

資料夾仍保留早期開發名稱,但本文的版本座標是 v3-day-12。這個 Lab 不急著模擬完整活動 網站,只留下兩個 route,好讓註冊、解除、去重與失敗恢復可以被單獨觀察。

Imperative API 由 JavaScript 明確定義 Tool

Declarative API 從 HTML form 產生契約;Imperative API 則由 JavaScript 提供名稱、用途、 input schema、annotation 與執行 callback,再註冊到目前 Document 的 model context。

今天的最小 Tool 長這樣:

const detailsTool = {
  name: "get_event_details",
  description: "依活動 ID 讀取公開活動資訊。",
  inputSchema: {
    type: "object",
    additionalProperties: false,
    required: ["event_id"],
    properties: {
      event_id: {
        type: "string",
        description: "公開活動 ID。"
      }
    }
  },
  annotations: {
    readOnlyHint: true,
    untrustedContentHint: true
  },
  execute: async (input) => ({
    ok: true,
    eventId: input.event_id
  })
};

直接使用瀏覽器 API 時,註冊動作是:

await document.modelContext.registerTool(detailsTool);

專案沒有讓每個頁面自行散落這行程式,而是包進 ToolRegistry 與 model-context adapter。 這層不是另一套 WebMCP;它只負責把 route 切換、解除、重複註冊與失敗恢復集中管理。

Chrome 官方文件已註記 navigator.modelContext 自 Chrome 150 起棄用,因此本文統一使用 document.modelContext。

昨天和今天的差別,可以收成這張表:

比較項目Declarative APIImperative API
契約來源HTML form 與欄位標註JavaScript Tool 物件
適合情境搜尋、篩選與既有表單動態內容、非表單操作與 route context
input schema瀏覽器依表單語意整理開發者明確撰寫
生命週期通常跟著 form 出現或移除由程式註冊與解除

知道怎麼註冊只是前半段。現在要回答的,是目前頁面不再需要它時,誰負責請它離場。

route 切換時的 Imperative Tool 生命週期

圖 1:Lab 用 list 與 about 隔離生命週期問題。離開有效情境後,Tool 必須解除;回來時才重新註冊。

Registry 接收「現在應該存在什麼」,不是「再加什麼」

Lab 只保留兩個 route:

  • list:可以公開 get_event_details。
  • about:不應公開這支 Tool。

每次 route 改變,我不再叫 registry「新增一支 Tool」,而是交給它目前完整的 desired state:

await registry.sync(
  route === "list" ? [detailsTool] : []
);

這個差異看起來小,後果很大。只會做加法的 registry 會一路累積歷史能力;desired-state 版本則能比較「現在有什麼」和「現在應該有什麼」,該補的補上,該離場的解除。

for (const [name, controller] of this.controllers) {
  if (!desired.has(name)) {
    controller.abort();
    this.controllers.delete(name);
  }
}

Registry 保存的因此不是「這個網站曾經註冊過什麼」,而是「目前頁面允許 Agent 使用什麼」。 這個觀念後面會直接用在正式 route-aware catalog。

隱藏 UI 不等於撤銷能力

活動卡片或側邊面板消失,只代表人類暫時看不到。只要 Tool 還留在 model context,Agent 仍可能 發現並呼叫它。

UI 隱藏與能力撤銷是兩件事。 Chrome 的 Imperative API 文件 提供的解除方式,是註冊時帶入 AbortSignal,離開情境時再 abort():

const controller = new AbortController();

await document.modelContext.registerTool(tool, {
  signal: controller.signal
});

controller.abort();

專案替每支已註冊 Tool 保存一個 controller。新 desired set 不再包含它時,registry 先 abort, 再從本地 map 移除,讓瀏覽器與應用程式看見同一份狀態。

這也是為什麼我不只在畫面上寫「目前無此功能」。真正的能力邊界必須反映在 catalog,不能靠 CSS 幫忙演下班。

同一個 route 重跑兩次,不能註冊兩支同名 Tool

Route 更新可能連續發生。兩次 sync() 若同時進來,都可能在註冊前看到「目前還沒有 get_event_details」,接著各自新增一次。

ToolRegistry 因此用 Promise queue 讓更新依序套用,並以 Tool name 去重:

sync(tools: ProjectTool<object, unknown>[]): Promise<void> {
  const operation = this.queue.then(() => this.apply(tools));
  this.queue = operation.catch(() => undefined);
  return operation;
}

這裡還有一個容易被忽略的恢復問題。第一次 register 若暫時失敗,呼叫端應該收到錯誤;但 registry 內部的 queue 不能從此維持 rejected,不然後面每次 route 更新都會被同一個舊錯誤 擋住。

因此程式會保留當次失敗,同時把內部 queue 接回可工作的狀態,讓下一次 sync() 還有機會 成功。這不是吞掉錯誤,而是避免一次失敗把整個生命週期永久鎖死。

用四個斷言驗收 Registry,而不是只看畫面

這個 Lab 的 focused tests 主要檢查:

  1. 同時送出兩次相同 sync(),register 仍只發生一次。
  2. 第一次 register 失敗時,呼叫端確實收到錯誤。
  3. 第二次 sync() 可以恢復並成功註冊。
  4. Desired set 變成空清單後,對應 controller 已被 aborted。

網站啟動後,也可以手動走一次:

list
→ Active project Tools: get_event_details

about
→ Active project Tools: none

list
→ Tool 重新註冊,Register calls = 2

接著執行:

npm test -- tests/unit/day-11-registry.test.ts tests/unit/day-11-result.test.ts
npx playwright test tests/browser/day-11-lifecycle.spec.ts

固定版本中的程式與測試可以從這裡查看:

目前 main 另有較好找的讀者入口 labs/day-12-imperative-lifecycle/index.html?route=list,但 main 會持續累積後續改動;本文 操作與測試結果仍以 v3-day-12 為準。

Result 契約先留最小版本,今天不往外岔題

Lab 另外保留最小的成功與失敗結果:

success({ id: "evt-1" });
failure("VALIDATION_ERROR", "BAD_INPUT");
failure("TEMPORARY_FAILURE", "API_UNAVAILABLE");

目的只是讓呼叫端不必從任意 error message 猜測是否能重試。完整的 result 欄位、資料白名單與 錯誤分類,會在後面另外處理;今天先把主題停在 Tool 何時存在,以及何時必須失效。

今天證明的是生命週期,不是 Agent 自主呼叫

這輪測試能證明 registry 的註冊、去重、解除與失敗恢復可以穩定重播,證據停在 E2。它沒有 證明真實 Agent 已經從自然語言選中 get_event_details,測試用的 register callback 也不能 冒充正式 discovery。

但地基至少整理乾淨了。昨天讓靜態表單能描述自己,今天則讓動態能力知道什麼時候該下班。 明天第一次按下 Inspector 的 Send,不從下拉選單替 Agent 指定 Tool,只送一句自然語言。 如果先收到的不是活動,而是另一個環境錯誤,也會照原樣留下。

參考資料

讀完了!你覺得這篇如何?

你的回饋會決定下一篇要寫什麼。

還有疑問

Learning path

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站

這篇是系列中的第 12 / 30 篇。可以順著列表補完整脈絡,也可以直接跳到需要的段落。

  1. 01 Day 01|網站真的會被 Agent 使用嗎?我先丟了一句自然語言
  2. 02 Day 02|換到全新資料夾後,網站還能不能跑?
  3. 03 Day 03|先別急著做 Tool,我把活動網站自己走了一遍
  4. 04 Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?
  5. 05 Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
  6. 06 Day 06|Playwright、REST API、MCP 與 WebMCP,各自負責哪一段?
  7. 07 Day 07|Agent 偶爾答對還不夠,我先出二十道測試題
  8. 08 Day 08|WebMCP Tool 該怎麼設計?把「點按鈕」改成「搜尋活動」
  9. 09 Day 09|網站該公開哪些 WebMCP Tool?從操作盤點到五支正式能力
  10. 10 Day 10|Inspector 裝好了,為什麼 Tool 面板還是一片空白?
  11. 11 Day 11|什麼是 Declarative API?讓 HTML 表單成為 WebMCP Tool
  12. 12 Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來? 目前閱讀
  13. 13 Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
  14. 14 Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約
  15. 15 Day 15|讓人類與 Agent 共用同一套活動搜尋規則
  16. 16 Day 16|Agent 怎麼知道「我現在看的活動」是哪一場?
  17. 17 Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
  18. 18 Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
  19. 19 Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?
  20. 20 Day 20|表單填好了,為什麼這次報名仍然不能算通過?
  21. 21 Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
  22. 22 Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?
  23. 23 Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
  24. 24 Day 24|活動摘要叫 Agent 洩漏祕密,它會把這句話當資料還是命令?
  25. 25 Day 25|localhost 成功不算交付:怎麼證明公開網址跑的是同一份程式?
  26. 26 Day 26|把自然語言送進公開站,Agent 真的會自己選對 Tool 嗎?
  27. 27 Day 27|寫入型 Tool 怎麼驗收?把 Trace、Network、UI 與 Server 狀態放在一起看
  28. 28 Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?
  29. 29 Day 29|把網站交給 Agent 前,我最後會檢查哪十件事?
  30. 30 Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼?

Further reading

Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼? 封面圖 2026-09-30 Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼? Day 29|把網站交給 Agent 前,我最後會檢查哪十件事? 封面圖 2026-09-29 Day 29|把網站交給 Agent 前,我最後會檢查哪十件事? Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層? 封面圖 2026-09-28 Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?

留言討論

用 GitHub 帳號留言,留言會同步到這個 Blog 的 GitHub Discussions。

閱讀導覽

約 5 分鐘

本文目錄

本篇所在系列 · 第 12 / 30 篇

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站

  1. Day 01|網站真的會被 Agent 使用嗎?我先丟了一句自然語言
  2. Day 02|換到全新資料夾後,網站還能不能跑?
  3. Day 03|先別急著做 Tool,我把活動網站自己走了一遍
  4. Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?
  5. Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
  6. Day 06|Playwright、REST API、MCP 與 WebMCP,各自負責哪一段?
  7. Day 07|Agent 偶爾答對還不夠,我先出二十道測試題
  8. Day 08|WebMCP Tool 該怎麼設計?把「點按鈕」改成「搜尋活動」
  9. Day 09|網站該公開哪些 WebMCP Tool?從操作盤點到五支正式能力
  10. Day 10|Inspector 裝好了,為什麼 Tool 面板還是一片空白?
  11. Day 11|什麼是 Declarative API?讓 HTML 表單成為 WebMCP Tool
  12. Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來?
  13. Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
  14. Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約
  15. Day 15|讓人類與 Agent 共用同一套活動搜尋規則
  16. Day 16|Agent 怎麼知道「我現在看的活動」是哪一場?
  17. Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
  18. Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
  19. Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?
  20. Day 20|表單填好了,為什麼這次報名仍然不能算通過?
  21. Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
  22. Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?
  23. Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
  24. Day 24|活動摘要叫 Agent 洩漏祕密,它會把這句話當資料還是命令?
  25. Day 25|localhost 成功不算交付:怎麼證明公開網址跑的是同一份程式?
  26. Day 26|把自然語言送進公開站,Agent 真的會自己選對 Tool 嗎?
  27. Day 27|寫入型 Tool 怎麼驗收?把 Trace、Network、UI 與 Server 狀態放在一起看
  28. Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?
  29. Day 29|把網站交給 Agent 前,我最後會檢查哪十件事?
  30. Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼?
看完整系列 30 篇