用 WordPress Abilities API 寫 MCP server:從手刻 Function Call 到官方標準

之前做 DWP 的 LINE 聊天機器人查 WooCommerce 訂單,會員可以直接在 LINE 裡問訂單狀態、查活動,背後是 OpenAI 的 Function Call 在跑。整個架子是自己刻的——每一個會員問句能對應到的後端動作,要自己定義一份 JSON Schema 告訴模型「這個工具吃什麼參數、回什麼結構」,再寫一個 dispatcher 收到 model 的 tool_calls 之後手動 routing 到對應的 WP 函式,回傳值也要自己塞回對話 context。

換一家模型供應商就要重做一次 schema 格式(OpenAI、Anthropic、Gemini 的欄位名稱都不一樣),漏處理一個錯誤路徑 LINE 那邊就吐出一堆奇怪的字。一份「查訂單狀態」的能力,被綁死在這個 LINE Bot 專案裡,搬不到別處用。

直到 WordPress 6.9 把 WordPress Abilities API 正式收進 core,加上 Automattic 維護的 MCP Adapter,這套東西就有了官方標準。同一個 ability 一次註冊,就同時能從 PHP、REST API、跟 MCP(給 Claude Code、Claude Desktop、Cursor 這些 AI client 直接呼叫)三個路徑使用。這篇就是用我們做的 cdx-mcp 外掛當實例,把整條路走過一次。

WordPress Abilities API 是什麼

Abilities API 是 WordPress 6.9 開始進 core 的官方標準,讓你把一段「可被外部呼叫的功能」用統一格式註冊起來,附上輸入/輸出 schema、權限檢查跟標籤——之後 PHP、REST、AI agent 都能用同一份定義去呼叫它。

對應到之前 LINE Bot 自刻 Function Call 的痛點,差別在:

手刻 Function CallAbilities API
每個 model 廠商一份 schema一份 input/output schema 全平台共用
自己寫 dispatcher routingwp_register_ability( $name, $args ) 註冊
自己驗證權限、自己回錯誤permission_callback + 回 WP_Error 是標準約定
只能在那個專案內用註冊完同時走 PHP/REST/MCP

註冊一個 ability 需要的關鍵欄位:

  • labeldescription:description 是寫給 AI 看的,講清楚做什麼、吃什麼、回什麼
  • category:先用 wp_register_ability_category() 註冊好,ability 才能歸到那個 category 底下
  • input_schema / output_schema:JSON Schema 格式
  • execute_callback:實際做事的 PHP function
  • permission_callbackRequired,不是 optional(官方 PHP API 文件曾經寫成 optional,後來修正)
  • meta.annotations:標示這個 ability 的特性(唯讀、是否會破壞資料、是否冪等)

meta.annotations 這幾個旗標被 MCP Adapter 自動映射成 readOnlyHintdestructiveHintidempotentHint,AI 會看這幾個值來決定要不要自動呼叫。特別注意 destructive 預設值是 true,純查詢的 ability 一定要手動設成 false,不然 Claude 會以為這動作有破壞性而拒絕自動執行。這是文件埋得比較深的雷。

MCP Adapter 把 ability 轉成 MCP tool

Abilities API 解決了「功能怎麼註冊」,MCP Adapter 解決的是「怎麼讓 AI agent 找得到並呼叫它」。

Model Context Protocol(MCP)是 Anthropic 推出的開放協定,讓 AI client(Claude Code、Claude Desktop、Cursor、VS Code 等)能透過統一介面去呼叫外部工具。MCP Adapter 就是 WordPress 這端的橋——把已註冊的 ability 包裝成 MCP server 暴露出去,AI client 用 tools/listtools/call 兩個方法就能讀到 schema 並執行。

裝起來有兩種選擇:

  1. 用 default server:裝完 mcp-adapter 外掛就會自動建一個叫 mcp-adapter-default-server 的 server,要把 ability 加進去得在註冊時加一個 meta.mcp.public = true 旗標
  2. 自己建 custom server:在自己的外掛裡明確指定要暴露哪些 ability,乾淨可控,且不需要那個 public 旗標

我走第二條直接用 Composer 安裝,就不用讓使用者還要另外去裝 adapter 外掛了。

實戰:cdx-mcp 外掛把 WooCommerce 訂單查詢變 MCP tool

我做了一個外掛叫 cdx-mcp,功能很單純:給一個 WooCommerce 訂單 ID,回傳這筆訂單的完整結構化資料(狀態、金額、客戶、付款方式、品項),給 Claude Code 直接拿來查訂單。整個外掛大概 200 行 PHP,下面拆開講重點。

