本教程會帶你構建一個Model Context Protocol (MCP) 伺服器,等Claude AI可以管理WordPress網站——建立文章、更新SEO、上傳圖片,淨係要同佢講嘢就得,而且可以喺任何裝置上完成。
呢度唔淨止講要跑咩,仲會講點解要咁樣做,因為官方文件通常都係將工具同命令一股腦掟晒畀你,冇任何上下文,結果好容易洗成幾個鐘去debug嗰啲本應顯而易見嘅問題。
我哋究竟喺構建緊咩?
喺寫任何代碼之前,先理解吓咩係MCP伺服器,以及點解需要所有呢啲組件,會好有幫助。
MCP(Model Context Protocol)係一個開放標準,等好似Claude咁嘅AI模型可以使用外部工具。可以將佢想像成畀Claude一對手——佢唔再只係回答問題,而係真正可以做嘢:讀取你嘅WordPress文章、更新SEO欄位、將圖片上傳去儲存。
各個部分係咁樣組合埋一齊嘅:
You (talking to Claude AI)
|
v
Claude AI (claude.ai)
|
v
Cloudflare Worker <-- This is what we're building
(your MCP server)
| \
v v
WordPress Cloudflare R2
REST API (image storage)
Cloudflare Worker係一個運行喺雲端嘅無伺服器函數,一直在線,唔使管理伺服器,而且有相當慷慨嘅免費額度。佢扮演緊橋樑角色:Claude將工具請求發畀佢,佢再將呢啲請求轉換成WordPress API調用或儲存操作。
點解specifically係Cloudflare Workers? Claude AI嘅MCP整合要求你嘅伺服器能夠透過HTTPS、用一個真實域名訪問到。Worker可以即刻滿足呢一點——唔使搭建伺服器,唔使SSL證書,唔使部署流水線。佢即刻就用得。
前置條件
- Node.js v18+,本地使用嘅JavaScript執行環境。用
node --version檢查 - npm,隨Node.js一齊安裝,用嚟安裝依賴包
- 一個啟用咗REST API嘅WordPress網站(現代WordPress安裝預設就係啟用嘅)
- 一個Cloudflare帳戶,免費套餐就夠用。去cloudflare.com註冊
- 對TypeScript有基本嘅了解,唔使係專家,但知道「型別」係咩會有幫助
第一步:安裝Wrangler並登入
Wrangler係Cloudflare用嚟管理Worker嘅命令列工具。可以將佢想像成Cloudflare嘅git——你用佢由終端機建立、測試同部署Worker。
全域安裝佢,咁樣你就可以喺任何地方運行wrangler命令:
npm install -g wrangler
然後用你嘅Cloudflare帳戶進行身份驗證:
wrangler login
呢個會打開一個瀏覽器視窗,等你授權Wrangler訪問你嘅Cloudflare帳戶。喺運行任何部署命令之前,你必須先完成呢一步,否則Wrangler根本唔知道應該部署去邊個帳戶。
第二步:建立項目
為你嘅項目建立一個新資料夾,並將佢初始化為Node.js項目:
mkdir wordpress-mcp && cd wordpress-mcp
npm init -y
npm init -y會建立一個package.json檔案,佢只係一份記錄項目名稱、版本同依賴關係嘅清單檔案。-y參數會跳過互動式提示,直接接受所有預設值。
而家安裝所需嘅依賴包。呢度版本好關鍵:
npm install @modelcontextprotocol/sdk agents@latest ai zod @cloudflare/workers-oauth-provider
npm install -D @cloudflare/workers-types typescript wrangler
每個包嘅作用:
@modelcontextprotocol/sdk,官方嘅MCP SDK。佢畀咗我哋處理MCP協定嘅McpServer類別,咁樣我哋就唔使自己實作協定。agents@latest,Cloudflare嘅智能體框架。佢提供咗McpAgent,負責處理我哋Worker同MCP協定之間透過HTTP進行嘅連接。務必使用@latest,npm預設嘅版本(0.0.74)存在路由問題,會導致已認證嘅請求出現悄無聲息嘅404。你需要0.7.5或以上版本。ai,Vercel AI SDK。最新嘅agents包將佢作為peer dependency要求安裝。你唔會直接用到佢,但冇佢安裝會失敗。zod,一個schema驗證庫。我哋用佢嚟定義同驗證MCP工具所接受嘅輸入(例如「呢個工具需要一個叫post_id嘅數字」)。佢仲會自動生成Claude理解每個工具所需嘅型別定義。@cloudflare/workers-oauth-provider,處理OAuth 2.1身份驗證。Claude AI要求用完整嘅OAuth,而唔係簡單嘅API密鑰,嚟連接遠端MCP伺服器。呢個庫會幫你處理大部分繁重嘅工作。@cloudflare/workers-types(dev),Cloudflare特有API(例如KV、R2、Durable Objects)嘅TypeScript型別定義。冇佢,TypeScript唔知道R2Bucket或KVNamespace係咩。typescript(dev),TypeScript編譯器。本地需要佢嚟做型別檢查。wrangler(dev),同時作為項目依賴安裝,咁樣版本就固定咗。第一步嘅全域安裝係用嚟運行命令嘅;呢份本地副本可以確保項目入面每個人都用同一個版本。
-D參數將呢啲包標記為「開發依賴」——佢哋只係喺開發階段需要,運行時唔需要。Wrangler會喺部署前將你嘅TypeScript打包,所以編譯後嘅產物唔需要呢啲包。
第三步:設定TypeScript
TypeScript需要一個設定檔嚟知道點樣編譯你嘅代碼。喺項目根目錄建立tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"]
}
值得了解嘅幾個設定:
"target": "ES2022",編譯為現代JavaScript。Cloudflare Workers運行喺V8上,原生支援ES2022。"moduleResolution": "bundler",話畀TypeScript知由Wrangler(打包工具)嚟處理引入解析,而唔係用Node.js自己嘅解析邏輯。"types": ["@cloudflare/workers-types"],自動喺每個檔案入面包含Cloudflare嘅型別定義,咁樣你就可以攞到Worker特有API嘅自動補全。"noEmit": true,TypeScript唔會輸出編譯後嘅檔案。真正嘅編譯由Wrangler處理;TypeScript只用於型別檢查。"strict": true,啟用嚴格型別檢查。編譯階段捕獲嘅錯誤越多,運行時嘅意外就越少。
第四步:設定Wrangler
wrangler.toml係Cloudflare Worker嘅設定檔,就好似一份藍圖,話畀Cloudflare知你嘅Worker係咩、需要邊啲資源、結構係點樣嘅。喺項目根目錄建立佢:
name = "my-wordpress-mcp"
main = "src/index.ts"
compatibility_date = "2025-04-01"
compatibility_flags = ["nodejs_compat"]
# Durable Objects - required for persistent MCP connections
[[durable_objects.bindings]]
name = "MCP_OBJECT"
class_name = "WordPressMcp"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["WordPressMcp"]
# KV namespace for OAuth token storage
[[kv_namespaces]]
binding = "OAUTH_KV"
id = "your-kv-namespace-id"
# R2 bucket for image storage
[[r2_buckets]]
binding = "R2"
bucket_name = "your-bucket-name"
[vars]
WP_URL = "https://your-wordpress-site.com"
等我哋拆解吓唔係咁直觀嘅部分:
Durable Objects係Cloudflare嘅一個基礎組件,等Worker可以保持狀態並維持長連接。MCP喺Claude同你嘅伺服器之間使用持久連接(SSE或WebSocket),而普通嘅Worker因為每個請求都係無狀態嘅,做唔到呢一點。Durable Objects透過畀我哋一個持有嗰個連接嘅單一持久化實例,解決咗呢個問題。
坑: 綁定名稱必須係MCP_OBJECT。agents包將呢個名字硬編碼為佢預設搵嘅綁定名。用其他名字會導致SSE/MCP連接悄無聲息咁報401錯誤失敗。
坑: 喺migrations入面要用new_sqlite_classes,唔係new_classes。較新嘅agents包要求使用基於SQLite嘅Durable Objects。呢度整錯嘅話,會攞到一個冇任何有用訊息嘅500錯誤。
KV Namespace係Cloudflare嘅鍵值儲存,一個用嚟存放細嚿數據嘅簡單數據庫。我哋用佢嚟儲存OAuth令牌(等Claude可以對我哋嘅伺服器進行身份驗證嘅憑證)。你需要先建立一個:
- 進入Cloudflare Dashboard、Workers & Pages、KV
- 撳「Create namespace」,隨便改個名(例如「mcp-oauth-kv」)
- 複製ID並貼上去
wrangler.toml入面
R2 Bucket係Cloudflare嘅物件儲存(類似AWS S3)。我哋會用佢嚟托管透過MCP伺服器上傳嘅圖片。喺Dashboard、R2度建立一個,將bucket名稱填入wrangler.toml。
compatibility_flags = ["nodejs_compat"]會喺Worker執行環境入面啟用Node.js嘅API。一啲依賴用到咗好似Buffer咁嘅Node.js內建功能,冇呢個標記,佢哋喺運行時會失敗。
第五步:設定WordPress身份驗證
我哋會使用Application Passwords連接WordPress,呢個係WordPress內建嘅功能,等你可以為API訪問建立獨立嘅密碼,而唔使暴露主帳戶密碼。
- 喺WordPress後台,進入Users, Your Profile
- 向下捲動到Application Passwords
- 輸入一個名字(例如「MCP Server」),撳Add New Application Password
- 複製生成嘅密碼,WordPress唔會再顯示第二次
而家喺項目根目錄建立一個叫.dev.vars嘅檔案。呢個檔案保存本地開發用嘅密鑰,Wrangler喺本地運行時會自動將佢哋注入為環境變數:
WP_USER=your_wordpress_username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
ACCESS_PASSWORD=mypassword123
咩係ACCESS_PASSWORD? 呢個係你自己建立嘅一個密碼,當Claude要求你授權MCP連接嗰陣,你就輸入呢個密碼。可以將佢想像成一條門匙:Claude敲你伺服器嘅OAuth頁面嘅門,而你透過輸入呢個密碼嚟證明自己係伺服器嘅擁有者。
重要提示: 將.dev.vars加入.gitignore,永遠唔好將密鑰提交去版本控制入面。
坑: .dev.vars只喺本地生效。生產環境部署時,你必須用以下命令分別設定每個密鑰:
wrangler secret put WP_USER
wrangler secret put WP_APP_PASSWORD
wrangler secret put ACCESS_PASSWORD
每條命令都會提示你輸入對應嘅值。咁樣密鑰就會被加密儲存喺Cloudflare入面,永遠唔會出現喺你嘅代碼或設定檔入面。
坑: 將ACCESS_PASSWORD設短啲(32個字符以內)。太長嘅密碼可能會喺瀏覽器嘅OAuth彈出視窗表格入面出問題。
第六步:構建WordPress API客戶端
建立一個src/資料夾,然後建立src/wp-client.ts。呢個係WordPress REST API嘅一個簡單封裝——與其周圍寫帶認證標頭嘅fetch調用,不如喺呢度寫一次,反覆重用:
import type { Env } from "./index";
export class WpClient {
private _baseUrl: string;
private authHeader: string;
constructor(env: Env) {
// Remove trailing slash to avoid double slashes in URLs
this._baseUrl = env.WP_URL.replace(/\/$/, "");
// WordPress uses HTTP Basic Auth for Application Passwords
// btoa() converts "username:password" to a Base64 string
this.authHeader =
"Basic " + btoa(`${env.WP_USER}:${env.WP_APP_PASSWORD}`);
}
get baseUrl() { return this._baseUrl; }
async request<T = unknown>(
path: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this._baseUrl}/wp-json${path}`;
const res = await fetch(url, {
...options,
headers: {
Authorization: this.authHeader,
"Content-Type": "application/json",
...((options.headers as Record<string, string>) || {}),
},
});
if (!res.ok) {
const body = await res.text();
throw new Error(`WP API ${res.status}: ${body}`);
}
return res.json() as Promise<T>;
}
get<T = unknown>(path: string) {
return this.request<T>(path);
}
post<T = unknown>(path: string, body: unknown) {
return this.request<T>(path, {
method: "POST",
body: JSON.stringify(body),
});
}
delete<T = unknown>(path: string) {
return this.request<T>(path, { method: "DELETE" });
}
}
WordPress嘅REST API位於/wp-json/下,所以對/wp/v2/posts嘅請求會變成https://your-site.com/wp-json/wp/v2/posts。WpClient會自動處理呢個前綴。
第七步:理解點解需要OAuth 2.1
呢個係最容易令人卡住嘅部分,值得花啲時間講清楚。
當你將一個遠端MCP伺服器加入去Claude AI度,Claude唔會盲目咁連接過去,佢需要驗證係你喺授權呢次連接。為此,佢使用OAuth 2.1——呢個同網絡上「用Google登入」按鈕背後用嘅係同一個標準。
流程係咁樣:
- 你將Worker嘅URL加入去Claude嘅設定入面
- Claude訪問你Worker嘅
/.well-known/oauth-authorization-server端點,嚟了解應該點樣進行身份驗證 - Claude將你重新導向去你Worker嘅
/authorize頁面(我哋自己搭建嘅一個表格) - 你輸入ACCESS_PASSWORD,證明你係伺服器嘅擁有者
- 你嘅Worker頒發一個授權碼,Claude用佢嚟兌換存取令牌
- Claude喺之後所有請求中都使用呢個令牌
@cloudflare/workers-oauth-provider庫會自動處理大部分呢啲流程。但有一個坑:Claude AI用https://claude.ai作為佢嘅OAuth客戶端ID。大部分OAuth伺服器會拒絕呢個,因為佢唔係一個註冊過嘅客戶端。呢個庫會嘗試由嗰個URL攞到元數據(以了解佢使用邊啲重新導向URI),但嗰個URL返回嘅係一個網頁而唔係元數據,所以會失敗。
解決方法係喺OAuth流程開始之前,直接喺KV入面預先將Claude AI註冊為一個已知客戶端。呢個喺第九步展示嘅/authorize路由處理程式入面完成。
第八步:建立你嘅MCP工具
每個「工具」都係Claude可以調用嘅函數。我哋定義佢接受咩輸入(用Zod schema)、佢做咩、返回咩。Claude會讀取呢啲定義,並根據你嘅提問判斷幾時使用佢哋。
SEO工具
建立src/tools/seo.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { WpClient } from "../wp-client";
export function registerSeoTools(server: McpServer, wp: WpClient) {
server.tool(
"get-seo-meta",
"Get SEO meta for a post (title, description, focus keyword)",
{ post_id: z.number().describe("WordPress post/page ID") },
async ({ post_id }) => {
const post = await wp.get<any>(
`/wp/v2/posts/${post_id}?_fields=id,title,slug,meta`
);
const meta = post.meta || {};
return {
content: [{
type: "text" as const,
text: JSON.stringify({
id: post.id,
title: post.title?.rendered,
seo_title: meta.rank_math_title || "",
seo_description: meta.rank_math_description || "",
focus_keyword: meta.rank_math_focus_keyword || "",
robots: meta.rank_math_robots || [],
}, null, 2),
}],
};
}
);
server.tool(
"update-seo-meta",
"Update only SEO fields for a post. Never touches post content.",
{
post_id: z.number(),
seo_title: z.string().optional(),
seo_description: z.string().optional(),
focus_keyword: z.string().optional(),
},
async ({ post_id, seo_title, seo_description, focus_keyword }) => {
const meta: Record<string, unknown> = {};
if (seo_title) meta.rank_math_title = seo_title;
if (seo_description) meta.rank_math_description = seo_description;
if (focus_keyword) meta.rank_math_focus_keyword = focus_keyword;
await wp.post(`/wp/v2/posts/${post_id}`, { meta });
return {
content: [{
type: "text" as const,
text: `SEO meta updated for post ${post_id}`,
}],
};
}
);
}
留意每個工具都有三個部分:一個name(Claude用嚟識別佢)、一個description(Claude靠佢嚟判斷幾時使用呢個工具,盡量寫得清楚),以及一個Zod schema(佢需要咩輸入)。description好重要:Claude會讀佢嚟理解呢個工具係做咩嘅,所以要寫得有描述性。
內容工具
建立src/tools/content.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { WpClient } from "../wp-client";
export function registerContentTools(server: McpServer, wp: WpClient) {
server.tool(
"list-posts",
"List WordPress posts. Use this when asked to find or search for posts.",
{
per_page: z.number().default(10).describe("How many posts to return"),
search: z.string().optional().describe("Search term to filter posts"),
},
async ({ per_page, search }) => {
const params = new URLSearchParams({
per_page: String(per_page),
_fields: "id,title,slug,status,date,link",
});
if (search) params.set("search", search);
const posts = await wp.get(`/wp/v2/posts?${params}`);
return {
content: [{
type: "text" as const,
text: JSON.stringify(posts, null, 2),
}],
};
}
);
server.tool(
"create-post",
"Create a new WordPress post or page",
{
title: z.string().describe("Post title"),
content: z.string().describe("Post content in HTML"),
status: z.enum(["publish", "draft"]).default("draft")
.describe("publish to make it live, draft to save without publishing"),
},
async ({ title, content, status }) => {
const post = await wp.post("/wp/v2/posts", { title, content, status });
return {
content: [{
type: "text" as const,
text: JSON.stringify(post, null, 2),
}],
};
}
);
}
基於R2嘅圖片工具
建立src/tools/images.ts。之所以用R2而唔係WordPress嘅媒體庫,係因為R2速度更快(透過Cloudflare嘅CDN提供服務),而且唔會佔用你伺服器嘅儲存空間:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerImageTools(server: McpServer, r2: R2Bucket) {
server.tool(
"upload-image",
"Download an image from a URL and store it in R2 storage",
{
key: z.string().describe("Storage path, e.g. '2025/03/photo.jpg'"),
url: z.string().describe("Public URL to download the image from"),
},
async ({ key, url }) => {
// Fetch the image from the given URL
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch image: ${res.status}`);
const data = await res.arrayBuffer();
const contentType = res.headers.get("content-type") || "image/jpeg";
// Store it in R2 with the correct content type
await r2.put(key, data, {
httpMetadata: { contentType },
});
return {
content: [{
type: "text" as const,
text: JSON.stringify({
key,
size: data.byteLength,
content_type: contentType,
status: "uploaded",
}, null, 2),
}],
};
}
);
server.tool(
"list-images",
"List images stored in R2",
{
prefix: z.string().optional().describe("Filter by path prefix, e.g. '2025/'"),
limit: z.number().default(50),
},
async ({ prefix, limit }) => {
const listed = await r2.list({ prefix: prefix || undefined, limit });
const objects = listed.objects.map((o) => ({
key: o.key,
size: o.size,
uploaded: o.uploaded.toISOString(),
}));
return {
content: [{
type: "text" as const,
text: JSON.stringify(objects, null, 2),
}],
};
}
);
}
第九步:將所有嘢連埋一齊
建立src/index.ts,呢個係Worker嘅入口點。佢處理接收到嘅HTTP請求,並將佢哋路由去正確嘅地方:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import { WpClient } from "./wp-client";
import { registerSeoTools } from "./tools/seo";
import { registerContentTools } from "./tools/content";
import { registerImageTools } from "./tools/images";
// This interface defines what environment variables/bindings
// are available to our Worker. TypeScript uses this to give
// you autocomplete and catch typos.
export interface Env {
MCP_OBJECT: DurableObjectNamespace;
R2: R2Bucket;
OAUTH_KV: KVNamespace;
WP_URL: string;
WP_USER: string;
WP_APP_PASSWORD: string;
ACCESS_PASSWORD: string;
}
// WordPressMcp is our Durable Object class. It extends McpAgent,
// which handles the persistent MCP connection. We register our
// tools in init() - this runs once when the connection is established.
export class WordPressMcp extends McpAgent<Env> {
server = new McpServer({
name: "wordpress-mcp",
version: "1.0.0",
});
async init() {
const wp = new WpClient(this.env);
registerSeoTools(this.server, wp);
registerContentTools(this.server, wp);
registerImageTools(this.server, this.env.R2);
}
}
// This is the main Worker export. All HTTP requests come here first.
export default new OAuthProvider({
// The URL prefix where our MCP server lives
apiRoute: "/mcp",
// apiHandler handles authenticated MCP requests
// (after OAuth is complete, Claude's requests come here)
apiHandler: {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return WordPressMcp.serve("/mcp").fetch(request, env, ctx);
},
},
// defaultHandler handles everything else:
// the OAuth flow, the authorize page, etc.
defaultHandler: {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// The /authorize route shows a login form to the user.
// After they enter ACCESS_PASSWORD, we issue an auth code.
if (url.pathname === "/authorize") {
// Pre-register Claude AI as a known OAuth client.
// Claude sends https://claude.ai as its client_id, which
// the library doesn't recognize by default. We write it
// directly to KV so the library trusts it.
const claudeClientId = "https://claude.ai";
const existing = await env.OAUTH_KV.get(`client:${claudeClientId}`);
if (!existing) {
await env.OAUTH_KV.put(
`client:${claudeClientId}`,
JSON.stringify({
clientId: claudeClientId,
redirectUris: ["https://claude.ai/api/mcp/auth/callback"],
grantTypes: ["authorization_code", "refresh_token"],
responseTypes: ["code"],
tokenEndpointAuthMethod: "none",
registrationDate: Math.floor(Date.now() / 1000),
})
);
}
if (request.method === "POST") {
const form = await request.formData();
const password = form.get("password");
if (password !== env.ACCESS_PASSWORD) {
return new Response(renderForm("Wrong password. Try again."), {
status: 401,
headers: { "Content-Type": "text/html" },
});
}
// Password correct - return a redirect with the auth code
// The OAuthProvider intercepts this and issues a real code
return new Response(null, {
status: 302,
headers: { Location: url.href + "?approved=true" },
});
}
return new Response(renderForm(), {
headers: { "Content-Type": "text/html" },
});
}
return new Response("WordPress MCP Server", { status: 200 });
},
},
// Where to store OAuth tokens
storage: {
get kv() { return (globalThis as any).__oauth_kv; }
},
});
function renderForm(error?: string) {
return `<!DOCTYPE html>
<html>
<head><title>Authorize MCP Access</title></head>
<body style="font-family:sans-serif;max-width:400px;margin:100px auto;padding:20px">
<h2>Authorize WordPress MCP</h2>
<p>Enter your access password to connect Claude AI to this server.</p>
${error ? `<p style="color:red">${error}</p>` : ""}
<form method="POST">
<input type="password" name="password" placeholder="Access password"
style="width:100%;padding:8px;margin-bottom:12px;font-size:16px" autofocus />
<button type="submit"
style="width:100%;padding:10px;background:#1a1a1a;color:white;border:none;font-size:16px;cursor:pointer">
Authorize
</button>
</form>
</body>
</html>`;
}
坑: 你畀Claude AI嘅URL係https://your-worker.workers.dev/mcp,而唔係根URL。Claude會直接向你提供嘅URL發送POST請求,所以/mcp呢個路徑必須要有。
第十步:部署去Cloudflare
喺部署之前,你需要一個具有正確權限嘅Cloudflare API令牌。wrangler login生成嘅預設令牌冇KV寫入權限,當佢試圖設定KV命名空間嗰陣,部署會失敗。
建立一個自訂令牌:
- 進入Cloudflare Dashboard、My Profile、API Tokens
- 撳「Create Token」、「Create Custom Token」
- 加入以下權限:
- Account, Workers Scripts, Edit
- Account, Workers R2 Storage, Edit
- Account, Workers KV Storage, Edit
- 建立令牌並複製佢
而家開始部署:
export CLOUDFLARE_API_TOKEN=your_token_here
npx wrangler deploy
部署完成後,設定你嘅生產環境密鑰(呢啲同.dev.vars入面嘅值一樣):
wrangler secret put WP_USER
wrangler secret put WP_APP_PASSWORD
wrangler secret put ACCESS_PASSWORD
每條命令都會提示你貼上對應嘅值。呢啲值會被加密儲存喺Cloudflare入面,並喺運行時作為環境變數注入,永遠唔會出現喺你嘅代碼或日誌入面。
第十一步:連接去Claude AI
而家到咗有趣嘅部分:
- 進入Claude.ai、Settings、Integrations
- 撳Add MCP Server
- 輸入:
https://my-wordpress-mcp.yourname.workers.dev/mcp - Claude會喺彈出視窗中打開你Worker嘅
/authorize頁面 - 輸入你嘅ACCESS_PASSWORD
- 而家Claude已經連接好嘞
試吓:
- 「列出我最近5篇WordPress文章」
- 「將文章42嘅SEO描述更新為……」
- 「建立一篇標題為'Hello World'、內容係……嘅草稿文章」
- 「將呢個圖片URL上傳去R2,存為2025/03/cover.jpg」
項目結構
wordpress-mcp/
src/
index.ts # Worker entry point, OAuth setup, routing
wp-client.ts # WordPress REST API wrapper
tools/
seo.ts # get-seo-meta, update-seo-meta tools
content.ts # list-posts, create-post tools
images.ts # upload-image, list-images tools
.dev.vars # Local secrets (never commit this)
.gitignore
package.json
tsconfig.json
wrangler.toml # Worker config: bindings, KV, R2, DO
故障排除:所有值得知道嘅坑
將常見問題整理成一份清單:
- Wrangler未認證,每次部署前一定要先運行
wrangler login。冇呢一步,部署命令會即刻失敗。 .dev.vars喺生產環境唔生效,嗰個檔案只係用於本地開發。生產環境入面每個變數都要用wrangler secret put嚟設定。- Durable Objects語法寫錯,migrations入面要用
[[durable_objects.bindings]]配合new_sqlite_classes。唔係inline陣列,亦唔係new_classes。 - 綁定名稱必須係MCP_OBJECT,
agents包將呢個名字硬編碼喺入面。用第二個名字會導致悄無聲息嘅401失敗。 - Claude AI要求完整嘅OAuth 2.1,唔係Bearer令牌,亦唔係API密鑰,而係帶有discovery、authorize、token端點嘅完整OAuth。
- Claude嘅client_id係一個URL,佢將
https://claude.ai作為client_id發送,並跳過動態註冊。需要手動喺KV入面預先註冊佢。 - 部署前需要KV命名空間,先喺控制台入面建立佢,再將ID加入去
wrangler.toml入面。 - API令牌需要KV權限,預設嘅
wrangler login令牌冇Workers KV Storage Edit權限。需要建立一個自訂令牌。 agents包嘅版本,npm預設係0.0.74。你需要0.7.5以上。同時亦要將ai作為peer dependency安裝,否則構建會失敗。- MCP協定版本,新版SDK只接受協定版本
2025-03-26。舊版本會導致一個容易誤導人嘅「Mcp-Session-Id header is required」錯誤。 - Claude AI入面嘅URL填錯,要用
/mcp路徑,唔係根URL。 - ACCESS_PASSWORD太長會整壞表格,保持喺32個字符以內。
接下嚟可以構建咩
以上涵蓋咗基礎部分。一旦佢跑起身,你可以喺呢個基礎上擴展:
- 重新導向管理,由Rank Math讀取404日誌,自動建立重新導向
- 批量圖片遷移,將
wp-content/uploads入面所有圖片遷移去R2 - WooCommerce工具,透過Claude管理商品、訂單同庫存
- 網站健康監控,檢查插件更新、快取狀態同錯誤日誌
MCP協定非常靈活,任何REST API都可以變成一套AI驅動嘅工具集。真正困難嘅唔係編寫工具本身,而係同OAuth設定同依賴包版本較勁,等成個連接跑起身。