Day 24~26 已經完成文章搜尋、商品搜尋與購物車操作。今天保留這些功能,把 Day 27 的程式拆成不同模組,讓新增 Tool 時不必同時修改請求、畫面與註冊程式。
這次使用 WP WebMCP Lab 0.5.0。前幾天的頁面繼續保留,Day 27 使用新的模組入口。
本次新增的結構如下;Day 23~26 的檔案另行保留。
wp-webmcp-lab/
├── wp-webmcp-lab.php
├── includes/
│ ├── class-assets.php
│ └── day27-page.php
└── assets/
└── js/
├── package.json
├── index.js
├── registry.js
├── api/
│ ├── url.js
│ ├── wordpress.js
│ └── woocommerce.js
├── services/
│ ├── posts.js
│ ├── products.js
│ └── cart.js
└── tools/
├── posts.js
├── products.js
└── cart.js
| 位置 | 負責的事情 |
|---|---|
| includes/class-assets.php | 判斷 Day 27 頁面、工具範圍、WooCommerce 是否存在,載入設定與模組 |
| includes/day27-page.php | 顯示範圍切換、註冊控制與操作結果 |
| api/ | 組 REST URL、送出請求、更新 Store API nonce、保留 HTTP 錯誤 |
| services/ | 驗證輸入、整理文章與商品、處理購物車操作及畫面同步 |
| tools/ | 定義名稱、描述、輸入 Schema、annotations 與 execute |
| registry.js | 管理本頁工具的註冊與移除 |
| index.js | 組裝以上模組、接上頁面按鈕 |
例如更改購物車的錯誤處理,主要看 Service;調整 Agent 看到的描述,主要看 Tool;REST URL 的問題則到 API 模組處理。
圖片 1|Day 27 將 PHP、API、操作邏輯、Tool 定義與 Registry 拆成不同檔案。
主外掛檔新增:
require_once __DIR__ . '/includes/class-assets.php';
require_once __DIR__ . '/includes/day27-page.php';
Assets 類別先判斷 webmcp_lab=27,才載入 Day 27 的 JavaScript。一般首頁不會因為啟用外掛,就自動出現這一整組工具。
wp_localize_script() 提供本頁設定,主要欄位如下:
array(
'enabledTools' => self::enabled_tools(),
'postsUrl' => esc_url_raw( rest_url( 'wp/v2/posts' ) ),
'productsUrl' => esc_url_raw( rest_url( 'wc/store/v1/products' ) ),
'cartUrl' => esc_url_raw( rest_url( 'wc/store/v1/cart' ) ),
)
商店與完整實驗範圍另外提供 wc_store_api nonce、幣別與小數位。文章範圍不需要 Store API nonce。
Day 27 的入口使用 ES Modules:
import {createRegistry} from './registry.js?v=0.5.0';
import {createPublicApi} from './api/wordpress.js?v=0.5.0';
import {createStoreApi} from './api/woocommerce.js?v=0.5.0';
因此載入標籤必須是 type="module"。本例沿用外掛的 WordPress 6.0 最低需求,以 wp_enqueue_script() 搭配只針對 wp-webmcp-day27 的 script_loader_tag filter,產生 module 標籤。其他 script 標籤保持原樣。
WordPress 6.5 以上的新專案也可以採用 wp_enqueue_script_module()。本例不需要打包工具;瀏覽器直接載入相對路徑的模組。各 import 帶同一個版本參數,更新時一起調整,避免入口更新了,依賴檔仍取到舊快取。
https://wordpress.local/?webmcp_lab=27。scope: posts,只有 search_posts 與 get_post。本頁有三個範圍;下表是 WooCommerce 已啟用時的清單:
| 範圍 | 工具數 | 工具 |
|---|---|---|
| 文章 posts | 2 | search_posts、get_post |
| 商店 shop | 6 | search_products、get_product、get_cart、add_to_cart、update_cart_item、remove_from_cart |
| 完整實驗 all | 8 | 文章與商店工具合併 |
未啟用 WooCommerce 時,文章範圍仍提供 2 個工具,商店範圍為 0 個,完整實驗範圍只保留文章工具。
圖片 2|文章範圍只註冊 search_posts 與 get_post,讓本頁工具清單對應目前用途。
工具範圍是減少無關工具的方式。網址參數或前端清單不能取代後端驗證。
本篇文章與商品查詢使用公開 API;購物車沿用 Cookie 工作階段與 Store API nonce。Day 23 的會員資料仍由 PHP 檢查登入與 capability。隱藏一個 Tool,不會自動封鎖它背後的 REST API。
點「完整實驗」,網址會帶上 scope=all。此時頁面與 Inspector 應出現 8 個工具。
index.js 先建立工具定義,再依 PHP 提供的清單挑選:
const selected = all.filter(tool =>
config.enabledTools.includes(tool.name)
);
await registry.replace(selected);
replace() 的順序是:
abort()。signal 傳給 WebMCP。底層註冊仍使用:
const controller = new AbortController();
await context.registerTool(wrapped, {
signal: controller.signal
});
registry.clear() 與 registry.replace() 是本例自己的包裝函式。WebMCP 的移除方式是中止註冊時傳入的 signal。
按「重新註冊本頁工具」後,工具數仍應是 8,不會累積成 16。某個工具註冊失敗時,本例會清除這一輪的部分註冊,顯示錯誤,避免留下看起來正常的半套清單。
圖片 3|完整實驗範圍註冊 8 個工具,重新註冊後仍維持同一組名稱。
[],Inspector 清單也清空。移除的是目前頁面的工具註冊。既有購物車仍由 WooCommerce 的工作階段保存。
圖片 4|移除本頁工具後,Registry 清單為空;按重新註冊即可恢復本頁工具。
若工具正在執行,Registry 會拒絕移除或替換,等操作完成後再試。這能避免把「移除工具」誤當成「取消已送出的購物車操作」。跨分頁的操作仍由伺服器處理,本頁的執行狀態只管理本頁。
Tool 模組接收 run,將 execute 交給 Service。以下以文章搜尋的核心結構示範:
{
name: 'search_posts',
inputSchema: {
type: 'object',
properties: {
keyword: {type:'string', minLength:1, maxLength:100},
limit: {type:'integer', minimum:1, maximum:10, default:5}
},
required: ['keyword'],
additionalProperties: false
},
annotations: {readOnlyHint:true},
execute: args => run('search_posts', args)
}
完整定義另外包含 description;名稱、參數上限與 Day 24 一致。Service 再檢查空白關鍵字、整數與額外參數,並把 REST API 的資料整理成 Tool 回傳。
組裝時才把實際 API 傳入:
createPostsService({
api: createPublicApi({base:config.postsUrl, href:location.href}),
config,
document,
show
});
這樣測試時可以注入假 API,驗證錯誤與回傳格式;實際頁面則使用同一份 Service 配上真正的 API。
api/wordpress.js 的公開 GET adapter 由文章與商品共用:
{
credentials: 'omit',
cache: 'no-store',
headers: {Accept:'application/json'}
}
api/woocommerce.js 管理購物車請求:保留同源 Cookie、傳送 Store API nonce,並接收回應中的新 nonce。公開商品的前置查詢仍省略 Cookie 與 nonce。
URL 組裝也集中處理:
?rest_route= 格式:修改 rest_route 參數。不能直接把 posts/9 接在一個含 query string 的 REST URL 後方。
拆檔後仍維持:
恢復 8 個工具後,在 Inspector 選 search_products,輸入:
{"keyword":"鍵盤","maxPrice":2000,"limit":5}
按 Execute Tool。本機測試商品對應結果為:
{
"status": "success",
"count": 1,
"products": [
{
"id": 101,
"name": "Day25 機械鍵盤",
"price": {"amount":1290,"currency":"TWD","range":null}
}
]
}
這裡節錄主要欄位。接著選 get_cart,輸入 {},核對目前工作階段的購物車。重新註冊工具不會把購物車重設成空車。
頁面的「執行所選工具」與 WebMCP execute 共用操作邏輯,頁面紀錄會區分「頁面按鈕」與「Tool」。手動執行使用 Execute Tool;自然語言測試才使用 User Prompt → Send。
圖片 5|恢復工具後執行 search_products,回傳商品 ID 101 與 1,290 TWD 的價格。
本例提供 wp_webmcp_lab_enabled_tools filter。它能從目前範圍移除工具,例如讓本頁保留查詢、停用購物車寫入:
add_filter(
'wp_webmcp_lab_enabled_tools',
function ( $tools, $scope ) {
return array_values( array_diff(
$tools,
array( 'add_to_cart', 'update_cart_item', 'remove_from_cart' )
) );
},
10,
2
);
PHP 會再與本頁已知工具取交集。重複名稱不會增加工具,未知名稱也不會變成可執行 JavaScript。
要新增 Tool,需要實作自己的 Service 與 Tool factory,再加入入口組裝及 PHP 允許清單。這個 filter 只負責縮小清單,不能單靠填入一段字串產生 execute。
本機瀏覽器確認工具註冊與生命週期;資料查詢使用頁面按鈕執行共用操作邏輯。
| 操作 | 觀察結果 |
|---|---|
| 開啟文章範圍 | 2 個工具:search_posts、get_post |
| 開啟商店範圍 | 6 個商品與購物車工具 |
| 開啟完整實驗 | 8 個工具 |
| 再次註冊完整清單 | 仍為 8 個,沒有重複名稱 |
| 移除本頁工具 | 頁面清單為 [],瀏覽器工具清單清空 |
| 恢復本頁工具 | 重新出現 8 個工具 |
| 搜尋「測試」文章 | HTTP 200,回傳 ID 9 |
| 搜尋 2,000 TWD 以下鍵盤 | HTTP 200,回傳 ID 101、1,290 TWD |
| 讀取測試工作階段購物車 | HTTP 200,0 件、0 TWD |
Day 23~27 的 JavaScript 測試共 21 項通過。新增測試涵蓋註冊佇列、部分失敗清理、執行中的保護、失效工具參照、兩種 REST URL,以及拆分後的購物車流程。
PHP 的隔離測試另外檢查 WooCommerce 存在與不存在時的工具範圍、filter 限制與 module 標籤。安裝 ZIP 保持單一 wp-webmcp-lab/ 根目錄,版本為 0.5.0。
拆模組的成果,可以用實際行為核對:不同頁面範圍出現不同工具;重複註冊不累積;移除與恢復後,原本的查詢仍能執行。
PHP 管理頁面設定,API 管理請求,Service 管理操作,Tool 定義 Agent 介面,Registry 管理生命週期。之後新增功能時,就能找到對應位置,也能針對那一層寫測試。