Migrating an existing WordPress API to MCP: a 4-week playbook

Migrating an existing WordPress API to MCP: a 4-week playbook

Last verified: October 24, 2026
18 min read
Guide
500+ WP projects
AI integration

#Migrating an existing WordPress API to MCP: a 4-week playbook

A clean greenfield MCP build is straightforward. Migrating from an existing, working WordPress REST API while it keeps serving production traffic is the harder shape. This playbook is what I use to move from “we have /wp-json/ and one partner consumer” to “we have /wp-json/, that partner, and an LLM-friendly MCP server in front” without breaking anything.

This article anchors to the MCP server development service pillar.

#TL;DR

  • Four weeks: audit, scaffold, parallel-run, cutover.
  • REST stays live for existing consumers; MCP is an addition, not a replacement.
  • Zod schemas come from the REST audit, not from a wishlist.
  • Parallel-run with one internal agent before any external traffic touches MCP.
  • Logpush captures every tool call; mismatches between MCP and REST responses surface as schema failures.

#How to audit WordPress REST API endpoints

The audit is a spreadsheet. One row per endpoint, the columns are:

ColumnWhat goes in it
Endpoint/wp-json/wp/v2/posts, /wp-json/wc/v3/products, etc.
HTTP verbsGET, POST, PUT, DELETE supported
Current consumersStorefront, partner ERP, webhook receivers
Traffic volumeRequests per day from access logs
Data sensitivityPublic, customer-only, admin-only
Mutates state?Yes/no
Maps to MCP tool?Proposed tool name and intent
NotesPlugins involved, custom field shapes, gotchas

For a typical WooCommerce store the spreadsheet has 20 to 60 rows. Most of them map cleanly to MCP tools; a handful (admin internals, plugin-specific endpoints, webhook receivers) stay REST-only.

The output of week 1 is two artefacts:

  1. The proposed tool inventory: catalogue.list, product.detail, order.intent, order.status, inventory.check, plus whatever is specific to your build.
  2. A Zod schema first draft for each tool’s input and output, derived from the actual REST responses you captured during the audit.

I capture REST responses with curl plus jq for shape inspection, or a Postman collection for the team to share. The point is empirical truth, not what the README says the response should look like. WordPress plugins are notorious for adding fields that documentation never gets around to mentioning; the audit catches them.

#How to build an MCP server on Cloudflare Workers

The week-2 deliverable is a working MCP server, deployed to a non-production Cloudflare Workers environment, with the tool inventory from week 1 implemented as thin adapters over the existing REST endpoints.

The server skeleton:

import { createMcpHandler, hostHeaderValidationResponse, McpServer } from "@modelcontextprotocol/server";
import { catalogueListInput, catalogueListOutput, handleCatalogueList } from "./tools/catalogue-list.js";
import { productDetailInput, productDetailOutput, handleProductDetail } from "./tools/product-detail.js";
// ... one import per tool

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // reject requests whose Host is not mine (DNS rebinding guard)
    const rejected = hostHeaderValidationResponse(request, [env.MCP_HOSTNAME]);
    if (rejected) return rejected;

    const handler = createMcpHandler(() => {
      const server = new McpServer({ name: "wppoland-mcp", version: "0.1.0" });

      server.registerTool(
        "catalogue.list",
        {
          description: "List WooCommerce products with filters",
          inputSchema: catalogueListInput,
          outputSchema: catalogueListOutput,
          annotations: { readOnlyHint: true },
        },
        (input) => handleCatalogueList(input, env),
      );

      server.registerTool(
        "product.detail",
        {
          description: "Return one product by SKU",
          inputSchema: productDetailInput,
          outputSchema: productDetailOutput,
          annotations: { readOnlyHint: true },
        },
        (input) => handleProductDetail(input, env),
      );

      // ... one registerTool() call per tool

      return server;
    });

    return handler.fetch(request);
  },
};

The skeleton follows version 2 of the official TypeScript SDK (@modelcontextprotocol/server), which implements the 2026-07-28 revision of the specification. createMcpHandler returns an object with a fetch method, the shape a Cloudflare Workers module expects as its default export, and the factory builds a fresh McpServer for every request (SDK guide to web-standard runtimes). The same guide warns that the handler performs no Host or Origin validation on its own, which is why the guard sits in front of it. Projects still on @modelcontextprotocol/sdk 1.x keep getting bug fixes and security updates for at least six months after the v2 release on 2026-07-27 (SDK README), so a migration that starts now should target v2.

