Cómo crear un servidor MCP: ejemplo en Python y TypeScript
En corto: un servidor MCP es un programa pequeño escrito con un SDK oficial que declara herramientas: nombre, descripción y parámetros. En Python, un servidor funcional ocupa una decena de líneas con la librería FastMCP; en TypeScript, algo más. Se empieza con un servidor local (stdio), se comprueba en MCP Inspector, se conecta a Claude Code y, cuando hay que entregárselo a otras personas, se pasa a HTTP. Abajo está todo el camino y los tropiezos que nos llevamos al operar 26 servidores remotos en producción.
Primero decide si necesitas un servidor propio
Para los servicios populares —GitHub, Figma, Notion, un navegador, la búsqueda— ya existen servidores hechos; ver los mejores servidores MCP. Uno propio se escribe cuando el modelo necesita acceso a lo tuyo: una API interna, una base de datos, un CRM, los scripts del equipo. Si todavía estás averiguando qué es MCP, empieza por el repaso de los servidores MCP.
Ejemplo en Python (FastMCP)
Instala el SDK; lo más cómodo es con uv:
uv init my-mcp && cd my-mcp
uv add "mcp[cli]"El archivo server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()
def order_status(order_id: str) -> str:
"""Estado de un pedido por su número. Úsalo cuando el usuario pregunte dónde está su pedido."""
# aquí va la llamada a tu API o a tu base de datos
return f"Pedido {order_id}: entregado al transportista"
if __name__ == "__main__":
mcp.run()Ése es el servidor entero. FastMCP convierte la firma de la función en un esquema de parámetros y el docstring en la descripción de la herramienta. La descripción es justo lo que lee el modelo al decidir si llamarla, así que no escribas «qué hace la función», sino cuándo usarla.
Ejemplo en TypeScript
npm init -y
npm install @modelcontextprotocol/sdk zodEl archivo server.ts (en package.json hace falta "type": "module"):
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'orders', version: '1.0.0' });
server.registerTool(
'order_status',
{
description:
'Estado de un pedido por su número. Úsalo cuando el usuario pregunte dónde está su pedido.',
inputSchema: { orderId: z.string().describe('Número de pedido') },
},
async ({ orderId }) => ({
content: [{ type: 'text', text: `Pedido ${orderId}: entregado al transportista` }],
}),
);
await server.connect(new StdioServerTransport());En un servidor stdio no se puede escribir en stdout
print() o console.log() rompe el protocolo: el cliente recibirá basura en lugar de JSON. Escribe los logs en stderr (console.error; el logging de Python va ahí por defecto).Comprobación en MCP Inspector
Antes de conectar el servidor a un cliente, compruébalo en Inspector, el depurador oficial con interfaz web: muestra la lista de herramientas y sus esquemas, y permite llamar a cada una a mano.
# Python
uv run mcp dev server.py
# TypeScript y cualquier otro servidor
npx @modelcontextprotocol/inspector npx tsx server.tsConexión a Claude Code y otros clientes
Un servidor local se conecta con su comando de arranque. En Claude Code, todo lo que va después de -- es ese comando:
claude mcp add orders -- uv run --directory /path/to/my-mcp server.py
claude mcp add orders-ts -- npx tsx /path/to/server.tsEn los clientes con mcp.json —Cursor, LM Studio y otros— el mismo comando se escribe con los campos command y args. Dónde guarda cada uno su configuración: Claude, Cursor, LM Studio, Codex, OpenCode.
De local a remoto: HTTP
Un servidor local sólo funciona para quien lo arrancó. Para conectarse por una URL —desde claude.ai, desde otra máquina, con todo el equipo— el servidor pasa al transporte Streamable HTTP. En FastMCP es una línea:
mcp.run(transport="streamable-http")En TypeScript, StreamableHTTPServerTransport sustituye a StdioServerTransport dentro de tu servidor HTTP (Express, Fastify). Con HTTP aparecen preguntas que el servidor local no tenía:
| La pregunta | Qué resolver |
|---|---|
| Quién llama | El acceso: OAuth 2.1 como describe la especificación de MCP, o una clave en la cabecera Authorization. Claude.ai y Cursor saben OAuth; LM Studio y CI son más cómodos con clave, así que lo sensato es admitir ambos |
| El estado | Un servidor sin sesiones (stateless) es más fácil de escalar: cada petición se basta a sí misma |
| Las llamadas largas | Las operaciones de minutos mantienen la conexión abierta: hacen falta defensas contra los timeouts (ver abajo) |
| El gasto | Si una herramienta cuesta dinero, reserva el importe antes de llamar a la API externa, no después |
Tropiezos de producción
Mantenemos en marcha 26 servidores MCP remotos: búsqueda y generación de imágenes, vídeo, música y voz. Esto es lo que las guías breves no cuentan y a nosotros nos costó incidentes.
1. El nombre de una herramienta es más largo de lo que parece
Muchos clientes y modelos pegan el nombre del servidor y el de la herramienta; por ejemplo happyhorse-video-generation_happyhorse-reference-to-video. Las API compatibles con OpenAI validan ese nombre contra ^[a-zA-Z0-9_-]{1,64}$ y, si se incumple, rechazan la petición entera, no sólo esa herramienta. Usa nombres cortos, en alfabeto latino, sin puntos ni espacios, y comprueba la longitud contando el prefijo del servidor.
2. Más de cinco minutos de silencio cortan la llamada
La generación de vídeo no envía nada hasta tener el resultado. Los clientes HTTP habituales de Node.js traen timeouts de cabeceras y cuerpo de 300 segundos por defecto, y a los cinco minutos justos el cliente corta la conexión, mientras el servidor sigue trabajando, guarda el archivo y cobra por él. Se cura con comentarios keepalive en el flujo SSE (una línea tipo : keepalive cada 20 segundos): el analizador de eventos los descarta, pero los temporizadores de la conexión se reinician.
3. Sin un 401 correcto, el cliente no encuentra OAuth
Para que Claude o Cursor ofrezcan el acceso por su cuenta, una petición sin token debe responderse con 401 y la cabecera WWW-Authenticate: Bearer resource_metadata="…" apuntando a /.well-known/oauth-protected-resource, que a su vez apunta a un servidor de autorización con registro dinámico de clientes y PKCE. Si falta la cabecera, el cliente se limitará a mostrar «error de conexión».
4. La descripción es un prompt
El modelo elige la herramienta sólo por su descripción. Un difuso «trabaja con pedidos» lleva o a llamadas de más, o a que no la llame nunca. Escribe: cuándo llamarla, cuándo no llamarla, qué devuelve y qué hacer ante un error. Y devuelve los errores como texto con una pista: el modelo lo leerá y corregirá los parámetros solo.
Si quieres un servidor propio sólo para buscar o generar
Preguntas frecuentes
¿Cómo creo mi propio servidor MCP?
@mcp.tool() y una llamada a mcp.run(). Comprueba el servidor en MCP Inspector (mcp dev server.py) y conéctalo a un cliente, por ejemplo con claude mcp add nombre -- uv run server.py.¿En qué lenguaje escribo un servidor MCP?
¿En qué se diferencia un servidor MCP local de uno remoto?
¿Cómo depuro un servidor MCP?
npx @modelcontextprotocol/inspector más el comando de arranque del servidor, o mcp dev server.py en Python. Inspector muestra las herramientas y sus esquemas y permite llamar a cada una a mano. En un servidor local, escribe los logs en stderr: stdout es del protocolo.¿Cómo hago la autorización en un servidor MCP remoto?
WWW-Authenticate que apunta a los metadatos del recurso protegido, y el servidor de autorización admite PKCE y registro dinámico de clientes. Para clientes sin OAuth conviene aceptar además una clave en la cabecera Authorization: Bearer.Pruébalo tú mismo
Todo lo de esta guía funciona en Twelver
Un chat para texto, imágenes, vídeo, música y voz — sin servicios ni suscripciones aparte.
Abrir el chat de Twelver