前兩天整理了登入、權限與跨 Origin 的邊界,今天把這些觀念接到 WordPress。
這次做一個 get_current_profile:Agent 呼叫頁面上的 JavaScript Tool,Tool 再向 WordPress REST API 發出請求,由 PHP 檢查登入與權限,回傳目前帳號的 ID 與顯示名稱。
Agent
→ JavaScript Tool:get_current_profile
→ fetch:WordPress REST API
→ WordPress:Cookie、Nonce 與權限檢查
→ 目前使用者資料
→ JSON 回傳給 Tool
JavaScript 負責工具名稱、描述、輸入格式與呼叫方式。PHP 負責判斷身分、檢查權限與讀取資料。既有 WordPress 功能可以沿用,不需要另外重寫一套後端。
圖片 1|JavaScript 提供 Tool,WordPress 負責登入、權限與資料。
外掛目錄如下:
wp-content/plugins/wp-webmcp-lab/
├── wp-webmcp-lab.php
└── assets/
├── webmcp.js
└── webmcp.css
wp-webmcp-lab-0.1.0.zip,按「立即安裝」,再按「啟用」。?webmcp_lab=1,開啟 Day 23 測試頁。這一天的程式放在 WordPress 外掛內。測試頁只在指定網址載入 Tool;網站其他頁面維持原本的功能。
自訂路由在 rest_api_init 註冊。以下是外掛中的登入、權限與資料回傳核心:
function wp_webmcp_lab_profile_permission() {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'webmcp_not_logged_in',
'請先登入 WordPress。',
array( 'status' => 401 )
);
}
if ( ! current_user_can( 'read' ) ) {
return new WP_Error(
'webmcp_forbidden',
'目前帳號沒有讀取權限。',
array( 'status' => 403 )
);
}
return true;
}
add_action( 'rest_api_init', function () {
register_rest_route( 'webmcp/v1', '/profile', array(
'methods' => WP_REST_Server::READABLE,
'permission_callback' => 'wp_webmcp_lab_profile_permission',
'callback' => function () {
$user = wp_get_current_user();
$response = rest_ensure_response( array(
'id' => $user->ID,
'name' => $user->display_name,
) );
$response->header( 'Cache-Control', 'no-store, private' );
return $response;
},
) );
} );
permission_callback 先檢查是否登入,再檢查帳號是否具有 read 權限。通過之後,才讀取目前帳號的資料。
這個入口沒有 userId 參數,呼叫者不能自行指定另一個帳號。回傳內容也只包含 ID 與顯示名稱。
在 wp_enqueue_scripts 中載入 JS,並傳入請求需要的設定。以下是外掛的載入核心:
add_action( 'wp_enqueue_scripts', function () {
if ( ! wp_webmcp_lab_is_demo() ) {
return;
}
wp_enqueue_script(
'wp-webmcp-lab',
plugin_dir_url( __FILE__ ) . 'assets/webmcp.js',
array(),
'0.1.0',
true
);
wp_localize_script( 'wp-webmcp-lab', 'wpWebMCP', array(
'profileUrl' => esc_url_raw( rest_url( 'webmcp/v1/profile' ) ),
'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
) );
} );
wp_webmcp_lab_is_demo() 是外掛提供的測試頁判斷函式,用來檢查網址中的 webmcp_lab=1。完整外掛另外包含測試頁與樣式;上面的片段用來說明載入設定。
API 網址由 rest_url() 產生,不在 JS 中拼接 /wp-json/。一般永久連結的網址會包含 index.php?rest_route=/webmcp/v1/profile,同樣可以使用。
| 項目 | 本篇的用途 |
|---|---|
| 登入 Cookie | 讓 WordPress 驗證登入帳號 |
wp_rest Nonce |
配合 Cookie 驗證 REST 請求,提供 CSRF 防護 |
current_user_can('read') |
檢查目前帳號是否具備此功能需要的權限 |
Nonce 放在 X-WP-Nonce 請求標頭。它不會替帳號增加權限;登入驗證與功能權限仍由 WordPress 後端檢查。
使用 Cookie 驗證 REST API 時,若沒有提供 Nonce,WordPress 會把這次請求視為未登入。因此,登入後要重新載入測試頁,讓頁面取得登入狀態對應的設定。
下面整理外掛的請求與註冊核心。完整 webmcp.js 另外把結果同步顯示在「最後一次請求」,並讓頁面按鈕共用同一個請求函式。
(() => {
async function profile() {
try {
const endpoint = new URL(wpWebMCP.profileUrl, location.href);
if (endpoint.origin !== location.origin) {
throw new Error('REST API 與頁面必須使用相同 Origin。');
}
const headers = { Accept: 'application/json' };
if (wpWebMCP.nonce) {
headers['X-WP-Nonce'] = wpWebMCP.nonce;
}
const response = await fetch(endpoint, {
credentials: 'same-origin',
cache: 'no-store',
headers,
});
const body = await response.json();
return {
httpStatus: response.status,
status: response.ok ? 'success' : 'error',
...(response.ok
? { profile: body }
: { code: body.code, message: body.message }),
};
} catch (error) {
return { status: 'request_error', message: error.message };
}
}
async function register() {
if (!document.modelContext?.registerTool) return;
await document.modelContext.registerTool({
name: 'get_current_profile',
description:
'Read the ID and display name of the currently signed-in WordPress user. ' +
'Requires WordPress login. Does not read other users or modify data.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
annotations: { readOnlyHint: true },
execute: async (args) => {
if (!args || typeof args !== 'object' ||
Array.isArray(args) || Object.keys(args).length) {
return JSON.stringify({ status: 'invalid_input' });
}
return JSON.stringify(await profile());
},
});
}
register().catch(console.error);
})();
這裡使用一般 JS 檔案,把 await 放在 async 函式內,再執行註冊。請求失敗時保留 HTTP 狀態與 WordPress 錯誤碼,方便分辨未登入、權限不足與請求本身失敗。
readOnlyHint 描述這個 Tool 的用途;真正能否讀取資料,仍由 PHP 的權限檢查決定。
get_current_profile。{},按 Execute Tool。httpStatus: 200、status: success,以及 profile 裡的 id 與 name。這次直接執行指定 Tool,不需要在 User Prompt 填入文字。頁面下方會同步顯示這次請求;從 Inspector 執行時,來源為 get_current_profile。
圖片 2|登入後執行 get_current_profile,取得目前帳號的 ID 與顯示名稱。
{},再次按 Execute Tool。{
"httpStatus": 401,
"status": "error",
"code": "webmcp_not_logged_in",
"message": "請先登入 WordPress。"
}
測試頁在未登入時仍會註冊 Tool,讓這個案例直接呈現後端的登入檢查:工具可以被看見,但讀取帳號資料的請求會被拒絕。
圖片 3|未登入仍能看見 Tool,但 WordPress 回傳 401,拒絕讀取使用者資料。
頁面的「直接查詢 REST API」按鈕與 Tool 共用請求函式,適合核對 API 回應;完整 Tool 流程則使用 Inspector 的 Execute Tool。
WordPress 已有文章查詢入口,例如 wp/v2/posts。如果原生入口的資料與權限規則符合需求,就讓 Tool 呼叫它。
需要縮小回傳欄位、整合多個查詢,或加入特定業務規則時,再建立自己的路由。本篇使用 webmcp/v1/profile,回傳範圍固定為目前帳號的 ID 與顯示名稱。
本篇使用 read。如果之後增加發布文章的功能,就要依操作檢查 publish_posts;涉及特定文章時,還要檢查該文章的操作權限。
前端可以決定要顯示哪些 Tool,後端則必須在每次請求時驗證權限。即使有人繞過頁面直接呼叫 REST API,也要經過相同檢查。
rest_url() 產生,避免永久連結格式不同造成錯誤。下一篇接著把 WordPress 文章搜尋包成 search_posts。