<!-- Canonical: https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration
Language: es
Content-SHA256: 2626098400b09f9239e1491037f6f9db7cd57e8ca374901a31a8050fdf78bf65
-->

AI/ML

# Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas

2026-03-0810 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration>)

## Arquitectura del Protocolo MCP

[Model Context Protocol](<https://modelcontextprotocol.io/>) es un protocolo basado en JSON-RPC 2.0 que define cómo las aplicaciones de IA (clientes) se comunican con capacidades externas (servidores). Esa es la versión de una oración. La realidad tiene más matices.

El protocolo define tres tipos de primitivas que un servidor puede exponer:

1. **Resources**, Datos de solo lectura que el cliente puede traer al contexto (considerar en archivos, registros de base de datos, respuestas de APIs)
2. **Tools**, Funciones ejecutables que el modelo puede invocar con parámetros
3. **Prompts**, Templates de prompts reutilizables con argumentos

La mayoría solo le da bola a los tools. Ese es un error inicial común. Los resources son arguably más importantes porque te permiten alimentar contexto al modelo *sin* quemar un round trip de tool-call.

Acá va cómo se ve la negociación de capabilities a nivel protocolo:

```json
// Client -> Server (initialize request)
{
 "jsonrpc": "2.0",
 "id": 1,
 "method": "initialize",
 "params": {
 "protocolVersion": "2025-03-26",
 "capabilities": {
 "roots": { "listChanged": true }
 },
 "clientInfo": {
 "name": "mi-agente",
 "version": "1.0.0"
 }
 }
}

// Server -> Client (initialize response)
{
 "jsonrpc": "2.0",
 "id": 1,
 "result": {
 "protocolVersion": "2025-03-26",
 "capabilities": {
 "tools": { "listChanged": true },
 "resources": { "subscribe": true },
 "prompts": { "listChanged": true }
 },
 "serverInfo": {
 "name": "postgres-mcp",
 "version": "0.3.1"
 }
 }
}
```

Observar en el flag de capability ` listChanged `. Este es uno de ese es detalles que nadie menciona, significa que el servidor puede notificar al cliente cuando su lista de tools cambia en runtime. Crítico para ambientes dinámicos donde las herramientas aparecen y desaparecen.

## La Capa de Transporte, Consideraciones Clave

MCP define dos transportes oficiales, y entender cuándo usar cada uno es crítico para evitar incidentes en producción.

### Transporte stdio

La opción más simple. El cliente levanta el servidor como proceso hijo y se comunica por stdin/stdout. Cada mensaje JSON-RPC es una sola línea terminada en ` \n `.

```javascript
import { spawn } from 'child_process';

const server = spawn('node', ['./mi-mcp-server.js'], {
 stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
});

// CRÍTICO: stderr NO es parte del protocolo
// Si tu server hace console.log(), rompe el stream JSON-RPC
server.stdout.on('data', (chunk) => {
 const lines = chunk.toString().split('\n').filter(Boolean);
 for (const line of lines) {
 const message = JSON.parse(line); // Acá explota
 handleMessage(message); // si loggeás a stdout
 }
});
```

Un error frecuente: un ` console.log('Server started') ` en la inicialización del servidor que solo se dispara en producción por una diferencia en la ruta de carga de la versión de Node.js. El parser de JSON se atraganta con ` Server started\n{"jsonrpc":"2.0"... ` y el mensaje de error es completamente inútil: "Unexpected token S in JSON at position 0". Esto puede consumir horas de debugging.

### Transporte Streamable HTTP

Esto es lo que se desea para deployments en producción. El cliente se comunica vía HTTP POST a un único endpoint, y el servidor opcionalmente puede upgradear a Server-Sent Events para respuestas streaming.

```javascript
// Implementación del server con express
import express from 'express';

const app = express();
app.use(express.json());

// Un solo endpoint maneja todo el tráfico MCP
app.post('/mcp', async (req, res) => {
 const message = req.body;

 // Chequear si el cliente acepta SSE
 const acceptsSSE = req.headers.accept?.includes('text/event-stream');

 if (acceptsSSE && isLongRunningTool(message)) {
 // Streamear resultados vía SSE
 res.writeHead(200, {
 'Content-Type': 'text/event-stream',
 'Cache-Control': 'no-cache',
 'Connection': 'keep-alive',
 'Mcp-Session-Id': sessionId
 });

 await streamToolExecution(message, res);
 res.end();
 } else {
 // Respuesta JSON-RPC simple
 const result = await handleMessage(message);
 res.json(result);
 }
});
```

El header ` Mcp-Session-Id ` es como el servidor trackea las sesiones de clientes. Sin él, no es posible implementar tools con estado o subscripciones a resources a través de múltiples requests HTTP.

## Armando un MCP Server de Verdad

A continuación se presenta un servidor MCP de producción, una herramienta de query PostgreSQL que expone tablas de la base como resources y queries de lectura seguras como tools.

```javascript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import pg from 'pg';
import { z } from 'zod';

const pool = new pg.Pool({
 connectionString: process.env.DATABASE_URL,
 max: 5,
 idleTimeoutMillis: 30000
});

const server = new McpServer({
 name: 'postgres-readonly',
 version: '1.0.0'
});

// Exponer schemas de tablas como resources
server.resource(
 'table-schema',
 'schema://tables/{tableName}',
 async (uri, { tableName }) => {
 const result = await pool.query(`
 SELECT column_name, data_type, is_nullable, column_default
 FROM information_schema.columns
 WHERE table_name = $1
 ORDER BY ordinal_position
 `, [tableName]);

 return {
 contents: [{
 uri: uri.href,
 mimeType: 'application/json',
 text: JSON.stringify(result.rows, null, 2)
 }]
 };
 }
);

// Tool: ejecutar SQL de solo lectura
server.tool(
 'query',
 'Ejecutar una query SQL de solo lectura contra la base',
 {
 sql: z.string().describe('Query SQL SELECT a ejecutar'),
 params: z.array(z.unknown()).optional()
 .describe('Valores parametrizados de la query')
 },
 async ({ sql, params }) => {
 // SEGURIDAD: enforce read-only en múltiples niveles
 const normalized = sql.trim().toLowerCase();
 const forbidden = ['insert', 'update', 'delete', 'drop', 'alter',
 'create', 'truncate', 'grant', 'revoke'];

 for (const keyword of forbidden) {
 const regex = new RegExp(`\\b${keyword}\\b`, 'i');
 if (regex.test(normalized)) {
 return {
 content: [{
 type: 'text',
 text: `Rechazado: la query contiene keyword prohibido "${keyword}"`
 }],
 isError: true
 };
 }
 }

 const client = await pool.connect();
 try {
 await client.query('SET TRANSACTION READ ONLY');
 await client.query('BEGIN');

 const result = await client.query(sql, params || []);

 await client.query('COMMIT');

 return {
 content: [{
 type: 'text',
 text: JSON.stringify({
 rowCount: result.rowCount,
 rows: result.rows.slice(0, 100)
 }, null, 2)
 }]
 };
 } catch (err) {
 await client.query('ROLLBACK');
 return {
 content: [{ type: 'text', text: `Error en query: ${err.message}` }],
 isError: true
 };
 } finally {
 client.release();
 }
 }
);

// Template de prompt para patrones de análisis comunes
server.prompt(
 'analyze-table',
 'Generar queries de análisis para una tabla específica',
 { tableName: z.string() },
 ({ tableName }) => ({
 messages: [{
 role: 'user',
 content: {
 type: 'text',
 text: `Analizá la tabla "${tableName}". Primero leé su schema usando el resource table-schema, después ejecutar queries para entender la distribución de datos, tasas de nulls, e identificar anomalías. Enfocate en insights accionables.`
 }
 }]
 })
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

## Las Primitivas en la Práctica

### Resources vs Tools, El Framework de Decisión

Un error común es exponer todo como tools. Acá va un modelo mental útil:

- **Resource**: Datos que el modelo necesita *leer* para tomar decisiones. Pull-based. El modelo decide cuándo fetchear.
- **Tool**: Una *acción* con side effects o computación. Invocado explícitamente por el modelo con parámetros específicos.

Si exponers una tabla de configuración de 200 filas como un tool call que devuelve JSON, el modelo tiene que invocarlo, esperar la respuesta, y después seguir razonando. Si lo exponers como resource, el cliente puede prefetchearlo al contexto *antes* de que el modelo arranque a generar.

### Prompt Templates, La Primitiva Subutilizada

Los prompts en MCP no son solo texto estático. Soportan argumentos y pueden referenciar resources:

```javascript
server.prompt(
 'incident-response',
 'Guiar a través de investigación de incidentes',
 {
 severity: z.enum(['p1', 'p2', 'p3']),
 service: z.string()
 },
 ({ severity, service }) => ({
 messages: [{
 role: 'user',
 content: {
 type: 'text',
 text: `Respuesta a incidente para ${severity.toUpperCase()} en ${service}.\n` +
 `1. Consultá logs de error recientes usando el tool de logs\n` +
 `2. Verificar historial de deploys vía el resource de deploys\n` +
 `3. Evaluá blast radius y sugerí mitigación`
 }
 }]
 })
);
```

Esto es enormemente útil para estandarizar el comportamiento de agentes en tu equipo. En vez de que todos escriban system prompts ligeramente distintos, se define templates de prompts del lado del servidor y son descubribles a través del protocolo.

## Manejo Robusto de Errores

La spec define códigos de error JSON-RPC estándar, pero también agrega unos específicos de MCP. Este es un patrón recomendado:

```javascript
server.tool('deploy', 'Deployar un servicio', { /* ... */ }, async (args) => {
 try {
 const result = await deployService(args);
 return {
 content: [{ type: 'text', text: JSON.stringify(result) }]
 };
 } catch (err) {
 if (err.code === 'RATE_LIMITED') {
 // Devolver como error de tool, el modelo puede reintentar o informar al usuario
 return {
 content: [{
 type: 'text',
 text: `Rate limited. Reintentá después de ${err.retryAfter}s`
 }],
 isError: true
 };
 }
 // Errores inesperados, throw para triggear respuesta de error JSON-RPC
 throw err;
 }
});
```

La distinción entre ` isError: true ` en un resultado de tool vs throwear un error es sutil pero importante. Los errores de tool son "fallas esperadas", el modelo puede razonar sobre ellos. Los errores throweados son fallas a nivel protocolo que típicamente abortan la interacción actual.

## Manejo de Sesiones y Estado

Un detalle importante: las sesiones MCP tienen estado. El header ` Mcp-Session-Id ` no es solo para logging, es cómo el servidor correlaciona subscripciones a resources, trackea qué tools fueron listados, y maneja cleanup.

```javascript
const sessions = new Map();

function getOrCreateSession(sessionId) {
 if (!sessions.has(sessionId)) {
 sessions.set(sessionId, {
 id: sessionId,
 subscriptions: new Set(),
 createdAt: Date.now(),
 lastActivity: Date.now()
 });
 }
 const session = sessions.get(sessionId);
 session.lastActivity = Date.now();
 return session;
}

// Limpiar sesiones viejas cada 5 minutos
setInterval(() => {
 const cutoff = Date.now() - 30 * 60 * 1000; // 30 min TTL
 for (const [id, session] of sessions) {
 if (session.lastActivity < cutoff) {
 session.subscriptions.forEach(sub => sub.unsubscribe());
 sessions.delete(id);
 }
 }
}, 5 * 60 * 1000);
```

## Problemas Frecuentes en Producción

**La calidad de la descripción del tool importa más de lo que considerars.** El modelo usa las descripciones de tools para decidir *cuándo* llamar a un tool. Un tool descrito como "Buscar en la base de datos" se llama para cada pregunta, incluso cuando un resource read más simple alcanza. Cambiarlo a "Ejecutar queries SQL SELECT para filtrado complejo, agregación o joins que no se pueden resolver leyendo resources de tablas directamente" reduce las llamadas innecesarias un 60%.

**El JSON Schema para parámetros de tools tiene que ser preciso.** No uses ` z.any() ` ni ` z.record(z.unknown()) `. Cuanto más restringido sea tu schema, mejor el modelo llena los parámetros. Los modelos alucinan objetos anidados enteros cuando el schema es demasiado permisivo.

**Colocar timeout a las ejecuciones de tus tools.** MCP no define un mecanismo de timeout a nivel protocolo, eso es responsabilidad del servidor. Se recomienda wrappear cada handler de tool:

```javascript
function withTimeout(fn, ms = 30000) {
 return async (...args) => {
 const controller = new AbortController();
 const timer = setTimeout(() => controller.abort(), ms);
 try {
 return await fn(...args, { signal: controller.signal });
 } finally {
 clearTimeout(timer);
 }
 };
}
```

## Hacia Dónde Va Todo Esto

MCP todavía es joven, pero la trayectoria es clara. El protocolo ya tiene buy-in de los grandes vendors de IDEs, frameworks de agentes y proveedores de modelos. La spec está avanzando hacia OAuth 2.1 para autenticación, lo que va a hacer viables los servidores MCP multi-tenant para productos SaaS.

El aspecto más prometedor es el patrón de notificación ` listChanged `. Imaginá un servidor MCP que expone tools dinámicamente basándose en los permise es del usuario, el contexto del proyecto actual, o incluso la competencia demostrada del modelo. El protocolo soporta esto hoy, la mayoría de las implementaciones simplemente no alcanzaron todavía.

Para cualquier tipo de sistema de agentes de IA, construir servidores MCP es preferible a escribir integraciones custom. La inversión se justifica cuando el cliente decide cambiar de modelo por tercera vez.

## Implementación reproducible: contratos e interoperabilidad

[MCP Contract Harness](<https://lucioduran.com/es/projects/mcp-contract-harness>) publica un servidor de solo lectura para MCP 2025-06-18 por stdio. Lo ejercitan un cliente propio, el SDK oficial Python 2.3.0 y el SDK oficial TypeScript 1.32.1. La evidencia conserva los mensajes reales de negociación, paginación, lectura, errores y cancelación.

El cliente propio completó 13 controles y cada SDK completó ocho. Los clientes también verificaron que una cancelación no recibiera respuesta y que un ping posterior sí la recibiera. La ficha enlaza la matriz por cliente, las trazas y los comandos de reproducción. Que initialize conteste es el comienzo de una integración, no su certificado de funcionamiento. Este proyecto cubre un subconjunto stdio; no afirma conformidad MCP completa ni compatibilidad probada con hosts gráficos.

## Preguntas frecuentes

### ¿Qué es el Model Context Protocol (MCP) y por qué importa?

MCP es un protocolo abierto que estandariza cómo las aplicaciones de IA se conectan con herramientas externas, fuentes de datos y servicios. En vez de escribir integraciones custom para cada proveedor de LLM, implementás un servidor MCP y cualquier cliente compatible lo consume.

### ¿En qué se diferencia MCP del function calling de OpenAI?

Function calling es una feature específica del proveedor embebida en la API de chat completion. MCP es un protocolo agnóstico al transporte que funciona entre proveedores, soporta recurse es persistentes, templates de prompts y comunicación bidireccional, no solo tool calls request-response.

### ¿Puedo usar MCP con modelos que no sean Claude?

Sí. MCP es una especificación abierta. Si bien Anthropic lo creó, cualquier modelo o cliente puede implementar el protocolo. La spec está basada en JSON-RPC 2.0 y es agnóstica al transporte.

## Tips

1. **Tip:** Siempre implementar timeout en el handshake ` initialize ` del lado del cliente, un servidor MCP colgado bloquea todo el loop del agente sin hacer ruido. Un máximo recomendado es 8 segundos con un ` AbortController `.
2. **Tip:** Usar ` streamable-http ` para producción y ` stdio ` solo para dev local. Con stdio los crash logs se mezclan en el stream JSON-RPC y se pueden pasar horas debuggeando errores de parseo fantasma.
3. **Tip:** Poner namespace a los nombres de tools estilo reverse-domain como ` com.miservicio.query_users `, cuando hay 4+ servidores MCP conectados, nombres genéricos tipo ` search ` colisionan y el LLM elige el equivocado la mitad de las veces.

- mcp
- integración-ia
- llm-tooling
- anthropic
- diseño-protocolos
- json-rpc

[Volver al Blog](<https://lucioduran.com/es/blog>)
