Cómo crear tu propio servidor MCP en Python (paso a paso)

Si los 20.000 servidores MCP del catálogo no resuelven exactamente tu caso, escribir uno propio es más fácil de lo que parece. En 30 minutos vas a tener un servidor MCP en Python ejecutándose contra Claude Desktop, con tools custom y manejo correcto de errores.

Por qué Python para tu primer servidor MCP

El SDK oficial mcp en Python tiene una API limpia, async-friendly, y aprovecha el ecosistema científico (pandas, requests, sqlalchemy) que probablemente ya conoces. Si vas a integrar APIs internas o lógica de procesamiento de datos, Python es la ruta más rápida. Si tu servidor va a ser distribuible y consumir poca memoria, considera TypeScript/Rust después.

Setup del proyecto

Usa uv de Astral, no pip directo: arranca proyectos en segundos y maneja dependencias correctamente.

uv init mi-mcp-server
cd mi-mcp-server
uv add "mcp[cli]"

Esto te genera un pyproject.toml y un entorno aislado. Crea server.py con el esqueleto mínimo:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("mi-mcp-server")

@mcp.tool()
def saludar(nombre: str) -> str:
    """Devuelve un saludo personalizado."""
    return f"Hola, {nombre}, desde mi servidor MCP."

if __name__ == "__main__":
    mcp.run()

Pruébalo localmente:

uv run mcp dev server.py

Abre el inspector que arranca en localhost — verás la tool saludar registrada y la podrás invocar manualmente.

Conectar a Claude Desktop

Edita claude_desktop_config.json:

{
  "mcpServers": {
    "mi-server": {
      "command": "uv",
      "args": ["--directory", "/ruta/absoluta/a/mi-mcp-server", "run", "server.py"]
    }
  }
}

Reinicia Claude Desktop completo. La tool aparece en el ícono de martillo.

Tools con argumentos complejos y validación

FastMCP usa los type hints de Python para autovalidar argumentos. Para casos avanzados, declara tipos Pydantic:

from pydantic import BaseModel, Field

class CrearTareaArgs(BaseModel):
    titulo: str = Field(..., min_length=3, max_length=120)
    prioridad: int = Field(default=2, ge=1, le=5)
    tags: list[str] = []

@mcp.tool()
def crear_tarea(args: CrearTareaArgs) -> dict:
    # Tu lógica aquí: insertar en DB, llamar API, etc.
    return {"id": 42, "titulo": args.titulo}

El modelo recibe el schema generado y lo respeta — menos alucinaciones, mejores invocaciones.

Manejo de errores: explícito, no excepciones desnudas

No dejes que las excepciones de Python lleguen al cliente como tracebacks crípticos. Captura y devuelve mensajes accionables:

@mcp.tool()
def consultar_api(endpoint: str) -> dict:
    try:
        r = requests.get(f"https://api.ejemplo.com/{endpoint}", timeout=10)
        r.raise_for_status()
        return r.json()
    except requests.Timeout:
        return {"error": "La API tardó más de 10 segundos. Reintenta o reduce el alcance."}
    except requests.HTTPError as e:
        return {"error": f"API devolvió {e.response.status_code}: {e.response.text[:200]}"}

El modelo lee el campo error y reacciona en lenguaje natural en vez de mostrar un wall of text al usuario.

Resources y prompts (más allá de tools)

MCP soporta tres primitivas: tools (acciones), resources (datos referenciables) y prompts (templates parametrizados). Para un servidor de notas, expón cada nota como un resource en vez de una tool — el modelo puede leerlo proactivamente sin invocar nada.

Empaquetar y publicar en PyPI

Cuando esté maduro, publica para que otros lo instalen con uvx tu-paquete:

uv build
uv publish

Y luego envíalo al directorio MCP·es para que aparezca en el catálogo en español.

Buenas prácticas finales

  • Logs a stderr, no stdout — stdout es el canal del protocolo, escribir ahí lo rompe.
  • Idempotencia en tools que mutan estado.
  • Timeouts explícitos en cualquier llamada de red.
  • Documenta el docstring de cada tool — eso es lo que el modelo ve para decidir cuándo invocarla.