Saltar al contenido principal

Construcción de servidores MCP

Definición

Un servidor MCP es un proceso que expone capacidades a aplicaciones de IA compatibles con MCP a través del Model Context Protocol. Actúa como el puente entre un modelo de IA y algún sistema externo — un sistema de archivos, una API REST, una base de datos, un ejecutor de código — envolviendo la funcionalidad de ese sistema en una interfaz bien definida y descubrible. El servidor posee los detalles de implementación; el cliente solo necesita conocer el protocolo. Cualquier cliente compatible con MCP puede conectarse a cualquier servidor MCP e inmediatamente descubrir y usar sus capacidades sin trabajo de integración personalizado.

Un servidor MCP puede exponer tres categorías de capacidades. Las herramientas son funciones invocables que aceptan entrada estructurada y devuelven salida — la IA las invoca para tomar acciones o recuperar información dinámica. Los recursos son fuentes de datos de solo lectura, direccionadas por URI, que la IA puede leer para obtener contexto — archivos, registros de bases de datos, instantáneas de API. Los prompts son plantillas de prompts reutilizables y parametrizadas almacenadas en el servidor que los clientes pueden presentar a los usuarios o inyectar en conversaciones. Un solo servidor puede ofrecer cualquier combinación de estos; muchos servidores solo exponen herramientas.

El ciclo de vida del servidor sigue un patrón predecible: el servidor inicia y se vincula a un transporte (stdio o HTTP/SSE), espera a que un cliente se conecte, completa el handshake initialize para negociar versiones del protocolo y capacidades, luego entra en su bucle principal respondiendo a las solicitudes. Cuando el cliente se desconecta o envía una señal de apagado, el servidor realiza la limpieza y sale. Dado que el protocolo tiene estado dentro de una sesión, el servidor puede mantener estado por conexión — por ejemplo, almacenando en caché respuestas costosas de la API durante la duración de una sesión.

Cómo funciona

Configuración e inicialización del servidor

Configurar un servidor MCP comienza con la creación de una instancia McpServer (del SDK de alto nivel) o una instancia Server (del SDK de bajo nivel) con un nombre y versión. El nombre y la versión se envían al cliente durante el handshake initialize y ayudan con la depuración y el registro. Después de crear el servidor, registras las capacidades — herramientas, recursos, prompts — antes de conectarte a un transporte. La clase McpServer de alto nivel del SDK proporciona métodos ergonómicos server.tool(), server.resource() y server.prompt() que manejan el enrutamiento de solicitudes y la validación de esquemas internamente. El paso de conexión (server.connect(transport)) inicia el bucle de eventos y bloquea hasta que la sesión termina.

Definición de herramientas

Las herramientas son la capacidad MCP más comúnmente usada. Cada herramienta tiene tres elementos requeridos: un nombre (un identificador corto y en minúsculas usado en solicitudes tools/call), una descripción (una explicación en lenguaje natural que la IA usa para decidir cuándo y cómo llamar a la herramienta) y un esquema de entrada (un esquema Zod en el SDK de TypeScript, convertido a JSON Schema para el protocolo). Cuando un cliente llama a una herramienta, el SDK valida los argumentos entrantes contra el esquema antes de invocar tu función manejadora, por lo que recibes una entrada tipada y validada. El manejador devuelve un array content de bloques de contenido — texto, imágenes o recursos incrustados — que el cliente pasa de vuelta al modelo de IA. Las herramientas también pueden establecer isError: true en su respuesta para señalar un error recuperable, lo que permite a la IA reintentar o recurrir de forma elegante.

Definición de recursos

