{"schemaVersion":1,"id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration","title":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration","description":"An examination of MCP's architecture, transport layers, resource/tool/prompt primitives, and how to build custom MCP servers that function reliably in production.","language":"en","datePublished":"2026-03-08","dateModified":"2026-10-21","contentFormat":"text/markdown","content":"AI/ML\n\n# Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration\n\n2026-03-0810 min read\n\n[Lucio Durán](<https://lucioduran.com/>)\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration>)\n\n## MCP Protocol Architecture\n\n[Model Context Protocol](<https://modelcontextprotocol.io/>) is a JSON-RPC 2.0 based protocol that defines how AI applications (clients) communicate with external capabilities (servers). That's the one-sentence version. The reality is more nuanced.\n\nThe protocol defines three primitive types that a server can expose:\n\n1. **Resources**, Read-only data that the client can pull into context (think files, database records, API responses)\n2. **Tools**, Executable functions the model can invoke with parameters\n3. **Prompts**, Reusable prompt templates with arguments\n\nMost people only care about tools. That is a common initial mistake. Resources are arguably more important because they let you feed context to the model *without* burning a tool-call round trip.\n\nThe capability negotiation at the protocol level:\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\": \"my-agent\",\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\nNotice the ` listChanged ` capability flag. This is one of those details nobody talks about, it means the server can notify the client when its tool list changes at runtime. Critical for dynamic environments where tools come and go.\n\n## The Transport Layer, Where Things Get Interesting\n\nMCP defines two official transports, and understanding when to use each one is critical for avoiding production incidents.\n\n### stdio Transport\n\nThe simplest option. The client spawns the server as a child process and communicates over stdin/stdout. Each JSON-RPC message is a single line terminated by ` \\n `.\n\n```javascript\nimport { spawn } from 'child_process';\n\nconst server = spawn('node', ['./my-mcp-server.js'], {\n stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr\n});\n\n// CRITICAL: stderr is NOT part of the protocol\n// If your server console.log()s, it breaks the JSON-RPC stream\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); // This is where it blows up\n handleMessage(message); // if you log to stdout\n }\n});\n```\n\nA common pitfall: a ` console.log('Server started') ` in the server initialization that only triggers in production because of a different Node.js version loading path. The JSON parser chokes on ` Server started\\n{\"jsonrpc\":\"2.0\"... ` and the error message is completely unhelpful: \"Unexpected token S in JSON at position 0\". This can consume hours of debugging.\n\n### Streamable HTTP Transport\n\nThis is what you want for production deployments. The client communicates via HTTP POST to a single endpoint, and the server can optionally upgrade to Server-Sent Events for streaming responses.\n\n```javascript\n// Server implementation with express\nimport express from 'express';\n\nconst app = express();\napp.use(express.json());\n\n// Single endpoint handles all MCP traffic\napp.post('/mcp', async (req, res) => {\n const message = req.body;\n\n // Check if client accepts SSE\n const acceptsSSE = req.headers.accept?.includes('text/event-stream');\n\n if (acceptsSSE && isLongRunningTool(message)) {\n // Stream results back via 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 // Simple JSON-RPC response\n const result = await handleMessage(message);\n res.json(result);\n }\n});\n```\n\nThe ` Mcp-Session-Id ` header is how the server tracks client sessions. Without it, you can't implement stateful tools or resource subscriptions across multiple HTTP requests.\n\n## Building a Real MCP Server\n\nThe following walks through a production MCP server, a PostgreSQL query tool that exposes database tables as resources and safe read queries as 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// Expose table schemas as 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: execute read-only SQL\nserver.tool(\n 'query',\n 'Execute a read-only SQL query against the database',\n {\n sql: z.string().describe('SQL SELECT query to execute'),\n params: z.array(z.unknown()).optional()\n .describe('Parameterized query values')\n },\n async ({ sql, params }) => {\n // SECURITY: enforce read-only at multiple levels\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 // Check for keyword at word boundary, not inside identifiers\n const regex = new RegExp(`\\\\b${keyword}\\\\b`, 'i');\n if (regex.test(normalized)) {\n return {\n content: [{\n type: 'text',\n text: `Refused: query contains forbidden keyword \"${keyword}\"`\n }],\n isError: true\n };\n }\n }\n\n // Also use a read-only transaction as defense in depth\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) // Cap output\n }, null, 2)\n }]\n };\n } catch (err) {\n await client.query('ROLLBACK');\n return {\n content: [{ type: 'text', text: `Query error: ${err.message}` }],\n isError: true\n };\n } finally {\n client.release();\n }\n }\n);\n\n// Prompt template for common analysis patterns\nserver.prompt(\n 'analyze-table',\n 'Generate analysis queries for a specific table',\n { tableName: z.string() },\n ({ tableName }) => ({\n messages: [{\n role: 'user',\n content: {\n type: 'text',\n text: `Analyze the \"${tableName}\" table. First read its schema using the table-schema resource, then run queries to understand data distribution, null rates, and identify any anomalies. Focus on actionable insights.`\n }\n }]\n })\n);\n\n// Start server\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n## The Primitives in Practice\n\n### Resources vs Tools, The Decision Framework\n\nA common mistake is exposing everything as tools. Here is a useful mental model:\n\n- **Resource**: Data the model needs to *read* to make decisions. Pull-based. Model decides when to fetch.\n- **Tool**: An *action* with side effects or computation. Called explicitly by the model with specific parameters.\n\nIf you expose a 200-row config table as a tool call that returns JSON, the model has to invoke it, wait for the response, then continue reasoning. If you expose it as a resource, the client can prefetch it into context *before* the model even starts generating.\n\n### Prompt Templates, The Underused Primitive\n\nPrompts in MCP aren't just static text. They support arguments and can reference resources:\n\n```javascript\nserver.prompt(\n 'incident-response',\n 'Guide through incident investigation',\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: `Incident response for ${severity.toUpperCase()} on ${service}.\\n` +\n `1. Query recent error logs using the logs tool\\n` +\n `2. Check deployment history via the deploys resource\\n` +\n `3. Assess blast radius and suggest mitigation`\n }\n }]\n })\n);\n```\n\nThis is enormously useful for standardizing agent behavior across your team. Instead of everyone writing slightly different system prompts, you define prompt templates on the server side and they're discoverable via the protocol.\n\n## Robust Error Handling\n\nThe spec defines standard JSON-RPC error codes, but also adds MCP-specific ones. Here is a recommended pattern:\n\n```javascript\nserver.tool('deploy', 'Deploy a service', { /* ... */ }, 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 // Return as tool error, model can retry or inform user\n return {\n content: [{\n type: 'text',\n text: `Rate limited. Retry after ${err.retryAfter}s`\n }],\n isError: true\n };\n }\n // Unexpected errors, throw to trigger JSON-RPC error response\n throw err;\n }\n});\n```\n\nThe distinction between ` isError: true ` in a tool result vs throwing an error is subtle but important. Tool errors are \"expected failures\", the model can reason about them. Thrown errors are protocol-level failures that typically abort the current interaction.\n\n## Session Management and Statefulness\n\nAn important detail: MCP sessions are stateful. The ` Mcp-Session-Id ` header isn't just for logging, it's how the server correlates resource subscriptions, tracks which tools have been listed, and manages 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// Cleanup stale sessions every 5 minutes\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## Real-World Gotchas\n\n**Tool description quality matters more than expected.** The model uses tool descriptions to decide *when* to call a tool. A tool described as \"Search the database\" gets called for every question, even when a simpler resource read would suffice. Changing it to \"Execute SQL SELECT queries for complex filtering, aggregation, or joins that can't be answered by reading table resources directly\" reduces unnecessary calls by 60%.\n\n**JSON Schema for tool parameters needs to be precise.** Don't use ` z.any() ` or ` z.record(z.unknown()) `. The more constrained your schema, the better the model fills in parameters. Models hallucinate entire nested objects when the schema is too permissive.\n\n**Timeout tool executions.** MCP does not define a timeout mechanism at the protocol level, that is the server's responsibility. Wrapping every tool handler is recommended:\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## Where This Is All Going\n\nMCP is still young, but the trajectory is clear. The protocol already has buy-in from major IDE vendors, agent frameworks, and model providers. The spec is moving toward OAuth 2.1 for authentication, which will make multi-tenant MCP servers viable for SaaS products.\n\nThe most promising aspect is the ` listChanged ` notification pattern. Imagine an MCP server that dynamically exposes tools based on the user's permissions, the current project context, or even the model's demonstrated competence. The protocol supports this today, most implementations just haven't caught up yet.\n\nFor any AI agent system, building MCP servers is preferable to writing custom integrations. The investment pays off when the client decides to switch models for the third time.\n\n## Reproducible implementation: contracts and interoperability\n\n[MCP Contract Harness](<https://lucioduran.com/projects/mcp-contract-harness>) publishes a read-only MCP 2025-06-18 stdio server. A custom client, official Python SDK 2.3.0 and official TypeScript SDK 1.32.1 exercise it. Evidence preserves actual negotiation, pagination, read, error and cancellation messages.\n\nThe custom client completed 13 checks and each SDK completed eight. Clients also checked that a cancelled request received no response and that a later ping did receive one. The project page links the per-client matrix, traces and reproduction commands. An initialize response is the beginning of an integration, not its certificate of correctness. This project covers a stdio subset; it claims neither full MCP conformance nor tested compatibility with graphical hosts.\n\n## Frequently asked questions\n\n### What is the Model Context Protocol (MCP) and why does it matter?\n\nMCP is an open protocol that standardizes how AI applications connect to external tools, data sources, and services. Instead of writing custom integrations for every LLM provider, you implement one MCP server and any MCP-compatible client can consume it.\n\n### How does MCP differ from OpenAI function calling?\n\nFunction calling is a provider-specific feature baked into the chat completion API. MCP is a transport-agnostic protocol that works across providers, supports persistent resources, prompt templates, and bidirectional communication, not just request-response tool calls.\n\n### Can I use MCP with models other than Claude?\n\nYes. MCP is an open specification. While Anthropic created it, any model or client can implement the protocol. The spec is JSON-RPC 2.0 based and transport-agnostic.\n\n## Tips\n\n1. **Tip:** Always implement ` initialize ` handshake timeout on the client side, a hung MCP server will silently block the entire agent loop. A recommended maximum is 8 seconds using an ` AbortController ` signal.\n2. **Tip:** Use ` streamable-http ` transport for production and ` stdio ` for local dev only. The stdio approach pipes crash logs into your JSON-RPC stream and you'll spend hours debugging phantom parse errors.\n3. **Tip:** Namespace tool names with reverse-domain style like ` com.myservice.query_users `, once 4+ MCP servers are connected, generic names like ` search ` will collide and the LLM picks the wrong one half the time.\n\n- mcp\n- ai-integration\n- llm-tooling\n- anthropic\n- protocol-design\n- json-rpc\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#title"},{"level":2,"text":"MCP Protocol Architecture","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#mcp-protocol-architecture"},{"level":2,"text":"The Transport Layer, Where Things Get Interesting","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#the-transport-layer-where-things-get-interesting"},{"level":3,"text":"stdio Transport","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#stdio-transport"},{"level":3,"text":"Streamable HTTP Transport","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#streamable-http-transport"},{"level":2,"text":"Building a Real MCP Server","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#building-a-real-mcp-server"},{"level":2,"text":"The Primitives in Practice","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#the-primitives-in-practice"},{"level":3,"text":"Resources vs Tools, The Decision Framework","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#resources-vs-tools-the-decision-framework"},{"level":3,"text":"Prompt Templates, The Underused Primitive","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#prompt-templates-the-underused-primitive"},{"level":2,"text":"Robust Error Handling","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#robust-error-handling"},{"level":2,"text":"Session Management and Statefulness","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#session-management-and-statefulness"},{"level":2,"text":"Real-World Gotchas","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#real-world-gotchas"},{"level":2,"text":"Where This Is All Going","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#where-this-is-all-going"},{"level":2,"text":"Reproducible implementation: contracts and interoperability","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#reproducible-implementation-contracts-and-interoperability"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#questions"},{"level":3,"text":"What is the Model Context Protocol (MCP) and why does it matter?","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#question-1"},{"level":3,"text":"How does MCP differ from OpenAI function calling?","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#question-2"},{"level":3,"text":"Can I use MCP with models other than Claude?","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#article-tips"}],"sections":[{"heading":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration","level":1,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration","content":"AI/ML"},{"heading":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration","level":1,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#title","content":"2026-03-0810 min read\n\n[Lucio Durán](<https://lucioduran.com/>)\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration>)"},{"heading":"MCP Protocol Architecture","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#mcp-protocol-architecture","content":"[Model Context Protocol](<https://modelcontextprotocol.io/>) is a JSON-RPC 2.0 based protocol that defines how AI applications (clients) communicate with external capabilities (servers). That's the one-sentence version. The reality is more nuanced.\n\nThe protocol defines three primitive types that a server can expose:\n\n1. **Resources**, Read-only data that the client can pull into context (think files, database records, API responses)\n2. **Tools**, Executable functions the model can invoke with parameters\n3. **Prompts**, Reusable prompt templates with arguments\n\nMost people only care about tools. That is a common initial mistake. Resources are arguably more important because they let you feed context to the model *without* burning a tool-call round trip.\n\nThe capability negotiation at the protocol level:\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\": \"my-agent\",\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\nNotice the ` listChanged ` capability flag. This is one of those details nobody talks about, it means the server can notify the client when its tool list changes at runtime. Critical for dynamic environments where tools come and go."},{"heading":"The Transport Layer, Where Things Get Interesting","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#the-transport-layer-where-things-get-interesting","content":"MCP defines two official transports, and understanding when to use each one is critical for avoiding production incidents."},{"heading":"stdio Transport","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#stdio-transport","content":"The simplest option. The client spawns the server as a child process and communicates over stdin/stdout. Each JSON-RPC message is a single line terminated by ` \\n `.\n\n```javascript\nimport { spawn } from 'child_process';\n\nconst server = spawn('node', ['./my-mcp-server.js'], {\n stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr\n});\n\n// CRITICAL: stderr is NOT part of the protocol\n// If your server console.log()s, it breaks the JSON-RPC stream\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); // This is where it blows up\n handleMessage(message); // if you log to stdout\n }\n});\n```\n\nA common pitfall: a ` console.log('Server started') ` in the server initialization that only triggers in production because of a different Node.js version loading path. The JSON parser chokes on ` Server started\\n{\"jsonrpc\":\"2.0\"... ` and the error message is completely unhelpful: \"Unexpected token S in JSON at position 0\". This can consume hours of debugging."},{"heading":"Streamable HTTP Transport","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#streamable-http-transport","content":"This is what you want for production deployments. The client communicates via HTTP POST to a single endpoint, and the server can optionally upgrade to Server-Sent Events for streaming responses.\n\n```javascript\n// Server implementation with express\nimport express from 'express';\n\nconst app = express();\napp.use(express.json());\n\n// Single endpoint handles all MCP traffic\napp.post('/mcp', async (req, res) => {\n const message = req.body;\n\n // Check if client accepts SSE\n const acceptsSSE = req.headers.accept?.includes('text/event-stream');\n\n if (acceptsSSE && isLongRunningTool(message)) {\n // Stream results back via 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 // Simple JSON-RPC response\n const result = await handleMessage(message);\n res.json(result);\n }\n});\n```\n\nThe ` Mcp-Session-Id ` header is how the server tracks client sessions. Without it, you can't implement stateful tools or resource subscriptions across multiple HTTP requests."},{"heading":"Building a Real MCP Server","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#building-a-real-mcp-server","content":"The following walks through a production MCP server, a PostgreSQL query tool that exposes database tables as resources and safe read queries as 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// Expose table schemas as 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: execute read-only SQL\nserver.tool(\n 'query',\n 'Execute a read-only SQL query against the database',\n {\n sql: z.string().describe('SQL SELECT query to execute'),\n params: z.array(z.unknown()).optional()\n .describe('Parameterized query values')\n },\n async ({ sql, params }) => {\n // SECURITY: enforce read-only at multiple levels\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 // Check for keyword at word boundary, not inside identifiers\n const regex = new RegExp(`\\\\b${keyword}\\\\b`, 'i');\n if (regex.test(normalized)) {\n return {\n content: [{\n type: 'text',\n text: `Refused: query contains forbidden keyword \"${keyword}\"`\n }],\n isError: true\n };\n }\n }\n\n // Also use a read-only transaction as defense in depth\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) // Cap output\n }, null, 2)\n }]\n };\n } catch (err) {\n await client.query('ROLLBACK');\n return {\n content: [{ type: 'text', text: `Query error: ${err.message}` }],\n isError: true\n };\n } finally {\n client.release();\n }\n }\n);\n\n// Prompt template for common analysis patterns\nserver.prompt(\n 'analyze-table',\n 'Generate analysis queries for a specific table',\n { tableName: z.string() },\n ({ tableName }) => ({\n messages: [{\n role: 'user',\n content: {\n type: 'text',\n text: `Analyze the \"${tableName}\" table. First read its schema using the table-schema resource, then run queries to understand data distribution, null rates, and identify any anomalies. Focus on actionable insights.`\n }\n }]\n })\n);\n\n// Start server\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```"},{"heading":"The Primitives in Practice","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#the-primitives-in-practice","content":""},{"heading":"Resources vs Tools, The Decision Framework","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#resources-vs-tools-the-decision-framework","content":"A common mistake is exposing everything as tools. Here is a useful mental model:\n\n- **Resource**: Data the model needs to *read* to make decisions. Pull-based. Model decides when to fetch.\n- **Tool**: An *action* with side effects or computation. Called explicitly by the model with specific parameters.\n\nIf you expose a 200-row config table as a tool call that returns JSON, the model has to invoke it, wait for the response, then continue reasoning. If you expose it as a resource, the client can prefetch it into context *before* the model even starts generating."},{"heading":"Prompt Templates, The Underused Primitive","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#prompt-templates-the-underused-primitive","content":"Prompts in MCP aren't just static text. They support arguments and can reference resources:\n\n```javascript\nserver.prompt(\n 'incident-response',\n 'Guide through incident investigation',\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: `Incident response for ${severity.toUpperCase()} on ${service}.\\n` +\n `1. Query recent error logs using the logs tool\\n` +\n `2. Check deployment history via the deploys resource\\n` +\n `3. Assess blast radius and suggest mitigation`\n }\n }]\n })\n);\n```\n\nThis is enormously useful for standardizing agent behavior across your team. Instead of everyone writing slightly different system prompts, you define prompt templates on the server side and they're discoverable via the protocol."},{"heading":"Robust Error Handling","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#robust-error-handling","content":"The spec defines standard JSON-RPC error codes, but also adds MCP-specific ones. Here is a recommended pattern:\n\n```javascript\nserver.tool('deploy', 'Deploy a service', { /* ... */ }, 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 // Return as tool error, model can retry or inform user\n return {\n content: [{\n type: 'text',\n text: `Rate limited. Retry after ${err.retryAfter}s`\n }],\n isError: true\n };\n }\n // Unexpected errors, throw to trigger JSON-RPC error response\n throw err;\n }\n});\n```\n\nThe distinction between ` isError: true ` in a tool result vs throwing an error is subtle but important. Tool errors are \"expected failures\", the model can reason about them. Thrown errors are protocol-level failures that typically abort the current interaction."},{"heading":"Session Management and Statefulness","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#session-management-and-statefulness","content":"An important detail: MCP sessions are stateful. The ` Mcp-Session-Id ` header isn't just for logging, it's how the server correlates resource subscriptions, tracks which tools have been listed, and manages 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// Cleanup stale sessions every 5 minutes\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":"Real-World Gotchas","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#real-world-gotchas","content":"**Tool description quality matters more than expected.** The model uses tool descriptions to decide *when* to call a tool. A tool described as \"Search the database\" gets called for every question, even when a simpler resource read would suffice. Changing it to \"Execute SQL SELECT queries for complex filtering, aggregation, or joins that can't be answered by reading table resources directly\" reduces unnecessary calls by 60%.\n\n**JSON Schema for tool parameters needs to be precise.** Don't use ` z.any() ` or ` z.record(z.unknown()) `. The more constrained your schema, the better the model fills in parameters. Models hallucinate entire nested objects when the schema is too permissive.\n\n**Timeout tool executions.** MCP does not define a timeout mechanism at the protocol level, that is the server's responsibility. Wrapping every tool handler is recommended:\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":"Where This Is All Going","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#where-this-is-all-going","content":"MCP is still young, but the trajectory is clear. The protocol already has buy-in from major IDE vendors, agent frameworks, and model providers. The spec is moving toward OAuth 2.1 for authentication, which will make multi-tenant MCP servers viable for SaaS products.\n\nThe most promising aspect is the ` listChanged ` notification pattern. Imagine an MCP server that dynamically exposes tools based on the user's permissions, the current project context, or even the model's demonstrated competence. The protocol supports this today, most implementations just haven't caught up yet.\n\nFor any AI agent system, building MCP servers is preferable to writing custom integrations. The investment pays off when the client decides to switch models for the third time."},{"heading":"Reproducible implementation: contracts and interoperability","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#reproducible-implementation-contracts-and-interoperability","content":"[MCP Contract Harness](<https://lucioduran.com/projects/mcp-contract-harness>) publishes a read-only MCP 2025-06-18 stdio server. A custom client, official Python SDK 2.3.0 and official TypeScript SDK 1.32.1 exercise it. Evidence preserves actual negotiation, pagination, read, error and cancellation messages.\n\nThe custom client completed 13 checks and each SDK completed eight. Clients also checked that a cancelled request received no response and that a later ping did receive one. The project page links the per-client matrix, traces and reproduction commands. An initialize response is the beginning of an integration, not its certificate of correctness. This project covers a stdio subset; it claims neither full MCP conformance nor tested compatibility with graphical hosts."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#questions","content":""},{"heading":"What is the Model Context Protocol (MCP) and why does it matter?","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#question-1","content":"MCP is an open protocol that standardizes how AI applications connect to external tools, data sources, and services. Instead of writing custom integrations for every LLM provider, you implement one MCP server and any MCP-compatible client can consume it."},{"heading":"How does MCP differ from OpenAI function calling?","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#question-2","content":"Function calling is a provider-specific feature baked into the chat completion API. MCP is a transport-agnostic protocol that works across providers, supports persistent resources, prompt templates, and bidirectional communication, not just request-response tool calls."},{"heading":"Can I use MCP with models other than Claude?","level":3,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#question-3","content":"Yes. MCP is an open specification. While Anthropic created it, any model or client can implement the protocol. The spec is JSON-RPC 2.0 based and transport-agnostic."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#article-tips","content":"1. **Tip:** Always implement ` initialize ` handshake timeout on the client side, a hung MCP server will silently block the entire agent loop. A recommended maximum is 8 seconds using an ` AbortController ` signal.\n2. **Tip:** Use ` streamable-http ` transport for production and ` stdio ` for local dev only. The stdio approach pipes crash logs into your JSON-RPC stream and you'll spend hours debugging phantom parse errors.\n3. **Tip:** Namespace tool names with reverse-domain style like ` com.myservice.query_users `, once 4+ MCP servers are connected, generic names like ` search ` will collide and the LLM picks the wrong one half the time.\n\n- mcp\n- ai-integration\n- llm-tooling\n- anthropic\n- protocol-design\n- json-rpc\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Lucio Durán","url":"https://lucioduran.com/"},{"text":"Español","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration"},{"text":"Model Context Protocol","url":"https://modelcontextprotocol.io/"},{"text":"MCP Contract Harness","url":"https://lucioduran.com/projects/mcp-contract-harness"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com/"},{"@type":"WebSite","@id":"https://lucioduran.com/#website","url":"https://lucioduran.com/","name":"Lucio Durán","inLanguage":["en","es"],"publisher":{"@id":"https://lucioduran.com/#person"}},{"@type":"WebPage","@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#page","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration","name":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration | Lucio Durán","description":"An examination of MCP's architecture, transport layers, resource/tool/prompt primitives, and how to build custom MCP servers that function reliably in production.","inLanguage":"en","author":{"@id":"https://lucioduran.com/#person"},"publisher":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"},"isAccessibleForFree":true,"datePublished":"2026-03-08","dateModified":"2026-10-21","mainEntity":{"@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#article"},"breadcrumb":{"@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#breadcrumb"}},{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#article","mainEntityOfPage":{"@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#page"},"headline":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration","description":"An examination of MCP's architecture, transport layers, resource/tool/prompt primitives, and how to build custom MCP servers that function reliably in production.","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":{"@id":"https://lucioduran.com/#person"},"url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"AI/ML","keywords":"mcp, ai-integration, llm-tooling, anthropic, protocol-design, json-rpc","wordCount":1808,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration#article","inLanguage":"es","url":"https://lucioduran.com/es/blog/model-context-protocol-mcp-ai-integration"}],"isAccessibleForFree":true},{"@type":"Blog","@id":"https://lucioduran.com/blog#blog","url":"https://lucioduran.com/blog","name":"Blog","inLanguage":"en","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com/"},"publisher":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"}},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Model Context Protocol (MCP): The Missing Standardization Layer for AI-Tool Integration","item":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration"}],"@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#breadcrumb"},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is the Model Context Protocol (MCP) and why does it matter?","acceptedAnswer":{"@type":"Answer","text":"MCP is an open protocol that standardizes how AI applications connect to external tools, data sources, and services. Instead of writing custom integrations for every LLM provider, you implement one MCP server and any MCP-compatible client can consume it."}},{"@type":"Question","name":"How does MCP differ from OpenAI function calling?","acceptedAnswer":{"@type":"Answer","text":"Function calling is a provider-specific feature baked into the chat completion API. MCP is a transport-agnostic protocol that works across providers, supports persistent resources, prompt templates, and bidirectional communication, not just request-response tool calls."}},{"@type":"Question","name":"Can I use MCP with models other than Claude?","acceptedAnswer":{"@type":"Answer","text":"Yes. MCP is an open specification. While Anthropic created it, any model or client can implement the protocol. The spec is JSON-RPC 2.0 based and transport-agnostic."}}],"@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#questions","url":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#questions","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration#page"},"author":{"@id":"https://lucioduran.com/#person"}}]}],"contentHash":"38f54fd3b052c4e4fb2cd6df484b76aeb9d14845adc86da40452bf0982df7235","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/model-context-protocol-mcp-ai-integration","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":73928,"contentBytes":15336}}
