Crear un servidor MCP para WooCommerce: guía práctica
Model Context Protocol da a un agente de IA una superficie tipada e introspeccionable para actuar sobre la tienda. WooCommerce ya ofrece una REST API en /wp-json/wc/v3/. Poner MCP delante convierte esa superficie en algo que un LLM puede llamar sin adivinar nombres de parámetros. Esta es la arquitectura que implemento para clientes que quieren el catálogo y la intención de pedido accesibles desde Claude, ChatGPT o su propio runtime de agentes.
El artículo forma parte del servicio de desarrollo de servidores MCP y del pilar Universal Commerce Protocol.
TL;DR
- MCP se sitúa delante del REST de WooCommerce como una superficie JSON-RPC tipada.
- Tres herramientas cubren la mayor parte de lo que necesita el agente:
catalogue.list,product.detail,order.intent. - Los esquemas Zod definen cada entrada y salida y se ejecutan en cada llamada.
- Mapea los campos de WooCommerce a
schema.orgProduct y Offer para que las respuestas del agente sean coherentes entre superficies. - Cloudflare Workers es mi destino de despliegue por defecto para servidores MCP dominados por lecturas.
Qué es realmente MCP
Anthropic anunció Model Context Protocol el 25 de noviembre de 2024 (anthropic.com/news/model-context-protocol). Es un protocolo abierto basado en JSON-RPC 2.0 que permite a un cliente (un host LLM como Claude Desktop, un IDE o tu propio runtime de agentes) hablar con un servidor (tu implementación MCP) por stdio o por Streamable HTTP, el transporte HTTP que desde la versión 2025-03-26 de la especificación sustituye al antiguo HTTP con Server-Sent Events.
Tres primitivas:
- Tools. Funciones invocables con entrada y salida tipadas. El agente elige una y la llama.
- Resources. Referencias legibles a documentos o registros que el agente puede traer al contexto.
- Prompts. Plantillas de prompt que proporciona el servidor y que el host puede invocar.
En una tienda WooCommerce, las tools cargan con casi todo el trabajo. Recorrer el catálogo es una llamada a una herramienta. Los detalles de un producto son una llamada a una herramienta. Proponer un pedido es una llamada a una herramienta. Los resources sirven cuando el agente necesita el texto completo de la política de devoluciones o una descripción larga de producto; los prompts sirven cuando quieres ofrecer interacciones listas (“propón tres productos para este perfil de cliente”).
Qué cambia la especificación MCP 2026-07-28 en un servidor para WooCommerce
La revisión vigente de la especificación es la del 28 de julio de 2026 (registro de cambios). Para un servidor que vive delante de una tienda importan cuatro cambios.
El protocolo deja de tener sesiones. Desaparece el saludo initialize y, en el transporte Streamable HTTP, también la cabecera Mcp-Session-Id. Cada petición lleva su versión de protocolo y las capacidades del cliente en _meta. Un Worker que construye un servidor nuevo para cada petición encaja con ese modelo, porque no queda estado de conexión que conservar entre llamadas.
El estado viaja como argumento. Si un servidor necesita estado entre llamadas, la especificación recomienda devolver un identificador explícito desde una herramienta de creación y aceptarlo como argumento en las siguientes (herramientas con estado). Su propio ejemplo es una cesta de la compra con basket_id. El borrador que devuelve order.intent sigue ese patrón: el ID del borrador es el handle. La misma sección pide comprobar la autorización del llamante contra el handle en cada llamada, indicar la caducidad en la descripción de la herramienta (aquí, 24 horas) y devolver un error de ejecución cuando llega un handle caducado o desconocido, para que el modelo cree uno nuevo.
El servidor tiene que implementar server/discover, el método con el que anuncia las versiones de protocolo que soporta, sus capacidades y su identidad.
Las listas se pueden cachear. Los resultados de tools/list llevan ahora ttlMs y cacheScope, y el servidor debería devolver las herramientas en orden determinista. Cinco herramientas que solo cambian con cada despliegue admiten un ttlMs largo.
El transporte HTTP+SSE está obsoleto desde la versión 2025-03-26 y los servidores nuevos usan Streamable HTTP. La versión 2 del SDK de TypeScript sirve las dos generaciones del protocolo desde el mismo punto de entrada (versiones de protocolo en el SDK), así que un cliente que todavía empieza con initialize sigue funcionando.
Qué herramientas MCP necesita una tienda WooCommerce
Antes de escribir una línea de código, anoto las intenciones que el agente debe poder expresar frente a la tienda. Para una tienda WooCommerce típica, la lista es corta:
- Recorrer el catálogo con filtros (
catalogue.list). - Detalles completos de un producto (
product.detail). - Proponer un pedido en nombre del cliente, devuelto como borrador para que lo confirme una persona (
order.intent). - Consultar el estado de un pedido existente por número y dirección de correo electrónico (
order.status). - Consultar el stock de un SKU (
inventory.check).
Cada intención corresponde exactamente a una herramienta. Resiste la tentación de exponer wc.products.get, wc.products.list, wc.orders.create como un wrapper uno a uno del REST. Al agente no le aportan nada los verbos REST; le aportan los verbos de intención.
Cómo definir herramientas MCP en Zod
El SDK de TypeScript (paquete @modelcontextprotocol/server en la versión 2; en la versión 1 era @modelcontextprotocol/sdk) usa Zod (zod.dev) para los esquemas de entrada y salida. Así es el contrato de catalogue.list:
import { z } from "zod";
export const catalogueListInput = z.object({
query: z.string().min(2).max(200).optional(),
category: z.string().optional(),
in_stock: z.boolean().optional(),
price_min: z.number().nonnegative().optional(),
price_max: z.number().nonnegative().optional(),
page: z.number().int().min(1).max(100).default(1),
per_page: z.number().int().min(1).max(50).default(12),
});
export const catalogueListOutput = z.object({
results: z.array(
z.object({
sku: z.string(),
name: z.string(),
url: z.string().url(),
price: z.object({
amount: z.number().nonnegative(),
currency: z.string().length(3),
}),
availability: z.enum(["InStock", "OutOfStock", "BackOrder", "PreOrder"]),
image: z.string().url().optional(),
})
),
total: z.number().int().nonnegative(),
page: z.number().int(),
});Aquí importan dos cosas. Primero, la salida usa el vocabulario de schema.org (InStock, OutOfStock, BackOrder, PreOrder son valores tomados directamente de ItemAvailability). Segundo, la entrada rechaza la basura en la frontera del protocolo; el handler nunca verá query: "" ni un price_min negativo.
Declaro todas las herramientas con el mismo patrón: esquema de entrada, esquema de salida, handler. El handler es un adaptador fino sobre /wp-json/wc/v3/. Zod se ejecuta dos veces por llamada: una en la entrada y otra en la salida. La comprobación de la salida detecta errores del handler que, si no, llegarían al agente como datos mal formados.
Cómo registrar herramientas MCP con anotaciones y salida estructurada
Con la versión 2 del SDK, el paquete del lado servidor es @modelcontextprotocol/server y cada herramienta se registra con registerTool, que recibe el nombre, la configuración y el handler (herramientas en el SDK). Así queda catalogue.list con los esquemas de arriba:
import { McpServer } from "@modelcontextprotocol/server";
export function createWooCommerceMcpServer(env: Env): McpServer {
const server = new McpServer({ name: "woocommerce", version: "1.0.0" });
server.registerTool(
"catalogue.list",
{
title: "Browse the catalogue",
description: "List store products with filters and pagination",
inputSchema: catalogueListInput,
outputSchema: catalogueListOutput,
annotations: { readOnlyHint: true },
},
async (input) => {
const output = await handleCatalogueList(input);
return {
content: [{ type: "text", text: JSON.stringify(output) }],
structuredContent: output,
};
}
);
// product.detail, inventory.check, order.status, order.intent
return server;
}Cuando una herramienta declara outputSchema, la especificación obliga al servidor a devolver structuredContent conforme a ese esquema, y el cliente debería validarlo (herramientas en la especificación). El SDK hace esa validación antes de que el resultado salga del servidor. Por compatibilidad con clientes antiguos, la especificación recomienda devolver también el JSON serializado en un bloque de texto, que es el content del ejemplo.
Las anotaciones describen el comportamiento de la herramienta. readOnlyHint indica que no modifica su entorno. destructiveHint (por defecto true) e idempotentHint (por defecto false) solo cuentan cuando readOnlyHint es false. openWorldHint (por defecto true) dice si la herramienta interactúa con un mundo abierto de entidades externas. Las cuatro herramientas de lectura llevan readOnlyHint: true. order.intent lleva readOnlyHint: false y destructiveHint: false, porque crea un borrador y no borra nada. Dejo idempotentHint en false, porque la idempotency_key es opcional y, sin ella, dos llamadas iguales crean dos borradores. Según la documentación del SDK, un host puede aprobar automáticamente una herramienta de solo lectura y pedir confirmación antes de una destructiva. Pero la especificación obliga a los clientes a tratar las anotaciones como no fiables salvo que vengan de un servidor de confianza, así que la comprobación del alcance del token se queda en el servidor.
Los nombres con punto, como catalogue.list, cumplen la recomendación de la especificación: entre 1 y 128 caracteres, solo letras ASCII, dígitos, guion bajo, guion y punto. Para los fallos hay dos canales. Una herramienta desconocida o una petición mal formada es un error de protocolo y viaja como error JSON-RPC. Un fallo de la API de WooCommerce o un argumento fuera de rango es un error de ejecución y viaja dentro del resultado con isError: true y un texto que el modelo pueda usar para corregirse. Un product.detail con un SKU inexistente debería devolver ese segundo tipo, con un mensaje que diga que el SKU no existe, para que el modelo cambie el argumento y vuelva a intentarlo.
Cómo conectar el servidor MCP con la REST API de WooCommerce
El handler de catalogue.list lee de /wp-json/wc/v3/products:
async function handleCatalogueList(input: z.infer<typeof catalogueListInput>) {
const params = new URLSearchParams();
if (input.query) params.set("search", input.query);
if (input.category) params.set("category", input.category);
if (input.in_stock) params.set("stock_status", "instock");
if (input.price_min !== undefined) params.set("min_price", String(input.price_min));
if (input.price_max !== undefined) params.set("max_price", String(input.price_max));
params.set("page", String(input.page));
params.set("per_page", String(input.per_page));
const response = await fetch(
`${WC_BASE}/wp-json/wc/v3/products?${params.toString()}`,
{ headers: { Authorization: `Basic ${WC_AUTH}` } }
);
const products = await response.json();
const total = Number(response.headers.get("X-WP-Total") ?? 0);
return catalogueListOutput.parse({
results: products.map(mapWooProductToSchemaOrg),
total,
page: input.page,
});
}El helper mapWooProductToSchemaOrg traduce los nombres de campo de WooCommerce (stock_status, regular_price, permalink) a la forma compatible con schema.org de la declaración de salida. El mapeo de campos vive en un solo sitio, llamado desde una herramienta, llamada desde un handler. Las desviaciones entre las actualizaciones de WooCommerce y el contrato del agente las detecta la llamada parse en la salida, no producción.
En order.intent nunca llamo desde el handler a los endpoints de escritura de WooCommerce. La herramienta devuelve un borrador de pedido que el host muestra al usuario, el usuario lo confirma y solo entonces una ruta separada y autenticada llama a POST /wp-json/wc/v3/orders. Escribir a ciegas a partir de un LLM es un mal comportamiento por defecto. Además encaja con el art. 25 del RGPD (protección de datos desde el diseño); el borrador permite ajustar el botón de compra y los consentimientos de marketing antes de ejecutar la operación.
Cómo crear las claves de la REST API de WooCommerce para el servidor MCP
El servidor MCP habla con WooCommerce con un par de claves de la REST API: la Consumer Key y la Consumer Secret. Según la documentación oficial (autenticación de la REST API), se generan en los ajustes avanzados de WooCommerce, en la pantalla de la REST API (wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys). Tres propiedades de esas claves condicionan el diseño del servidor.
Cada clave pertenece a un usuario de WordPress, y las peticiones con ella respetan los roles y capacidades de ese usuario. Si se borra el usuario, la clave deja de funcionar y no pasa a otro usuario. Por eso creo un usuario técnico dedicado al servidor MCP en lugar de colgar la clave de la cuenta de una persona que algún día dejará la empresa.
El nivel de acceso es lectura, escritura o lectura y escritura. Las herramientas de lectura usan una clave de solo lectura. La clave con escritura solo la conoce la ruta separada que confirma el pedido con POST /wp-json/wc/v3/orders.
Sobre HTTPS, la autenticación es HTTP Basic, con la Consumer Key como usuario y la Consumer Secret como contraseña. Ese es el Authorization: Basic del handler de catalogue.list.
La paginación también sale de la documentación (paginación). Por defecto la API devuelve 10 elementos por página, per_page y page cambian eso, y las cabeceras X-WP-Total y X-WP-TotalPages llevan el número total de recursos y de páginas. El handler lee X-WP-Total para rellenar total.
Dos detalles del endpoint de productos (listar productos) afectan al esquema. El parámetro category filtra por el ID de la categoría, no por su slug, así que el agente necesita IDs o el servidor tiene que traducir el slug antes de llamar. Y stock_status admite instock, outofstock y onbackorder. Para el tercer valor schema.org tiene BackOrder, y por eso el enum de salida de catalogue.list lo incluye: sin él, un producto con reservas haría fallar la validación de salida.
Actualización, : WooCommerce anunció el 21 de octubre que la versión 11.3 elimina su propio endpoint MCP, /wp-json/woocommerce/mcp, junto con la cabecera X-MCP-API-Key con la que se autenticaba mediante una clave de la REST API. El servidor de esta guía no usa ese endpoint. Llama a /wp-json/wc/v3/ con claves consumer, y esa ruta se mantiene. Si además conectaste un cliente MCP al endpoint integrado, cámbialo antes de actualizar: instala el plugin MCP Adapter, crea una contraseña de aplicación para un usuario que solo tenga los permisos que necesita el cliente y apunta el cliente a /wp-json/mcp/mcp-adapter-default-server. El patrón de servidor externo sirve también para una tienda en Shopify o un backend headless, porque solo cambian los handlers que consultan la tienda.
Actualización, : WooCommerce 11.3 añade además una API experimental de abilities extensibles (blog de desarrolladores de WooCommerce, 22 de octubre de 2026). Las extensiones pueden añadir campos propios, filtros de consulta y validadores a las abilities de productos, pedidos y líneas de pedido, de modo que un agente lee y modifica los datos de la extensión con las abilities que ya usa. La función viene desactivada por defecto, se activa en WooCommerce > Settings > Advanced > Features, en la sección experimental, y WooCommerce pide no usarla todavía en producción. Para un servidor externo como este no cambia nada. Empieza a importar cuando los datos de extensiones, como campos de producto propios, tengan que llegar a los agentes a través del adaptador.
Cómo mapear campos de WooCommerce a schema.org
Dos razones para mantener las salidas de las herramientas en el vocabulario de schema.org:
Los agentes usan las mismas palabras entre fuentes. Cuando un agente responde a “¿está disponible este producto?”, combina una entidad Product de tu servidor MCP, una Offer con availability y quizá una Review. Si tu servidor MCP devuelve { "stock": "sí" } y otra fuente availability: "https://schema.org/InStock", el agente tiene que conciliar dos vocabularios. Con la alineación a schema.org de tu lado, no hace falta.
Los datos estructurados de tu tienda ya hablan schema.org. Tu /wp-content/themes/<theme>/single-product.php (o su equivalente headless) emite JSON-LD de tipo Product. Mantener la salida de MCP con la misma forma hace que la misma página de producto sirva los mismos datos al rastreador de Google, al rastreador de OpenAI por URL y al agente que llama directamente a tu herramienta MCP.
El mapeo de un producto WooCommerce típico queda así:
| Campo de WooCommerce | Campo de schema.org |
|---|---|
sku | sku |
name | name |
permalink | url |
regular_price + currency | offers.price + priceCurrency |
stock_status: "instock" | availability: InStock |
stock_status: "outofstock" | availability: OutOfStock |
stock_status: "onbackorder" | availability: BackOrder |
images[0].src | image |
description | description |
Las variaciones requieren una capa adicional (una lista hasVariant de tipo Product), pero la forma básica es la misma.
Cómo hacer idempotentes los pedidos en el servidor MCP
Las herramientas de lectura (catalogue.list, product.detail, inventory.check, order.status) son idempotentes por naturaleza. Tres llamadas devuelven la misma respuesta.
order.intent es la trampa. El agente puede llamar dos veces si la red entre la respuesta y el host falla un instante. El contrato que implemento:
order.intentacepta unaidempotency_keyopcional (UUID proporcionado por el host).- El handler guarda la clave en Workers KV con un TTL de 24 horas junto al ID del borrador de pedido creado.
- Una nueva llamada con la misma clave devuelve el mismo borrador en lugar de crear otro.
El patrón corresponde a la idempotencia de Stripe en POST /v1/charges. Es bien conocido; agentes y personas se benefician por igual.
Cómo desplegar el servidor MCP en Cloudflare Workers
La versión 2 del SDK de TypeScript exporta createMcpHandler, que devuelve un objeto { fetch } con la forma que Cloudflare Workers espera como exportación por defecto y sirve el transporte Streamable HTTP (runtimes web-standard en el SDK). La entrada del Worker es esta:
import {
createMcpHandler,
hostHeaderValidationResponse,
originValidationResponse,
} from "@modelcontextprotocol/server";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const rejected =
hostHeaderValidationResponse(request, [env.MCP_HOSTNAME]) ??
originValidationResponse(request, [env.SHOP_HOSTNAME]);
if (rejected) return rejected;
const authInfo = await verifyAgentToken(request, env);
if (!authInfo) {
return new Response("Unauthorized", { status: 401 });
}
const handler = createMcpHandler(() => createWooCommerceMcpServer(env));
return handler.fetch(request, { authInfo });
},
};verifyAgentToken comprueba tokens con alcance limitado en Workers KV y devuelve los datos del token, que los handlers leen como ctx.http.authInfo. Las herramientas de solo lectura pasan con un token de permisos más bajos; order.intent exige un token con el alcance orders:write. La estrategia de autenticación es uno de los dos patrones descritos en la guía de patrones de autenticación MCP.
wrangler.toml declara la URL de origen de WordPress, la clave de consumidor y el secreto de WooCommerce (como secretos del Worker, nunca en el código) y un namespace de KV para la idempotencia. El despliegue se hace con wrangler deploy. El Worker sigue siendo pequeño, porque solo es enrutamiento JSON-RPC más adaptadores REST.
Cómo proteger el endpoint MCP con OAuth y validación de Origin
La autorización es opcional en MCP. Cuando una implementación sobre HTTP la ofrece, debería seguir la especificación de autorización (autorización); un servidor stdio, en cambio, no debería seguirla y toma las credenciales del entorno. Estas son las reglas que afectan a este Worker.
El servidor MCP actúa como servidor de recursos OAuth 2.1 y tiene que publicar los metadatos de recurso protegido de la RFC 9728, para que el cliente descubra qué servidor de autorización emite sus tokens.
Tiene que validar que el token se emitió para él como audiencia, según la RFC 8707. Un token inválido o caducado recibe un HTTP 401.
No puede aceptar ni reenviar ningún otro token. En una tienda eso significa que el token del agente nunca llega a /wp-json/wc/v3/. Hacia WooCommerce solo viajan las claves de la REST API guardadas como secretos del Worker.
El transporte añade otra obligación (Streamable HTTP): el servidor tiene que validar la cabecera Origin en todas las conexiones para evitar ataques de DNS rebinding, y responder 403 si la cabecera está presente y no es válida. createMcpHandler no hace esa comprobación por sí mismo. La documentación del SDK indica poner delante hostHeaderValidationResponse y originValidationResponse, que es lo que hace el Worker de arriba. Una petición sin cabecera Origin pasa esa comprobación, así que el token sigue siendo la barrera principal.
Para el registro de la sección siguiente ayuda otra novedad del transporte. Cada POST lleva las cabeceras Mcp-Method y Mcp-Name, que en tools/call contiene el nombre de la herramienta, de modo que un proxy o el propio Worker puede anotar qué herramienta se llamó sin parsear el cuerpo JSON.
Actualización, : cada plugin que lleva MCP al interior de WordPress añade su propia superficie de ataque. Wordfence publicó tres entradas de este tipo el 15 y el 16 de octubre de 2026: a AI Engine hasta la versión 3.8.3 le faltaba una comprobación de permisos, así que un colaborador (contributor) con sesión iniciada podía ejecutar una acción no autorizada (CVE-2026-110888); la herramienta MCP wsp_get_posts de WSP MCP hasta la versión 2.9.5 permitía a cualquier suscriptor con sesión iniciada leer títulos, autores y extractos de borradores y de entradas pendientes o programadas (CVE-2026-109382); y xSpeed Cache hasta la versión 1.4.2, un plugin de caché con funciones MCP, permitía cross-site scripting almacenado por parte de colaboradores (CVE-2026-109985). Ninguna de las tres requiere una cuenta de administrador. El Worker de esta guía funciona fuera de WordPress y solo llega a la tienda con sus claves de la REST API, así que un rol de WordPress no da acceso a sus herramientas. Lo protegen las comprobaciones del token y de la cabecera Origin descritas arriba.
Cómo registrar y monitorizar las llamadas a herramientas MCP
Registro cada llamada a una herramienta con:
- El nombre de la herramienta.
- Un hash SHA-256 de la entrada (no la entrada en sí; riesgo de datos personales según el art. 32 del RGPD).
- La latencia.
- Si la validación de la salida pasó o no.
- El alcance del token del agente.
Los registros van por Cloudflare Logpush a un almacenamiento a largo plazo. Dos hábitos operativos que esto hace posibles:
Ajustar los esquemas. Seis semanas después del lanzamiento reviso los fallos de validación. Cada fallo de salida es un bug del handler o un campo de WooCommerce sin mapear. Cada fallo de entrada es o un agente defectuoso o un esquema demasiado restrictivo. Ambos entran en la siguiente versión.
Atribución de costes. Las llamadas a herramientas son carga medible sobre el origen de WooCommerce. El registro muestra qué token de agente genera la carga y con qué herramienta. Un único agente que se porta mal y entra en bucle con catalogue.list está a una consulta al registro de quedar identificado.
Otras guías sobre MCP y WooCommerce
Este artículo muestra el recorrido de implementación. Una arquitectura tolerante a fallos para la sincronización con ERP, colas Redis y transacciones de base de datos se describe en nuestra guía de arquitectura de integración de WooCommerce con ERP 2026. La página del servicio es desarrollo de servidores MCP. La estrategia más amplia está en Universal Commerce Protocol.
El presupuesto es individual, porque el alcance depende del número de herramientas que necesites, de la complejidad de las extensiones de WooCommerce y de los runtimes de agentes que quieras soportar.







