本文同步自 2026 iThome 鐵人賽原文 ,發布日期為 2026-08-13。完整系列可見 iThome 系列頁

安安~我是ChiYu~

昨天打開 Inspector 後,Chrome 終於把目前頁面的 Tool 列了出來。面板裡有名稱、 description,還有一大段 input schema。

看到 catalog 當然很開心,但我很快又卡在下一個問題:我並沒有另外寫一份 JSON Schema, 這些欄位到底是從哪裡冒出來的?

如果直接研究完成版搜尋頁,表單、API、畫面更新與商業規則會全部擠在一起。每個東西都像 答案,也就很難知道真正的答案是哪一個。因此我先把正式網站放到旁邊,另外做了一張最小 搜尋表單,只觀察 HTML 如何變成 Declarative Tool。

先切到只有 Declarative Tool 核心的版本

今天使用的程式里程碑是 v3-day-11。先把版本切對,再往下看表單,不然目前的 main 已經累積了後續的自動送出與參數說明,很容易把明後天才會出現的答案一起帶進來:

git switch --detach v3-day-11
npm ci

這個版本只替原本可操作的搜尋表單加入 Declarative Tool 的核心身分,還沒有 toolautosubmitrespondWith() 與正式 Agent submission。換句話說,今天可以驗證三件事:

  1. 人類仍能使用同一張表單搜尋活動。
  2. toolnametooldescription 會形成穩定的 Tool 身分。
  3. 表單欄位會提供 schema 所需的參數語意。

tag 內的 Lab 路徑仍保留早期名稱 labs/day-08-declarative-tool/,這只是資料夾名稱沒有隨文章 天數搬家;重播座標仍以 v3-day-11 為準。

HTML 表單語意形成 Declarative Tool 契約

圖 1:Tool 的身分來自 form,表單欄位則提供參數語意。這張圖說明 E2 契約;實際產生的 schema 仍要以瀏覽器輸出為準。

Declarative API 如何把現有表單變成 WebMCP Tool?

先把 Declarative Tool 拆開來看。它不是要我們用 JavaScript 另外重寫一套功能,而是替 現有的 HTML <form> 加上 WebMCP 標註。

瀏覽器會讀取表單的 toolnametooldescription,再從欄位的 name、label、選項與 限制條件整理出結構化的 input schema。於是同一張表單會有兩種讀法:人類看到輸入框、 下拉選單與按鈕;Agent 看到 Tool 名稱、用途與可接受的參數。

換句話說,我不用另外維護一份「Agent 專用搜尋 API 說明」。原本就存在的表單,補上必要 標註後,便能成為瀏覽器可以公開給 Agent 的 Tool。

Declarative Tool 可以做什麼在這個活動網站的例子
把既有 HTML form 公開成 Tool將活動搜尋表單公開為 search_events
從表單欄位形成結構化參數整理出 querylocationpricelevel
讓 Agent 將參數填回可見表單把「台北、免費、入門」放進對應欄位
保留人類原本的操作方式沒有 WebMCP 時仍可手動填表並搜尋
視設定決定是否自動送出後續會再加入 toolautosubmit 與 Tool result

Declarative Tool 最適合原本就能用表單表達的任務,例如搜尋、篩選、建立客服需求或準備 預約資料。它的優點是人類 UI 與 Agent Tool 共用同一份欄位語意,不必各自維護一套契約。

但它不是所有功能的萬用轉接頭。像「讀取目前 route 的活動」「依頁面狀態動態註冊或移除 Tool」,沒有一張固定表單可以承載,這種情況就比較適合使用明天會介紹的 Imperative API。

還有一條界線要先畫清楚:表單成功註冊成 Declarative Tool,只代表網站已經把能力與參數 說明交給瀏覽器。它不能保證 Agent 一定會選中這支 Tool,也不會替 server 完成權限、安全 與商業規則檢查。

所以今天只拆解「Tool 是怎麼從 HTML 形成的」。真正的 Agent discovery、參數選擇與 invocation,仍要交給後面的 Inspector trace 驗證。

先讓同一張搜尋表單繼續服務人類

這份 Lab 原本就是一張可以操作的 HTML 表單。人類選擇地點、費用與程度,按下「搜尋活動」 後,頁面仍會顯示符合條件的結果。

我沒有另外複製一張「Agent 專用表單」,只在原本的 <form> 補上兩個 Attribute:

<form
  id="event-search"
  toolname="search_events"
  tooldescription="依關鍵字、地點、費用與程度搜尋目前公開活動,並更新使用者可見的活動列表。">
  <!-- query、location、price、level -->