Each handler is a thin adapter. It takes the validated input, builds a query string, fetches from /wp-json/, maps the response into the schema.org-aligned output shape, runs the output parse, returns. Business logic stays in WordPress; the handler is mechanical translation.

Week 2 also includes the auth scaffolding from the MCP authentication patterns guide. For the parallel-run in week 3 I use a single test token with all scopes; production scopes get tightened in week 4.

The wrangler.toml for the preview environment points at a staging WordPress install or a copy of production. The MCP server is reachable on a *.mcp-staging.wppoland.workers.dev URL accessible only to the team.

#How to test an MCP server against the REST API

Week 3 is where bugs surface. The pattern:

Build a comparison harness. A small TypeScript script that takes a tool name and an input, calls the MCP server, then calls the equivalent REST endpoint directly, and diffs the two responses. The diff is logged with the tool name, input, and a JSON-pointer-style path to each mismatch.

async function compareToolToRest(toolName: string, input: unknown) {
  const mcpResponse = await callMcp(toolName, input);
  const restResponse = await callEquivalentRest(toolName, input);
  const diff = jsonDiff(restResponse, mcpResponse);
  if (diff.length > 0) {
    await logMismatch({ toolName, input, diff });
  }
}

Run every tool against a representative input set. For catalogue.list: empty query, a query that returns hundreds of products, a query with category filter, a query with price range, a query that returns nothing. For product.detail: known SKU, unknown SKU, SKU with variations, SKU with custom fields. Cover the edge cases the audit surfaced.

Point one internal agent at the MCP server. Claude Desktop with the MCP server configured as a remote tool source is the configuration I use. A team member spends 30 minutes per day for the week interacting with their own store via the agent, capturing surprises in a shared doc.

Run the same golden prompts on three hosts. Luca Rossi’s October 2026 essay How to design a good MCP server, written with the Manufact team, points out that the same server can produce very different outcomes in ChatGPT, Claude and the long tail of other clients, so a short set of golden prompts on a few real hosts belongs in normal QA. Its checklist adds one owner for MCP health who re-checks the server after every major MCP, Claude or ChatGPT release. In this playbook that means the week 3 agent sessions run on Claude Desktop plus two other clients your consumers actually use, and the comparison harness runs again after every protocol revision as well as after WordPress upgrades.

Tighten schemas based on what comes back. Every Zod validation failure in the log is a schema bug or a mapping bug. Fix it; redeploy the preview Worker; rerun the harness.

The output of week 3 is a passing harness with zero diffs and a tool inventory that has survived 30 minutes of real agent interaction per day for five days. If either of those is missing, week 4 does not start.

#How to deploy an MCP server to production and monitor it

The cutover is anticlimactic if weeks 1 to 3 went well.

Deploy production Workers. wrangler deploy --env production. The production MCP server points at the production WordPress origin, with production-scoped tokens issued through the WordPress admin.

Issue tokens to the first real agent runtime. This is usually one of: an internal agent that staff use, a partner integration that has asked for an MCP surface, or a public-facing assistant on the storefront. Start with the smallest, lowest-risk consumer.

Wire Cloudflare Logpush to a long-term store. The log fields covered in the building an MCP server for WooCommerce article are the right defaults: tool name, input hash, latency, validation outcome, token scope. The store is whatever your team already uses (BigQuery, ClickHouse, S3 + Athena).

Set up the watch dashboard. Three queries on day one:

  1. Tool calls per minute, broken down by tool name. Detects burst load.
  2. Validation failure rate per tool, breakdown by code (input_invalid, output_invalid). Detects regression.
  3. Latency p50/p95/p99 per tool. Detects upstream WordPress slowness leaking through.

Keep REST live, untouched. The existing storefront, the existing partner integrations, the existing webhook receivers all keep using /wp-json/ exactly as before. MCP is an addition, not a replacement. This is the single most important rule of the migration.

#Common failures when migrating REST API to MCP

Six things I have watched go wrong on real migrations:

