Crear un servidor MCP en Python listo para producción con FastMCP dejó de ser un experimento a mediados de 2026. El 70% de los servidores MCP publicados en Q1 2026 usan alguna versión de FastMCP y el ecosistema pasó de 200 servidores comunitarios en 2025 a más de 2.000 este año. Este tutorial no es un "hola mundo": cubre los diez pasos que separan un prototipo de un servicio empaquetado, autenticado, testeado y desplegado.
Si aún no dominas el protocolo, empieza por nuestra guía completa de Model Context Protocol y la variante TypeScript para Claude Desktop. Aquí nos centramos en el flujo Python de producción con FastMCP 1.27.
Por qué Python es la primera opción para servidores MCP en 2026
El razonamiento es infraestructural. Casi todo el tejido de datos internos de una empresa (Airflow, dbt, Django, FastAPI, pandas, LangChain, LlamaIndex, backends ML) vive en Python. Escribir el servidor MCP en el mismo lenguaje evita puentes RPC y serializaciones cruzadas. FastMCP colapsa el protocolo JSON-RPC 2.0, la validación con Pydantic y el transporte HTTP streamable en tres decoradores: @server.tool, @server.resource y @server.prompt.
Anthropic publicó la especificación MCP en noviembre de 2024. En marzo de 2025 llegó Streamable HTTP en sustitución del legacy HTTP+SSE (deprecado). La revisión de noviembre de 2025 hizo obligatorio OAuth 2.1 con PKCE para cualquier servidor expuesto en internet, y FastMCP 3.0 unificó la API en enero de 2026. A 1 de agosto de 2026 el SDK Python oficial está en la versión 1.27.0.
Requisitos previos y hardware recomendado para desarrollar
Antes de tocar código necesitas Python 3.11 o superior (mejor 3.12 por asyncio), uv como gestor de proyecto (10-100 veces más rápido que pip) y una máquina cómoda para Docker, tests y varios servidores MCP a la vez. Estas son las piezas que uso a diario en Arkaia:
- Portátil de desarrollo Mac: MacBook Pro 14" con chip M4 Pro. El chip M4 Pro maneja stacks Docker completos con inferencia local de LLM (Ollama) sin ventilador.
- Alternativa mini-PC potente y silenciosa: Beelink SER8 con AMD Ryzen 7 8745HS y 32 GB DDR5. Ryzen 7 8745HS con 32 GB DDR5, ideal para dejarlo como servidor de staging de tus MCP en LAN.
- Monitor 27" QHD alto refresco: LG UltraGear 27GP850P 27" QHD 180 Hz IPS. Terminal + inspector MCP + navegador sin cambios de contexto.
- Teclado mecánico programador: Keychron K8 mecánico inalámbrico o el Logitech MX Mechanical retroiluminado si prefieres perfil bajo tipo portátil.
- Ratón ergonómico: Logitech MX Master 3S para desarrolladores, referencia absoluta para sesiones largas de refactor.
- Auriculares con cancelación: Sony WH-1000XM5 con cancelación de ruido. Bloquean el ruido cuando depuras timeouts SSE a las tres de la tarde en verano.
- Silla ergonómica: Secretlab TITAN Evo. Reposabrazos 4D y soporte lumbar magnético; se nota tras la primera semana.
Paso 1: preparar el entorno con uv
uv es el gestor de proyectos que Astral publicó en 2024 y que en 2026 ya reemplaza a pip, poetry y virtualenv en la mayoría de proyectos Python nuevos. Su instalación es un solo comando y crea el venv automáticamente al añadir la primera dependencia. Para un servidor MCP importa además que uvx permita ejecutar el paquete publicado en PyPI sin instalación previa, algo equivalente a npx en el mundo JavaScript.
# Instalar uv (gestor de paquetes ultrarrápido)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Crear proyecto nuevo y añadir dependencias
uv init mcp-server-prod
cd mcp-server-prod
uv add "mcp[cli]>=1.27.0" pydantic httpx structlog
# Comprobar versión instalada
uv run python -c "import mcp; print(mcp.__version__)"
# Salida esperada: 1.27.0 (o superior)
Resultado esperado: un directorio mcp-server-prod con pyproject.toml, .python-version, uv.lock y un venv aislado en .venv/. La primera ejecución tarda unos segundos; a partir de ahí uv run es instantáneo.
Trampa común: si trabajas en Windows y arrancas el servidor por stdio desde Claude Desktop, el encoding por defecto de la consola puede corromper la salida JSON-RPC. Fuerza siempre PYTHONIOENCODING=utf-8 en las variables de entorno del cliente MCP.
Paso 2: estructura mínima de un servidor FastMCP
FastMCP funciona con una única clase, FastMCP, que se instancia con el nombre del servidor y expone tres decoradores. Todo lo demás (transporte, negociación de capacidades, esquema JSON) se autogenera a partir de las anotaciones de tipos. La primera versión ejecutable son diez líneas:
# server.py
from mcp.server.fastmcp import FastMCP
# El nombre identifica el servidor ante los clientes MCP
server = FastMCP("arkaia-demo")
@server.tool()
def hello(name: str) -> str:
"""Devuelve un saludo personalizado."""
return f"Hola, {name}!"
if __name__ == "__main__":
# Por defecto arranca en modo stdio (Claude Desktop, Cursor)
server.run()
Resultado esperado: al arrancar el servidor no debes ver nada en stdout (todo lo que no sea JSON-RPC rompería el canal); los logs van a stderr. Un cliente MCP puede llamar a tools/list y recibirá el esquema del tool hello.
Trampa común: nunca hagas print() en un servidor stdio; sobrescribe el flujo JSON-RPC y el cliente cierra la conexión. Usa structlog o logging.basicConfig(stream=sys.stderr).
Paso 3: tool con validación Pydantic y respuesta estructurada
La razón por la que FastMCP se impuso frente a soluciones caseras es la validación de tipos con Pydantic v2. Al declarar un tool con un modelo Pydantic como argumento, el SDK genera automáticamente el JSON Schema que el cliente ve, valida la entrada, y rechaza llamadas mal formadas antes de que tu código las procese. Devolver otro modelo Pydantic da al LLM una estructura fiable en vez de un string libre.
from typing import Literal
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
import httpx
server = FastMCP("arkaia-weather")
class WeatherQuery(BaseModel):
city: str = Field(..., min_length=2, description="Ciudad en formato UTF-8")
units: Literal["metric", "imperial"] = "metric"
class WeatherReport(BaseModel):
city: str
temperature: float
conditions: str
source: str
@server.tool()
async def get_weather(query: WeatherQuery) -> WeatherReport:
"""Consulta la temperatura actual de una ciudad."""
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": 40.4, "longitude": -3.7, "current": "temperature_2m"},
)
r.raise_for_status()
data = r.json()
return WeatherReport(
city=query.city,
temperature=data["current"]["temperature_2m"],
conditions="soleado",
source="open-meteo",
)
Resultado esperado: el tool get_weather aparece en el inspector con un formulario que valida los campos antes de enviarlos. Si el cliente pasa city="A", la respuesta es un error 400 estructurado sin llegar a tu función.
Trampa común: el timeout por defecto de MCP es 60 segundos por llamada. Si tu tool llama a APIs externas, usa httpx.AsyncClient(timeout=...) con un valor inferior y devuelve un error explícito; nunca dejes que el cliente MCP corte con un timeout genérico.
Paso 4: exponer contenido con Resource y URI dinámica
Los resources MCP son fuentes de solo lectura que el cliente carga como contexto (documentación, esquemas, snapshots de configuración). A diferencia de los tools, no producen efectos secundarios: el LLM los lee y decide si citarlos. FastMCP soporta URIs con parámetros al estilo Flask, y valida los tipos al vuelo.
from mcp.server.fastmcp import FastMCP
server = FastMCP("arkaia-docs")
# Los recursos se identifican por URI. Los parámetros entre llaves
# se inyectan como argumentos tipados de la función.
@server.resource("docs://{topic}")
def get_doc(topic: str) -> str:
"""Devuelve la documentación cacheada de un tema."""
catalogo = {
"mcp": "Model Context Protocol es un estándar JSON-RPC 2.0...",
"fastmcp": "FastMCP es el SDK Python oficial de Anthropic...",
"pydantic": "Pydantic valida los argumentos y las respuestas...",
}
return catalogo.get(topic, f"Sin documentación para: {topic}")
Resultado esperado: el cliente puede pedir docs://mcp, docs://fastmcp o cualquier valor arbitrario y recibir el contenido correspondiente. Un cliente Claude Desktop lo expone como recurso adjuntable en la conversación.
Trampa común: los resources no deben devolver bloques enormes de texto (más de 100 KB satura el contexto del LLM). Si tu recurso es grande, expón un resumen o un índice y crea tools que devuelvan fragmentos concretos.
Paso 5: prompt template reutilizable
Los prompts MCP son plantillas parametrizadas equivalentes a los slash commands de Claude Desktop. Sirven para que el cliente ofrezca a la persona usuaria patrones probados ("revisar código", "traducir a español técnico", "resumir un ticket de Jira") sin que el LLM tenga que reinventar el enunciado cada vez.
from mcp.server.fastmcp import FastMCP
from mcp.types import PromptMessage, TextContent
server = FastMCP("arkaia-prompts")
@server.prompt()
def code_review(language: str, code: str) -> list[PromptMessage]:
"""Plantilla reutilizable de revisión de código."""
return [
PromptMessage(
role="user",
content=TextContent(
type="text",
text=(
f"Actúa como revisor senior de {language}. Analiza el "
f"siguiente código detectando bugs, olores de diseño y "
f"riesgos de seguridad. Devuelve un informe estructurado.\n\n"
f"```\n{code}\n```"
),
),
)
]
Resultado esperado: en Claude Desktop aparece un nuevo comando /code_review con dos campos rellenables (language, code). Al ejecutarlo, el servidor devuelve el mensaje ya montado y el LLM contesta.
Trampa común: los prompts se sirven al cliente antes de la conversación; no metas datos privados directamente. Si necesitas contexto sensible, combínalos con un tool que lo lea autenticado.
Paso 6: autenticación con API key y middleware
Un servidor MCP local por stdio no necesita autenticación: el cliente lo lanza como subproceso. Pero en cuanto lo expones por HTTP la historia cambia. La auditoría de seguridad de 2026 encontró que el 25% de los servidores MCP públicos no tiene ninguna autenticación y otro 53% depende de API keys estáticas de larga vida. Para casos internos, una API key con rotación mensual es suficiente; para servicios expuestos en internet, la especificación MCP de noviembre de 2025 exige OAuth 2.1 con PKCE.
import os
from mcp.server.fastmcp import FastMCP
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
server = FastMCP("arkaia-secure")
VALID_KEYS = set(os.getenv("MCP_API_KEYS", "").split(","))
class ApiKeyAuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# No autenticamos los health checks internos
if request.url.path in ("/health", "/ready"):
return await call_next(request)
auth = request.headers.get("Authorization", "")
token = auth.removeprefix("Bearer ").strip()
if token not in VALID_KEYS:
return JSONResponse(
{"error": "unauthorized"}, status_code=401
)
return await call_next(request)
app = server.streamable_http_app()
app.add_middleware(ApiKeyAuthMiddleware)
Resultado esperado: cualquier petición sin cabecera Authorization: Bearer <clave> recibe 401. Los health checks internos siguen abiertos para que Kubernetes o Docker los usen.
Trampa común: no guardes las claves en el código ni en el Dockerfile. Usa EnvironmentFile= en systemd, secretos de Kubernetes o gestores dedicados (AWS Secrets Manager, Vault). Si vas a producción abierta, migra a OAuth 2.1: WorkOS y Scalekit tienen SDKs listos que se enchufan al mismo middleware.
Paso 7: transportes stdio, HTTP streamable y SSE legacy
El transporte determina dónde vive tu servidor y cuántos clientes lo consumen. La regla de 2026:
| Transporte | Cuándo usarlo | Limitaciones |
|---|---|---|
| stdio | Cliente único en local (Claude Desktop, Cursor, Codex CLI) | Un solo proceso cliente; sin autenticación; sin red |
| Streamable HTTP | Producción, multi-cliente, remoto, con autenticación | Requiere reverse proxy con TLS; sesiones en memoria por defecto |
| SSE (legacy) | Servidores antiguos, migración | Deprecado en marzo 2025; retirada en muchas plataformas durante 2026 |
# Transporte stdio: para Claude Desktop / Cursor en el mismo equipo
if __name__ == "__main__":
server.run() # equivalente a transport="stdio"
# Transporte HTTP streamable: para producción, multi-cliente, remoto
# ejecutar con: uvicorn server:app --host 0.0.0.0 --port 8080
# Registro en Claude Desktop (~/.config/claude/mcp.json)
# {
# "mcpServers": {
# "arkaia-demo": {
# "command": "uvx",
# "args": ["arkaia-mcp-server"]
# }
# }
# }
Trampa común: stdio colapsa con carga concurrente. En pruebas públicas, 20 conexiones simultáneas producen 18 fallos. Si necesitas más de un cliente, ve directo a Streamable HTTP.
Paso 8: testing con MCP Inspector y pytest
Las pruebas son el detalle que separa un servidor MCP hobby de uno de producción. Hay dos herramientas complementarias: el MCP Inspector oficial (una SPA que se lanza con npx y permite explorar tools, resources y prompts en vivo) y pytest con un cliente en memoria del propio SDK, que ejecuta el servidor sin sockets y da feedback sub-segundo.
# tests/test_server.py
import pytest
from mcp.client.session import ClientSession
from mcp.shared.memory import create_connected_server_and_client_session
from server import server
@pytest.fixture
async def client():
"""Cliente MCP en memoria conectado al servidor real (sin sockets)."""
async with create_connected_server_and_client_session(server) as (_, cli):
yield cli
@pytest.mark.asyncio
async def test_hello_tool(client: ClientSession):
result = await client.call_tool("hello", {"name": "Arkaia"})
assert "Arkaia" in result.content[0].text
@pytest.mark.asyncio
async def test_list_tools(client: ClientSession):
tools = await client.list_tools()
assert any(t.name == "hello" for t in tools.tools)
# Ejecución exploratoria con el inspector oficial:
# npx @modelcontextprotocol/inspector uv run server.py
Resultado esperado: uv run pytest -v ejecuta los tests en menos de un segundo. El inspector muestra los tools con sus JSON Schema derivados de Pydantic y permite invocarlos manualmente.
Trampa común: si escribes tests end-to-end conectando al servidor por HTTP dentro del propio proceso, cuida los event loops asyncio anidados. La ventaja del cliente en memoria de FastMCP es que evita ese problema entero.
Paso 9: empaquetado y publicación en PyPI
Un servidor MCP profesional se distribuye como paquete Python instalable. El estándar de facto es publicar en PyPI con un entry point CLI, para que los clientes puedan lanzarlo con uvx nombre-del-paquete sin clonar el repo. Añade el sufijo -mcp al nombre del paquete para que sea descubrible.
# pyproject.toml
[project]
name = "arkaia-mcp-server"
version = "0.1.0"
description = "Servidor MCP de ejemplo listo para producción"
requires-python = ">=3.11"
dependencies = [
"mcp[cli]>=1.27.0",
"pydantic>=2.9",
"httpx>=0.27",
"structlog>=24.4",
]
[project.scripts]
arkaia-mcp-server = "arkaia_mcp_server.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Publicar en PyPI:
# uv build
# uv publish --token $PYPI_TOKEN
# El usuario final lo ejecuta con:
# uvx arkaia-mcp-server
Resultado esperado: tras publicar, cualquiera puede probarlo con uvx arkaia-mcp-server. En Claude Desktop se registra con "command": "uvx", "args": ["arkaia-mcp-server"].
Trampa común: los ficheros __init__.py y __main__.py son obligatorios si el entry point apunta a un módulo. Además, verifica el readme: PyPI renderiza Markdown y una descripción vacía es la razón número uno por la que los servidores publicados quedan invisibles.
Paso 10: despliegue en Docker y systemd
Para el despliegue definitivo, dos rutas: Docker (Kubernetes o docker compose) para orquestación multi-instancia, y systemd para una VM tradicional con reverse proxy. Ambas comparten un principio: transporte Streamable HTTP, TLS terminado en Nginx o Traefik, secretos fuera de la imagen y logs estructurados en JSON para poder buscar por campos.
# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ ./src/
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
EXPOSE 8080
HEALTHCHECK CMD python -c "import httpx; httpx.get('http://localhost:8080/health').raise_for_status()"
CMD ["uvicorn", "src.arkaia_mcp_server.server:app", \
"--host", "0.0.0.0", "--port", "8080"]
# /etc/systemd/system/arkaia-mcp.service
[Unit]
Description=Arkaia MCP Server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/arkaia-mcp
EnvironmentFile=/etc/arkaia-mcp.env
ExecStart=/opt/arkaia-mcp/.venv/bin/uvicorn \
arkaia_mcp_server.server:app --host 127.0.0.1 --port 8080
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
Y la pieza que casi nadie recuerda hasta que un cliente pierde una consulta a las tres de la mañana: logs estructurados. structlog convierte cada entrada en JSON, indexable en Loki, Datadog o CloudWatch.
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
]
)
log = structlog.get_logger()
@server.tool()
def priced_lookup(sku: str) -> dict:
log.info("lookup_start", sku=sku)
...
log.info("lookup_ok", sku=sku, latency_ms=42)
return {"sku": sku, "status": "ok"}
Trampa común: si escalas horizontalmente con Streamable HTTP, las sesiones se guardan en memoria por instancia. Necesitas sticky sessions en el load balancer o un backend Redis para sesiones distribuidas. Sin ello, un reconexión SSE saltará a otra instancia y perderá el estado.
Errores comunes y estrategias de debugging
Los cinco errores que se repiten en producción:
- Salida contaminada en stdio: cualquier
print,tqdmo traza a stdout rompe el canal JSON-RPC. Redirige todo a stderr. - Timeouts de 60 segundos: llamadas externas lentas cortan la conexión. Añade
httpxcon timeouts propios y devuelve errores explícitos. - Reconexiones SSE fallidas: si tu reverse proxy hace buffering (Nginx por defecto), los eventos se agrupan. Añade
proxy_buffering off;a la ubicación/mcp. - Encoding en Windows: rutas y salidas UTF-16 rompen JSON-RPC. Fuerza
PYTHONUTF8=1. - Esquemas Pydantic anidados: los clientes MCP viejos no soportan referencias
$ref. Usamodel_config = ConfigDict(json_schema_extra={"$defs": {}})o aplanha el modelo.
La observabilidad no es opcional en MCP. Un servidor sin logs estructurados es un servidor que fallará en silencio la primera vez que un cliente cambie el orden de los argumentos.
Casos de uso reales para servidores MCP en Python
Estos son los cinco flujos donde un servidor MCP en Python demuestra su valor:
- RAG interno de documentación: tool
search_docscontra tu vector store (Qdrant, Weaviate, pgvector) y resourcedoc://{id}para el documento completo. - Gestión de CRM: tools tipados
create_lead,update_deal_stage,list_open_opportunitiescon validación Pydantic y auditoría. - Jira o Slack: envuelve los servidores vendor-maintained con tu propia lógica: pre-filtros, plantillas de tickets, permisos por equipo.
- Data ops sobre PostgreSQL:
run_sqlde solo lectura con lista blanca de esquemas y timeouts agresivos, más resourceschema://public/{table}. - Operaciones: prompts reutilizables para incidencias P1, tools contra PagerDuty, resources con el estado del clúster.
Recursos y libros recomendados
Estos tres libros son la base técnica para construir servidores MCP en serio:
- Fluent Python (2ª edición) de Luciano Ramalho. La biblia del Python idiomático moderno. Cubre asyncio, tipos y patrones que aparecen constantemente en FastMCP.
- Architecture Patterns with Python de Harry Percival y Bob Gregory. Domain Driven Design aplicado a Python; te enseña a separar puertos y adaptadores para que tu servidor MCP no se convierta en un fajo de spaghetti.
- Building Microservices (2ª edición) de Sam Newman. Un servidor MCP suele exponer un microservicio; este es el libro para entender cómo diseñar sus fronteras.
Preguntas frecuentes (FAQ)
¿Qué versión de Python es la mínima para FastMCP en 2026?
Python 3.11 es el mínimo soportado. Se recomienda 3.12 por las mejoras de rendimiento en asyncio, decisivas para servidores con muchas llamadas concurrentes.
¿Puedo usar el mismo servidor en Claude Desktop y expuesto por HTTP?
Sí. FastMCP permite arrancar el mismo código con server.run() (stdio) o con server.streamable_http_app() montado en Uvicorn. Ajusta el transporte según el cliente.
¿Necesito OAuth 2.1 obligatoriamente?
Solo si el servidor está expuesto en internet. La revisión de noviembre de 2025 lo exige para MCP público; en redes internas o VPN una API key rotada mensualmente sigue siendo aceptable.
¿Cómo depuro un servidor que Claude Desktop no reconoce?
Lanza el mismo comando con MCP Inspector: npx @modelcontextprotocol/inspector uv run server.py. Ver el schema y ejecutar tools manualmente aísla si el problema es del servidor o del cliente.
¿Cuánta latencia añade FastMCP frente a llamar la API directamente?
Menos de 5 ms para un tool sin I/O. La mayor parte del coste es la validación Pydantic, que a cambio elimina errores de tipos.
¿Puedo compartir sesión entre varias instancias de mi servidor?
Por defecto no: Streamable HTTP guarda las sesiones en memoria por instancia. Necesitas sticky sessions en el load balancer o un backend externo (Redis) para verdadera escalabilidad horizontal.
¿Merece la pena publicar en PyPI si es un servidor privado?
Sí: puedes usar un índice privado (Nexus, Artifactory, AWS CodeArtifact) y beneficiarte del mismo flujo uvx. Evita instalaciones manuales para cada equipo.
¿Y si prefiero TypeScript?
Tenemos un tutorial dedicado con el mismo enfoque: crear servidor MCP en TypeScript paso a paso. La API mental es idéntica; solo cambia el lenguaje.
Conclusión: del prototipo a la producción en un día
Cuando MCP nació en noviembre de 2024, montar un servidor con autenticación, tests y despliegue reproducible era un proyecto de semanas. En agosto de 2026 el ecosistema Python permite hacerlo en una jornada: uv resuelve el entorno, FastMCP el protocolo, Pydantic la validación, pytest los tests, Docker el despliegue. Un servidor sin autenticación, sin tests o sin logs estructurados es un incidente esperando ocurrir.
Para la parte conceptual, vuelve a la guía completa de MCP o a la versión TypeScript. La ruta Python que has visto es la que usamos en Arkaia para todos los servidores internos: cada uno vive en su repo, se publica en PyPI privado y se despliega con la misma plantilla systemd descrita aquí.
Este artículo contiene enlaces afiliados a Amazon con el tag webmasteroson-21. Si compras a través de estos enlaces, Arkaia puede recibir una comisión sin coste adicional para ti. Nuestra valoración editorial es independiente y no varía según los ingresos por afiliación.
Comentarios
Inicia sesion para dejar un comentario
Acceder