本教程将带你构建一个Model Context Protocol (MCP) 服务器,让Claude AI能够管理WordPress网站——创建文章、更新SEO、上传图片,只需要跟它说话,而且可以在任何设备上完成。
这里不仅讲要运行什么,还会讲为什么要这样做,因为官方文档往往是把工具和命令一股脑扔给你,却没有任何上下文,结果很容易花上好几个小时去调试那些本应显而易见的问题。
我们到底在构建什么?
在写任何代码之前,先理解一下什么是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,一个模式校验库。我们用它来定义和校验MCP工具所接受的输入(比如"这个工具需要一个叫post_id的数字")。它还会自动生成Claude理解每个工具所需的类型定义。@cloudflare/workers-oauth-provider,处理OAuth 2.1身份验证。Claude AI要求用完整的OAuth,而不是简单的API密钥,来连接远程MCP服务器。这个库会帮你处理大部分繁重的工作。@cloudflare/workers-types(开发依赖),Cloudflare特有API(比如KV、R2、Durable Objects)的TypeScript类型定义。没有它,TypeScript不知道R2Bucket或KVNamespace是什么。typescript(开发依赖),TypeScript编译器。本地需要它来做类型检查。wrangler(开发依赖),同时作为项目依赖安装,这样版本就固定了。第一步的全局安装是用来运行命令的;这份本地副本能确保项目里的每个人使用同一个版本。
-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模式)、它做什么、返回什么。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模式(它需要什么输入)。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。不是内联数组,也不是new_classes。 - 绑定名称必须是MCP_OBJECT,
agents包把这个名字硬编码在里面。用别的名字会导致悄无声息的401失败。 - Claude AI要求完整的OAuth 2.1,不是Bearer令牌,也不是API密钥,而是带有发现、授权、令牌端点的完整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作为对等依赖安装,否则构建会失败。- 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设置以及依赖包版本较劲,让整个连接跑起来。