Probablemente hayas escuchado el discurso: MCP es "USB-C para la IA." Un protocolo, conectividad universal, todo simplemente funciona. Esa es la versión de marketing. Vamos a construir lo real.
Al final de este tutorial, tendrás un servidor MCP funcional — escrito dos veces, una en TypeScript y otra en Python — que expone tools que un agente de IA puede descubrir y llamar en tiempo de ejecución. Entenderás la arquitectura del protocolo, sabrás cómo funcionan juntos los tres primitivos (tools, resources, prompts) y tendrás un servidor que puedes conectar a Claude Desktop o probar con el MCP Inspector.
Sin vaguedades. Código real, resultados reales.
| Concepto | Lo que aprenderás |
|---|---|
| Arquitectura MCP | Modelo cliente-servidor sobre JSON-RPC 2.0 con conexiones persistentes y bidireccionales |
| Tres primitivos | Tools (acciones), Resources (datos de solo lectura), Prompts (plantillas reutilizables) |
| Servidor TypeScript | Implementación completa usando @modelcontextprotocol/sdk y esquemas Zod |
| Servidor Python | Implementación equivalente usando FastMCP con registro basado en decoradores |
| Capas de transporte | stdio para local, Streamable HTTP para remoto, y por qué SSE está deprecado |
| Testing | MCP Inspector, configuración de Claude Desktop e integración con Claude Code |
Qué es realmente MCP (y por qué debería importarte)
MCP (Model Context Protocol) es un estándar abierto que permite a cualquier aplicación de IA descubrir y llamar herramientas externas y fuentes de datos a través de un único protocolo. Escribe un servidor MCP, y cualquier cliente compatible con MCP — Claude, ChatGPT, VS Code o tu agente personalizado — puede usarlo sin código de integración personalizado.
El Model Context Protocol es un estándar abierto que define cómo las aplicaciones de IA se comunican con herramientas externas y fuentes de datos. Anthropic lo lanzó en noviembre de 2024, y en menos de un año cada plataforma de IA importante — OpenAI, Google, Microsoft — lo adoptó. En diciembre de 2025 se trasladó a la Linux Foundation bajo gobernanza neutral. Ya no es la API de un proveedor. Es un estándar.
El problema que resuelve es simple: antes de MCP, cada framework de IA tenía su propia forma de conectarse a herramientas. El function calling de OpenAI, la abstracción de herramientas de LangChain, la especificación de tool use de Anthropic — todos resolvían el mismo problema de manera diferente. Si querías que tu consulta a la base de datos funcionara con Claude y ChatGPT y tu framework de agentes personalizado, escribías tres integraciones diferentes. MCP dice: escribe un servidor, y cualquier cliente compatible con MCP puede usarlo.
La arquitectura sigue un modelo cliente-servidor sobre JSON-RPC 2.0. Un servidor MCP expone capacidades. Un cliente MCP (tu app de IA, Claude Desktop, VS Code, etc.) descubre y llama a esas capacidades. La conexión es persistente y bidireccional, lo cual es una gran mejora respecto al function calling sin estado donde declaras todo por adelantado en el prompt.
¿Cuáles son los tres primitivos de MCP?
MCP expone tres tipos de capacidades: tools (acciones que el modelo puede ejecutar), resources (datos de solo lectura que el modelo puede obtener como contexto) y prompts (plantillas reutilizables que codifican las mejores prácticas). Cada uno cumple un rol distinto en el modelo cliente-servidor.
MCP organiza todo en tres primitivos. Entender estos es la clave de todo el protocolo.
Tools son acciones que el agente puede realizar. Piensa en llamadas a API, consultas a bases de datos, cálculos, operaciones con archivos — cualquier cosa con efectos secundarios o computación. Cada tool tiene un nombre, una descripción (que el LLM lee para decidir cuándo usarla) y un esquema de entrada. Las tools son el primitivo más comúnmente usado, y en el que nos enfocaremos en este tutorial.
Resources son datos de solo lectura que el agente puede obtener como contexto. El contenido de un archivo, un registro de base de datos, un valor de configuración. Los resources son como endpoints GET — proporcionan información sin cambiar nada. Se identifican por URIs como file:///logs/app.log o db://users/123. Si estás construyendo agentes que necesitan recuperar y razonar sobre grandes colecciones de documentos, un pipeline de RAG es el complemento natural de los resources de MCP.
Prompts son plantillas reutilizables que codifican las mejores prácticas para trabajar con un servicio específico. Si tu servidor MCP envuelve una API compleja, podrías incluir un prompt que le enseñe al agente cómo consultarla efectivamente. Piensa en los prompts como manuales de instrucciones que vienen junto con tus herramientas.
Así es como se relacionan:
Qué vamos a construir
Construiremos un servidor MCP de servicio meteorológico con tres tools:
- get-current-weather — devuelve las condiciones actuales para una ciudad
- get-forecast — devuelve un pronóstico de varios días
- convert-temperature — convierte entre Celsius, Fahrenheit y Kelvin
También expondremos un resource — una lista estática de ciudades soportadas. Esto es deliberadamente simple para que el enfoque se mantenga en la estructura de MCP, no en la lógica de negocio. En producción, reemplazarías los datos simulados por llamadas reales a una API.
Lo construiremos primero en TypeScript usando el SDK oficial @modelcontextprotocol/sdk, luego en Python usando el paquete oficial mcp. Mismo servidor, mismas tools, dos lenguajes.
Parte 1: Servidor MCP en TypeScript
Configuración del proyecto
Crea un nuevo directorio e inicializa el proyecto:
mkdir mcp-weather-server && cd mcp-weather-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --initActualiza tu tsconfig.json con estas configuraciones:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}Y actualiza tu package.json para incluir el script de build y la configuración de tipo:
{
"name": "mcp-weather-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.24.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
}
}El código del servidor
Aquí está el servidor MCP completo. Lo recorreremos sección por sección después.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// — Mock weather data ---
interface WeatherData {
city: string;
temperature: number;
unit: string;
condition: string;
humidity: number;
windSpeed: number;
}
const weatherDatabase: Record<string, WeatherData> = {
"new york": {
city: "New York",
temperature: 5,
unit: "celsius",
condition: "Partly Cloudy",
humidity: 62,
windSpeed: 18,
},
london: {
city: "London",
temperature: 8,
unit: "celsius",
condition: "Overcast",
humidity: 78,
windSpeed: 12,
},
tokyo: {
city: "Tokyo",
temperature: 14,
unit: "celsius",
condition: "Clear",
humidity: 45,
windSpeed: 8,
},
sydney: {
city: "Sydney",
temperature: 26,
unit: "celsius",
condition: "Sunny",
humidity: 55,
windSpeed: 15,
},
paris: {
city: "Paris",
temperature: 7,
unit: "celsius",
condition: "Light Rain",
humidity: 82,
windSpeed: 10,
},
};
// — Temperature conversion helper ---
function convertTemp(
value: number,
from: string,
to: string
): number {
// Normalize to Celsius first
let celsius: number;
switch (from.toLowerCase()) {
case "fahrenheit":
celsius = (value - 32) * (5 / 9);
break;
case "kelvin":
celsius = value - 273.15;
break;
default:
celsius = value;
}
// Convert from Celsius to target
switch (to.toLowerCase()) {
case "fahrenheit":
return Math.round((celsius * (9 / 5) + 32) * 100) / 100;
case "kelvin":
return Math.round((celsius + 273.15) * 100) / 100;
default:
return Math.round(celsius * 100) / 100;
}
}
// — Create the MCP server ---
const server = new McpServer({
name: "weather-service",
version: "1.0.0",
});
// — Register tools ---
server.tool(
"get-current-weather",
"Get the current weather conditions for a city",
{
city: z.string().describe("City name (e.g., 'London', 'New York')"),
},
async ({ city }) => {
const data = weatherDatabase[city.toLowerCase()];
if (!data) {
return {
content: [
{
type: "text",
text: `City "${city}" not found. Available cities: ${Object.values(weatherDatabase)
.map((w) => w.city)
.join(", ")}`,
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
city: data.city,
temperature: `${data.temperature}°C`,
condition: data.condition,
humidity: `${data.humidity}%`,
windSpeed: `${data.windSpeed} km/h`,
},
null,
2
),
},
],
};
}
);
server.tool(
"get-forecast",
"Get a multi-day weather forecast for a city",
{
city: z.string().describe("City name"),
days: z
.number()
.min(1)
.max(7)
.default(3)
.describe("Number of days (1-7, default 3)"),
},
async ({ city, days }) => {
const data = weatherDatabase[city.toLowerCase()];
if (!data) {
return {
content: [
{
type: "text",
text: `City "${city}" not found. Available cities: ${Object.values(weatherDatabase)
.map((w) => w.city)
.join(", ")}`,
},
],
};
}
// Generate mock forecast with slight variations
const forecast = Array.from({ length: days }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() + i + 1);
const tempVariation = Math.round((Math.random() - 0.5) * 6);
return {
date: date.toISOString().split("T")[0],
temperature: `${data.temperature + tempVariation}°C`,
condition: data.condition,
humidity: `${data.humidity + Math.round((Math.random() - 0.5) * 10)}%`,
};
});
return {
content: [
{
type: "text",
text: JSON.stringify(
{ city: data.city, forecast },
null,
2
),
},
],
};
}
);
server.tool(
"convert-temperature",
"Convert a temperature between Celsius, Fahrenheit, and Kelvin",
{
value: z.number().describe("Temperature value to convert"),
from: z
.enum(["celsius", "fahrenheit", "kelvin"])
.describe("Source unit"),
to: z
.enum(["celsius", "fahrenheit", "kelvin"])
.describe("Target unit"),
},
async ({ value, from, to }) => {
const result = convertTemp(value, from, to);
return {
content: [
{
type: "text",
text: `${value}° ${from} = ${result}° ${to}`,
},
],
};
}
);
// — Register a resource ---
server.resource(
"supported-cities",
"weather://cities",
{
description: "List of cities with available weather data",
mimeType: "application/json",
},
async () => ({
contents: [
{
uri: "weather://cities",
mimeType: "application/json",
text: JSON.stringify(
Object.values(weatherDatabase).map((w) => ({
name: w.city,
currentCondition: w.condition,
})),
null,
2
),
},
],
})
);
// — Start the server ---
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio");
}
main().catch(console.error);Desglose del código
Veamos las partes clave.
Inicialización del servidor. Creas un McpServer con un nombre y una versión. Estos se envían al cliente durante el handshake del protocolo, para que el cliente sepa con qué está hablando.
Registro de tools. El método server.tool() toma cuatro argumentos: el nombre de la tool, una descripción (esto es lo que el LLM lee), un esquema Zod para los parámetros de entrada, y una función handler asíncrona. El handler recibe los parámetros validados y devuelve un array de content. Cada elemento de contenido tiene un type (generalmente "text") y los datos reales.
Observa que el esquema Zod cumple doble función — valida los parámetros entrantes y genera el JSON Schema que se envía al cliente durante el descubrimiento de capacidades. Cuando Claude u otro LLM ve estas tools, lee las descripciones y esquemas para decidir cuándo y cómo llamarlas. Por eso .describe() en cada campo importa: es documentación para la IA, no solo para humanos. Escribir descripciones efectivas de tools es en sí misma una habilidad de ingeniería de prompts — los mismos principios de claridad y especificidad que hacen funcionar los system prompts también aplican a los esquemas de tools.
Registro de resources. server.resource() registra una fuente de datos de solo lectura. El segundo argumento es el URI, y el handler devuelve el contenido del resource. Los resources son ideales para proporcionar contexto que no requiere computación — archivos de configuración, datos de referencia, documentación.
Transporte. StdioServerTransport significa que nuestro servidor se comunica a través de entrada/salida estándar. Este es el transporte más simple y el que Claude Desktop utiliza. Cubriremos otros transportes más adelante.
Compilar y probar
npm run buildSi todo compila sin errores, tu servidor está listo. No intentes ejecutarlo directamente con node build/index.js — espera mensajes JSON-RPC en stdin, no entrada de terminal. Lo probaremos correctamente en un momento.
Parte 2: Servidor MCP en Python
Mismo servidor, mismas tools, pero en Python usando el paquete oficial mcp y su framework FastMCP integrado.
Configuración del proyecto
mkdir mcp-weather-server-python && cd mcp-weather-server-python
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install mcpSi prefieres, aquí está el requirements.txt:
mcp>=1.6.0El código del servidor
from mcp.server.fastmcp import FastMCP
import json
import random
from datetime import datetime, timedelta
# — Create the MCP server ---
mcp = FastMCP("weather-service")
# — Mock weather data ---
WEATHER_DATABASE: dict[str, dict] = {
"new york": {
"city": "New York",
"temperature": 5,
"unit": "celsius",
"condition": "Partly Cloudy",
"humidity": 62,
"wind_speed": 18,
},
"london": {
"city": "London",
"temperature": 8,
"unit": "celsius",
"condition": "Overcast",
"humidity": 78,
"wind_speed": 12,
},
"tokyo": {
"city": "Tokyo",
"temperature": 14,
"unit": "celsius",
"condition": "Clear",
"humidity": 45,
"wind_speed": 8,
},
"sydney": {
"city": "Sydney",
"temperature": 26,
"unit": "celsius",
"condition": "Sunny",
"humidity": 55,
"wind_speed": 15,
},
"paris": {
"city": "Paris",
"temperature": 7,
"unit": "celsius",
"condition": "Light Rain",
"humidity": 82,
"wind_speed": 10,
},
}
VALID_UNITS = ("celsius", "fahrenheit", "kelvin")
# — Temperature conversion helper ---
def convert_temp(value: float, from_unit: str, to_unit: str) -> float:
"""Convert temperature between Celsius, Fahrenheit, and Kelvin."""
# Normalize to Celsius first
if from_unit == "fahrenheit":
celsius = (value - 32) * 5 / 9
elif from_unit == "kelvin":
celsius = value - 273.15
else:
celsius = value
# Convert from Celsius to target
if to_unit == "fahrenheit":
return round(celsius * 9 / 5 + 32, 2)
elif to_unit == "kelvin":
return round(celsius + 273.15, 2)
else:
return round(celsius, 2)
# — Register tools ---
@mcp.tool()
def get_current_weather(city: str) -> str:
"""Get the current weather conditions for a city.
Args:
city: City name (e.g., 'London', 'New York')
"""
data = WEATHER_DATABASE.get(city.lower())
if not data:
available = ", ".join(d["city"] for d in WEATHER_DATABASE.values())
return f'City "{city}" not found. Available cities: {available}'
return json.dumps(
{
"city": data["city"],
"temperature": f"{data['temperature']}°C",
"condition": data["condition"],
"humidity": f"{data['humidity']}%",
"windSpeed": f"{data['wind_speed']} km/h",
},
indent=2,
)
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""Get a multi-day weather forecast for a city.
Args:
city: City name
days: Number of forecast days (1-7, default 3)
"""
days = max(1, min(7, days))
data = WEATHER_DATABASE.get(city.lower())
if not data:
available = ", ".join(d["city"] for d in WEATHER_DATABASE.values())
return f'City "{city}" not found. Available cities: {available}'
forecast = []
for i in range(1, days + 1):
date = datetime.now() + timedelta(days=i)
temp_variation = round(random.uniform(-3, 3))
humidity_variation = round(random.uniform(-5, 5))
forecast.append(
{
"date": date.strftime("%Y-%m-%d"),
"temperature": f"{data['temperature'] + temp_variation}°C",
"condition": data["condition"],
"humidity": f"{data['humidity'] + humidity_variation}%",
}
)
return json.dumps({"city": data["city"], "forecast": forecast}, indent=2)
@mcp.tool()
def convert_temperature(value: float, from_unit: str, to_unit: str) -> str:
"""Convert a temperature between Celsius, Fahrenheit, and Kelvin.
Args:
value: Temperature value to convert
from_unit: Source unit (celsius, fahrenheit, or kelvin)
to_unit: Target unit (celsius, fahrenheit, or kelvin)
"""
if from_unit not in VALID_UNITS or to_unit not in VALID_UNITS:
return f"Invalid unit. Use one of: {', '.join(VALID_UNITS)}"
result = convert_temp(value, from_unit, to_unit)
return f"{value}° {from_unit} = {result}° {to_unit}"
# — Register a resource ---
@mcp.resource("weather://cities")
def list_supported_cities() -> str:
"""List of cities with available weather data."""
cities = [
{"name": d["city"], "currentCondition": d["condition"]}
for d in WEATHER_DATABASE.values()
]
return json.dumps(cities, indent=2)
# — Start the server ---
if __name__ == "__main__":
mcp.run(transport="stdio")Python vs. TypeScript: ¿Qué es diferente?
La versión en Python es notablemente más corta, y eso es intencional. FastMCP (que viene integrado en el paquete oficial mcp) usa las anotaciones de tipo y los docstrings de Python para auto-generar los esquemas de las tools. Donde TypeScript necesita un esquema Zod explícito con llamadas a .describe(), Python infiere los tipos de parámetros de las anotaciones y lee la sección Args: del docstring para las descripciones.
El decorador @mcp.tool() hace el trabajo pesado. Inspecciona la firma de la función, construye el JSON Schema y registra todo con el servidor. El decorador @mcp.resource() funciona de la misma manera para los resources — solo pasas el URI.
Ambas versiones producen servidores compatibles con MCP idénticos. Un cliente no puede distinguir en qué lenguaje fue escrito el servidor. Ese es todo el punto de un protocolo.
¿Cómo se prueba un servidor MCP?
Prueba servidores MCP usando el MCP Inspector (una herramienta basada en navegador en localhost:6274), conectándote a Claude Desktop mediante su archivo de configuración, o agregando el servidor al .mcp.json de Claude Code. El Inspector es el más rápido para desarrollo; Claude Desktop muestra cómo un cliente de IA real interactúa con tus tools.
Tienes un servidor TypeScript compilado y un servidor Python. Ahora asegurémonos de que realmente funcionan.
Opción 1: MCP Inspector
El MCP Inspector es una herramienta basada en navegador para probar servidores MCP de forma interactiva. Piensa en Postman, pero para MCP. Es la forma más rápida de verificar que tus tools funcionan correctamente.
# For the TypeScript server
npx @modelcontextprotocol/inspector node build/index.js
# For the Python server
npx @modelcontextprotocol/inspector python server.pyEsto abre una interfaz en http://localhost:6274. Desde ahí puedes:
- Ver todas las tools, resources y prompts registrados
- Llamar a cualquier tool con parámetros de entrada personalizados
- Inspeccionar los mensajes JSON-RPC sin procesar que van y vienen
- Verificar que los esquemas se están generando correctamente
Intenta llamar a get-current-weather con {"city": "London"}. Deberías ver la respuesta JSON con temperatura, condición y humedad. Intenta una ciudad que no exista y verifica que recibes el mensaje de error informativo.
Para depuración más profunda, habilita el modo detallado:
DEBUG=true npx @modelcontextprotocol/inspector node build/index.jsEsto registra cada mensaje JSON-RPC, lo cual es invaluable cuando estás depurando problemas de esquemas o comportamiento inesperado.
Opción 2: Claude Desktop
Para usar tu servidor con Claude, agrégalo al archivo de configuración de Claude Desktop.
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Para el servidor TypeScript:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/mcp-weather-server/build/index.js"]
}
}
}Para el servidor Python:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/absolute/path/to/mcp-weather-server-python/server.py"]
}
}
}Reinicia Claude Desktop después de guardar la configuración. Deberías ver un ícono de martillo en el área de entrada del chat indicando que las tools MCP están disponibles. Pregúntale a Claude "What's the weather in Tokyo?" y observa cómo descubre y llama a tu tool get-current-weather.
Opción 3: Claude Code
Si estás usando Claude Code (el CLI de Anthropic), puedes agregar servidores MCP al .mcp.json de tu proyecto:
{
"mcpServers": {
"weather": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/mcp-weather-server/build/index.js"]
}
}
}Claude Code lo detecta automáticamente y hace las tools disponibles en tu sesión de desarrollo.
¿Qué capa de transporte MCP deberías usar?
Usa stdio para herramientas locales donde el cliente inicia el servidor como un proceso hijo. Usa Streamable HTTP para servidores remotos o desplegados en la nube que necesitan manejar múltiples clientes a través de la red. SSE (el transporte HTTP original) está deprecado desde la actualización de la especificación de marzo 2025.
Hasta ahora hemos usado stdio, que es el transporte más simple. Pero MCP soporta múltiples transportes para diferentes escenarios de despliegue. Entender cuándo usar cada uno importa.
stdio (Standard I/O)
El cliente inicia el servidor como un proceso hijo. La comunicación ocurre a través de stdin/stdout. Esto es lo que Claude Desktop y la mayoría de las integraciones locales usan.
Cuándo usarlo: Herramientas locales, integraciones CLI, desarrollo. El cliente y el servidor se ejecutan en la misma máquina.
Pros: Cero configuración de red, aislamiento de procesos, configuración simple.
Contras: No funciona a través de una red. El cliente debe poder ejecutar el binario del servidor.
Streamable HTTP
Introducido en la actualización de la especificación MCP de marzo 2025, Streamable HTTP es el transporte moderno para servidores MCP remotos. El servidor se ejecuta como un servicio HTTP, y los clientes se comunican mediante solicitudes POST. El servidor puede opcionalmente transmitir respuestas usando Server-Sent Events.
Así se agrega Streamable HTTP al servidor TypeScript:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
// ... (same server setup and tool registration as before)
const app = express();
app.use(express.json());
// Handle MCP requests at /mcp endpoint
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless mode
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// Optional: health check
app.get("/health", (_req, res) => {
res.json({ status: "ok", server: "weather-service" });
});
app.listen(3050, () => {
console.log("MCP HTTP server listening on port 3050");
});Y en Python:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-service")
# ... (same tool and resource registration as before)
if __name__ == "__main__":
# FastMCP supports transport switching via the run method
mcp.run(transport="streamable-http", host="0.0.0.0", port=3050)Cuándo usarlo: Servidores remotos, despliegues en la nube, configuraciones multi-tenant. Cualquier momento en que el cliente y el servidor no estén en la misma máquina.
Pros: Funciona a través de la red, soporta modos con estado y sin estado, puede manejar múltiples clientes.
Contras: Requiere infraestructura HTTP, más complejo de asegurar.
SSE (Server-Sent Events) — Deprecado
El transporte HTTP original en la especificación de noviembre 2024 usaba SSE para streaming del servidor al cliente con endpoints POST separados para mensajes del cliente al servidor. La actualización de la especificación de marzo 2025 deprecó SSE en favor de Streamable HTTP, que es más simple y más resiliente.
Si estás construyendo algo nuevo, usa Streamable HTTP. Si necesitas soportar clientes más antiguos, puedes ejecutar ambos transportes en paralelo.
¿Cómo se agregan capacidades avanzadas a un servidor MCP?
Extiende tu servidor con esquemas de entrada complejos (objetos anidados, arrays, enums vía Zod o anotaciones de tipo en Python), manejo estructurado de errores usando respuestas isError: true, y plantillas de resources dinámicos con patrones de URI como weather://cities/{city}. Cada uno sigue el mismo patrón de registro que ya conoces.
Una vez que dominas el patrón básico, extender tu servidor es sencillo. Aquí hay patrones que usarás en proyectos reales.
Tools con esquemas de entrada complejos
Zod (TypeScript) y las anotaciones de tipo (Python) manejan objetos anidados, arrays, enums y campos opcionales:
server.tool(
"search-weather-history",
"Search historical weather data with filters",
{
city: z.string(),
dateRange: z.object({
start: z.string().describe("ISO date string"),
end: z.string().describe("ISO date string"),
}),
metrics: z
.array(z.enum(["temperature", "humidity", "wind"]))
.describe("Which metrics to include"),
format: z
.enum(["json", "csv"])
.optional()
.default("json")
.describe("Output format"),
},
async ({ city, dateRange, metrics, format }) => {
// Your implementation here
return {
content: [{ type: "text", text: "results..." }],
};
}
);Manejo de errores
Devuelve isError: true para señalar fallos en la tool sin causar un crash:
server.tool(
"fetch-live-weather",
"Fetch real-time weather from external API",
{ city: z.string() },
async ({ city }) => {
try {
const data = await fetchFromWeatherAPI(city);
return {
content: [{ type: "text", text: JSON.stringify(data) }],
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Failed to fetch weather for ${city}: ${error.message}`,
},
],
};
}
}
);Resources dinámicos con plantillas
Los resources pueden usar plantillas de URI para servir contenido dinámico:
server.resource(
"city-weather",
"weather://cities/{city}",
{ description: "Detailed weather data for a specific city" },
async (uri, { city }) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(weatherDatabase[city.toLowerCase()]),
},
],
})
);¿Qué cambia cuando llevas un servidor MCP a producción?
Los servidores MCP en producción necesitan autenticación (OAuth para HTTP, variables de entorno para stdio), disciplina estricta de logging (solo stderr — stdout rompe JSON-RPC), testing automatizado de tools e inicialización lazy para minimizar la latencia de arranque. Estas preocupaciones son mínimas en desarrollo pero críticas a escala.
Construir un servidor de tutorial es una cosa. Llevarlo a producción es otra. Esto es lo que cambia.
Autenticación
MCP no prescribe un mecanismo de autenticación específico, pero el SDK de TypeScript incluye helpers de OAuth para servidores Streamable HTTP. Para servidores stdio, la autenticación típicamente ocurre a nivel de proceso — variables de entorno, archivos de configuración, o el almacén de credenciales de la aplicación host.
{
"mcpServers": {
"weather-api": {
"command": "node",
"args": ["build/index.js"],
"env": {
"WEATHER_API_KEY": "your-api-key-here"
}
}
}
}Logging
Escribe la salida de diagnóstico en stderr, no en stdout. Stdout está reservado para mensajes JSON-RPC. Este es un error común — un console.log() suelto en TypeScript o un print() en Python corromperá el flujo del protocolo.
// GOOD - writes to stderr, won't interfere with MCP
console.error("Processing request for city:", city);
// BAD - writes to stdout, will break JSON-RPC
console.log("Processing request for city:", city);Testing sistemático de Tools
Una vez que tu servidor tiene más de un puñado de tools, el testing manual con el Inspector no escala. Vas a necesitar pruebas automatizadas que ejerciten cada tool con entradas válidas, entradas inválidas y casos límite. Aquí es donde el testing basado en escenarios brilla — define un conjunto de interacciones (el agente pide el clima, obtiene una respuesta, hace un seguimiento) y verifica que todo el flujo funcione de extremo a extremo.
Si estás ejecutando servidores MCP en producción, monitorear los patrones de llamadas a tools y las tasas de fallo se vuelve crítico. Una tool que silenciosamente devuelve datos incorrectos es peor que una que lanza un error. Los scorecards que rastrean la confiabilidad de las tools junto con la calidad de la conversación te dan el panorama completo.
Rendimiento
Para servidores stdio, el tiempo de arranque importa. El cliente inicia tu proceso en cada nueva sesión (en la mayoría de implementaciones). Si tu servidor tarda 5 segundos en inicializarse porque está cargando un modelo grande o conectándose a una base de datos, eso son 5 segundos de latencia antes de la primera llamada a una tool. Considera la inicialización lazy — conecta a la base de datos en el primer uso, no al arranque.
Para servidores HTTP, piensa en la concurrencia. Una instancia única de servidor MCP podría manejar solicitudes de múltiples clientes simultáneamente. Asegúrate de que tus handlers de tools sean sin estado o estén correctamente sincronizados.
Qué sigue
Ahora tienes la base para construir servidores MCP tanto en TypeScript como en Python. El ejemplo del clima es simple por diseño, pero los patrones se transfieren directamente a servidores del mundo real:
- Servidor MCP de base de datos: Reemplaza los datos simulados con consultas reales. Cada tool se convierte en una consulta parametrizada —
search-customers,get-order-details,update-ticket-status. - Wrapper de API: Convierte cualquier API REST en un servidor MCP. El servidor maneja autenticación, rate limiting y formateo de respuestas. La IA solo ve tools limpias.
- Herramientas internas: Operaciones con archivos, triggers de CI/CD, comandos de despliegue. Los servidores MCP son una forma limpia de darle a los agentes acceso controlado a tu infraestructura.
La especificación oficial de MCP es la referencia autoritativa. El directorio examples/ del SDK de TypeScript tiene servidores ejecutables que cubren desde calculadoras simples hasta servicios completos protegidos con OAuth. El repositorio del SDK de Python incluye ejemplos similares.
MCP sigue evolucionando. El comité de la especificación (ahora bajo la Linux Foundation) está trabajando activamente en mejoras de negociación de capacidades, mejor semántica de streaming y flujos de autenticación estandarizados. Pero los primitivos centrales — tools, resources, prompts — son estables y es poco probable que cambien de forma incompatible.
Construye algo. Despliégalo. Mira lo que tus agentes pueden hacer con herramientas reales a su disposición.
Co-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
Aprende IA Agéntica
Una lección por semana: técnicas prácticas para construir, probar y lanzar agentes IA. Desde ingeniería de prompts hasta monitoreo en producción. Aprende haciendo.



