La Mayoría de MCP Servers Mueren en Staging
Construyes tu primer MCP server. Funciona perfecto en local.
Lo conectas a Claude. Las tools responden. Te emociona.
Luego lo subes a producción y *se rompe todo*.
Timeouts silenciosos. Tools que no se registran. Contexto que se corrompe entre llamadas.
El problema real del Model Context Protocol no es la especificación. Es que nadie te enseña a construir servidores que sobrevivan tráfico real.
Este artículo lo hace.
---
El Malentendido Fundamental sobre MCP
La mayoría cree que MCP es un sistema para "darle herramientas a Claude".
No lo es.
*El real MCP no es un sistema de tools. Es un protocolo de contexto con herramientas como ciudadanos de primera clase.*
La diferencia importa en producción.
Un sistema de tools ejecuta funciones. Un protocolo de contexto mantiene estado coherente entre el modelo, tus datos, y el entorno — con tools, resources, y prompts como primitivas coordinadas.
Si solo construyes tools, estás usando el 30% de la especificación.
Los otros dos pilares — Resources y Prompts — son los que hacen que Claude razone bien con tu dominio específico.
---
Arquitectura de un MCP Server Robusto
Antes de escribir código, define tres cosas:
→ ¿Qué tools ejecuta tu servidor? (acciones con side effects)
→ ¿Qué resources expone? (datos que Claude puede leer y razonar)
→ ¿Qué prompts inyecta? (instrucciones de dominio pre-construidas)
La mayoría solo implementa tools. Ahí empieza el problema.
1. Setup Correcto con el SDK Oficial
Usa `@modelcontextprotocol/sdk`. No construyas el protocolo a mano.
```typescript
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: "mi-servidor-produccion",
version: "1.0.0",
});
// Transport por stdio — el más estable para Claude Desktop
const transport = new StdioServerTransport();
await server.connect(transport);
```
Por qué `stdio` y no HTTP por defecto: stdio elimina la complejidad de autenticación en el primer deploy. HTTP con SSE viene después, cuando ya tienes el servidor estabilizado.
2. Tools con Error Handling Real
❌ Lo que hace el 90% de developers:
```typescript
server.tool("buscar_usuario", { id: z.string() }, async ({ id }) => {
const user = await db.findUser(id);
return { content: [{ type: "text", text: JSON.stringify(user) }] };
});
```
Esto falla silenciosamente. Claude recibe un error de runtime como respuesta vacía y no sabe qué pasó.
✅ La implementación correcta:
```typescript
server.tool(
"buscar_usuario",
{
description: "Busca un usuario por ID en la base de datos. Devuelve null si no existe.",
inputSchema: { id: z.string().min(1).describe("UUID del usuario") },
},
async ({ id }) => {
try {
const user = await db.findUser(id);
if (!user) {
return {
content: [{ type: "text", text: `Usuario ${id} no encontrado` }],
isError: false, // No es error — es resultado válido
};
}
return {
content: [{
type: "text",
text: JSON.stringify(user, null, 2),
}],
};
} catch (error) {
// Error real — Claude necesita saberlo
return {
content: [{
type: "text",
text: `Error al buscar usuario: ${error instanceof Error ? error.message : "desconocido"}`,
}],
isError: true,
};
}
}
);
```
La clave es `isError: true`. Claude distingue entre "no hay datos" y "algo falló" — y razona diferente en cada caso.
---
Resources: El Pilar que Nadie Implementa
Las tools ejecutan acciones. Los Resources exponen datos de lectura.
Piénsalo así: una tool es `POST /api/action`. Un resource es `GET /api/data`.
Pero la diferencia real es semántica: los resources son contexto persistente que Claude puede consultar sin ejecutar side effects.
```typescript
// Resource estático: documentación de tu dominio
server.resource(
"esquema-base-datos",
"db://schema/tables",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "text/markdown",
text: `# Esquema de Base de Datos\n\n## Tabla: usuarios\n- id: UUID (PK)\n- email: VARCHAR(255)\n- created_at: TIMESTAMP\n\n## Tabla: pedidos\n- id: UUID (PK)\n- usuario_id: UUID (FK)\n- estado: ENUM('pendiente', 'enviado', 'entregado')`,
}],
})
);
// Resource dinámico: datos en tiempo real
server.resource(
"metricas-actuales",
new ResourceTemplate("metrics://{periodo}", { list: undefined }),
async (uri, { periodo }) => {
const data = await fetchMetrics(periodo as string);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(data),
}],
};
}
);
```
Cuando Claude tiene acceso al esquema de tu base de datos como resource, genera queries correctas en el primer intento. Sin el resource, adivina. La diferencia en precisión es drástica.
---
Prompts: Instrucciones de Dominio Pre-construidas
Los Prompts en MCP no son system prompts globales. Son plantillas reutilizables que el usuario puede invocar explícitamente.
```typescript
server.prompt(
"analizar-pedido",
{ pedido_id: z.string() },
async ({ pedido_id }) => {
const pedido = await db.getOrder(pedido_id);
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Analiza el siguiente pedido e identifica posibles problemas:\n\n${JSON.stringify(pedido, null, 2)}\n\nRevisa: estado de pago, dirección de envío válida, stock disponible.`,
},
},
],
};
}
);
```
Esto es lo que diferencia un MCP server genérico de uno especializado en tu dominio. Los prompts encapsulan el conocimiento experto de tu equipo.
---
Los 3 Errores que Matan MCP en Producción
Error #1: Tools sin descriptions
Claude elige qué tool llamar basándose en la descripción. Sin descripción, elige mal.
❌ `server.tool("get_data", { id: z.string() }, handler)`
✅ `server.tool("get_data", { description: "Obtiene datos de cliente por ID. Incluye historial de compras y preferencias.", inputSchema: { id: z.string() } }, handler)`
Error #2: Serializar objetos complejos directamente
MCP transporta texto. Si devuelves un objeto con referencias circulares o funciones, el transporte silently falla.
Siempre `JSON.stringify()` con manejo de errores antes de devolver datos complejos.
Error #3: No validar inputs con Zod
Claude puede llamar tus tools con datos malformados — especialmente cuando está en cadenas largas de razonamiento. Zod intercepta eso antes de que llegue a tu base de datos.
```typescript
// Zod valida automáticamente antes de ejecutar el handler
const schema = {
email: z.string().email(),
limite: z.number().min(1).max(100).default(10),
filtros: z.array(z.string()).optional(),
};
```
---
Deploy: De Local a Producción
Para Claude Desktop (desarrollo local):
```json
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"mi-servidor": {
"command": "node",
"args": ["/ruta/absoluta/a/tu/servidor/build/index.js"],
"env": {
"DATABASE_URL": "postgresql://localhost/midb",
"API_KEY": "tu-api-key"
}
}
}
}
```
Para producción con múltiples clientes, el transporte cambia a HTTP con SSE:
```typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
const transports: Record<string, SSEServerTransport> = {};
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports[transport.sessionId] = transport;
const server = createMcpServer(); // Tu factory function
await server.connect(transport);
req.on("close", () => {
delete transports[transport.sessionId];
});
});
app.post("/messages", async (req, res) => {
const { sessionId } = req.query;
const transport = transports[sessionId as string];
if (transport) await transport.handlePostMessage(req, res);
});
app.listen(3000);
```
Esta arquitectura aguanta múltiples sesiones concurrentes. Cada cliente tiene su propio transport aislado.
---
Takeaways Clave
→ MCP no es solo tools. Resources y Prompts son igual de críticos para producción.
→ `isError: true` es obligatorio en error handling — Claude razona diferente cuando sabe que algo falló.
→ Descriptions en todas las tools. Sin descripción, Claude elige mal. Siempre.
→ Zod en todos los inputs. Valida antes de tocar tu base de datos.
→ stdio para desarrollo, SSE para producción multi-cliente. No mezcles los contextos.
→ Los Resources exponen tu esquema de datos. Claude genera código correcto cuando conoce tu dominio.
El model context protocol guide que la mayoría lee te enseña a conectar una tool. Este te enseña a construir sistemas que Claude puede usar para razonar de verdad.
La siguiente frontera de MCP no es más tools — es servers que exponen contexto tan rico que Claude deja de adivinar y empieza a saber.
Lee el artículo completo en brianmenagomez.com