REST response field that the audit missed. A plugin adds meta_data: [...] to product responses. The MCP output schema does not include it. The Zod parse fails on real data. Fix: rerun the audit on production traffic, expand the schema or explicitly drop the field with a .transform().

Permalink mismatch between staging and production. The MCP product.detail tool returns the WooCommerce permalink field. Staging permalinks are https://staging.example.com/...; production permalinks are https://example.com/.... Test data passes; production fails the URL validator. Fix: configure the staging Worker with a permalink-rewrite step that mirrors production behaviour.

WooCommerce variation handling. The audit captured the simple-product response shape. Variation responses are different (the variant SKUs live under /wp-json/wc/v3/products/<id>/variations). Fix: handle variations as a separate fetch in product.detail, mirror to schema.org hasVariant.

Auth token leaks into logs. A handler logs the full Authorization header for debugging. The token ends up in the log store. Fix: redact the header in the log layer; rotate every token issued before the redaction was deployed.

Plugin update breaks a REST response. WooCommerce 9.x renames a field, the MCP handler still expects the old name, the Zod parse fails. Fix: pin WooCommerce versions in staging, run the comparison harness on every WordPress upgrade, treat the harness as part of the upgrade gate.

Agent loops on a malformed tool call. A buggy agent retries order.intent 100 times in a minute when the input fails validation. Without rate limiting the WordPress origin sees 100 fan-out calls. Fix: rate-limit per principal as documented in the MCP authentication patterns guide, and return a retry_after_seconds hint in the error envelope.

#How long a WordPress MCP migration takes

Four weeks is the default. Two adjustments:

Smaller surface, two weeks. Three tools, one consumer, no auth complexity. Compress audit and scaffold into week 1, parallel-run and cutover into week 2. The pattern is the same; the calendar is shorter.

Larger surface, six weeks. A dozen tools, multiple auth modes, sensitive mutating actions. Add a week between scaffold and parallel-run for security review. Add a week between parallel-run and cutover for a soft launch with a single OAuth-authorized user before the wider rollout.

The four phases stay in the same order regardless of the time budget.

#Does WordPress change after adding MCP

This is worth saying explicitly. After the migration:

  • The WordPress admin UI is unchanged.
  • The Block Editor is unchanged.
  • The user authentication system is unchanged.
  • The existing REST endpoints are unchanged and continue to serve their existing consumers.
  • The webhook firing logic is unchanged.
  • The data layer (wp_posts, wp_postmeta, WooCommerce tables) is unchanged.

What is added: a Cloudflare Workers MCP server, a token-issuance UI on the WordPress admin side (a small plugin or theme function), a Cloudflare Logpush configuration. Everything else is unchanged.

This is what makes the migration low-risk. If the MCP server has a bad day, you turn off the Worker. The agent surface goes down. The storefront keeps working, the partner integrations keep working, orders keep coming in.

#How to map REST verbs to MCP tool annotations

The audit spreadsheet already has a “modifies state” column. In MCP that column turns into tool annotations. The schema of the 2026-07-28 specification defines four boolean hints in ToolAnnotations: readOnlyHint (default false), destructiveHint (default true), idempotentHint (default false) and openWorldHint (default true). The destructive and idempotent hints only mean something when readOnlyHint is false (MCP schema).

A mapping that follows straight from the audit columns:

REST verb in the auditAnnotations on the MCP tool
GET on /wp/v2/posts or /wc/v3/productsreadOnlyHint: true
POST that creates a record (order intent, draft post)readOnlyHint: false, destructiveHint: false
PUT or PATCH on a known IDreadOnlyHint: false, idempotentHint: true
DELETEreadOnlyHint: false, destructiveHint: true

The defaults are the reason to set them. A tool registered without annotations counts as possibly destructive and not idempotent, so a read-only catalogue tool without readOnlyHint looks riskier to the client than it is. The SDK guide puts the practical effect plainly: a host can auto-approve a read-only tool and ask for confirmation before a destructive one (SDK tools guide). The specification also says clients must treat annotations as untrusted unless they come from a trusted server, so annotations never replace the capability check on the WordPress side.