Los recursos exponen datos similares a archivos que la IA puede leer para obtener contexto. Un recurso se identifica por una URI (p. ej., file:///path/to/data.json o postgres://mydb/users/123) y tiene un tipo MIME que le dice al cliente cómo manejar el contenido. Los recursos se descubren a través de resources/list y se leen a través de resources/read. El SDK soporta tanto recursos estáticos (registrados con una URI fija y contenido) como recursos dinámicos (registrados con un patrón de plantilla de URI, resuelto en el momento de lectura). Las plantillas de recursos usan la sintaxis de plantilla de URI RFC 6570 — por ejemplo, file:///{path} coincide con cualquier ruta de archivo. Cuando el cliente lee una URI de recurso que coincide con una plantilla, tu manejador recibe las variables de plantilla extraídas y devuelve el contenido. Los recursos deben usarse para datos que la IA necesita leer pero no modificar; para operaciones de escritura, usa una herramienta.

Definición de prompts

Los prompts son plantillas de interacción reutilizables. Un prompt tiene un nombre, una descripción y una lista opcional de argumentos (nombre, descripción, indicador de requerido). Cuando un cliente solicita un prompt a través de prompts/get, tu manejador recibe los valores de los argumentos y devuelve una lista de mensajes — típicamente una combinación de mensajes con rol user y assistant — que el cliente inyecta en la conversación. Los prompts permiten a los autores de servidores codificar conocimiento del dominio sobre cómo interactuar con las capacidades del servidor. Por ejemplo, un servidor de bases de datos podría exponer un prompt query_builder que acepta una descripción en lenguaje natural de una consulta y devuelve un prompt estructurado que guía a la IA para producir SQL seguro y parametrizado.

Configuración del transporte

La capa de transporte determina cómo el servidor se comunica con los clientes. El transporte stdio (StdioServerTransport) lee desde process.stdin y escribe en process.stdout. Es el predeterminado para servidores de herramientas locales — la aplicación host genera el servidor como proceso hijo y se comunica a través de los flujos del proceso. No se necesita configuración de red, y el stderr del servidor está disponible para el registro sin interferir con el protocolo. El transporte HTTP con SSE (SSEServerTransport) acepta solicitudes HTTP POST para mensajes de cliente a servidor y transmite mensajes de servidor a cliente a través de un endpoint SSE /sse. Esto es apropiado para servidores compartidos a los que múltiples clientes pueden conectarse simultáneamente, o para servidores que necesitan ejecutarse como servicios de larga duración en lugar de procesos bajo demanda.

Cuándo usar / Cuándo NO usar

EscenarioConstruir un servidor MCPConsiderar alternativas
Exponer una API o servicio existente a múltiples aplicaciones de IAMejor opción — un servidor, cualquier cliente puede usarloLlamada a funciones directa si solo importa un proveedor y una app
Envolver un sistema de archivos, base de datos o fuente de datos interna para contexto de IAMejor opción — los recursos y herramientas se mapean naturalmentePipeline RAG personalizado si la recuperación semántica es la necesidad principal
Proporcionar plantillas de prompts específicas del dominio a usuarios de IALa capacidad Prompts está específicamente diseñada para estoInyección de prompt del sistema si las plantillas son simples y estáticas
Construyendo herramientas para una única aplicación de IA interna con un proveedorMCP añade estructura útil pero puede ser excesivoLas funciones de herramientas en proceso son más simples
Exponer herramientas que requieren resultados en streaming en tiempo realSoportado a través del transporte SSEWebSockets o streaming personalizado si la sobrecarga del protocolo importa
Herramientas que necesitan mantener estado de larga duración por usuario a través de sesionesRequiere diseño cuidadoso del servidor — las sesiones son 1:1Servicio de backend con estado con un envoltorio MCP delgado

Ejemplos de código

Servidor MCP completo con herramientas, recursos y un prompt

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

const server = new McpServer({
name: "file-and-weather-server",
version: "1.0.0",
});

// -----------------------------------------------------------------------
// Tool 1: Get weather forecast (mock — replace with a real weather API)
// -----------------------------------------------------------------------
server.tool(
"get_weather",
"Fetches the current weather forecast for a given city. Returns temperature, conditions, and humidity.",
{
city: z.string().describe("The city name, e.g. 'Tokyo' or 'Berlin'"),
units: z
.enum(["celsius", "fahrenheit"])
.default("celsius")
.describe("Temperature units"),
},
async ({ city, units }) => {
// In production, call a real weather API here (e.g. Open-Meteo, WeatherAPI)
const mockData = {
city,
temperature: units === "celsius" ? 18 : 64,
units,
condition: "Partly cloudy",
humidity_percent: 72,
forecast: "Light rain expected in the evening",
};

return {
content: [
{
type: "text",
text: JSON.stringify(mockData, null, 2),
},
],
};
}
);

// -----------------------------------------------------------------------
// Tool 2: List directory contents
// -----------------------------------------------------------------------
server.tool(
"list_directory",
"Lists the files and subdirectories in a given directory path. Use this to explore file system structure.",
{
dir_path: z
.string()
.describe("Absolute path to the directory to list"),
},
async ({ dir_path }) => {
try {
const entries = await fs.readdir(dir_path, { withFileTypes: true });
const listing = entries.map((e) => ({
name: e.name,
type: e.isDirectory() ? "directory" : "file",
}));

return {
content: [
{
type: "text",
text: JSON.stringify(listing, null, 2),
},
],
};
} catch (err) {
return {
isError: true,
content: [
{
type: "text",
text: `Error listing directory: ${(err as Error).message}`,
},
],
};
}
}
);

// -----------------------------------------------------------------------
// Resource: Read any text file by path (URI template)
// -----------------------------------------------------------------------
server.resource(
"text-file",
new ResourceTemplate("file:///{file_path}", { list: undefined }),
async (uri, { file_path }) => {
const resolvedPath = path.resolve(String(file_path));

try {
const content = await fs.readFile(resolvedPath, "utf-8");
return {
contents: [
{
uri: uri.href,
mimeType: "text/plain",
text: content,
},
],
};
} catch (err) {
throw new Error(`Cannot read file at ${resolvedPath}: ${(err as Error).message}`);
}
}
);

// -----------------------------------------------------------------------
// Prompt: Guided file analysis template
// -----------------------------------------------------------------------
server.prompt(
"analyze_file",
"Generates a structured prompt that guides the AI to analyze a text file for issues, patterns, or summaries.",
{
file_path: z.string().describe("Path to the file to analyze"),
focus: z
.string()
.optional()
.describe("Optional: specific aspect to focus on, e.g. 'security issues' or 'performance'"),
},
async ({ file_path, focus }) => {
const focusInstruction = focus
? `Focus specifically on: ${focus}.`
: "Provide a general analysis.";

return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Please analyze the file at \`${file_path}\`.

${focusInstruction}

Use the \`read_file\` resource (URI: file:///${file_path}) to read its contents, then provide:
1. A brief summary of what the file contains
2. Key observations or findings
3. Any recommendations or concerns

Be concise and structured.`,
},
},
],
};
}
);

// -----------------------------------------------------------------------
// Start the server
// -----------------------------------------------------------------------
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// Log to stderr so it doesn't interfere with the stdio protocol
console.error("file-and-weather-server is running on stdio");
}

main().catch((err) => {
console.error("Server failed to start:", err);
process.exit(1);
});

Transporte HTTP/SSE (para servidores remotos compartidos)

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();
const server = new McpServer({ name: "remote-server", version: "1.0.0" });

// Register your tools, resources, and prompts here (same API as stdio)
// server.tool(...), server.resource(...), server.prompt(...)

// SSE endpoint: clients connect here to receive server-to-client messages
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});

// POST endpoint: clients send messages here
app.post("/messages", express.json(), async (req, res) => {
// The SSEServerTransport handles routing from the active session
res.status(200).send("ok");
});

app.listen(3000, () => {
console.log("MCP server listening on http://localhost:3000");
});

Recursos prácticos

Ver también