Servidor MCP en Python con FastMCP desplegado en producción sobre Docker y systemd
Volver al blog
TUTORIALES 1 Agosto 2026 18 min lectura 11 visitas

Tutorial FastMCP Python: crea un servidor MCP listo para producción paso a paso (agosto 2026)

Arkaia
Arkaia Editor

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.

Servidor MCP en Python desplegado en producción con FastMCP
Un servidor MCP en Python bien empaquetado se convierte en la interfaz oficial entre tus datos internos y cualquier cliente compatible (Claude, Cursor, ChatGPT desktop).

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:

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()
Arquitectura interna de FastMCP con decoradores tool, resource y prompt
FastMCP colapsa el protocolo MCP en tres decoradores: tools ejecutables, resources de solo lectura y prompts reutilizables.

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:

TransporteCuándo usarloLimitaciones
stdioCliente único en local (Claude Desktop, Cursor, Codex CLI)Un solo proceso cliente; sin autenticación; sin red
Streamable HTTPProducción, multi-cliente, remoto, con autenticaciónRequiere reverse proxy con TLS; sesiones en memoria por defecto
SSE (legacy)Servidores antiguos, migraciónDeprecado en marzo 2025; retirada en muchas plataformas durante 2026
Comparativa visual entre transporte stdio local y HTTP streamable en producción
stdio es rey en el escritorio; HTTP streamable es el único transporte apto para producción compartida.
# 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
Interfaz web del MCP Inspector con tools listados y respuesta JSON
El MCP Inspector reproduce la vista que un cliente Claude tendría de tus tools, con validación de esquema en directo.

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"]
Servidor MCP dockerizado detrás de un reverse proxy con TLS
Un despliegue reproducible en Docker con multi-stage build reduce la imagen final por debajo de 200 MB.
# /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, tqdm o 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 httpx con 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. Usa model_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_docs contra tu vector store (Qdrant, Weaviate, pgvector) y resource doc://{id} para el documento completo.
  • Gestión de CRM: tools tipados create_lead, update_deal_stage, list_open_opportunities con 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_sql de solo lectura con lista blanca de esquemas y timeouts agresivos, más resource schema://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:

Estación de trabajo de desarrollo Python con monitor QHD, teclado mecánico y ratón ergonómico
Estación de trabajo para desarrollo MCP.

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.

Compartir:

Comentarios

Cargando comentarios...