iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Modern Web

WebMCP:30 天打造 AI Agent 看得懂、也操作得動的網站系列 第 27 篇

Day 27|Demo 寫完才是麻煩的開始:把 WebMCP 整理成可維護的 WordPress Plugin

  • 分享至 

  • xImage
  •  

Day 24~26 已經完成文章搜尋、商品搜尋與購物車操作。今天保留這些功能,把 Day 27 的程式拆成不同模組,讓新增 Tool 時不必同時修改請求、畫面與註冊程式。

這次使用 WP WebMCP Lab 0.5.0。前幾天的頁面繼續保留,Day 27 使用新的模組入口。

本篇重點

  • PHP 決定本頁的工具範圍,提供 REST URL 與必要的 nonce。
  • API 模組處理請求;Service 處理驗證、資料整理與操作流程。
  • Tool 模組提供名稱、描述與 Schema。
  • Registry 統一註冊、移除與重新註冊。
  • 用同一組功能核對拆分前後的結果。

一、先把目錄與責任分開

本次新增的結構如下;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 拆成不同檔案。
https://ithelp.ithome.com.tw/upload/images/20261006/20121296JtTBdzjv6B.png

二、PHP 只在指定頁面載入

主外掛檔新增:

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。

import 必須搭配 module

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 帶同一個版本參數,更新時一起調整,避免入口更新了,依賴檔仍取到舊快取。

三、先核對文章範圍

  1. 將 WP WebMCP Lab 更新到 0.5.0,確認外掛已啟用。
  2. 開啟 https://wordpress.local/?webmcp_lab=27。
  3. 打開 WebMCP Inspector。
  4. 核對頁面顯示 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,讓本頁工具清單對應目前用途。
https://ithelp.ithome.com.tw/upload/images/20261006/20121296b3OC2wNXmP.png

清單不等於權限

工具範圍是減少無關工具的方式。網址參數或前端清單不能取代後端驗證。

本篇文章與商品查詢使用公開 API;購物車沿用 Cookie 工作階段與 Store API nonce。Day 23 的會員資料仍由 PHP 檢查登入與 capability。隱藏一個 Tool,不會自動封鎖它背後的 REST API。

四、Registry 管理整組工具

點「完整實驗」,網址會帶上 scope=all。此時頁面與 Inspector 應出現 8 個工具。

index.js 先建立工具定義,再依 PHP 提供的清單挑選:

const selected = all.filter(tool =>
  config.enabledTools.includes(tool.name)
);

await registry.replace(selected);

replace() 的順序是:

  1. 等待前一筆註冊操作完成。
  2. 檢查新清單是否有無效定義或重複名稱。
  3. 對舊工具的 AbortController 呼叫 abort()。
  4. 逐一註冊新工具,將各自的 signal 傳給 WebMCP。
  5. 全部完成後,更新頁面清單。

底層註冊仍使用:

const controller = new AbortController();
await context.registerTool(wrapped, {
  signal: controller.signal
});

registry.clear() 與 registry.replace() 是本例自己的包裝函式。WebMCP 的移除方式是中止註冊時傳入的 signal。

按「重新註冊本頁工具」後,工具數仍應是 8,不會累積成 16。某個工具註冊失敗時,本例會清除這一輪的部分註冊,顯示錯誤,避免留下看起來正常的半套清單。

圖片 3|完整實驗範圍註冊 8 個工具,重新註冊後仍維持同一組名稱。
https://ithelp.ithome.com.tw/upload/images/20261006/2012129621LlH1QsJ5.png

移除與恢復

  1. 按「移除本頁工具」。
  2. 核對頁面顯示 0 個 Tools、清單為 [],Inspector 清單也清空。
  3. 按「重新註冊本頁工具」,恢復 8 個工具。

移除的是目前頁面的工具註冊。既有購物車仍由 WooCommerce 的工作階段保存。

圖片 4|移除本頁工具後,Registry 清單為空;按重新註冊即可恢復本頁工具。
https://ithelp.ithome.com.tw/upload/images/20261006/201212961TzoUB3KpX.png

若工具正在執行,Registry 會拒絕移除或替換,等操作完成後再試。這能避免把「移除工具」誤當成「取消已送出的購物車操作」。跨分頁的操作仍由伺服器處理,本頁的執行狀態只管理本頁。

五、Tool 不直接處理 fetch

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 組裝也集中處理:

  • 一般永久連結:在 pathname 加上 ID 或操作名稱。
  • ?rest_route= 格式:修改 rest_route 參數。
  • 跨 Origin 的設定直接拒絕。

不能直接把 posts/9 接在一個含 query string 的 REST URL 後方。

保留 Day 26 的行為

拆檔後仍維持:

  • 加入前確認商品為簡單商品。
  • 寫入前先讀取購物車並更新 nonce。
  • 接受 HTTP 201 等成功狀態。
  • 加入是額外件數,更新是新的總件數。
  • 修改與移除使用購物車項目 key。
  • 409 回應附帶購物車時同步狀態,同時保留錯誤。
  • 寫入後沒有拿到完整回應時,回傳 unknown_outcome,不自動重送。

六、重新註冊後,實際執行一次

恢復 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 的價格。
https://ithelp.ithome.com.tw/upload/images/20261006/20121296FrAzN0UQeG.png

七、用 Filter 縮小工具範圍

本例提供 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。

九、下一輪測試清單

  • 在完整實驗範圍用 Send 輸入「找測試文章,再找 2,000 元以下的鍵盤」,核對兩種搜尋工具。
  • 在獨立測試工作階段重做 Day 26 的加入兩件、改成一件、移除。
  • 套用唯讀 filter,重新整理後核對商店只剩 3 個查詢工具。
  • 在未啟用 WebMCP 的瀏覽器查看提示;工具操作應保持停用。
  • 在另一個測試站停用 WooCommerce,核對文章工具仍可使用。
  • 發佈前補上管理設定、相容版本測試、必要的稽核紀錄與錯誤回報;紀錄排除 nonce、Cookie 和顧客私人資料。

結論

拆模組的成果,可以用實際行為核對:不同頁面範圍出現不同工具;重複註冊不累積;移除與恢復後,原本的查詢仍能執行。

PHP 管理頁面設定,API 管理請求,Service 管理操作,Tool 定義 Agent 介面,Registry 管理生命週期。之後新增功能時,就能找到對應位置,也能針對那一層寫測試。

參考資料


上一篇
Day 26|「加入兩件」和「改成兩件」不同:WooCommerce 購物車 Tool 實戰
下一篇
Day 28|我不告訴 AI 要用哪個 Tool:5 個自然語言任務驗收 WebMCP Agent
系列文
WebMCP:30 天打造 AI Agent 看得懂、也操作得動的網站 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言