Servidor MCP para tu CMS: Deja que los Agentes IA Lean y Escriban Contenido
Pasé los últimos tres meses construyendo servidores MCP para proyectos de clientes, y ahora estoy convencido de que así es como se accederá a todos los CMS en dos años. No a través de paneles de control. No a través de APIs REST que los humanos llaman manualmente. A través de agentes de IA que entienden tu modelo de contenido y pueden leer, crear y actualizar contenido en tu nombre.
El Protocolo de Contexto de Modelo (MCP) es un estándar abierto que Anthropic lanzó a finales de 2024. Proporciona a los modelos de IA una forma estructurada de interactuar con herramientas y fuentes de datos externas. Piénsalo como un adaptador universal entre un LLM y... cualquier cosa. Tu base de datos. Tu sistema de archivos. Tu CMS.
Este artículo te guía a través de la construcción de servidores MCP para tres backends de CMS populares: Payload CMS, Supabase (utilizado como un CMS headless) y WordPress. Cubriremos la arquitectura, el código real y los problemas en los que me tropecé en el camino.
Tabla de Contenidos
- ¿Qué es MCP y por qué debería importarte
- Arquitectura MCP para integración CMS
- Construir un servidor MCP para Payload CMS
- Construir un servidor MCP para Supabase
- Construir un servidor MCP para WordPress
- Comparación de los tres enfoques
- Consideraciones de seguridad
- Casos de uso en el mundo real
- Preguntas frecuentes
¿Qué es MCP y por qué debería importarte
El Protocolo de Contexto de Modelo es esencialmente un protocolo basado en JSON-RPC 2.0 que define cómo los modelos de IA se comunican con sistemas externos. Antes de MCP, si querías que Claude o GPT interactuara con tu CMS, tendrías que construir una integración de llamada de función personalizada para cada modelo. Esquemas diferentes, flujos de autenticación diferentes, todo diferente.
MCP lo estandariza. Construyes un servidor, y cualquier cliente compatible con MCP (Claude Desktop, Cursor, Cline, agentes personalizados) puede conectarse a él.
Aquí está lo que hace esto interesante específicamente para el trabajo con CMS:
- Los equipos de contenido pueden pedir a una IA que "encuentre todas las publicaciones de blog publicadas el mes pasado que mencionen React" y obtener resultados reales de su CMS actual
- Los desarrolladores pueden hacer que su asistente de código cree y actualice entradas de contenido sin salir de su editor
- Los flujos editoriales pueden automatizarse -- un agente de IA que redacta contenido, lo verifica contra tu guía de estilo y lo publica a través de tu flujo de trabajo existente
El protocolo define tres primitivos:
- Herramientas -- Funciones que la IA puede llamar (como
create_post,update_entry,search_content) - Recursos -- Datos que la IA puede leer (como tu esquema de contenido, publicaciones recientes, biblioteca de medios)
- Indicaciones -- Plantillas de indicaciones reutilizables para operaciones comunes
Para la integración de CMS, trabajarás principalmente con herramientas y recursos.
Arquitectura MCP para integración CMS
Antes de escribir código, entendamos la arquitectura. Un servidor MCP se sitúa entre el cliente de IA y tu CMS:
[Cliente de IA] <--Protocolo MCP--> [Servidor MCP] <--REST/GraphQL/SDK--> [Tu CMS]
(Claude) (stdio/SSE) (Node.js) (HTTP/BD) (Payload/WP/Supabase)
El servidor MCP expone herramientas y recursos que se asignan a operaciones de tu CMS. La capa de transporte puede ser:
- stdio -- Para desarrollo local, el cliente de IA genera el servidor MCP como un subproceso
- SSE (Server-Sent Events) -- Para servidores remotos, utiliza HTTP con SSE para transmisión
- HTTP transmisible -- La opción de transporte más nueva en la revisión de especificación 2025
Para servidores CMS, recomiendo comenzar con stdio para desarrollo y pasar a SSE o HTTP transmisible para implementaciones de producción donde el servidor MCP se ejecuta en tu infraestructura junto al CMS.
El esqueleto del servidor básico
Cada servidor MCP sigue el mismo patrón. Aquí está el esqueleto en TypeScript utilizando el @modelcontextprotocol/sdk oficial:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-cms-mcp",
version: "1.0.0",
});
// Define una herramienta
server.tool(
"search_content",
"Buscar entradas de contenido por consulta",
{
query: z.string().describe("Consulta de búsqueda"),
collection: z.string().describe("Colección/tipo de publicación a buscar"),
limit: z.number().optional().default(10),
},
async ({ query, collection, limit }) => {
// Tu lógica específica del CMS aquí
const results = await searchCMS(query, collection, limit);
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
}
);
// Define un recurso
server.resource(
"schema",
"cms://schema",
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(await getCMSSchema()),
},
],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Esa es la base. Ahora vamos a construir implementaciones reales.
Construir un servidor MCP para Payload CMS
Payload es mi CMS favorito para proyectos headless (y lo usamos mucho en nuestro trabajo de desarrollo de CMS headless). Es nativo de TypeScript, tiene una gran API, y su arquitectura basada en colecciones se asigna muy bien a las herramientas MCP.
Configurar el servidor MCP de Payload
Primero, instala las dependencias:
mkdir payload-mcp-server && cd payload-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Aquí está la implementación completa. La API REST de Payload es lo suficientemente limpia como para que esto sea sorprendentemente sencillo:
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const PAYLOAD_URL = process.env.PAYLOAD_URL || "http://localhost:3000";
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || "";
const headers = {
"Content-Type": "application/json",
Authorization: `users API-Key ${PAYLOAD_API_KEY}`,
};
async function payloadFetch(path: string, options: RequestInit = {}) {
const res = await fetch(`${PAYLOAD_URL}/api${path}`, {
...options,
headers: { ...headers, ...options.headers },
});
if (!res.ok) {
throw new Error(`Payload API error: ${res.status} ${await res.text()}`);
}
return res.json();
}
const server = new McpServer({
name: "payload-cms-mcp",
version: "1.0.0",
});
// Listar todas las colecciones
server.tool(
"list_collections",
"Listar todas las colecciones disponibles de Payload CMS",
{},
async () => {
// Payload no tiene un punto final de meta para esto por defecto,
// así que golpeamos la configuración o codificamos colecciones conocidas
const collections = ["posts", "pages", "media", "categories"];
return {
content: [{ type: "text", text: JSON.stringify(collections) }],
};
}
);
// Buscar/listar documentos en una colección
server.tool(
"find_documents",
"Encontrar documentos en una colección de Payload con filtrado opcional",
{
collection: z.string().describe("Slug de colección (por ejemplo, 'posts', 'pages')"),
where: z.string().optional().describe("Consulta where de Payload como cadena JSON"),
limit: z.number().optional().default(10),
page: z.number().optional().default(1),
sort: z.string().optional().describe("Campo por el que ordenar, prefijo con - para descendente"),
},
async ({ collection, where, limit, page, sort }) => {
const params = new URLSearchParams();
params.set("limit", String(limit));
params.set("page", String(page));
if (sort) params.set("sort", sort);
if (where) {
try {
const whereObj = JSON.parse(where);
// Convertir al formato de cadena de consulta de Payload
for (const [key, val] of Object.entries(whereObj)) {
if (typeof val === "object" && val !== null) {
for (const [op, v] of Object.entries(val as Record<string, unknown>)) {
params.set(`where[${key}][${op}]`, String(v));
}
} else {
params.set(`where[${key}][equals]`, String(val));
}
}
} catch {
return { content: [{ type: "text", text: "JSON where inválido" }] };
}
}
const data = await payloadFetch(`/${collection}?${params}`);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Obtener un solo documento
server.tool(
"get_document",
"Obtener un solo documento por ID de una colección de Payload",
{
collection: z.string(),
id: z.string().describe("ID del documento"),
},
async ({ collection, id }) => {
const data = await payloadFetch(`/${collection}/${id}`);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Crear un documento
server.tool(
"create_document",
"Crear un nuevo documento en una colección de Payload",
{
collection: z.string(),
data: z.string().describe("Datos del documento como cadena JSON"),
},
async ({ collection, data }) => {
const result = await payloadFetch(`/${collection}`, {
method: "POST",
body: data,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
// Actualizar un documento
server.tool(
"update_document",
"Actualizar un documento existente en una colección de Payload",
{
collection: z.string(),
id: z.string(),
data: z.string().describe("Datos parciales del documento como cadena JSON"),
},
async ({ collection, id, data }) => {
const result = await payloadFetch(`/${collection}/${id}`, {
method: "PATCH",
body: data,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Conectarse a Claude Desktop
Agrega esto a tu configuración de Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json en Mac):
{
"mcpServers": {
"payload-cms": {
"command": "npx",
"args": ["tsx", "/path/to/payload-mcp-server/src/index.ts"],
"env": {
"PAYLOAD_URL": "http://localhost:3000",
"PAYLOAD_API_KEY": "your-api-key-here"
}
}
}
}
Reinicia Claude Desktop, y ahora puedes pedirle cosas como "Encuentra todas las publicaciones en borrador en mi CMS" o "Crea una nueva publicación de blog sobre servidores MCP".
Construir un servidor MCP para Supabase
Supabase es interesante porque no es un CMS de fábrica -- es una base de datos Postgres con una API REST. Pero muchos equipos lo usan como uno, especialmente con paneles de administración personalizados construidos en Next.js (algo que hacemos regularmente en nuestra práctica de desarrollo Next.js).
El servidor MCP de Supabase es en realidad el más poderoso de los tres porque obtienes acceso directo a SQL.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createClient } from "@supabase/supabase-js";
import { z } from "zod";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Usar rol de servicio para lado del servidor
);
const server = new McpServer({
name: "supabase-cms-mcp",
version: "1.0.0",
});
// Listar todas las tablas (tipos de contenido)
server.tool(
"list_tables",
"Listar todas las tablas en el esquema público",
{},
async () => {
const { data, error } = await supabase.rpc("get_tables");
// Necesitarás una función Postgres para esto, o usa:
const { data: tables } = await supabase
.from("information_schema.tables" as any)
.select("table_name")
.eq("table_schema", "public");
return {
content: [{ type: "text", text: JSON.stringify(tables, null, 2) }],
};
}
);
// Consultar contenido con filtros
server.tool(
"query_content",
"Consultar filas de una tabla con filtros opcionales",
{
table: z.string().describe("Nombre de la tabla"),
select: z.string().optional().default("*").describe("Columnas a seleccionar"),
filters: z.array(z.object({
column: z.string(),
operator: z.enum(["eq", "neq", "gt", "lt", "gte", "lte", "like", "ilike", "is"]),
value: z.string(),
})).optional().describe("Matriz de condiciones de filtro"),
limit: z.number().optional().default(20),
orderBy: z.string().optional(),
ascending: z.boolean().optional().default(false),
},
async ({ table, select, filters, limit, orderBy, ascending }) => {
let query = supabase.from(table).select(select).limit(limit);
if (filters) {
for (const f of filters) {
query = query.filter(f.column, f.operator, f.value);
}
}
if (orderBy) {
query = query.order(orderBy, { ascending });
}
const { data, error } = await query;
if (error) {
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
}
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Insertar contenido
server.tool(
"insert_row",
"Insertar una nueva fila en una tabla",
{
table: z.string(),
data: z.string().describe("Datos de fila como cadena JSON"),
},
async ({ table, data: rowData }) => {
const parsed = JSON.parse(rowData);
const { data, error } = await supabase.from(table).insert(parsed).select();
if (error) {
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
}
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// Búsqueda de texto completo (la característica asesina de Supabase para uso de CMS)
server.tool(
"full_text_search",
"Realizar búsqueda de texto completo en una columna de tabla",
{
table: z.string(),
column: z.string().describe("Columna con índice tsvector"),
query: z.string().describe("Consulta de búsqueda"),
limit: z.number().optional().default(10),
},
async ({ table, column, query, limit }) => {
const { data, error } = await supabase
.from(table)
.select()
.textSearch(column, query)
.limit(limit);
if (error) {
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
}
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Lo que me encanta del enfoque de Supabase: porque Supabase expone tu esquema Postgres, puedes crear un recurso que proporcione a la IA tu definición de esquema completa. La IA entonces sabe exactamente qué campos existen, qué tipos tienen y cómo se relacionan las tablas entre sí. Esto hace que sus consultas sean mucho más precisas.
Construir un servidor MCP para WordPress
WordPress. Aún impulsando el 43% de la web en 2025. Te guste o no, vas a encontrarlo.
La API REST de WordPress es en realidad sólida para la integración de MCP. La parte complicada es la autenticación -- WordPress tiene como cinco métodos de autenticación diferentes y ninguno de ellos es excelente.
Para servidores MCP, recomiendo contraseñas de aplicación (incorporadas en WordPress desde 5.6):
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const WP_URL = process.env.WP_URL!;
const WP_USER = process.env.WP_USER!;
const WP_APP_PASSWORD = process.env.WP_APP_PASSWORD!;
const authHeader = `Basic ${Buffer.from(`${WP_USER}:${WP_APP_PASSWORD}`).toString("base64")}`;
async function wpFetch(endpoint: string, options: RequestInit = {}) {
const res = await fetch(`${WP_URL}/wp-json/wp/v2${endpoint}`, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
...options.headers,
},
});
if (!res.ok) {
throw new Error(`WP API error: ${res.status} ${await res.text()}`);
}
return res.json();
}
const server = new McpServer({
name: "wordpress-mcp",
version: "1.0.0",
});
server.tool(
"search_posts",
"Buscar publicaciones de WordPress",
{
search: z.string().optional().describe("Consulta de búsqueda"),
status: z.enum(["publish", "draft", "pending", "private", "any"]).optional().default("any"),
per_page: z.number().optional().default(10),
categories: z.array(z.number()).optional(),
tags: z.array(z.number()).optional(),
after: z.string().optional().describe("Fecha ISO 8601 -- solo publicaciones después de esta fecha"),
before: z.string().optional().describe("Fecha ISO 8601 -- solo publicaciones antes de esta fecha"),
},
async (params) => {
const searchParams = new URLSearchParams();
if (params.search) searchParams.set("search", params.search);
if (params.status) searchParams.set("status", params.status);
searchParams.set("per_page", String(params.per_page));
if (params.categories) searchParams.set("categories", params.categories.join(","));
if (params.tags) searchParams.set("tags", params.tags.join(","));
if (params.after) searchParams.set("after", params.after);
if (params.before) searchParams.set("before", params.before);
const posts = await wpFetch(`/posts?${searchParams}`);
// Eliminar HTML del contenido para un consumo por IA más limpio
const cleaned = posts.map((p: any) => ({
id: p.id,
title: p.title.rendered,
slug: p.slug,
status: p.status,
date: p.date,
excerpt: p.excerpt.rendered.replace(/<[^>]*>/g, ""),
content: p.content.rendered.replace(/<[^>]*>/g, ""),
categories: p.categories,
tags: p.tags,
}));
return {
content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
};
}
);
server.tool(
"create_post",
"Crear una nueva publicación en WordPress",
{
title: z.string(),
content: z.string().describe("Contenido de la publicación en HTML"),
status: z.enum(["publish", "draft", "pending"]).optional().default("draft"),
categories: z.array(z.number()).optional(),
tags: z.array(z.number()).optional(),
excerpt: z.string().optional(),
},
async (params) => {
const result = await wpFetch("/posts", {
method: "POST",
body: JSON.stringify(params),
});
return {
content: [{
type: "text",
text: `Publicación creada "${result.title.rendered}" (ID: ${result.id}, Estado: ${result.status})`,
}],
};
}
);
server.tool(
"update_post",
"Actualizar una publicación de WordPress existente",
{
id: z.number().describe("ID de la publicación"),
title: z.string().optional(),
content: z.string().optional(),
status: z.enum(["publish", "draft", "pending", "trash"]).optional(),
excerpt: z.string().optional(),
},
async ({ id, ...updates }) => {
const result = await wpFetch(`/posts/${id}`, {
method: "POST",
body: JSON.stringify(updates),
});
return {
content: [{
type: "text",
text: `Publicación actualizada "${result.title.rendered}" (ID: ${result.id})`,
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
El mayor problema con MCP de WordPress: el contenido regresa como HTML renderizado. Querrás eliminar las etiquetas para la ventana de contexto de la IA o quemarás tokens en etiquetas <p>. Aprendí esto de la manera difícil después de ver a Claude quemar 50k tokens leyendo cinco publicaciones de blog.
Comparación de los tres enfoques
| Característica | Payload CMS | Supabase | WordPress |
|---|---|---|---|
| Complejidad de autenticación | Simple (claves API) | Simple (clave de rol de servicio) | Media (contraseñas de aplicación) |
| Conciencia de esquema | Buena (colecciones tipadas) | Excelente (esquema Postgres completo) | Pobre (sin punto final de esquema) |
| Formato de contenido | JSON limpio | JSON limpio | Pesado en HTML, requiere sanitización |
| Soporte de escritura | Excelente | Excelente | Bueno (algunas rarezas con bloques) |
| Manejo de medios | Carga vía API | API de almacenamiento | Punto final de medios REST |
| Flexibilidad de consulta | Buena (consultas de Payload) | Excelente (filtrado a nivel SQL) | Limitada (parámetros de WP_Query) |
| Opción autohospedada | Sí | Sí (o cloud) | Sí |
| Tiempo de configuración típico | 1-2 horas | 1-2 horas | 2-3 horas |
| Mejor para | Proyectos headless modernos | Estructuras de contenido personalizadas | Sitios WP existentes |
Consideraciones de seguridad
Esta es la sección que casi olvido escribir, y es posiblemente la más importante.
Cuando le das a un agente de IA acceso de escritura a tu CMS, lo confías en que no borre todo. Aquí está cómo no quemarse:
Principio de menor privilegio
Crea un usuario/clave de API dedicado para el servidor MCP con permisos mínimos. En Payload, crea un rol que solo pueda acceder a colecciones específicas. En Supabase, usa políticas de Row Level Security. En WordPress, crea un usuario con el rol Autor, no Administrador.
Agregar confirmación para operaciones destructivas
Siempre agrego un parámetro dryRun a herramientas de crear/actualizar/eliminar. Cuando se establece en verdadero, la herramienta devuelve lo que sucedería sin hacerlo realmente. La IA puede entonces confirmar con el usuario antes de proceder.
server.tool(
"delete_document",
"Eliminar un documento (¡usa dryRun primero!)",
{
collection: z.string(),
id: z.string(),
dryRun: z.boolean().default(true).describe("Si es verdadero, solo vista previa de la eliminación"),
},
async ({ collection, id, dryRun }) => {
const doc = await payloadFetch(`/${collection}/${id}`);
if (dryRun) {
return {
content: [{
type: "text",
text: `EJECUCIÓN EN SECO: Eliminaría "${doc.title}" (${id}) de ${collection}`,
}],
};
}
await payloadFetch(`/${collection}/${id}`, { method: "DELETE" });
return {
content: [{ type: "text", text: `Eliminado "${doc.title}" (${id})` }],
};
}
);
Limitación de velocidad y registro de auditoría
Registra cada invocación de herramienta. En serio. Querrás saber qué hizo tu agente de IA a las 2 AM cuando tu editor de contenido pregunta por qué hay 47 nuevas publicaciones en borrador sobre pingüinos.
Nunca expongas credenciales de base de datos directamente
El servidor MCP es el límite. La IA nunca ve tu contraseña de base de datos o clave de API -- solo ve las herramientas que expones.
Casos de uso en el mundo real
Aquí están los patrones que he visto funcionar bien en producción:
Migración de contenido: Un agente de IA que lee contenido de un sitio WordPress heredado, lo transforma para que coincida con tu nuevo esquema de Payload y crea entradas en el nuevo CMS. Hicimos esto para un cliente que migraba ~3,000 publicaciones. Lo que habría tardado semanas de trabajo manual tardó un día y medio con revisión humana.
Auditoría de SEO: Una IA que lee todo el contenido publicado, verifica descripciones meta faltantes, contenido delgado y enlaces internos rotos. Escribe sus hallazgos de vuelta como un informe estructurado en el CMS.
Actualizaciones de contenido masivo: "Cambia todas las referencias de 'nuestro nombre de marca antiguo' a 'nuestro nombre de marca nuevo' en todas las páginas publicadas". El agente lee, encuentra, propone cambios y actualiza con aprobación humana.
Gestión del calendario de contenido: Un agente que conoce tu cronograma de publicación, verifica qué está en borrador y avisa al equipo sobre próximos plazos -- todo leyendo datos del CMS.
Si estás construyendo algo como esto y quieres ayuda, tenemos experiencia profunda con las tres plataformas en nuestra práctica de CMS headless. También puedes consultar nuestra página de precios para tener una idea de lo que cuesta un proyecto como este.
Preguntas frecuentes
¿Qué es el Protocolo de Contexto de Modelo (MCP)? MCP es un protocolo abierto creado por Anthropic que estandariza cómo los modelos de IA interactúan con fuentes de datos y herramientas externas. Utiliza JSON-RPC 2.0 y define una arquitectura cliente-servidor donde las aplicaciones de IA (clientes) se conectan a servidores que exponen herramientas, recursos e indicaciones. Piensa en ello como un puerto USB-C para IA -- una interfaz estándar que se conecta a muchos sistemas diferentes.
¿Puedo usar MCP con ChatGPT o solo con Claude? MCP fue creado por Anthropic, por lo que Claude tiene el mejor soporte nativo a través de Claude Desktop y la API. Sin embargo, el protocolo es abierto y existen clientes MCP para otras plataformas. OpenAI anunció soporte MCP en marzo de 2025. Cursor, Cline, Windsurf y varias otras herramientas para desarrolladores también soportan MCP. Tu servidor funciona con cualquier cliente compatible.
¿Es seguro darles a los agentes de IA acceso de escritura a mi CMS? Puede serlo, con protecciones apropiadas. Utiliza credenciales de API dedicadas con permisos mínimos, implementa modos de ejecución en seco para operaciones destructivas, agrega registro de auditoría y considera requerir aprobación humana para acciones de publicación. Nunca des a un servidor MCP acceso a nivel de administrador. Trata como tratarías cualquier integración de terceros -- con límites de confianza apropiados.
¿Necesito alojar el servidor MCP separado de mi CMS? Para transporte stdio (desarrollo local), el servidor MCP se ejecuta como un subproceso en tu máquina local. Para producción con acceso remoto, necesitarás alojarlo en algún lugar -- esto podría ser en el mismo servidor que tu CMS, en un contenedor Docker junto a él, o como un servicio separado. La latencia entre el servidor MCP y tu CMS importa, por lo que colocarlos juntos es ideal.
¿Cómo se compara MCP con simplemente usar la API REST del CMS directamente con llamada de función? MCP agrega una capa de interfaz estándar. Con llamada de función cruda, defines esquemas personalizados por proveedor de IA, manejas la autenticación de manera diferente para cada uno, y reconstruyes cuando los proveedores cambian su API. MCP te proporciona un servidor que funciona con cualquier cliente compatible. También maneja transporte, formato de errores y descubrimiento de recursos de manera estandarizada. Para una integración única, la sobrecarga podría no valer la pena. Para cualquier cosa que reutilices, definitivamente lo es.
¿Qué CMS funciona mejor con MCP? Supabase te proporciona la mayor flexibilidad porque tienes acceso directo a nivel de base de datos y exploración de esquema completa. Payload CMS es excelente porque su enfoque primero TypeScript y API REST limpia se asignan naturalmente a herramientas MCP. WordPress funciona bien pero requiere más sanitización de contenido HTML y tiene una API de consulta menos flexible. La opción "mejor" depende de tu pila existente.
¿Puedo usar MCP para gestionar cargas de medios en mi CMS?
Sí, aunque es un poco más complicado que contenido de texto. Para Payload y WordPress, puedes crear herramientas que acepten datos de archivo codificados en base64 o URLs y usen las respectivas APIs de carga. Para Supabase, usarías la API de almacenamiento. Recomendaría crear herramientas upload_media y attach_media separadas en lugar de intentar manejar cargas de archivo dentro de herramientas de creación de contenido.
¿Cuánto cuesta ejecutar un servidor MCP? El servidor MCP en sí es ligero -- un proceso Node.js simple que usa recursos mínimos. El costo real está en los tokens de IA consumidos cuando tu agente lee y escribe contenido. Una operación típica de contenido podría usar 2,000-10,000 tokens dependiendo de cuánto contenido se está procesando. A los precios actuales de 2025 (Claude Sonnet a $3/$15 por millón de tokens de entrada/salida), eso son fracciones de centavo por operación. El costo de infraestructura para alojar el servidor MCP es insignificante -- un VPS pequeño o instancia de contenedor es más que suficiente.