WordPress no necesita un MCP: usa la REST API

·

Todo el mundo habla de MCP (Model Context Protocol) desde hace un tiempo. Anthropic lo presentó como el estándar universal para que las IA descubran y usen herramientas externas. Y el ecosistema WordPress se ha lanzado con entusiasmo: Automattic publicó wordpress-mcp, luego lo archivó en favor del oficial WordPress/mcp-adapter, WooCommerce tiene su propia integración MCP, y plugins como AI Engine de Meow Apps ya exponen endpoints MCP nativos.

La narrativa puede parecer seductora: «Tu WordPress necesita un MCP server para que las IAs puedan interactuar con él».

Pero cuando lo piensas un momento… ¿lo necesita? WordPress lleva desde la versión 4.7 (diciembre de 2016) con una REST API completa, madura y, esto es lo importante, auto-descubrible. Y desde la 5.6 (diciembre de 2020) con Application Passwords nativas. Es decir, WordPress ya tiene todo lo que MCP promete, sin instalar absolutamente nada extra.

Veamos por qué, y también dónde esta afirmación tiene matices que conviene no ignorar.

La REST API de WordPress ya es auto-documentada

El principio fundamental de MCP es que un cliente (la IA) pueda descubrir qué herramientas existen, qué parámetros aceptan y ejecutarlas. Eso es exactamente lo que la REST API de WordPress ya hace por diseño.

Discovery: encontrar la API

Una IA no necesita saber de antemano dónde está la API de un sitio WordPress. Puede descubrirlo con una simple petición HEAD a cualquier URL del site:

curl -I https://example.com/

En la respuesta, WordPress incluye siempre este header:

Link: <https://example.com/wp-json/>; rel="https://api.w.org/"

Ahí está. La IA ya sabe dónde está la API. Sin configuración previa, sin documentación que leer, sin un JSON de configuración como el que exige MCP (mcp.json o similar).

El índice de la API: qué hay disponible

Una vez que la IA conoce la URL base, hace un GET al raíz:

curl -s https://example.com/wp-json/ | jq '.namespaces'

Y obtiene algo como:

[
  "oembed/1.0/",
  "wp/v2",
  "wp-site-health/v1",
  "wp-block-editor/v1",
  "mi-plugin/v1"
]

Cada namespace es un conjunto de endpoints. La IA ya sabe qué operaciones están disponibles en este sitio en particular. No es una lista genérica: es la lista real de este sitio, incluyendo los plugins que tenga instalados.

OPTIONS: el schema de cada endpoint

Aquí es donde la cosa se pone interesante. Para cualquier ruta de la API, una petición OPTIONS devuelve el schema completo:

curl -s -X OPTIONS https://example.com/wp-json/wp/v2/posts | jq '.endpoints[] | {methods, args}'

La respuesta incluye, para cada método HTTP disponible (GET, POST, PUT, DELETE):

  • Los argumentos que acepta (con tipo, descripción, validaciones)
  • Las propiedades del recurso (con su schema JSON)
  • Qué permisos se necesitan (permission_callback)
  • Si requiere autenticación

Por ejemplo, para crear un post, la IA vería algo como:

{
  "methods": ["POST"],
  "args": {
    "title": {
      "type": "string",
      "description": "The title for the post.",
      "required": false
    },
    "content": {
      "type": "string",
      "description": "The content for the post.",
      "required": false
    },
    "status": {
      "type": "string",
      "enum": ["publish", "pending", "draft", "auto-draft", "future", "private", "trash"],
      "default": "draft"
    },
    "categories": {
      "type": "array",
      "items": {"type": "integer"}
    }
  }
}

Esto es, literalmente, lo que MCP llama una «tool definition»: nombre, parámetros, tipos, descripciones. Pero sin necesidad de un protocolo adicional. Es HTTP estándar + JSON Schema.

Hipermedia: navegación entre recursos

Las respuestas de la API incluyen _links y _embedded, siguiendo el patrón HAL. Una IA puede navegar de un post a su autor, de ahí a los posts del autor, de ahí a las categorías… sin conocer la estructura de URLs de antemano. La API se describe a sí misma en cada respuesta.

