{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration","title":"Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas","description":"Un recorrido profundo por la arquitectura de MCP, capas de transporte, primitivas resource/tool/prompt, y cómo construir servidores MCP custom que funcionen de manera confiable en producción.","language":"es","datePublished":"2026-03-08","dateModified":"2026-10-21","contentFormat":"text/markdown","content":"AI/ML\n\n# Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas\n\n2026-03-0810 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration>)\n\n## Arquitectura del Protocolo MCP\n\n[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.\n\nEl protocolo define tres tipos de primitivas que un servidor puede exponer:\n\n1. **Resources**, Datos de solo lectura que el cliente puede traer al contexto (considerar en archivos, registros de base de datos, respuestas de APIs)\n2. **Tools**, Funciones ejecutables que el modelo puede invocar con parámetros\n3. **Prompts**, Templates de prompts reutilizables con argumentos\n\nLa 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.\n\nAcá va cómo se ve la negociación de capabilities a nivel protocolo:\n\n```json\n// Client -> Server (initialize request)\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2025-03-26\",\n \"capabilities\": {\n \"roots\": { \"listChanged\": true }\n },\n \"clientInfo\": {\n \"name\": \"mi-agente\",\n \"version\": \"1.0.0\"\n }\n }\n}\n\n// Server -> Client (initialize response)\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"result\": {\n \"protocolVersion\": \"2025-03-26\",\n \"capabilities\": {\n \"tools\": { \"listChanged\": true },\n \"resources\": { \"subscribe\": true },\n \"prompts\": { \"listChanged\": true }\n },\n \"serverInfo\": {\n \"name\": \"postgres-mcp\",\n \"version\": \"0.3.1\"\n }\n }\n}\n```\n\nObservar 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.\n\n## La Capa de Transporte, Consideraciones Clave\n\nMCP define dos transportes oficiales, y entender cuándo usar cada uno es crítico para evitar incidentes en producción.\n\n### Transporte stdio\n\nLa 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 `.\n\n```javascript\nimport { spawn } from 'child_process';\n\nconst server = spawn('node', ['./mi-mcp-server.js'], {\n stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr\n});\n\n// CRÍTICO: stderr NO es parte del protocolo\n// Si tu server hace console.log(), rompe el stream JSON-RPC\nserver.stdout.on('data', (chunk) => {\n const lines = chunk.toString().split('\\n').filter(Boolean);\n for (const line of lines) {\n const message = JSON.parse(line); // Acá explota\n handleMessage(message); // si loggeás a stdout\n }\n});\n```\n\nUn 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.\n\n### Transporte Streamable HTTP\n\nEsto 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.\n\n```javascript\n// Implementación del server con express\nimport express from 'express';\n\nconst app = express();\napp.use(express.json());\n\n// Un solo endpoint maneja todo el tráfico MCP\napp.post('/mcp', async (req, res) => {\n const message = req.body;\n\n // Chequear si el cliente acepta SSE\n const acceptsSSE = req.headers.accept?.includes('text/event-stream');\n\n if (acceptsSSE && isLongRunningTool(message)) {\n // Streamear resultados vía SSE\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Mcp-Session-Id': sessionId\n });\n\n await streamToolExecution(message, res);\n res.end();\n } else {\n // Respuesta JSON-RPC simple\n const result = await handleMessage(message);\n res.json(result);\n }\n});\n```\n\nEl 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.\n\n## Armando un MCP Server de Verdad\n\nA 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.\n\n```javascript\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport pg from 'pg';\nimport { z } from 'zod';\n\nconst pool = new pg.Pool({\n connectionString: process.env.DATABASE_URL,\n max: 5,\n idleTimeoutMillis: 30000\n});\n\nconst server = new McpServer({\n name: 'postgres-readonly',\n version: '1.0.0'\n});\n\n// Exponer schemas de tablas como resources\nserver.resource(\n 'table-schema',\n 'schema://tables/{tableName}',\n async (uri, { tableName }) => {\n const result = await pool.query(`\n SELECT column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_name = $1\n ORDER BY ordinal_position\n `, [tableName]);\n\n return {\n contents: [{\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify(result.rows, null, 2)\n }]\n };\n }\n);\n\n// Tool: ejecutar SQL de solo lectura\nserver.tool(\n 'query',\n 'Ejecutar una query SQL de solo lectura contra la base',\n {\n sql: z.string().describe('Query SQL SELECT a ejecutar'),\n params: z.array(z.unknown()).optional()\n .describe('Valores parametrizados de la query')\n },\n async ({ sql, params }) => {\n // SEGURIDAD: enforce read-only en múltiples niveles\n const normalized = sql.trim().toLowerCase();\n const forbidden = ['insert', 'update', 'delete', 'drop', 'alter',\n 'create', 'truncate', 'grant', 'revoke'];\n\n for (const keyword of forbidden) {\n const regex = new RegExp(`\\\\b${keyword}\\\\b`, 'i');\n if (regex.test(normalized)) {\n return {\n content: [{\n type: 'text',\n text: `Rechazado: la query contiene keyword prohibido \"${keyword}\"`\n }],\n isError: true\n };\n }\n }\n\n const client = await pool.connect();\n try {\n await client.query('SET TRANSACTION READ ONLY');\n await client.query('BEGIN');\n\n const result = await client.query(sql, params || []);\n\n await client.query('COMMIT');\n\n return {\n content: [{\n type: 'text',\n text: JSON.stringify({\n rowCount: result.rowCount,\n rows: result.rows.slice(0, 100)\n }, null, 2)\n }]\n };\n } catch (err) {\n await client.query('ROLLBACK');\n return {\n content: [{ type: 'text', text: `Error en query: ${err.message}` }],\n isError: true\n };\n } finally {\n client.release();\n }\n }\n);\n\n// Template de prompt para patrones de análisis comunes\nserver.prompt(\n 'analyze-table',\n 'Generar queries de análisis para una tabla específica',\n { tableName: z.string() },\n ({ tableName }) => ({\n messages: [{\n role: 'user',\n content: {\n type: 'text',\n 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.`\n }\n }]\n })\n);\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n## Las Primitivas en la Práctica\n\n### Resources vs Tools, El Framework de Decisión\n\nUn error común es exponer todo como tools. Acá va un modelo mental útil:\n\n- **Resource**: Datos que el modelo necesita *leer* para tomar decisiones. Pull-based. El modelo decide cuándo fetchear.\n- **Tool**: Una *acción* con side effects o computación. Invocado explícitamente por el modelo con parámetros específicos.\n\nSi 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.\n\n### Prompt Templates, La Primitiva Subutilizada\n\nLos prompts en MCP no son solo texto estático. Soportan argumentos y pueden referenciar resources:\n\n```javascript\nserver.prompt(\n 'incident-response',\n 'Guiar a través de investigación de incidentes',\n {\n severity: z.enum(['p1', 'p2', 'p3']),\n service: z.string()\n },\n ({ severity, service }) => ({\n messages: [{\n role: 'user',\n content: {\n type: 'text',\n text: `Respuesta a incidente para ${severity.toUpperCase()} en ${service}.\\n` +\n `1. Consultá logs de error recientes usando el tool de logs\\n` +\n `2. Verificar historial de deploys vía el resource de deploys\\n` +\n `3. Evaluá blast radius y sugerí mitigación`\n }\n }]\n })\n);\n```\n\nEsto 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.\n\n## Manejo Robusto de Errores\n\nLa 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:\n\n```javascript\nserver.tool('deploy', 'Deployar un servicio', { /* ... */ }, async (args) => {\n try {\n const result = await deployService(args);\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }]\n };\n } catch (err) {\n if (err.code === 'RATE_LIMITED') {\n // Devolver como error de tool, el modelo puede reintentar o informar al usuario\n return {\n content: [{\n type: 'text',\n text: `Rate limited. Reintentá después de ${err.retryAfter}s`\n }],\n isError: true\n };\n }\n // Errores inesperados, throw para triggear respuesta de error JSON-RPC\n throw err;\n }\n});\n```\n\nLa 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.\n\n## Manejo de Sesiones y Estado\n\nUn 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.\n\n```javascript\nconst sessions = new Map();\n\nfunction getOrCreateSession(sessionId) {\n if (!sessions.has(sessionId)) {\n sessions.set(sessionId, {\n id: sessionId,\n subscriptions: new Set(),\n createdAt: Date.now(),\n lastActivity: Date.now()\n });\n }\n const session = sessions.get(sessionId);\n session.lastActivity = Date.now();\n return session;\n}\n\n// Limpiar sesiones viejas cada 5 minutos\nsetInterval(() => {\n const cutoff = Date.now() - 30 * 60 * 1000; // 30 min TTL\n for (const [id, session] of sessions) {\n if (session.lastActivity < cutoff) {\n session.subscriptions.forEach(sub => sub.unsubscribe());\n sessions.delete(id);\n }\n }\n}, 5 * 60 * 1000);\n```\n\n## Problemas Frecuentes en Producción\n\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%.\n\n**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.\n\n**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:\n\n```javascript\nfunction withTimeout(fn, ms = 30000) {\n return async (...args) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n try {\n return await fn(...args, { signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n };\n}\n```\n\n## Hacia Dónde Va Todo Esto\n\nMCP 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.\n\nEl 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.\n\nPara 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.\n\n## Implementación reproducible: contratos e interoperabilidad\n\n[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.\n\nEl 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.\n\n## Preguntas frecuentes\n\n### ¿Qué es el Model Context Protocol (MCP) y por qué importa?\n\nMCP 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.\n\n### ¿En qué se diferencia MCP del function calling de OpenAI?\n\nFunction 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.\n\n### ¿Puedo usar MCP con modelos que no sean Claude?\n\nSí. 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.\n\n## Tips\n\n1. **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 `.\n2. **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.\n3. **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.\n\n- mcp\n- integración-ia\n- llm-tooling\n- anthropic\n- diseño-protocolos\n- json-rpc\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#title"},{"level":2,"text":"Arquitectura del Protocolo MCP","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#arquitectura-del-protocolo-mcp"},{"level":2,"text":"La Capa de Transporte, Consideraciones Clave","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#la-capa-de-transporte-consideraciones-clave"},{"level":3,"text":"Transporte stdio","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#transporte-stdio"},{"level":3,"text":"Transporte Streamable HTTP","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#transporte-streamable-http"},{"level":2,"text":"Armando un MCP Server de Verdad","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#armando-un-mcp-server-de-verdad"},{"level":2,"text":"Las Primitivas en la Práctica","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#las-primitivas-en-la-prctica"},{"level":3,"text":"Resources vs Tools, El Framework de Decisión","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#resources-vs-tools-el-framework-de-decisin"},{"level":3,"text":"Prompt Templates, La Primitiva Subutilizada","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#prompt-templates-la-primitiva-subutilizada"},{"level":2,"text":"Manejo Robusto de Errores","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#manejo-robusto-de-errores"},{"level":2,"text":"Manejo de Sesiones y Estado","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#manejo-de-sesiones-y-estado"},{"level":2,"text":"Problemas Frecuentes en Producción","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#problemas-frecuentes-en-produccin"},{"level":2,"text":"Hacia Dónde Va Todo Esto","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#hacia-dnde-va-todo-esto"},{"level":2,"text":"Implementación reproducible: contratos e interoperabilidad","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#implementacin-reproducible-contratos-e-interoperabilidad"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#questions"},{"level":3,"text":"¿Qué es el Model Context Protocol (MCP) y por qué importa?","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#question-1"},{"level":3,"text":"¿En qué se diferencia MCP del function calling de OpenAI?","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#question-2"},{"level":3,"text":"¿Puedo usar MCP con modelos que no sean Claude?","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#article-tips"}],"sections":[{"heading":"Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas","level":1,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration","content":"AI/ML"},{"heading":"Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas","level":1,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#title","content":"2026-03-0810 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration>)"},{"heading":"Arquitectura del Protocolo MCP","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#arquitectura-del-protocolo-mcp","content":"[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.\n\nEl protocolo define tres tipos de primitivas que un servidor puede exponer:\n\n1. **Resources**, Datos de solo lectura que el cliente puede traer al contexto (considerar en archivos, registros de base de datos, respuestas de APIs)\n2. **Tools**, Funciones ejecutables que el modelo puede invocar con parámetros\n3. **Prompts**, Templates de prompts reutilizables con argumentos\n\nLa 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.\n\nAcá va cómo se ve la negociación de capabilities a nivel protocolo:\n\n```json\n// Client -> Server (initialize request)\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2025-03-26\",\n \"capabilities\": {\n \"roots\": { \"listChanged\": true }\n },\n \"clientInfo\": {\n \"name\": \"mi-agente\",\n \"version\": \"1.0.0\"\n }\n }\n}\n\n// Server -> Client (initialize response)\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"result\": {\n \"protocolVersion\": \"2025-03-26\",\n \"capabilities\": {\n \"tools\": { \"listChanged\": true },\n \"resources\": { \"subscribe\": true },\n \"prompts\": { \"listChanged\": true }\n },\n \"serverInfo\": {\n \"name\": \"postgres-mcp\",\n \"version\": \"0.3.1\"\n }\n }\n}\n```\n\nObservar 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."},{"heading":"La Capa de Transporte, Consideraciones Clave","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#la-capa-de-transporte-consideraciones-clave","content":"MCP define dos transportes oficiales, y entender cuándo usar cada uno es crítico para evitar incidentes en producción."},{"heading":"Transporte stdio","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#transporte-stdio","content":"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 `.\n\n```javascript\nimport { spawn } from 'child_process';\n\nconst server = spawn('node', ['./mi-mcp-server.js'], {\n stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr\n});\n\n// CRÍTICO: stderr NO es parte del protocolo\n// Si tu server hace console.log(), rompe el stream JSON-RPC\nserver.stdout.on('data', (chunk) => {\n const lines = chunk.toString().split('\\n').filter(Boolean);\n for (const line of lines) {\n const message = JSON.parse(line); // Acá explota\n handleMessage(message); // si loggeás a stdout\n }\n});\n```\n\nUn 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."},{"heading":"Transporte Streamable HTTP","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#transporte-streamable-http","content":"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.\n\n```javascript\n// Implementación del server con express\nimport express from 'express';\n\nconst app = express();\napp.use(express.json());\n\n// Un solo endpoint maneja todo el tráfico MCP\napp.post('/mcp', async (req, res) => {\n const message = req.body;\n\n // Chequear si el cliente acepta SSE\n const acceptsSSE = req.headers.accept?.includes('text/event-stream');\n\n if (acceptsSSE && isLongRunningTool(message)) {\n // Streamear resultados vía SSE\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Mcp-Session-Id': sessionId\n });\n\n await streamToolExecution(message, res);\n res.end();\n } else {\n // Respuesta JSON-RPC simple\n const result = await handleMessage(message);\n res.json(result);\n }\n});\n```\n\nEl 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."},{"heading":"Armando un MCP Server de Verdad","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#armando-un-mcp-server-de-verdad","content":"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.\n\n```javascript\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport pg from 'pg';\nimport { z } from 'zod';\n\nconst pool = new pg.Pool({\n connectionString: process.env.DATABASE_URL,\n max: 5,\n idleTimeoutMillis: 30000\n});\n\nconst server = new McpServer({\n name: 'postgres-readonly',\n version: '1.0.0'\n});\n\n// Exponer schemas de tablas como resources\nserver.resource(\n 'table-schema',\n 'schema://tables/{tableName}',\n async (uri, { tableName }) => {\n const result = await pool.query(`\n SELECT column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_name = $1\n ORDER BY ordinal_position\n `, [tableName]);\n\n return {\n contents: [{\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify(result.rows, null, 2)\n }]\n };\n }\n);\n\n// Tool: ejecutar SQL de solo lectura\nserver.tool(\n 'query',\n 'Ejecutar una query SQL de solo lectura contra la base',\n {\n sql: z.string().describe('Query SQL SELECT a ejecutar'),\n params: z.array(z.unknown()).optional()\n .describe('Valores parametrizados de la query')\n },\n async ({ sql, params }) => {\n // SEGURIDAD: enforce read-only en múltiples niveles\n const normalized = sql.trim().toLowerCase();\n const forbidden = ['insert', 'update', 'delete', 'drop', 'alter',\n 'create', 'truncate', 'grant', 'revoke'];\n\n for (const keyword of forbidden) {\n const regex = new RegExp(`\\\\b${keyword}\\\\b`, 'i');\n if (regex.test(normalized)) {\n return {\n content: [{\n type: 'text',\n text: `Rechazado: la query contiene keyword prohibido \"${keyword}\"`\n }],\n isError: true\n };\n }\n }\n\n const client = await pool.connect();\n try {\n await client.query('SET TRANSACTION READ ONLY');\n await client.query('BEGIN');\n\n const result = await client.query(sql, params || []);\n\n await client.query('COMMIT');\n\n return {\n content: [{\n type: 'text',\n text: JSON.stringify({\n rowCount: result.rowCount,\n rows: result.rows.slice(0, 100)\n }, null, 2)\n }]\n };\n } catch (err) {\n await client.query('ROLLBACK');\n return {\n content: [{ type: 'text', text: `Error en query: ${err.message}` }],\n isError: true\n };\n } finally {\n client.release();\n }\n }\n);\n\n// Template de prompt para patrones de análisis comunes\nserver.prompt(\n 'analyze-table',\n 'Generar queries de análisis para una tabla específica',\n { tableName: z.string() },\n ({ tableName }) => ({\n messages: [{\n role: 'user',\n content: {\n type: 'text',\n 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.`\n }\n }]\n })\n);\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```"},{"heading":"Las Primitivas en la Práctica","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#las-primitivas-en-la-prctica","content":""},{"heading":"Resources vs Tools, El Framework de Decisión","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#resources-vs-tools-el-framework-de-decisin","content":"Un error común es exponer todo como tools. Acá va un modelo mental útil:\n\n- **Resource**: Datos que el modelo necesita *leer* para tomar decisiones. Pull-based. El modelo decide cuándo fetchear.\n- **Tool**: Una *acción* con side effects o computación. Invocado explícitamente por el modelo con parámetros específicos.\n\nSi 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."},{"heading":"Prompt Templates, La Primitiva Subutilizada","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#prompt-templates-la-primitiva-subutilizada","content":"Los prompts en MCP no son solo texto estático. Soportan argumentos y pueden referenciar resources:\n\n```javascript\nserver.prompt(\n 'incident-response',\n 'Guiar a través de investigación de incidentes',\n {\n severity: z.enum(['p1', 'p2', 'p3']),\n service: z.string()\n },\n ({ severity, service }) => ({\n messages: [{\n role: 'user',\n content: {\n type: 'text',\n text: `Respuesta a incidente para ${severity.toUpperCase()} en ${service}.\\n` +\n `1. Consultá logs de error recientes usando el tool de logs\\n` +\n `2. Verificar historial de deploys vía el resource de deploys\\n` +\n `3. Evaluá blast radius y sugerí mitigación`\n }\n }]\n })\n);\n```\n\nEsto 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."},{"heading":"Manejo Robusto de Errores","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#manejo-robusto-de-errores","content":"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:\n\n```javascript\nserver.tool('deploy', 'Deployar un servicio', { /* ... */ }, async (args) => {\n try {\n const result = await deployService(args);\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }]\n };\n } catch (err) {\n if (err.code === 'RATE_LIMITED') {\n // Devolver como error de tool, el modelo puede reintentar o informar al usuario\n return {\n content: [{\n type: 'text',\n text: `Rate limited. Reintentá después de ${err.retryAfter}s`\n }],\n isError: true\n };\n }\n // Errores inesperados, throw para triggear respuesta de error JSON-RPC\n throw err;\n }\n});\n```\n\nLa 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."},{"heading":"Manejo de Sesiones y Estado","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#manejo-de-sesiones-y-estado","content":"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.\n\n```javascript\nconst sessions = new Map();\n\nfunction getOrCreateSession(sessionId) {\n if (!sessions.has(sessionId)) {\n sessions.set(sessionId, {\n id: sessionId,\n subscriptions: new Set(),\n createdAt: Date.now(),\n lastActivity: Date.now()\n });\n }\n const session = sessions.get(sessionId);\n session.lastActivity = Date.now();\n return session;\n}\n\n// Limpiar sesiones viejas cada 5 minutos\nsetInterval(() => {\n const cutoff = Date.now() - 30 * 60 * 1000; // 30 min TTL\n for (const [id, session] of sessions) {\n if (session.lastActivity < cutoff) {\n session.subscriptions.forEach(sub => sub.unsubscribe());\n sessions.delete(id);\n }\n }\n}, 5 * 60 * 1000);\n```"},{"heading":"Problemas Frecuentes en Producción","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#problemas-frecuentes-en-produccin","content":"**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%.\n\n**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.\n\n**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:\n\n```javascript\nfunction withTimeout(fn, ms = 30000) {\n return async (...args) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n try {\n return await fn(...args, { signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n };\n}\n```"},{"heading":"Hacia Dónde Va Todo Esto","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#hacia-dnde-va-todo-esto","content":"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.\n\nEl 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.\n\nPara 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."},{"heading":"Implementación reproducible: contratos e interoperabilidad","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#implementacin-reproducible-contratos-e-interoperabilidad","content":"[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.\n\nEl 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."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#questions","content":""},{"heading":"¿Qué es el Model Context Protocol (MCP) y por qué importa?","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#question-1","content":"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."},{"heading":"¿En qué se diferencia MCP del function calling de OpenAI?","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#question-2","content":"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."},{"heading":"¿Puedo usar MCP con modelos que no sean Claude?","level":3,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#question-3","content":"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."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#article-tips","content":"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 `.\n2. **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.\n3. **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.\n\n- mcp\n- integración-ia\n- llm-tooling\n- anthropic\n- diseño-protocolos\n- json-rpc\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration"},{"text":"Model Context Protocol","url":"https://modelcontextprotocol.io/"},{"text":"MCP Contract Harness","url":"https://lucioduran.com/es/projects/mcp-contract-harness"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration"},"headline":"Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas","description":"Un recorrido profundo por la arquitectura de MCP, capas de transporte, primitivas resource/tool/prompt, y cómo construir servidores MCP custom que funcionen de manera confiable en producción.","datePublished":"2026-03-08","dateModified":"2026-10-21","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"AI/ML","keywords":"mcp, integración-ia, llm-tooling, anthropic, diseño-protocolos, json-rpc","wordCount":1935,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Model Context Protocol (MCP): La Capa de Estandarización que Faltaba para Integrar IA con Herramientas","item":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué es el Model Context Protocol (MCP) y por qué importa?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"¿En qué se diferencia MCP del function calling de OpenAI?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"¿Puedo usar MCP con modelos que no sean Claude?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}]}],"contentHash":"f79d3ef9bd099579fbec301043a678070c4860c441ba332a28ee8840ba07c761","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":71639,"contentBytes":16190}}
