このチュートリアルでは、Claude AIがWordPressサイトを管理できるようにするModel Context Protocol (MCP) サーバーの構築方法を解説する。投稿の作成、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がそこにツールリクエストをUFCし、それをWordPress APIコールやストレージ操作に変換する。
なぜ特に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への基本的な慣れ。エキスパートである必要はないが、型が何かを知っていると役立つ
ステップ1: Wranglerをインストールしてログインする
WranglerはCloudflareがWorkerを管理するためのコマンドラインツールだ。Cloudflareのgitのようなものだと考えるといい。ターミナルからWorkerを作成、テスト、デプロイするために使う。
どこからでもwranglerコマンドを実行できるように、グローバルにインストールする:
npm install -g wrangler
次に、あなたのCloudflareアカウントで認証する:
wrangler login
これによりブラウザウィンドウが開き、WranglerがあなたのCloudflareアカウントにアクセスすることを承認する。デプロイコマンドを実行する前に必ずこれを行う必要がある。これがないと、Wranglerはどのアカウントにデプロイすればいいのか分からない。
ステップ2: プロジェクトを作成する
プロジェクト用の新しいフォルダを作り、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のエージェントフレームワークOK。これはMcpAgentを提供し、WorkerとMCPプロトコルの間の接続をHTTP経由で処理する。必ず@latestを使うこと。npmのデフォルトバージョン(0.0.74)にはルーティングの不具合があり、認証済みリクエストで静かに404が発生する。0.7.5以上が必要だ。ai、Vercel AI SDK。最新のagentsパッケージがピア依存関係として要求する。直接使うことはないが、これがないとインストールが失敗する。zod、スキーマ検証ライブラリ。MCPツールが受け付ける入力を定義・検証するために使う(例:「このツールにはpost_idという数値が必要」)。また、Claudeが各ツールの期待する内容を理解するために必要な型定義を自動生成してくれる。@cloudflare/workers-oauth-provider、OAuth 2.1認証を処理する。Claude AIはリモートMCPサーバーに接続するために単純なAPIキーではなく完全なOAuthを要求する。このライブラリが重労働のほとんどを処理してくれる。@cloudflare/workers-types(dev)、KV、R2、Durable ObjectsのようなCloudflare固有のAPIのTypeScript型定義。これがないと、TypeScriptはR2BucketやKVNamespaceが何なのか分からない。typescript(dev)、TypeScriptコンパイラ。コードの型チェックのためにローカルで必要。wrangler(dev)、プロジェクトの依存関係としてもインストールされ、バージョンが固定される。ステップ1でのグローバルインストールはコマンド実行用で、このローカルコピーによりプロジェクトの全員が同じバージョンを使うことが保証される。
-Dフラグはパッケージを「devの依存関係」としてマークする。開発中のみ必要で、ランタイムでは不要なものだ。Wranglerはデプロイ前にTypeScriptをバンドルするので、コンパイル済みの出力にはこれらのパッケージは不要だ。
ステップ3: 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"、Wrangler(バンドラー)がインポートの解決を処理することをTypeScriptに伝える。Node.js独自の解決ロジックではない。"types": ["@cloudflare/workers-types"]、Cloudflareの型定義をすべてのファイルに自動的に含める。これによりWorker固有のAPIのオートコンプリートが得られる。"noEmit": true、TypeScriptはコンパイル済みファイルを出力しない。実際のコンパイルはWranglerが処理し、TypeScriptは型チェックのみに使われる。"strict": true、厳格な型チェックを有効にする。コンパイル時に捕捉されるエラーが増えれば、ランタイムでの驚きは減る。
ステップ4: Wranglerを設定する
wrangler.tomlはCloudflare Worker用の設定ファイルで、Workerが何で、どんなリソースが必要で、どう構成されているかをCloudflareに伝える設計図のようなものだ。プロジェクトルートに作成する:
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は、Workerが状態を保持し、接続を開いたままにしておけるようにするCloudflareのプリミティブだ。MCPはClaudeとサーバーの間で永続的な接続(SSEまたはWebSocket)を使うが、各リクエストがステートレスな通常のWorkerではそれができない。Durable Objectsは、接続を保持する単一の永続的なインスタンスを与えることでこれを解決する。
落とし穴: バインディング名は必ずMCP_OBJECTでなければならない。agentsパッケージは、これをデフォルトで探すバインディング名としてハードコードしている。他の名前を使うと、SSE/MCP接続は401エラーとともに静かに失敗する。
落とし穴: マイグレーションではnew_classesではなくnew_sqlite_classesを使うこと。新しいagentsパッケージはSQLiteバックのDurable Objectsを要求する。ここを間違えると、有用なメッセージのない500エラーが発生する。
KV NamespaceはCloudflareのキーバリューストア、小さなデータ片のためのシンプルなデータベースだ。OAuthトークン(ClaudeがサーバーにAuthenticationするための認証情報)を保存するために使う。まず1つ作成する必要がある:
- Cloudflare Dashboard、Workers & Pages、KVに移動
- 「Create namespace」をクリックし、任意の名前を付ける(例:「mcp-oauth-kv」)
- IDをコピーして
wrangler.tomlに貼り付ける
R2 BucketはCloudflareのオブジェクトストレージ(AWS S3のようなもの)だ。MCPサーバー経由でアップロードされた画像をホストするために使う。Dashboard、R2で1つ作成し、バケット名をwrangler.tomlに入れる。
compatibility_flags = ["nodejs_compat"]は、Workerランタイム内でNode.js APIを有効にする。一部の依存関係はBufferのようなNode.jsの組み込み機能を使っているため、このフラグがないとランタイムで失敗する。
ステップ5: WordPress認証をセットアップする
アプリケーションパスワードを使って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ポップアップフォームで問題を引き起こすことがある。
ステップ6: 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はこのプレフィックスを自動的に処理する。
ステップ7: なぜOAuth 2.1が必要なのかを理解する
これは人々が最もつまずきやすい部分なので、少し時間をかける価値がある。
リモートMCPサーバーをClaude AIに追加すると、Claudeは盲目的に接続するわけではなく、あなたがこの接続を承認していることを検証する必要がある。そのためにClaudeはOAuth 2.1を使う。これはウェブ上の「Googleでログイン」ボタンを動かしているのと同じ標準だ。
流れはこうだ:
- Claudeの設定にあなたのWorkerのURLを追加する
- Claudeはあなたの Workerの
/.well-known/oauth-authorization-serverエンドポイントを訪れ、認証方法を発見する - Claudeはあなたを あなたのWorkerの
/authorizeページ(私たちが構築するフォーム)にリダイレクトする - あなたはACCESS_PASSWORDを入力する。これによりあなたがサーバーの所有者であることを証明する
- あなたのWorkerは認可コードを発行し、Claudeがそれをアクセストークンと交換する
- Claudeはそのトークンを今後のすべてのリクエストで使う
@cloudflare/workers-oauth-providerライブラリがこれらのほとんどを自動的に処理してくれる。しかし1つ注意点がある。Claude AIはOAuthクライアントIDとしてhttps://claude.aiを使う。ほとんどのOAuthサーバーは、これが登録済みのクライアントではないため拒否する。このライブラリはそのURLからメタデータを取得しようとする(どのリダイレクトURIを使うか知るため)が、そのURLはメタデータではなくウェブページを返すため失敗する。
修正方法は、OAuthフローが始まる前に、Claude AIを既知のクライアントとしてKVに直接事前登録することだ。これはステップ9で示す/authorizeルートハンドラー内で行われる。
ステップ8: 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}`,
}],
};
}
);
}
各ツールには3つの部分があることに注目してほしい。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を作成する。WordPressのメディアライブラリの代わりにR2を使うのは、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),
}],
};
}
);
}
ステップ9: すべてを繋ぎ合わせる
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はあなたがGiveした URLに直接POSTするので、/mcpパスが必ずそこになければならない。
ステップ10: 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内で暗号化されて保存され、ランタイムに環境変数として注入される。コードやログには決して現れない。
ステップ11: 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を2025/03/cover.jpgとしてR2にアップロードして」
プロジェクト構成
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の構文が間違っている、マイグレーションでは
[[durable_objects.bindings]]とnew_sqlite_classesを使うこと。インライン配列でもnew_classesでもない。 - バインディング名はMCP_OBJECTでなければならない、
agentsパッケージはこの名前をハードコードしている。他の名前を使うと静かに401エラーが発生する。 - Claude AIは完全なOAuth 2.1を要求する、Bearerトークンでも、APIキーでもない。discovery、authorize、tokenエンドポイントを備えた完全なOAuthだ。
- Claudeのclient_idはURLだ、
client_idとしてhttps://claude.aiを送信し、動的登録をスキップする。手動で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が間違っている、ルートURLではなく
/mcpパスを使うこと。 - 長いACCESS_PASSWORDはフォームを壊す、32文字未満に保つこと。
次に何を構築するか
これで基礎は完成した。動作するようになったら、以下のような拡張が可能だ:
- リダイレクト管理、Rank Mathから404ログを読み込み、自動的にリダイレクトを作成する
- 画像の一括移行、
wp-content/uploadsからすべての画像をR2に移動する - WooCommerceツール、Claudeを通じて商品、注文、在庫を管理する
- サイトヘルスモニタリング、プラグインのアップデート、キャッシュ状態、エラーログを確認する
MCPプロトコルは柔軟だ。あらゆるREST APIがAI駆動のツールセットになりうる。難しいのはツール自体を書くことではなく、OAuthのセットアップとパッケージのバージョンと格闘して接続を動作させることだ。