curl -s https://example.com/wp-json/wp/v2/posts/42 | jq '._links'
{
  "self": [{"href": "https://example.com/wp-json/wp/v2/posts/42"}],
  "collection": [{"href": "https://example.com/wp-json/wp/v2/posts"}],
  "author": [{"embeddable": true, "href": "https://example.com/wp-json/wp/v2/users/1"}],
  "replies": [{"embeddable": true, "href": "https://example.com/wp-json/wp/v2/comments?post=42"}],
  "wp:term": [
    {"taxonomy": "category", "embeddable": true, "href": "..."},
    {"taxonomy": "post_tag", "embeddable": true, "href": "..."}
  ]
}

Cada enlace es una posible siguiente acción. Una IA agéntica puede seguir estos enlaces para explorar el contenido del sitio de forma natural, igual que un humano navega por enlaces en una web.

Application Passwords: autenticación sin fricción

MCP requiere un mecanismo de autenticación. La opción oficial del mcp-adapter de WordPress usa… Application Passwords. Es decir, usa exactamente lo mismo que usarías si conectaras directamente con la REST API.

Application Passwords es una feature nativa de WordPress desde la 5.6 que permite generar credenciales revocables por aplicación, sin exponer la contraseña principal del usuario. Cada token:

  • Es específico para una aplicación (lo nombras al crearlo: «Claude Agent», «Cursor», etc.)
  • Es revocable individualmente desde el perfil de usuario
  • Registra último uso, IP y User-Agent
  • Funciona con HTTP Basic Auth, soportado por cualquier cliente HTTP del planeta

Para crear una Application Password desde la CLI (útil para automatización):

wp user application-password create ai-agent "Claude Agent" --user=ai-agent

Y para usarla:

curl -u "ai-agent:xxxx xxxx xxxx xxxx xxxx xxxx xxxx" \
     -X POST https://example.com/wp-json/wp/v2/posts \
     -H "Content-Type: application/json" \
     -d '{"title": "Hola desde la IA", "status": "draft"}'

No hay tokens JWT que expiran, no hay plugins de autenticación que mantener, no hay proxies locales que instalar. Basic Auth sobre HTTPS. Simple, seguro, universal.

Un matiz importante: Application Passwords requiere HTTPS en producción (WordPress lo bloquea sobre HTTP salvo en entornos locales reconocidos como tales), y no soporta scopes granulares por sí mismo: el control de acceso real recae en el rol del usuario, no en el token. Esto hay que tenerlo claro antes de asumir que «es seguro por defecto».

Usuario dedicado con rol personalizado: least privilege real

Aquí está la pieza que cierra el círculo. El error más común al conectar IA con WordPress es usar un usuario administrador. No. El principio de least privilege dice que el usuario de la IA debe tener solo las capabilities que necesita, ni una más.

WordPress permite crear roles personalizados con add_role(). Lo ideal es hacerlo en un plugin, o en mu-pluigin (nunca en functions.php del theme):

<?php
// mu-plugins/ai-agent-role.php
add_action('init', function () {
    add_role('ai_agent', 'AI Agent', [
        // Lectura
        'read'                   => true,
        // Posts
        'edit_posts'             => true,
        'edit_published_posts'   => true,
        'publish_posts'          => true,
        'delete_posts'           => true,
        'upload_files'           => true,
        // Sin edit_others_posts, sin manage_options, sin activate_plugins...
    ]);
});

Este rol permite a la IA crear, editar y publicar posts, y subir medios. Pero no puede editar posts de otros, no puede cambiar ajustes del sitio, no puede instalar plugins, no puede gestionar usuarios. La REST API respeta estas capabilities en cada endpoint de forma nativa: si la IA intenta hacer DELETE /wp-json/wp/v2/users/1, obtendrá un 403. Sin configuración extra, sin whitelists, sin lógica adicional.

Para crear el usuario:

wp user create ai-agent agente@example.com \
  --role=ai_agent \
  --display_name="AI Agent" \
  --porcelain

Y luego generarle su Application Password:

