安安~我是ChiYu~
昨天打開 Inspector 後,Chrome 終於把目前頁面的 Tool 列了出來。面板裡有名稱、description,
還有一大段 input schema。
看到 catalog 當然很開心,但我很快又卡在下一個問題:我並沒有另外寫一份 JSON Schema,
這些欄位到底從哪裡冒出來?
如果直接研究完成版搜尋頁,表單、API、畫面更新與商業規則會全部擠在一起。每個東西都像
答案,也就很難知道真正的答案是哪一個。因此我先把正式網站放到旁邊,另外做一張最小搜尋
表單,只觀察 HTML 如何形成 Declarative Tool。
今天使用的程式里程碑是 v3-day-11。先把版本切對,再往下看表單,不然目前的 main 已經
累積後續的自動送出與參數說明,很容易把明後天才會出現的答案一起帶進來:
git switch --detach v3-day-11
npm ci
這個版本只替原本可操作的搜尋表單加入 Declarative Tool 的核心身分,還沒有toolautosubmit、respondWith() 與正式 Agent submission。今天只驗證三件事:
toolname 與 tooldescription 會形成穩定的 Tool 身分。tag 內的 Lab 路徑仍保留早期名稱 labs/day-08-declarative-tool/。資料夾名稱沒有跟著文章天數
搬家,重播座標仍以 v3-day-11 為準。

圖 1:Tool 的身分來自 form,表單欄位則提供參數語意。這張圖說明 E2 契約;實際產生的 schema 仍要以瀏覽器輸出為準。
Declarative Tool 不是要我們用 JavaScript 另外重寫一套功能,而是替現有 HTML <form> 加上
WebMCP 標註。
瀏覽器會讀取表單的 toolname、tooldescription,再從欄位的 name、label、選項與限制
條件整理出結構化 input schema。於是同一張表單會有兩種讀法:人類看到輸入框、下拉選單與
按鈕;Agent 看到 Tool 名稱、用途與可接受的參數。
原本存在的表單補上必要標註後,就能成為瀏覽器可以公開給 Agent 的 Tool,不必另外維護一張
「Agent 專用搜尋表單」。
| Declarative Tool 可以做什麼 | 在這個活動網站的例子 |
|---|---|
| 把既有 HTML form 公開成 Tool | 將活動搜尋表單公開為 search_events |
| 從表單欄位形成結構化參數 | 整理出 query、location、price、level |
| 讓 Agent 將參數填回可見表單 | 把「台北、免費、入門」放進對應欄位 |
| 保留人類原本的操作方式 | 沒有 WebMCP 時仍可手動填表並搜尋 |
| 視設定決定是否自動送出 | 後續再加入 toolautosubmit 與 Tool result |
Declarative Tool 適合原本就能用表單表達的任務,例如搜尋、篩選、建立客服需求或準備預約
資料。它的優點是 Human UI 與 Agent Tool 共用同一份欄位語意,不必各自維護一套契約。
但它不是所有功能的萬用轉接頭。像「讀取目前 route 的活動」或「依頁面狀態動態註冊、移除
Tool」,沒有一張固定表單可以承載,就比較適合明天要介紹的 Imperative API。
還有一條界線要先畫清楚:表單成功註冊成 Declarative Tool,只代表網站已把能力與參數說明
交給瀏覽器。它不能保證 Agent 一定會選中,也不會替 server 完成權限、安全與商業規則檢查。
真正的 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 必須成對出現。移除其中任何一個,這張 form 就不再註冊為 Tool。也就是說,
按鈕上的「搜尋活動」可以改成「找活動」,search_events 卻不應跟著 UI 文案每天換名字。
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 映射結果 |
這裡最容易被忽略的是 name。id 讓 label 能找到欄位,name 才是提交資料與 schema 使用的
鍵。畫面上兩者常常剛好寫成一樣,所以少寫一個時,肉眼不一定立刻看得出來。
toolparamdescription 可以替個別參數補上更精確的說明。沒有這個 attribute 時,瀏覽器會先
使用關聯 label 的內容;若 label 不足以解釋輸入格式或資料來源,再補上參數說明,比每個欄位
預先貼一段重複文案更實在。
name,畫面照常出現,契約卻少一塊接著我故意把穩定鍵拿掉:
<label>
地點
<select>
<option value="taipei">台北</option>
</select>
</label>
瀏覽器還是會畫出一個寫著「地點」的下拉選單,人類也看得懂。問題出在資料送出時沒有欄位
名稱,Tool 無法穩定取得 location 這個 property。畫面沒有爆炸,契約只是安靜地缺了一角;
這種 bug 最擅長在 Demo 時裝沒事。
另一個常見誤會是把 placeholder 當成規格。placeholder 適合提示輸入範例,卻會跟著文案調整;
拿它代替 name 或欄位說明,等於讓資料契約跟著行銷文字一起漂流。
我替這張表單留下的規則很單純:可見文字要讓人類理解,name 與允許值要讓資料契約保持
穩定,HTML constraint 則繼續保護人類操作。兩邊描述的是同一個搜尋任務;Chrome 最後合成
哪一段 schema,交給實際 catalog 回答。
啟動網站後,開啟 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。
目前 main 的讀者版 Lab 已經累積後續功能,所以會看到 toolparamdescription 與toolautosubmit。那不是今天的程式壞掉,而是未來版本已經往前走;要重播本文,就留在
剛才切好的固定 tag。
今天完成後,同一張搜尋表單有了兩種讀法:人類照常看 label、選條件、按按鈕;瀏覽器則從toolname、tooldescription 與欄位語意整理出 Tool 契約。
搜尋表單一直待在頁面上,Declarative API 很適合它。活動詳情就麻煩了:使用者進入某場活動
時才應該看見 get_event_details,離開後舊 Tool 也得跟著消失。明天改用 Imperative API,
處理 Tool 的註冊、去重與解除,看看上一場活動的 event ID 會不會忘記下班。