Naming comes from the same specification page: tool names should be 1 to 128 characters, case-sensitive, and use only ASCII letters, digits, underscore, hyphen and dot (MCP tools). catalogue.list and product.detail fit that rule; a name copied from a REST route with slashes in it does not.

#How to return WordPress REST errors to an agent

A WordPress REST error arrives as JSON with a code, a message and a data.status. The MCP specification separates two kinds of failure. Protocol errors (unknown tool, malformed request, server error) go back as standard JSON-RPC errors. Tool execution errors (API failures, input validation errors, business logic errors) go back inside the tool result with isError: true, and clients should pass them to the model so it can correct the call and retry (MCP tools, error handling).

For a REST-backed handler, a 404 for an unknown product ID or a 400 for an invalid parameter is a tool execution error. Return isError: true with a text block that tells the model what to change, for example that no product has that SKU and that catalogue.list returns valid ones. Only a failure of the MCP layer itself becomes a JSON-RPC error. Turning every upstream 4xx into a JSON-RPC error hides the reason from the model, and the agent retries blind, which is exactly the retry loop from the failures list above.

The same page sets two rules for successful results. When a tool declares an outputSchema, the server must return structured results that conform to it. A tool that returns structuredContent should also return the serialized JSON in a text block for backwards compatibility. Version 2 of the TypeScript SDK validates structuredContent against outputSchema before the result leaves the server (SDK tools guide), the same check the week 3 harness depends on.

#Which credential the MCP server sends to WordPress

Two credentials meet in this architecture, and the specification keeps them apart. Authorization is optional in MCP. When a server on an HTTP transport supports it, the server acts as an OAuth 2.1 resource server: it must check that each access token was issued for it as the intended audience (RFC 8707), it must answer invalid or expired tokens with HTTP 401, and it must not accept or pass on any other token (MCP authorization). Servers on the stdio transport should skip that flow and read credentials from the environment.

For a Worker in front of /wp-json/ this rules out forwarding the agent’s token to WordPress. The agent’s token stops at the Worker. The Worker calls WordPress with a credential of its own, usually an Application Password for a dedicated WordPress user that holds only the capabilities the tool inventory needs. Application Passwords have shipped with WordPress since 5.6 and are sent with HTTP Basic authentication over HTTPS (REST API handbook, authentication). Keep that password as a Worker secret, set with wrangler secret put, not as a plain variable in wrangler.toml (Cloudflare Workers secrets).

The split has a practical upside for week 4. Rotating the WordPress credential does not touch agent tokens, and revoking an agent token does not touch the WordPress side. The audit’s “data sensitivity” column decides which capabilities the dedicated user gets: a catalogue-only inventory needs none of the order capabilities.

#When the WordPress MCP Adapter is the better route

This playbook puts MCP at the edge, in a Worker. WordPress also has an official route that puts MCP inside WordPress. The MCP Adapter, part of the AI Building Blocks for WordPress initiative, exposes abilities registered through the WordPress Abilities API as MCP tools, resources and prompts. On wordpress.org the plugin is at version 0.7.1, requires WordPress 6.9 and PHP 7.4, and was last updated on 16 October 2026 (plugin page).

What the adapter’s documentation says that shapes the choice:

  • Abilities are private by default. An ability reaches MCP only when meta.public (or meta.mcp.public) is true. On WordPress 6.9 and 7.0 that flag governs MCP only, and REST access still needs meta.show_in_rest; core applies meta.public to REST from 7.1.
  • The default server exposes three meta-tools (mcp-adapter/discover-abilities, mcp-adapter/get-ability-info, mcp-adapter/execute-ability) and answers HTTP at /wp-json/mcp/mcp-adapter-default-server.
  • wp mcp-adapter serve --user=<id> serves a server over stdio through WP-CLI.
  • Without a custom transport permission callback, a server admits logged-in users (is_user_logged_in()).
  • Annotations written in the Abilities API format (readonly, destructive, idempotent) are converted to readOnlyHint, destructiveHint and idempotentHint.

The adapter fits when the tools are WordPress logic that already lives in PHP and the agent clients can reach the site directly. The Worker route fits when the MCP surface needs its own token issuance, edge rate limits and Logpush, or has to keep answering while the WordPress origin is slow. The week 1 audit serves both: each row that maps to a tool becomes either a Worker handler or a registered ability.

