Managing a WordPress site through a chat window means copy-pasting JSON back and forth into wp-admin all day. An mcp server wordpress setup ends that: Claude reads and manages your site (posts, users, WooCommerce orders) directly through an MCP server that wraps the WordPress REST API, authenticated with an Application Password rather than your login. Four things keep it safe: start read-only, scope a dedicated user to the least privilege the task needs, keep credentials in environment variables, and never expose write or delete to the model on a production site without a confirmation step. Get those four right and the rest is wiring.
If you have not built an MCP server before, read what an MCP server actually is first, and how to connect Claude to your tools over MCP for the client-side picture. This post is the WordPress-specific build.
Why wrap the REST API instead of the database?
WordPress ships a full REST API at /wp-json/wp/v2/. It already enforces capabilities, validates input, fires the same hooks the admin UI does, and returns clean JSON. Talking to it means you never touch SQL, you inherit WordPress's own permission model, and a scoped user simply cannot do things its role does not allow. WooCommerce adds its own namespace at /wp-json/wc/v3/ for orders, products, and customers. So the MCP server's job is small: take a tool call from Claude, make an authenticated HTTP request to the right endpoint, and hand the JSON back.
Quick recap of MCP itself, since it is the load-bearing piece. MCP (Model Context Protocol) is an open standard from Anthropic for connecting AI assistants to external tools and data. An MCP server exposes three things: tools (actions the model can call), resources (data it can read), and prompts (reusable templates). MCP clients like Claude Desktop and Claude Code launch your server over stdio (local subprocess) or talk to it over Streamable HTTP (remote). Write the server once and every MCP-compatible client can use it.
How do I set up the WordPress side?
First, create a dedicated user. Do not point this at your own admin account. In wp-admin go to Users, Add New, and create something like claude-bot. For a read-only start, give it the Author or even a custom Subscriber-plus role; only widen it when a task genuinely needs writes. The role is your real security boundary, so the REST API will reject anything above that user's capabilities no matter what the model asks for.
Second, generate an Application Password for that user. Application Passwords are built into WordPress core (5.6+). Edit the user, scroll to the Application Passwords section near the bottom, type a name like mcp-server, and click Add. WordPress shows a 24-character password in xxxx xxxx xxxx xxxx xxxx xxxx format exactly once. Copy it now. This authenticates over HTTP Basic auth and, critically, is not the user's login password and can be revoked on its own without locking anyone out. If the section is missing, your site is not on HTTPS or a plugin disabled it; Application Passwords require a secure connection.
Here is the endpoint shape the server will hit. Test it with curl before writing a line of server code so you know auth works in isolation:
# List the 5 most recent posts as the scoped user.
# user:app_password goes in Basic auth; note the spaces in the
# application password are kept exactly as WordPress displayed them.
curl -s \
-u "claude-bot:abcd EFGH ijkl MNOP qrst UVWX" \
"https://example.com/wp-json/wp/v2/posts?per_page=5&_fields=id,title,status"
# WooCommerce orders live under a different namespace:
curl -s \
-u "claude-bot:abcd EFGH ijkl MNOP qrst UVWX" \
"https://example.com/wp-json/wc/v3/orders?per_page=5&status=processing"If those return JSON, you are done with WordPress. If you get a 401, the Application Password or username is wrong; a 403 means the user's role lacks the capability for that endpoint, which is the permission model doing its job.
What does the MCP server look like?
Use the official MCP SDK (TypeScript or Python). Below is a TypeScript server over stdio that exposes one read-only tool, list_posts. It reads the site URL and credentials from environment variables, never from the source. Each tool you register becomes callable by the model; the SDK handles the protocol handshake, so you write the WordPress call and the input schema and nothing else.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const BASE = process.env.WP_BASE_URL; // https://example.com
const USER = process.env.WP_USER; // claude-bot
const APP_PASS = process.env.WP_APP_PASSWORD; // the Application Password
if (!BASE || !USER || !APP_PASS) {
throw new Error("WP_BASE_URL, WP_USER, WP_APP_PASSWORD must all be set");
}
const auth = "Basic " + Buffer.from(`${USER}:${APP_PASS}`).toString("base64");
async function wp(path: string) {
const res = await fetch(`${BASE}/wp-json${path}`, {
headers: { Authorization: auth },
});
if (!res.ok) throw new Error(`WP ${res.status}: ${await res.text()}`);
return res.json();
}
const server = new McpServer({ name: "wordpress", version: "1.0.0" });
server.registerTool(
"list_posts",
{
description:
"List recent WordPress posts. Read-only. Use when the user asks " +
"what has been published or wants to find a post by status.",
inputSchema: {
status: z.enum(["publish", "draft", "pending"]).default("publish"),
perPage: z.number().int().min(1).max(50).default(10),
},
},
async ({ status, perPage }) => {
const posts = await wp(
`/wp/v2/posts?status=${status}&per_page=${perPage}` +
`&_fields=id,title,status,link`,
);
return { content: [{ type: "text", text: JSON.stringify(posts, null, 2) }] };
},
);
await server.connect(new StdioServerTransport());Notice the tool description tells the model when to call it, not just what it does. Recent Claude models reach for tools more conservatively, so a prescriptive when-to-use line measurably improves whether the right tool fires. The input schema constrains perPage to 1 through 50 so the model cannot ask for ten thousand rows.
How does the client register it?
On the client side, you point Claude at the server and pass the WordPress credentials through the environment. In Claude Code, add the server to your MCP config; the env block is read by your process and never written into the prompt or the server source. The config below launches the compiled server as a local stdio subprocess.
{
"mcpServers": {
"wordpress": {
"command": "node",
"args": ["/srv/mcp/wordpress/dist/server.js"],
"env": {
"WP_BASE_URL": "https://example.com",
"WP_USER": "claude-bot",
"WP_APP_PASSWORD": "${WP_APP_PASSWORD}"
}
}
}
}Keep the real Application Password in a shell environment variable or a secrets manager and let the config interpolate it with ${WP_APP_PASSWORD}; do not paste the literal string into a file you might commit. Once the client connects, it lists your server's tools and exposes them to the model. Ask Claude what is in the drafts queue and it calls list_posts with status set to draft, gets the JSON, and answers in plain language. That is the whole loop.
Where are the security guardrails?
This is the part people skip and regret. The guardrails are not optional on a site real users see:
- Start read-only. Ship only GET-backed tools (list_posts, get_order) first. Live with it for a while before adding anything that writes.
- Scope the user role to the task. A content-summarizing bot needs read access to posts, nothing else. Do not hand it WooCommerce order access it never uses; an over-broad role is blast radius waiting to happen.
- Keep credentials in the environment. The Application Password lives in env vars or a secrets manager, never in the server source, never in the system prompt, never in a message. Anything in the prompt or conversation is logged and retrievable.
- Never expose write or delete on production without confirmation. If you add an update_post or a delete tool, gate it: in Claude Desktop and Claude Code, tool calls can require human approval before they run. Use that for anything that mutates data.
- Rotate and revoke. Application Passwords are individually revocable from the user profile. If a laptop is lost or the server is compromised, revoke that one password; the user's real login and every other integration keep working.
The mistake I see most often is wiring an admin account straight into an MCP server with full write access and calling it a prototype. A prompt-injected web page the model summarizes can now delete your posts. Scope the user, start read-only, and gate every mutation. The cost is ten minutes; the alternative is a very bad afternoon.
Once read-only is solid and you trust the setup, widening it is incremental: add a single write tool, scope the user role up just enough for that endpoint, gate the call behind confirmation, and test against a staging site first. The architecture does not change. You have one thin server wrapping /wp-json, authenticated as a least-privilege user with a revocable Application Password, exposed to any MCP client you point at it. That is a clean, auditable way to let Claude manage a WordPress site, and it is the same pattern whether the site has ten posts or a WooCommerce catalog with thousands of orders. Build the read path, prove it, then earn each write.