wp user application-password create ai-agent "REST API Agent" --user=ai-agent

El flujo completo de setup, en tres comandos:

# 1. Crear el usuario con rol mínimo
wp user create ai-agent agente@example.com --role=ai_agent --porcelain

# 2. Generar Application Password
wp user application-password create ai-agent "AI Agent" --user=ai-agent

# 3. Verificar que funciona (debe devolver el usuario actual)
curl -u "ai-agent:XXXX XXXX XXXX XXXX XXXX XXXX XXXX" \
     https://example.com/wp-json/wp/v2/users/me

Rendimiento: no dejes que la IA te tumbe el servidor

Este es el punto que la mayoría de posts sobre «REST API para IA» se saltan, y es justo el que más importa cuando gestionas cientos de sitios en vez de uno.

_fields: pide solo lo que necesitas

Por defecto, GET /wp/v2/posts devuelve el objeto completo: contenido renderizado, contenido raw, GUID, meta, links… Si la IA solo necesita título y extracto para decidir qué hacer, pídelo explícitamente:

curl -s "https://example.com/wp-json/wp/v2/posts?_fields=id,title,excerpt,link"

En un sitio con miles de posts y una IA iterando en bucle, la diferencia entre payload completo y payload filtrado es notable tanto en tiempo de respuesta como en tokens consumidos si luego ese JSON entra en el contexto del modelo.

_embed con cuidado, no por defecto

_embed=true evita el problema N+1 (una petición por autor, una por términos, una por featured media) trayéndolo todo en la misma respuesta. Pero en colecciones grandes infla el payload considerablemente. Regla práctica: _embed en peticiones de detalle (un solo post), nunca en listados paginados de más de 10-20 items sin necesidad real.

Rate limiting a nivel de servidor, no de aplicación

WordPress no trae rate limiting nativo en la REST API. Si expones un usuario ai_agent con Application Password, nada impide que un bug en el agente (o un token filtrado) genere miles de peticiones por minuto. Esto se resuelve en la capa de proxy, no en PHP:

# Ejemplo con nginx/Angie: limitar por Application Password / IP
limit_req_zone $http_authorization zone=wp_api_agent:10m rate=30r/m;

location /wp-json/ {
    limit_req zone=wp_api_agent burst=10 nodelay;
    proxy_pass http://php-fpm-backend;
}

Con muchos sitios detrás del mismo balanceador, esto no es opcional: es lo que evita que un agente mal configurado en un sitio afecte al rendimiento de los demás.

Auditoría: saber qué hizo la IA y cuándo

Application Passwords registra último uso, pero no un log de acciones. Para trazabilidad real (qué post creó, qué borró, con qué payload), hace falta engancharse a los hooks nativos:

add_action('rest_after_insert_post', function ($post, $request) {
    if (wp_get_current_user()->user_login === 'ai-agent') {
        error_log(sprintf(
            '[AI-AGENT] post_id=%d action=%s ip=%s',
            $post->ID,
            $request->get_method(),
            $_SERVER['REMOTE_ADDR']
        ));
    }
}, 10, 2);

Esto es trivial de añadir y se olvida con frecuencia hasta que hace falta investigar qué pasó tras un incidente. Un MU plugin con un puñado de estos hooks (rest_after_insert_post, rest_delete_post, rest_insert_attachment) cubre el 90% de los casos de auditoría sin depender de plugins de terceros.

CORS y nonces: el caso del navegador

Todo lo anterior asume un agente hablando servidor-a-servidor con Application Passwords. Si en cambio la IA corre en el navegador del usuario (una extensión, un widget embebido en el admin), el mecanismo cambia: ahí se usa el nonce de WordPress (X-WP-Nonce) ligado a la sesión de cookies, no Application Passwords. Y si la IA vive en un dominio distinto al del WordPress, hay que configurar CORS explícitamente (Access-Control-Allow-Origin con el header Authorization permitido), algo que WordPress no habilita por defecto y que MCP tampoco resuelve mágicamente; cualquier solución basada en HTTP se enfrenta al mismo problema.

Endpoints custom de plugins: la API también es tuya