Update, 24 October 2026: in Andy Peatling’s Building with WordPress benchmark for October, Opus 5.5 in Claude Code and GPT-6.1-Sol in Codex finished 0 of 42 site-owner jobs when the adapter was their only way in, and 33 of 42 with an application password for the REST API. WordPress’s own three abilities only read site, user and server information, and the only writes came from WooCommerce product abilities. Peatling’s issue #402 plans experimental core abilities in four milestones to close that gap. On 21 October the WordPress AI team recorded approval to make MCP part of the default WordPress experience, with 7.3 discussed as a possible target and no release commitment (weekly summary). Until the write abilities exist, the adapter route means writing them yourself, while the REST endpoints from the week 1 audit already work.

Update, 24 October 2026: every plugin that brings MCP into WordPress adds attack surface of its own. Wordfence published three such entries on 15 and 16 October 2026: AI Engine up to 3.8.3 missed a capability check, so a logged-in contributor could perform an unauthorised action (CVE-2026-110888); the wsp_get_posts MCP tool in WSP MCP up to 2.9.5 let any logged-in subscriber read titles, authors and excerpts of draft, pending and scheduled posts (CVE-2026-109382); and xSpeed Cache up to 1.4.2, a caching plugin with MCP features, allowed stored cross-site scripting by contributors (CVE-2026-109985). None of the three needs an administrator account. Registering abilities inside WordPress puts the same question to your own code: give every ability a permission callback that checks a specific capability, not only whether the user is logged in.

#Which MCP specification version to target

The current revision of the specification is 2026-07-28, and two of its rules touch this playbook. First, MCP has no protocol-level session, so a server cannot rely on per-connection state to relate one tool call to the next. For state such as a basket, the specification’s guidance is to return an explicit handle from a creation tool, accept that handle on later calls, check the caller’s authorization against the handle on every call, and state the handle’s lifetime in the tool description (MCP tools, stateful tools). For order.intent the intent ID is that handle.

Second, every request carries its protocol version and client capabilities in _meta fields in the message body, and the Streamable HTTP transport mirrors them into HTTP headers so intermediaries can route requests without parsing the body (MCP transports). On the adapter side, wp mcp-adapter list --protocol=2026-07-28 counts the components available under that revision; the adapter accepts 2025-11-25 and 2026-07-28 there.

This article covers the migration shape. For the implementation walkthrough see building an MCP server for WooCommerce. For the auth strategy see MCP authentication patterns. For typed tool design see writing typed catalogue tools with Zod for MCP. For the protocol-level decision see MCP vs REST. The pillar is MCP server development.

Pricing is individual because the migration scope depends on the number of endpoints in the audit, the auth complexity, and the consumer count.

For the delivery side of this topic I keep the work under Next.js or Astro migration.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you are planning a Headless WordPress setup, frontend decoupling, or migration to Astro, I can design and build the architecture, API, and frontend.

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Frequently asked questions

Practical answers to apply the topic in real execution.

Do I need to deprecate the REST API to add MCP?#
No. MCP sits in front of REST; the REST API stays live and serves the consumers it has always served (storefronts, partner integrations, webhooks). MCP adds a new agent-facing surface without removing anything.
Why four weeks specifically?#
One week each for audit, scaffold, parallel-run, and cutover is the shape that has held up across multiple migrations. Smaller surfaces compress into two weeks; larger ones spread to six. The structure is what matters, not the exact day count.
What if my REST API is undocumented?#
Run the audit anyway. Hit every endpoint listed in /wp-json/ index, capture the response shape with a tool like wp-cli or a Postman collection, infer field types. The undocumented API becomes a Zod schema before anything else happens.
Can I migrate one tool at a time?#
Yes, and this is often the right approach. Ship catalogue.list and product.detail in week four. Add order.intent four weeks later when the OAuth scope strategy is settled. Add inventory.check the month after that.
What stays on the WordPress side?#
The data layer, the admin UI, the authoring flow, the existing REST endpoints, the webhook firing logic, the user authentication system. MCP wraps and re-exposes; it does not replace WordPress functionality.

Need an FAQ tailored to your industry and market? I can build one aligned with your business goals.

Let’s discuss

Related Articles