</form>

toolname 是 Tool 的穩定識別;tooldescription 則告訴 Agent,這支 Tool 會做什麼、 資料範圍在哪裡,以及結果會如何呈現在頁面上。

這兩個 Attribute 必須成對出現。Chrome 的 Declarative API 文件寫得很直接:移除其中 任何一個,這張 form 就不再註冊為 Tool。也就是說,按鈕上的「搜尋活動」可以改成 「找活動」,search_events 卻不應該跟著 UI 文案每天換名字。

property name 與允許值,原本就藏在表單欄位裡

Tool 有了名稱,接著輪到參數。Declarative API 會從表單欄位整理 property name、說明與 允許值。HTML constraint 也留在同一張表單裡,但是否被映射成特定 JSON Schema keyword, 仍要看當下瀏覽器輸出,不能只靠原始碼先替它宣布答案:

HTML 線索人類在畫面上看到什麼Tool schema 得到什麼
<label for="location">「地點」參數用途
name="location"不直接顯示穩定的 property name
<option value="taipei">「台北」可接受的 enum value
maxlength="100"關鍵字長度限制本篇只驗證 HTML constraint,不預先宣稱 schema 映射結果

這裡最容易被忽略的是 nameid 讓 label 能找到欄位,name 才是提交資料與 schema 使用的鍵。畫面上兩者常常剛好寫成一樣,所以少寫一個時,肉眼不一定立刻看得出來。

toolparamdescription 可以替個別參數補上更精確的說明,但它不是今天這個最小版本的主角。 欄位已經有正確關聯的 label 時,瀏覽器會先沿用 label 內容。等參數語意無法只靠「地點」或 「程度」說清楚時,再補 toolparamdescription,比每個欄位先貼一段重複文案更實在。

欄位少了 name,畫面照常出現,契約卻少了一塊

接著我故意把穩定鍵拿掉:

<label>
  地點
  <select>
    <option value="taipei">台北</option>
  </select>
</label>

瀏覽器還是會畫出一個寫著「地點」的下拉選單,人類也看得懂。問題出在資料送出時沒有 欄位名稱,Tool 無法穩定取得 location 這個 property。畫面沒有爆炸,契約只是安靜地 缺了一角,這種 bug 最擅長在 Demo 時裝沒事。

另一個常見誤會是把 placeholder 當成規格。placeholder 適合提示輸入範例,卻會隨著文案 調整;拿它代替 name 或欄位說明,等於讓 API 契約跟著行銷文案一起漂流。

所以我替這張表單留下的規則很單純:可見文字要讓人類理解,name 與允許值要讓資料契約 保持穩定,HTML constraint 則繼續保護人類操作。兩邊描述的是同一個搜尋任務,至於 Chrome 最後合成哪一段 schema,交給實際 catalog 回答。

實際驗收表單與 Tool 契約

啟動網站後,開啟 Lab、選擇台北並送出,先確認人類操作仍能得到「WebMCP 入門工作坊」。 畫面能用之後,再執行兩支 focused tests:

npm test -- tests/unit/day-08-declarative.test.ts
npx playwright test tests/browser/day-08-declarative.spec.ts

unit test 鎖住 search_events 與 description,避免重構時偷偷改掉 Tool 身分;browser test 則真的操作表單,確認多了 WebMCP Attribute 之後,人類 fallback 沒有被弄壞。

這些結果屬於 E2:程式契約與 deterministic tests 可以重播。它們還不能證明真實 Agent 會從自然語言選中 search_events,也沒有證明 Inspector 在這個 Lab 上完成 discovery。 昨天看見 catalog,今天拆出 schema 來源,兩份證據各自回答不同問題,不混在一起比較安全。

目前 main 的讀者版 Lab 已經累積後續功能,所以你會看到 toolparamdescriptiontoolautosubmit。那不是今天的程式壞掉,而是未來版本已經往前走了;要重播本文,就留在 剛才切好的固定 tag。

表單會描述能力了,動態頁面卻沒有固定的 form

今天完成後,同一張搜尋表單有了兩種讀法:人類照常看 label、選條件、按按鈕;瀏覽器則從 toolnametooldescription 與欄位語意整理出 Tool 契約。

搜尋表單一直待在頁面上,Declarative API 很適合它。活動詳情就麻煩了:使用者進入某場 活動時才應該看見 get_event_details,離開後舊 Tool 也得跟著消失。明天我們會改用 Imperative API,處理 Tool 的註冊、去重與解除,看看上一場活動的 event ID 會不會忘記 下班。

參考資料