Step 1:composer 引入兩個官方 package

外掛根目錄的 composer.json

{
    "name": "codotx/cdx-mcp",
    "type": "wordpress-plugin",
    "require": {
        "php": ">=8.1",
        "wordpress/abilities-api": "*",
        "wordpress/mcp-adapter": "*"
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

composer install 之後會把 wordpress/abilities-api(v0.4)、wordpress/mcp-adapter(v0.5)跟 wordpress/php-mcp-schema 一起拉下來。WordPress 6.9 之後 Abilities API 已經進 core,bundled 那份只會在舊版 WP 環境補位用,相同 class 已經存在時 bootstrap 會自動跳過載入,不會打架。

Step 2:主外掛檔載入 bootstrap

<?php
/**
 * Plugin Name: CDX MCP
 * Requires at least: 6.8
 * Requires PHP: 8.1
 */

defined( 'ABSPATH' ) || exit;

define( 'CDX_MCP_DIR', plugin_dir_path( __FILE__ ) );

if ( file_exists( CDX_MCP_DIR . 'vendor/autoload.php' ) ) {
    require_once CDX_MCP_DIR . 'vendor/autoload.php';
}

// Abilities API bootstrap(WP 6.9+ core 已有就跳過).
if ( ! function_exists( 'wp_register_ability' ) ) {
    require_once CDX_MCP_DIR . 'vendor/wordpress/abilities-api/abilities-api.php';
}

// MCP Adapter bootstrap.
if ( ! class_exists( \WP\MCP\Core\McpAdapter::class ) ) {
    require_once CDX_MCP_DIR . 'vendor/wordpress/mcp-adapter/mcp-adapter.php';
}

兩個 if ( ! ... ) 判斷的用意是讓外掛在 WP 6.9(core 已自帶 Abilities API)跟 6.8(要靠 bundled)都能跑。

Step 3:註冊 ability category

ability 必須先有 category 才能歸類,category 要在 wp_abilities_api_categories_init action 註冊:

add_action( 'wp_abilities_api_categories_init', function () {
    wp_register_ability_category( 'order-management', array(
        'label'       => __( 'Order Management', 'cdx-mcp' ),
        'description' => __( 'Abilities for querying WooCommerce orders.', 'cdx-mcp' ),
    ) );
} );

category slug 規則:只能小寫英數加連字號,不能用底線或斜線。

Step 4:註冊 ability,定義 input/output schema

這是整個系統的核心,AI 能不能用對全靠這份 schema 跟 description:

add_action( 'wp_abilities_api_init', function () {
    if ( ! class_exists( 'WooCommerce' ) ) {
        return;
    }

    wp_register_ability( 'cdx-mcp/get-order-status', array(
        'label'       => __( 'Get WooCommerce Order Status', 'cdx-mcp' ),
        'description' => __( 'Returns the WooCommerce order details for a given order ID, including order status, totals, customer info, payment method, shipping info and line items.', 'cdx-mcp' ),
        'category'    => 'order-management',

        'input_schema' => array(
            'type'       => 'object',
            'properties' => array(
                'order_id' => array(
                    'type'        => 'integer',
                    'minimum'     => 1,
                    'description' => 'The WooCommerce order ID to look up.',
                ),
            ),
            'required'             => array( 'order_id' ),
            'additionalProperties' => false,
        ),

        'output_schema' => array(
            'type'       => 'object',
            'properties' => array(
                'order_id'       => array( 'type' => 'integer' ),
                'status'         => array( 'type' => 'string' ),
                'total'          => array( 'type' => 'string' ),
                'customer'       => array(
                    'type'       => 'object',
                    'properties' => array(
                        'email' => array( 'type' => 'string' ),
                        'phone' => array( 'type' => 'string' ),
                    ),
                ),
                'items' => array(
                    'type'  => 'array',
                    'items' => array(
                        'type'       => 'object',
                        'properties' => array(
                            'product_id' => array( 'type' => 'integer' ),
                            'name'       => array( 'type' => 'string' ),
                            'quantity'   => array( 'type' => 'integer' ),
                        ),
                    ),
                ),
                // 省略其他欄位,實際外掛還含 currency、payment_method、date_paid 等
            ),
        ),

        'execute_callback'    => 'cdx_mcp_get_order_status',
        'permission_callback' => function ( $input ) {
            return current_user_can( 'manage_woocommerce' );
        },

        'meta' => array(
            'annotations'  => array(
                'readonly'    => true,   // 只讀
                'destructive' => false,  // 預設是 true,純查詢務必設 false
                'idempotent'  => true,   // 重複呼叫結果一樣
            ),
            'show_in_rest' => true,
        ),
    ) );
} );

幾個重點:

  • description 寫給 AI 看,不是寫給開發者看,要直接寫清楚「做什麼、吃什麼、回什麼」
  • permission_callbackmanage_woocommerce,等於只有能管 WC 的人才能查
  • meta.annotations.destructive 設成 false 是讓 Claude 知道這是純查詢可以放心執行的關鍵
  • show_in_rest = true 順便把這個 ability 暴露到 Abilities API 的 REST 端點,多一條呼叫路徑

Step 5:execute callback 抓 WooCommerce 訂單

function cdx_mcp_get_order_status( $input ) {
    $order_id = (int) ( $input['order_id'] ?? 0 );
    $order    = wc_get_order( $order_id );

    if ( ! $order ) {
        return new WP_Error(
            'order_not_found',
            sprintf( __( 'Order %d does not exist.', 'cdx-mcp' ), $order_id )
        );
    }

    $items = array();
    foreach ( $order->get_items() as $item ) {
        $items[] = array(
            'product_id' => (int) $item->get_product_id(),
            'name'       => (string) $item->get_name(),
            'quantity'   => (int) $item->get_quantity(),
        );
    }

    return array(
        'order_id' => $order->get_id(),
        'status'   => (string) $order->get_status(),
        'total'    => (string) $order->get_total(),
        'customer' => array(
            'email' => (string) $order->get_billing_email(),
            'phone' => (string) $order->get_billing_phone(),
        ),
        'items'    => $items,
    );
}

成功回符合 output_schema 的陣列、失敗回 WP_Error,這是 Abilities API 的標準錯誤約定,MCP Adapter 會自動把 WP_Error 轉成 AI 看得懂的 MCP error response,不用自己包 try/catch、也不用自己組錯誤訊息。

Step 6:建立 custom MCP server

最後一步是把這個 ability 暴露成 MCP tool。在 mcp_adapter_init action 裡呼叫 create_server()

add_action( 'plugins_loaded', function () {
    if ( class_exists( \WP\MCP\Core\McpAdapter::class ) ) {
        \WP\MCP\Core\McpAdapter::instance();
    }
}, 20 );

add_action( 'mcp_adapter_init', function ( $adapter ) {
    $adapter->create_server(
        'cdx-mcp-server', // server 唯一 ID
        'cdx-mcp', // REST namespace
        'mcp', // REST route
        'CDX MCP Server', // 顯示名稱
        'Exposes WooCommerce order lookup as MCP tools.', // 描述
        'v0.1.0', // 版本
        array( \WP\MCP\Transport\HttpTransport::class ), // 傳輸:HTTP
        \WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class, // 錯誤處理
        \WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class, // 觀測
        array( 'cdx-mcp/get-order-status' ), // 要暴露的 ability
        array(), // resources
        array() // prompts
    );
} );

倒數第三個參數那個陣列就是「我這個 server 要暴露哪幾個 ability」。之後要加新功能,只要再註冊一個 ability、把名字加進這個陣列就完成。REST endpoint 自動掛到 /wp-json/cdx-mcp/mcp

外掛啟用後在 WP-CLI 確認:

$ wp mcp-adapter list
ID                            Name                          Version  Tools  Resources  Prompts
cdx-mcp-server                CDX MCP Server                v0.1.0   1      0          0
mcp-adapter-default-server    MCP Adapter Default Server    v1.0.0   8      0          0

Tools 顯示 1,代表 cdx-mcp/get-order-status 已經被掛上去當 MCP tool。

連 Claude Code:本機 stdio 與生產站 HTTP

ability 跟 MCP server 都備好了,剩下讓 Claude Code 能找到它。兩種 transport 都試一遍。

本機開發:STDIO transport

mcp-adapter 自帶一個 WP-CLI 指令 wp mcp-adapter serve,會把 server 從 stdin/stdout 跑起來。Claude Code 在專案根目錄讀 .mcp.json 設定:

{
  "mcpServers": {
    "cdx-mcp-local": {
      "type": "stdio",
      "command": "wp",
      "args": [
        "--path=/Users/username/Sites/mysite",
        "mcp-adapter",
        "serve",
        "--server=cdx-mcp-server",
        "--user=mysite"
      ]
    }
  }
}

--user=mystie 帶身分(同時觸發 permission_callback 裡的 current_user_can 檢查)。

用 raw JSON-RPC 手動測一下 server 有沒有正常起來:

$ printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
' | wp mcp-adapter serve --server=cdx-mcp-server --user=mysite

{"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"CDX MCP Server","version":"v0.1.0"}, ... }}
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"cdx-mcp-get-order-status", "annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true}, ... }]}}

annotations 那三個 hint 正確映射出來,代表前面在 meta.annotations 設的 readonly / destructive / idempotent 都通了。

正式站 HTTP transport + Application Password

正式站走 HTTP,MCP server 已經自動掛在 https://mystie.com/wp-json/cdx-mcp/mcp,認證用 WordPress 內建的 Application Password:

# 在生產站建一組專用的 application password 給 Claude Code
wp user application-password create 2 'Claude Code CDX MCP' --porcelain
# 回傳 → xxxxxxxxxxxxxxxxxxxxxxxx(24 字元密碼,這裡用占位符代替,實際請保密)

⚠️ 這個密碼等同於 admin 帳號的完整權限,產出後請直接寫進密碼管理器或 .mcp.json,不要貼進文章、commit、聊天訊息或螢幕截圖。下面範例中的 Base64 字串都是示意,請用你自己生成的密碼編碼。

把帳號跟密碼編成 Basic Auth header,寫進 .mcp.json(這個檔案要記得加進 .gitignore):

{
  "mcpServers": {
    "cdx-mcp-prod": {
      "type": "http",
      "url": "https://oberonlai.blog/wp-json/cdx-mcp/mcp",
      "headers": {
        "Authorization": "Basic <Base64(帳號:應用程式密碼)>",
        "Accept": "application/json, text/event-stream"
      }
    }
  }
}

產 Base64 的方式:printf 'Developer:你的應用程式密碼' | base64

curl 直接打 initialize 確認端點通:

$ curl -s -u "Developer:<你的應用程式密碼>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-X POST https://mysite.com/wp-json/cdx-mcp/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}'

{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"CDX MCP Server","version":"v0.1.0"}, ... }}

重啟 Claude Code 進專案目錄,第一次會問是否信任 .mcp.json 裡的 MCP server,接受後 /mcp 指令會看到兩台都 connected

直接在對話裡打「查訂單 56789 的狀態」,Claude 會自動:

  1. tools/list 看可用 tool,發現 cdx-mcp-get-order-status 描述貼題
  2. tools/callname=cdx-mcp-get-order-statusarguments={"order_id": 56789}
  3. 拿到 JSON 後翻成中文摘要

踩過的幾個小坑

整理一下實作過程中卡到的點:

  1. WP 版本要 6.9 以上 — Abilities API 是 6.9 才進 core
  2. destructive 預設 true — 不手動設 false,AI 會以為這個 tool 會破壞資料而拒絕自動呼叫
  3. permission_callback 是 Required 不是 optional — 官方文件曾經標 optional,後來修正
  4. Abilities Registry 是 lazy initwp_abilities_api_init 這個 action 只有在第一次有人呼叫 WP_Abilities_Registry::get_instance() 之後才會 fire,而且必須在 init 之後

從自刻 Function Call 到官方標準

回頭看 LINE Bot 那套手刻 Function Call,痛點其實在綁死特定專案,同樣是「查訂單狀態」這個能力,當時為了 LINE Bot 寫的 schema 跟 dispatcher,要搬到 Claude Code、Cursor、或下一個聊天機器人專案,全部得重做。

WordPress Abilities API + MCP Adapter 等於把這層工作整合進核心跟官方 package:

  • ability 註冊一次,PHP/REST/MCP 三條路徑都能用
  • AI client 透過 MCP 標準介面拉 schema,不必為每家模型供應商重做格式
  • 權限、錯誤、annotations 都是約定好的協定,不用自己定義
  • 換 AI client(Claude Code → Cursor → Continue)只要改 .mcp.json 一行,server 端完全不動

之前在另一篇文章討論過 AI agent 管 WordPress 該選 MCP、REST API 還是 SSH + WP-CLI,就能透過 Abilities API 進 core、MCP Adapter 來設計自己的 MCP,對 WordPress 開發者來說「自己造 AI 工具」的門檻又更低了。

cdx-mcp 整個外掛大約 200 行 PHP,但每一行都在做真正的業務邏輯,不再是繞 schema 跟 dispatcher,WordPress 官方把 AI 整合這塊做順了,以後要整合就超方便了!

參考連結

目錄

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

Picture of 賴俊吾 / Oberon Lai
賴俊吾 / Oberon Lai

現為全職 WordPress 工程師,網站開發經歷 11 年,專攻前端工程與 WordPress 佈景主題、外掛客製化開發

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!

專注於分享 WordPress 開發、接案技巧、專案管理等自由工作者必備知識與心得

© 2025 想點創意科技有限公司

想點創意科技有限公司 | 統一編號 90516823
Designed by Hend Design | 隱私權政策

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!