Todo el razonamiento anterior no se limita a wp/v2. Cualquier plugin puede registrar su propio namespace con register_rest_route(), heredando automáticamente discovery, schema via OPTIONS y control de permisos:

register_rest_route('mi-plugin/v1', '/procesar', [
    'methods'             => 'POST',
    'callback'            => 'mi_plugin_procesar_callback',
    'permission_callback' => fn() => current_user_can('edit_posts'),
    'args'                => [
        'accion' => [
            'type'     => 'string',
            'enum'     => ['sincronizar', 'limpiar', 'validar'],
            'required' => true,
        ],
    ],
]);

Para una IA, este endpoint es indistinguible en forma de los nativos de wp/v2: aparece en /wp-json/, responde a OPTIONS con su schema, y respeta capabilities. Esto es relevante si trabajas en plugins propios: no hace falta pensar en «exponer esto a MCP» como un paso aparte, exponerlo bien vía REST ya es suficiente.

Webhooks: cuando la IA no debe preguntar, sino que le avisen

Todo el flujo descrito hasta ahora es pull: la IA pregunta, el servidor responde. Para casos donde la IA debe reaccionar a eventos (nuevo comentario, pedido de WooCommerce, post publicado) sin hacer polling constante, el patrón complementario es push vía webhooks, con hooks estándar de WordPress:

add_action('transition_post_status', function ($new, $old, $post) {
    if ($new === 'publish' && $old !== 'publish') {
        wp_remote_post('https://mi-agente.example.com/webhook', [
            'body'    => wp_json_encode(['post_id' => $post->ID, 'event' => 'published']),
            'headers' => ['Content-Type' => 'application/json'],
        ]);
    }
}, 10, 3);

Esto no es MCP ni REST discovery, es simplemente WordPress hooks + HTTP saliente. Pero completa el cuadro: REST API para que la IA pregunte y actúe, webhooks para que el sitio le avise cuando algo pasa.

El flujo completo que seguiría una IA agéntica

Pongámonos en el papel de una IA que acaba de recibir la URL de un WordPress y sus credenciales. El flujo sería:

# 1. Discovery: encontrar la API
curl -I https://example.com/
# → Link: <https://example.com/wp-json/>; rel="https://api.w.org/"

# 2. Explorar qué hay disponible
curl -s https://example.com/wp-json/ | jq '{namespaces, routes: (.routes | keys)}'

# 3. Entender un endpoint concreto
curl -s -X OPTIONS https://example.com/wp-json/wp/v2/posts | jq '.endpoints[]'

# 4. Autenticarse y leer contenido (con _fields para no traer de más)
curl -u "ai-agent:XXXX XXXX XXXX XXXX XXXX XXXX XXXX" \
     "https://example.com/wp-json/wp/v2/posts?per_page=5&_fields=id,title,status"

# 5. Crear contenido
curl -u "ai-agent:XXXX XXXX XXXX XXXX XXXX XXXX XXXX" \
     -X POST https://example.com/wp-json/wp/v2/posts \
     -H "Content-Type: application/json" \
     -d '{"title":"Post creado por IA","content":"<p>Contenido</p>","status":"draft"}'

# 6. Subir una imagen
curl -u "ai-agent:XXXX XXXX XXXX XXXX XXXX XXXX XXXX" \
     -X POST https://example.com/wp-json/wp/v2/media \
     -H "Content-Disposition: attachment; filename=imagen.jpg" \
     -H "Content-Type: image/jpeg" \
     --data-binary @imagen.jpg

En seis peticiones, la IA ha descubierto la API, entendido su estructura, leído contenido, creado un post y subido una imagen. Sin MCP. Sin plugins adicionales. Sin proxies locales. Solo HTTP.

Entonces, ¿para qué existe MCP en WordPress?

No quiero decir que MCP sea inútil. Tiene su lugar. Pero es importante entender qué añade realmente sobre la REST API directa:

