MCP vs REST: when each wins for AI agent integration
Treating MCP and REST as alternatives is the wrong frame. They sit at different layers: REST is a transport plus conventions, MCP is a typed protocol designed for an LLM consumer. The real decision is which surface each consumer talks to, and how the two surfaces share a backend. This article gives the framework I use when scoping AI integrations for WordPress and WooCommerce builds.
This article anchors to the MCP server development service pillar.
TL;DR
- MCP and REST are complementary, not alternatives.
- REST wins for deterministic clients with hardcoded action sets and OpenAPI docs.
- MCP wins for LLM agents that need runtime tool discovery and typed schemas with behaviour annotations for mutating actions.
- The production default is REST as system of record, MCP as agent-facing surface in front.
- Same backend, two surfaces, one source of truth.
What is MCP and what is a REST API
REST. An architectural style from Roy Fielding’s 2000 dissertation (the dissertation source), typically expressed over HTTP with verbs and resource URLs. WordPress exposes a REST API under /wp-json/wp/v2/ (WordPress REST API handbook); WooCommerce extends it under /wp-json/wc/v3/. Documentation is usually OpenAPI (OpenAPI specification) and SDKs are generated from the schema at build time.
MCP. A JSON-RPC 2.0 based protocol Anthropic announced on 25 November 2024 (Anthropic announcement) for connecting LLM hosts (Claude Desktop, IDEs, custom agent runtimes) to data and tools. Three primitives: tools (callable functions), resources (read-only documents), prompts (server-supplied templates). Discovery is runtime via tools/list. Since the 2026-07-28 revision the protocol has no sessions: every request carries the protocol version and client capabilities, and the client asks for the list again when its cached copy expires or a change notification arrives.
The shapes feel similar from a distance. They diverge fast under load.
MCP or REST API decision matrix
Six factors I score before picking a surface for a given action:
| Factor | REST favoured | MCP favoured |
|---|---|---|
| Consumer type | Deterministic web/partner client | LLM agent |
| Capability discovery | Compile-time via OpenAPI | Runtime via tools/list |
| Action shape | Fixed, well-known | Free-form intent |
| Mutation safety | HTTP method semantics (RFC 9110) | inputSchema + readOnlyHint, destructiveHint, idempotentHint annotations (hints only) |
| Authentication | Bearer tokens, OAuth | Same, plus per-tool scope tagging |
| Caching | HTTP cache headers | ttlMs and cacheScope on list results; no caching hint on tools/call |
The matrix tells me which protocol is preferable for one action, not for the whole API. Most projects end up split.
When to use a REST API instead of MCP
A storefront fetching a category page. Hardcoded against /wp-json/wp/v2/categories?slug=widgets. The shape is known, the cache headers do work, the CDN can cache by URL.
A partner ERP integration syncing inventory daily. The partner team reads OpenAPI, generates a typed SDK, schedules a cron at 03:00 UTC. They want stable URLs, predictable response shapes, and HTTP status codes. MCP would add complexity without value.
Public, anonymous read traffic. A SEO crawler hitting product pages, a price comparison aggregator pulling product feeds. REST plus rate limiting handles this case with the most-mature infrastructure.
Webhook delivery. When an order changes status, a WooCommerce webhook sends the event to a partner endpoint. That endpoint is an HTTP receiver. MCP is the wrong shape because the agent is not in the loop; the receiver is a deterministic system.
When to use MCP instead of REST
An LLM agent acting on a user’s free-form intent. “Find me a waterproof jacket under 200 euros that ships to Berlin.” The agent does not know in advance which filter combination to apply; it needs to introspect the available tools, read the descriptions, decide. REST gives it query parameters spread across several endpoints and no signal about which to use.
Mutating actions where idempotency matters. Creating an order from an LLM is the loudest example. An MCP order.intent tool with a typed input schema, a required idempotency_key parameter and destructiveHint: false tells the agent and the client what the call does before it runs. The protocol does not guarantee idempotency: the handler has to refuse a second order with the same key. The difference from a free-form POST /wp-json/wc/v3/orders is a contract the model can read.
A tool surface that evolves frequently. Adding a new search filter or a new product type means publishing a new MCP tool with a description and an inputSchema. The server sends notifications/tools/list_changed to clients that opened subscriptions/listen, and the agent picks the tool up on the next tools/list without code changes on the agent side. With REST, every change is a coordinated SDK release.
Multi-step agent workflows. “Check if SKU AC-101 is in stock; if not, suggest three alternatives; if found, propose an order.” Each step is one tool call; the agent composes them. With REST, the agent has to hardcode the workflow shape into its prompt.
MCP vs REST in a WooCommerce store
For a typical WooCommerce build I draw the boundary like this:
| Action | Consumer | Surface |
|---|---|---|
| Public catalogue browse | Web client, crawlers | REST |
| Product detail page render | Web client | REST |
| Inventory sync (B2B) | Partner ERP | REST + OpenAPI |
| Webhook delivery | Partner endpoints | REST |
| Agent: search for a product | LLM | MCP |
| Agent: read order status | LLM | MCP |
| Agent: propose order | LLM | MCP |
| Agent: cancel order | LLM | MCP |
| Admin operations | WordPress admin | REST + cookie auth |
The MCP server’s tool handlers call the same /wp-json/wc/v3/ endpoints the REST consumers use. One source of truth for the data layer, two surfaces for two consumer types.
MCP vs REST authentication and OAuth scopes
The WordPress REST API documents three ways to authenticate (REST API authentication). Cookie authentication serves code running on the site itself and needs a nonce created for the wp_rest action, sent in the X-WP-Nonce header; without the nonce the request runs as an anonymous user, even when someone is logged in. Application Passwords have shipped in core since WordPress 5.6 and work with HTTP Basic authentication, over HTTPS only. OAuth 1.0a and JSON Web Tokens come from plugins. Permission refinement happens per route, through user capabilities and each endpoint’s permission_callback.
MCP authorization is optional and applies to HTTP transports; over stdio, credentials come from the environment. When it is used, the MCP server is an OAuth 2.1 resource server, must publish Protected Resource Metadata (RFC 9728) and accepts only tokens issued for itself: the client names the server in the resource parameter (RFC 8707) and the server rejects tokens minted for another audience. The server may not accept or pass through any other token either, so it authenticates to WordPress with its own credentials, for example an Application Password belonging to a user with a narrow role.
Scopes work as in any OAuth API, with two rules that matter for agents. The server may return in tools/list only the tools the token’s scopes allow. And when a call needs more, it answers HTTP 403 with error="insufficient_scope" and the required scopes in the WWW-Authenticate header, so the client can request a new authorization with the wider set. The user grants orders:read to the agent; the agent never sees order.cancel, and if it tries to call it, it gets the 403 before the handler runs. OpenAPI expresses the same thing with securitySchemes and per-operation security (OpenAPI specification), but in practice many REST APIs document a single global scope and leave finer granularity to the application.
For agent integrations, this per-tool scope mapping is a real ergonomic win. Underneath it is the same OAuth that REST already uses, but the contract is more legible to both the user and the agent.
How caching MCP responses differs from REST HTTP cache
REST has decades of HTTP cache infrastructure: Cache-Control headers, ETag revalidation, Vary rules, CDN edge caching, browser caching, intermediate proxy caching. A GET /wp-json/wc/v3/products?stock_status=instock response with Cache-Control: public, max-age=60 caches at the edge with no extra effort.
MCP gained part of this in the 2026-07-28 revision (MCP caching). Results of tools/list, prompts/list, resources/list, resources/templates/list and resources/read must carry ttlMs, which works like max-age, and cacheScope, whose public and private values mirror the Cache-Control directives. tools/call results, which make up most of an agent’s traffic, are not on that list. They travel as JSON-RPC payloads inside POST requests, and HTTP cache headers do not apply. Caching calls has to happen application-level, inside the tool handler, against an explicit key derived from the input. This is fine when the MCP server’s handlers themselves cache the upstream REST call (which is what I do in production), but it means the caching layer is your problem to design.
This is the strongest argument for keeping public read traffic on REST and routing only agent traffic through MCP. The HTTP cache infrastructure is too valuable to give up for the public surface.
MCP tool annotations vs idempotent HTTP methods
In REST, the safety of an operation lives in the method. RFC 9110 defines GET, HEAD, OPTIONS and TRACE as safe, and PUT, DELETE and the safe methods as idempotent: several identical requests have the same intended effect as one. POST is not idempotent. A proxy, a crawler or a retry library can decide what to repeat from the method alone, without reading the body.
MCP sends everything as tools/call inside a POST, so the method says nothing. The information moves into the tool definition. The specification schema defines four boolean annotations on ToolAnnotations:
| Annotation | Meaning | Default |
|---|---|---|
readOnlyHint | the tool does not modify its environment | false |
destructiveHint | it may perform destructive updates, not only additive ones | true |
idempotentHint | repeating the call with the same arguments has no additional effect | false |
openWorldHint | it interacts with an open set of external entities, such as a web search | true |
The defaults are pessimistic: a tool with no annotations is treated as one that writes, may destroy data and is not idempotent. The specification states that all of these properties are hints, not a guaranteed description of behaviour, and that clients should never base decisions on annotations from untrusted servers. The tools page also recommends a human in the loop who can deny an invocation, and confirmation prompts for sensitive operations.
In a WooCommerce store this works out as follows: product.search gets readOnlyHint: true, order.cancel gets destructiveHint: true, and the idempotency of order.intent is enforced in the handler by a key the server checks, not by the annotation. The annotation helps the client decide when to ask for confirmation. It does not replace the server duties the specification lists: validate every input, enforce access control, rate limit invocations and sanitise outputs.
How MCP reports errors compared with HTTP status codes
A REST API signals failure through the status code: 400 for a malformed request, 401 without valid authentication, 403 without permission, 404 for an unknown route. The tools specification splits errors into two levels. Protocol errors (unknown tool, a request that does not match the tools/call schema, a server failure) come back as standard JSON-RPC errors, for example with code -32602. Execution errors (an upstream API failure, a date in the wrong format, a broken business rule) come back inside a normal result with isError: true and a text message.
The specification recommends that clients pass execution errors to the model, because they carry the information it needs to fix its arguments and try again. In an MCP server that wraps the WordPress REST API, a 400 validation response from WordPress should become a result with isError: true that names the missing parameter, not a protocol error the model has no way to correct.
WordPress REST API discovery vs MCP tools/list
WordPress REST describes itself too, in a different way. Site pages send a Link header with rel="https://api.w.org/" pointing at the API root, and the root at /wp-json/ returns an object with the available namespaces (wp/v2, wc/v3 and those added by plugins), the routes, and an authentication key listing supported methods (REST API discovery). An OPTIONS request to a route returns that route’s schema, written in a subset of JSON Schema draft 4 (REST API schema).
The difference is who reads it. The WordPress index is built for a developer or an SDK generator: it lists every registered route, and each route accepts global parameters such as _fields (partial responses, with nested properties since WordPress 5.3), _embed (linked resources in the same response) and _envelope (global parameters). Collections paginate with per_page up to 100 and return totals in the X-WP-Total and X-WP-TotalPages headers (pagination). All of that serves a programmed client well, but a model handed the whole index spends context choosing between routes it will never call.
MCP’s tools/list returns only what the server chose to expose, each tool with a description, an inputSchema (JSON Schema 2020-12 by default) and an optional outputSchema. The specification lets the list vary with the token’s scopes and asks for a deterministic order, so the client can cache it and the model benefits from prompt caching. An MCP server for WordPress is in practice a curated subset of the /wp-json/ index, with descriptions written for a model rather than a developer.
WordPress 6.9 added a piece in between, the Abilities API. A plugin registers an ability with an input_schema, an output_schema, an optional permission_callback and show_in_rest in its meta. It is a native way to describe units of functionality with a schema, which is exactly the contract an MCP server needs for each tool.
What changed in MCP 2026-07-28 for HTTP proxies and load balancers
The 2025 revisions had an initialize handshake, sessions identified by the Mcp-Session-Id header and an SSE stream opened with GET. With several servers behind a load balancer, a per-connection session forces you to route a client to the same server every time or to share state between servers, a problem stateless REST does not have. The 2026-07-28 revision removed all three. Every request carries the protocol version and client capabilities in _meta. A server that needs state across calls, such as a basket, returns an explicit handle from one tool and accepts it as an argument on the following calls.
The Streamable HTTP transport also mirrors metadata into headers now. MCP-Protocol-Version and Mcp-Method are required on every POST, Mcp-Name as well on tools/call, resources/read and prompts/get, and a tool can mark parameters with x-mcp-header to expose them as Mcp-Param-{name}. A WAF or gateway can then route or rate limit per tool without parsing the JSON body. If a header does not match the body, the server answers 400 with a HeaderMismatch error (-32020). The specification advises against marking sensitive parameters this way, because headers are visible to intermediaries.
For a team already running a REST API behind a CDN, the two surfaces now sit closer together operationally: the same header-based routing and rate limiting rules serve both. HTTP caching still does not apply to tools/call, because every call is a POST.
How to run MCP and REST on one WordPress backend
The default architecture I ship for WordPress and WooCommerce sites with AI agent ambitions:
┌──────────────────┐
│ WordPress core │
│ + WooCommerce │
│ (REST origin) │
└────────┬─────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌───────▼────────┐ ┌────▼─────┐ ┌──────▼──────┐
│ Public REST │ │ Webhook │ │ MCP server │
│ (cache at CDN) │ │ delivery │ │ (Workers) │
└───────┬────────┘ └────┬─────┘ └──────┬──────┘
│ │ │
┌───────▼────────┐ ┌────▼─────┐ ┌──────▼──────┐
│ Storefront, │ │ Partner │ │ LLM agent │
│ price feeds, │ │ endpoints│ │ (Claude, │
│ public APIs │ │ │ │ ChatGPT, │
│ │ │ │ │ custom) │
└────────────────┘ └──────────┘ └─────────────┘Same WordPress origin. Three surfaces, three consumer profiles. The MCP server’s handlers call the same REST endpoints the public surface caches; the webhook delivery shares the WordPress event hooks the MCP server’s invalidation logic listens to.
The boundary between MCP and REST is a deployment decision, not a coding decision. The data layer (WooCommerce database, REST endpoints) is shared. The presentation layer (typed tools vs JSON resources) is split.
Common MCP vs REST architecture mistakes
Trying to make MCP do REST’s job. Caching public catalogue pages through MCP is wrong. Use REST plus a CDN.
Trying to make REST do MCP’s job. Documenting an LLM-friendly action surface in OpenAPI plus expecting the agent to “figure it out” produces brittle results. Agents do better with MCP’s tool discovery.
Two parallel data layers. If the MCP handlers re-implement business logic instead of calling the REST origin, every WordPress upgrade breaks both surfaces. Keep the data layer in WordPress; keep the protocol surfaces thin.
Forgetting the cost. An MCP server is one more deployment unit, one more auth strategy, one more thing to monitor. Do not ship one if your only consumer is a partner ERP integration; ship OpenAPI and call it done.
Related topics
This article covers the protocol-level decision. For the implementation walkthrough see building an MCP server for WooCommerce. For the auth strategy see MCP authentication patterns. For the typed tool design see writing typed catalogue tools with Zod for MCP. For the migration pathway from an existing API see migrating an existing WordPress API to MCP. The pillar is MCP server development.
Pricing is individual because the right shape depends on which consumers you serve and which actions need agent-grade contracts.