MCP aporta valor cuando:

  • Curación explícita. Quieres exponer solo un subconjunto de operaciones, no todo lo que el rol permite. Con la REST API, el rol define el límite máximo; con MCP, puedes hacer whitelist de operaciones concretas.
  • Compatibilidad con clientes MCP-only. Claude Desktop, por ejemplo, solo habla MCP. Si quieres que Claude Desktop interactúe con tu WordPress, necesitas un MCP server (o un proxy).
  • Operaciones que no son REST. El mcp-adapter convierte «Abilities» (funciones internas de plugins) en tools MCP. Si un plugin no expone endpoints REST pero sí registra Abilities, MCP es el puente.
  • Estandarización multi-plataforma. MCP es un estándar que funciona para cualquier backend, no solo WordPress. Si tienes un ecosistema con múltiples sistemas (WordPress + Shopify + una API custom), MCP da uniformidad.

La REST API directa es suficiente cuando:

  • La IA ya puede hacer peticiones HTTP (que es básicamente todas las IAs agénticas modernas: Claude con tool use, GPT con function calling, etc.)
  • Quieres zero-dependency: sin plugins extra, sin proxies, sin configuración MCP
  • Operas muchos sitios y no quieres mantener un MCP server por cada uno
  • El principio de least privilege ya está garantizado por el rol del usuario
  • Quieres que la IA descubra dinámicamente qué puede hacer, en lugar de darle una lista estática de tools

Tabla comparativa rápida

AspectoREST API directaMCP server
DiscoveryNativo (/wp-json/, OPTIONS)Requiere definir tools explícitamente
AutenticaciónApplication Passwords (nativo)Normalmente reusa Application Passwords
AutorizaciónCapabilities de WordPressDepende del rol subyacente + whitelist MCP
Infraestructura extraNingunaServidor MCP (proceso, hosting, mantenimiento)
Escala a N sitiosTrivial (mismo cliente HTTP)Un servidor MCP por sitio o gateway centralizado
Compatible con Claude Desktop / clientes MCP-onlyNo, directamente
Curva de aprendizajeHTTP estándarProtocolo adicional sobre HTTP
Rate limiting / auditoríaHay que añadirlo tú (nginx, hooks)Igual, no lo resuelve por sí mismo

La paradoja del MCP Adapter

Hay un detalle que me parece revelador. El mcp-adapter oficial de WordPress, para autenticar las peticiones MCP… usa Application Passwords. Y para autorizar… respeta las capabilities nativas de WordPress. Es decir, MCP no añade ni autenticación ni autorización: usa exactamente lo mismo que usarías con la REST API directa.

Lo que MCP añade es una capa de traducción de protocolo (stdio → HTTP) y una capa de curación (whitelist de Abilities). Para muchos casos de uso, esa traducción es innecesaria: la IA ya habla HTTP.

API REST o MCP, según

Antes de instalar un MCP server en tu WordPress, hazte una pregunta: ¿mi IA no puede simplemente hacer GET /wp-json/ y descubrirlo todo por sí misma?

La REST API de WordPress es:

  • Auto-descubrible (discovery via headers, índice en /wp-json/, schema via OPTIONS, hipermedia via _links)
  • Segura (Application Passwords nativas, revocables, con audit trail básico: ampliable con hooks propios)
  • Granular (capabilities nativas de WordPress, roles personalizados, least privilege real)
  • Universal (HTTP + JSON, cualquier cliente, cualquier lenguaje)
  • Zero-dependency (viene en el core, sin plugins adicionales)
  • Escalable operacionalmente (rate limiting a nivel de proxy, mismo patrón para 1 sitio o para 200)

MCP es una capa de abstracción útil para el ecosistema de clientes que solo hablan ese protocolo, o cuando necesitas curar explícitamente qué puede tocar la IA más allá de lo que ya limita el rol. Pero si tu IA ya puede hacer HTTP (y todas las IAs agénticas modernas pueden) la REST API de WordPress ya es todo lo que necesitas para la mayoría de casos.

A veces la mejor arquitectura es la que ya existe. La otra mitad del trabajo, la que no viene gratis con el core, es la que rara vez se menciona: rate limiting, auditoría y CORS. Ahí es donde de verdad se nota si conoces WordPress o solo su REST API.

Comments

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *