Módulo 5: MCP Server en Python

Setup: MCP Server en Python

Setup: MCP Server en Python

Descripción de la cápsula

Antes de implementar tools, resources, o patrones async, necesitas un proyecto Python configurado y un MCP server corriendo. Esta cápsula te lleva de cero a un server funcional en menos de 10 minutos.

El setup de un MCP server en Python es significativamente más simple que en TypeScript. No hay tsconfig.json, no hay paso de build, no hay compilación. Creas un virtual environment, instalas el SDK, escribes un archivo Python, y ejecutas. Eso es todo.

La pieza central es FastMCP — un helper de alto nivel incluido en el SDK que abstrae la configuración del server, el transport, y el loop de eventos. Con FastMCP, tu server.py se ve limpio y declarativo desde la primera línea.


Instalación del SDK

Paso 1: Crear el directorio del proyecto

mkdir mcp-server-python
cd mcp-server-python

Paso 2: Crear un virtual environment

Siempre usa un virtual environment para proyectos Python. Aísla las dependencias y evita conflictos:

python -m venv .venv
source .venv/bin/activate  # macOS/Linux
# .venv\Scripts\activate   # Windows

Verificar que estás en el venv:

which python
# Debe mostrar: /ruta/a/tu/proyecto/.venv/bin/python

Paso 3: Instalar el SDK

pip install "mcp[cli]"

El flag [cli] instala herramientas adicionales de línea de comandos que te permiten ejecutar y testear el server directamente.

¿Qué se instala?

mcp           — El SDK core
├── pydantic  — Validación y serialización de datos
├── httpx     — Cliente HTTP async
├── uvicorn   — Server ASGI (para HTTP transport)
├── anyio     — Compatibilidad async
└── mcp[cli]  — CLI tools (mcp dev, mcp run, etc.)

Paso 4: Verificar la instalación

python -c "import mcp; print(mcp.__version__)"

Si ves un número de versión sin errores, el SDK está listo.

También puedes verificar las herramientas CLI:

mcp version

Estructura del proyecto

Estructura mínima

Para un MCP server simple, necesitas solo un archivo:

mcp-server-python/
├── .venv/
├── server.py          # Tu MCP server
└── requirements.txt   # Dependencias

Estructura recomendada para proyectos reales

Cuando el server crece, organízalo así:

mcp-server-python/
├── .venv/
├── src/
│   ├── __init__.py
│   ├── server.py       # Entry point del MCP server
│   ├── tools/          # Tools organizados por dominio
│   │   ├── __init__.py
│   │   ├── search.py
│   │   └── crud.py
│   ├── resources/      # Resources organizados por tipo
│   │   ├── __init__.py
│   │   └── data.py
│   └── models/         # Pydantic models para validación
│       ├── __init__.py
│       └── schemas.py
├── tests/
│   ├── __init__.py
│   └── test_server.py
├── requirements.txt
├── pyproject.toml      # Metadata del proyecto (opcional)
└── README.md

El archivo requirements.txt

mcp[cli]>=1.0.0
httpx>=0.27.0
pydantic>=2.0.0

Genera el lockfile después de instalar:

pip freeze > requirements.txt

Tu primer MCP server: Hello World

El server mínimo

Crea server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("hello-world")


@mcp.tool()
async def greet(name: str) -> str:
    """Saluda a una persona por su nombre."""
    return f"¡Hola, {name}! Bienvenido al mundo de MCP con Python."


@mcp.resource("info://server/status")
async def server_status() -> str:
    """Estado actual del servidor."""
    return "El servidor está funcionando correctamente."


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

Desglose línea por línea

from mcp.server.fastmcp import FastMCP

Importa FastMCP, el helper de alto nivel. Es la forma recomendada de crear MCP servers en Python.

mcp = FastMCP("hello-world")

Crea una instancia del server con un nombre. Este nombre aparece cuando Claude Code descubre el server.

@mcp.tool()
async def greet(name: str) -> str:
    """Saluda a una persona por su nombre."""
    return f"¡Hola, {name}! Bienvenido al mundo de MCP con Python."

Registra un tool. El decorador @mcp.tool() hace todo:

  • Nombre del tool: greet (del nombre de la función)
  • Descripción: "Saluda a una persona por su nombre." (del docstring)
  • Input schema: { name: string } (del type hint name: str)
  • Return type: texto plano (de -> str)
@mcp.resource("info://server/status")
async def server_status() -> str:
    """Estado actual del servidor."""
    return "El servidor está funcionando correctamente."

Registra un resource en el URI info://server/status. Cuando un cliente pide ese URI, ejecuta la función y retorna el resultado.

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

Inicia el server. mcp.run() configura automáticamente el transport stdio y el loop de eventos asyncio.

Ejecutar el server

python server.py

El server se inicia y espera conexiones via stdin/stdout. No verás output en la terminal porque usa stdio — la comunicación es via el protocolo JSON-RPC, no texto en consola.

Testear con MCP Inspector

La forma más rápida de verificar que tu server funciona:

mcp dev server.py

Esto abre MCP Inspector en tu navegador. Desde ahí puedes:

  • Ver los tools registrados
  • Ver los resources registrados
  • Invocar tools con parámetros
  • Leer resources por URI

Comparación con el setup de TypeScript

PasoTypeScriptPython
Inicializar proyectonpm init -y + tsconfig.jsonpython -m venv .venv
Instalar SDKnpm install @modelcontextprotocol/sdk zodpip install "mcp[cli]"
Archivo de configtsconfig.json + package.jsonNinguno necesario
Paso de buildtscNo hay build
Ejecutarnode dist/index.jspython server.py
Inspectornpx @modelcontextprotocol/inspectormcp dev server.py

Python elimina el paso de build completamente. Editas → ejecutas. Sin compilación.


Entendiendo FastMCP

¿Qué hace FastMCP por ti?

FastMCP es un wrapper de alto nivel que simplifica la API del SDK. Sin FastMCP, tendrías que manejar manualmente:

# Sin FastMCP (bajo nivel) — NO recomendado para empezar
from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types

server = Server("hello-world")

@server.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="greet",
            description="Saluda a una persona",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Nombre de la persona"}
                },
                "required": ["name"],
            },
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "greet":
        return [types.TextContent(type="text", text=f"Hola, {arguments['name']}!")]
    raise ValueError(f"Tool desconocido: {name}")

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream, write_stream,
            server.create_initialization_options()
        )

import asyncio
asyncio.run(main())

Compara con FastMCP:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("hello-world")

@mcp.tool()
async def greet(name: str) -> str:
    """Saluda a una persona."""
    return f"Hola, {name}!"

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

FastMCP reduce ~30 líneas a ~10. Lo logra automatizando:

  • Registro de tools: Extrae nombre, descripción, y schema de la función
  • Registro de resources: Asocia URI con handler
  • Transport: Configura stdio automáticamente
  • Event loop: Maneja asyncio por ti
  • Serialización: Convierte return values a contenido MCP

Opciones de configuración de FastMCP

mcp = FastMCP(
    "my-server",
    version="1.0.0",                    # Versión del server
    instructions="Servidor para...",     # Instrucciones para el modelo
)

El parámetro instructions es particularmente útil — le dice al modelo cómo usar tu server de forma efectiva.


Anatomía de los decoradores

@mcp.tool() — Registrar funciones como tools

El decorador extrae toda la información de la función Python:

@mcp.tool()
async def calculate_area(
    width: float,
    height: float,
    unit: str = "m"
) -> str:
    """Calcula el área de un rectángulo.

    Acepta ancho y alto, retorna el área con la unidad especificada.
    """
    area = width * height
    return f"Área: {area} {unit}²"

Lo que el decorador infiere:

  • name: "calculate_area" (del def)
  • description: "Calcula el área de un rectángulo..." (del docstring)
  • inputSchema:
    {
      "type": "object",
      "properties": {
        "width": { "type": "number" },
        "height": { "type": "number" },
        "unit": { "type": "string", "default": "m" }
      },
      "required": ["width", "height"]
    }
  • unit es opcional porque tiene valor default

@mcp.resource() — Registrar datos como resources

@mcp.resource("config://app/settings")
async def get_settings() -> str:
    """Configuración actual de la aplicación."""
    import json
    settings = {
        "debug": False,
        "version": "2.1.0",
        "max_connections": 100,
    }
    return json.dumps(settings, indent=2)

El URI config://app/settings es la dirección que los clientes usan para pedir este resource.

@mcp.prompt() — Registrar templates como prompts

@mcp.prompt()
async def code_review(code: str, language: str = "python") -> str:
    """Template para solicitar code review."""
    return f"""Revisa el siguiente código {language} y proporciona:

1. Errores potenciales
2. Mejoras de rendimiento
3. Mejoras de legibilidad
4. Adherencia a mejores prácticas de {language}

Código:
```{language}
{code}
```"""

Personalizar el nombre del tool

Si no quieres que el nombre del tool sea el nombre de la función:

@mcp.tool(name="search_files")
async def find_files(directory: str, pattern: str) -> str:
    """Busca archivos que coincidan con un patrón."""
    ...

Esto registra el tool como search_files aunque la función se llame find_files.


Conectar a Claude Code

Paso 1: Registrar el server

Desde la terminal (fuera de Claude Code):

claude mcp add my-python-server python /ruta/completa/a/server.py

O si usas un virtual environment:

claude mcp add my-python-server /ruta/completa/a/.venv/bin/python /ruta/completa/a/server.py

Es importante usar la ruta absoluta al Python del venv para que Claude Code use las dependencias correctas.

Paso 2: Verificar en Claude Code

Abre Claude Code e inicia una nueva sesión:

claude

Escribe /mcp para ver los servers conectados. Tu server debe aparecer en la lista.

Paso 3: Probar el server

En Claude Code, pide algo que use tu tool:

Saluda a María usando el MCP server

Claude Code debería invocar tu tool greet con name="María" y mostrar el resultado.

Configuración alternativa: archivo JSON

También puedes configurar el server editando directamente el archivo de configuración de Claude Code:

{
  "mcpServers": {
    "my-python-server": {
      "command": "/ruta/completa/a/.venv/bin/python",
      "args": ["/ruta/completa/a/server.py"]
    }
  }
}

Debug: si el server no aparece

Si /mcp no muestra tu server:

  1. Verifica que la ruta al Python del venv es correcta
  2. Verifica que el server corre sin errores: python server.py (debería quedarse esperando sin output de error)
  3. Revisa los logs de Claude Code: claude mcp list para ver el estado
  4. Usa MCP Inspector para testear independientemente: mcp dev server.py

Ejercicios

Ejercicio 1: Server con múltiples tools (Fácil)

Crea un MCP server llamado "text-utils" con 3 tools:

  • count_words: recibe un texto, retorna el conteo de palabras
  • reverse_text: recibe un texto, lo retorna al revés
  • to_uppercase: recibe un texto, lo retorna en mayúsculas
Ver solución
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("text-utils")


@mcp.tool()
async def count_words(text: str) -> str:
    """Cuenta el número de palabras en un texto."""
    word_count = len(text.split())
    return f"El texto tiene {word_count} palabras."


@mcp.tool()
async def reverse_text(text: str) -> str:
    """Invierte un texto de derecha a izquierda."""
    return text[::-1]


@mcp.tool()
async def to_uppercase(text: str) -> str:
    """Convierte un texto a mayúsculas."""
    return text.upper()


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

Ejercicio 2: Server con resources dinámicos (Medio)

Crea un MCP server "system-info" con:

  • Un resource system://time que retorne la fecha y hora actual
  • Un resource system://platform que retorne información del sistema operativo
  • Un tool run_command que ejecute un comando shell y retorne el output
Ver solución
import platform
import subprocess
from datetime import datetime

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("system-info")


@mcp.resource("system://time")
async def current_time() -> str:
    """Fecha y hora actual del sistema."""
    now = datetime.now()
    return now.strftime("%Y-%m-%d %H:%M:%S")


@mcp.resource("system://platform")
async def platform_info() -> str:
    """Información del sistema operativo."""
    import json

    info = {
        "system": platform.system(),
        "release": platform.release(),
        "version": platform.version(),
        "machine": platform.machine(),
        "python_version": platform.python_version(),
    }
    return json.dumps(info, indent=2)


@mcp.tool()
async def run_command(command: str) -> str:
    """Ejecuta un comando shell y retorna el output.

    Solo comandos de lectura para seguridad.
    """
    blocked = ["rm", "del", "format", "mkfs", "dd"]
    cmd_parts = command.split()
    if cmd_parts and cmd_parts[0] in blocked:
        return f"Error: el comando '{cmd_parts[0]}' está bloqueado por seguridad."

    try:
        result = subprocess.run(
            command, shell=True, capture_output=True, text=True, timeout=10
        )
        if result.returncode != 0:
            return f"Error (código {result.returncode}):\n{result.stderr}"
        return result.stdout or "(sin output)"
    except subprocess.TimeoutExpired:
        return "Error: el comando excedió el timeout de 10 segundos."
    except Exception as e:
        return f"Error ejecutando comando: {e}"


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

Ejercicio 3: Proyecto con estructura organizada (Medio)

Crea un MCP server "notes-manager" usando la estructura de proyecto recomendada:

  • src/server.py — entry point
  • src/tools/notes.py — tools para crear y listar notas
  • src/resources/notes.py — resource para ver las notas actuales

Las notas se almacenan en una lista en memoria.

Ver solución

src/tools/notes.py:

from datetime import datetime

notes: list[dict] = []


async def create_note(title: str, content: str) -> str:
    """Crea una nueva nota con título y contenido."""
    note = {
        "id": len(notes) + 1,
        "title": title,
        "content": content,
        "created_at": datetime.now().isoformat(),
    }
    notes.append(note)
    return f"Nota creada: '{title}' (ID: {note['id']})"


async def list_notes() -> str:
    """Lista todas las notas existentes."""
    import json

    if not notes:
        return "No hay notas todavía."
    return json.dumps(notes, indent=2, ensure_ascii=False)

src/resources/notes.py:

import json
from src.tools.notes import notes


async def get_all_notes() -> str:
    """Todas las notas almacenadas."""
    return json.dumps(
        {"total": len(notes), "notes": notes}, indent=2, ensure_ascii=False
    )

src/server.py:

from mcp.server.fastmcp import FastMCP
from src.tools.notes import create_note, list_notes
from src.resources.notes import get_all_notes

mcp = FastMCP("notes-manager")

mcp.tool()(create_note)
mcp.tool()(list_notes)
mcp.resource("notes://all")(get_all_notes)

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

Nota: cuando registras funciones definidas en otro módulo, puedes usar el decorador como función (mcp.tool()(func)) en lugar de la sintaxis @mcp.tool().

Ejercicio 4: Server con prompts (Medio)

Agrega al server del ejercicio anterior un prompt que genere un template de resumen diario basado en las notas existentes.

Ver solución
from mcp.server.fastmcp import FastMCP
from datetime import datetime

mcp = FastMCP("notes-with-prompts")

notes: list[dict] = []


@mcp.tool()
async def create_note(title: str, content: str, category: str = "general") -> str:
    """Crea una nueva nota con título, contenido y categoría."""
    note = {
        "id": len(notes) + 1,
        "title": title,
        "content": content,
        "category": category,
        "created_at": datetime.now().isoformat(),
    }
    notes.append(note)
    return f"Nota creada: '{title}' en categoría '{category}' (ID: {note['id']})"


@mcp.resource("notes://all")
async def get_notes() -> str:
    """Todas las notas almacenadas."""
    import json

    return json.dumps(notes, indent=2, ensure_ascii=False)


@mcp.prompt()
async def daily_summary(date: str = "") -> str:
    """Genera un template de resumen diario basado en las notas."""
    target_date = date or datetime.now().strftime("%Y-%m-%d")
    import json

    notes_text = json.dumps(notes, indent=2, ensure_ascii=False) if notes else "No hay notas."

    return f"""Genera un resumen diario para la fecha {target_date}.

Notas disponibles:
{notes_text}

El resumen debe incluir:
1. Resumen ejecutivo (2-3 oraciones)
2. Puntos clave por categoría
3. Próximos pasos sugeridos

Formato: Markdown con headers y bullet points."""


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

Ejercicio 5: Conectar a Claude Code (Práctico)

Toma cualquiera de los servers anteriores y:

  1. Regístralo en Claude Code con claude mcp add
  2. Verifica con /mcp que aparece
  3. Usa al menos un tool y un resource desde Claude Code
  4. Documenta qué pasos seguiste y qué output obtuviste
Ver solución
cd /ruta/a/mcp-server-python

source .venv/bin/activate

mcp dev server.py

claude mcp add notes-server \
  /ruta/completa/.venv/bin/python \
  /ruta/completa/server.py

claude

Dentro de Claude Code:

> /mcp
# Verificar que "notes-server" aparece con estado "connected"

> Crea una nota titulada "Reunión de equipo" con contenido
  "Discutir roadmap Q2 y asignación de tareas"

# Claude Code invoca create_note y muestra el resultado

> Muéstrame todas las notas actuales

# Claude Code lee el resource notes://all o invoca list_notes

La verificación exitosa muestra que Claude Code descubre y usa los tools/resources de tu server Python.


Troubleshooting

"ModuleNotFoundError: No module named 'mcp'"

Causa: El SDK no está instalado en el Python que estás usando, o no activaste el virtual environment.

Solución:

source .venv/bin/activate
pip install "mcp[cli]"
python -c "import mcp; print('OK')"

Si usas Claude Code, asegúrate de que el comando apunta al Python del venv:

claude mcp add my-server /ruta/completa/.venv/bin/python /ruta/completa/server.py

"SyntaxError: invalid syntax" en type hints

Causa: Estás usando Python 3.9 o inferior, que no soporta list[str] o str | None directamente.

Solución:

python --version
# Si es menor a 3.10, actualiza Python

# Alternativa: usar from __future__ import annotations
from __future__ import annotations

"El server se ejecuta pero Claude Code no lo encuentra"

Causa: La ruta al ejecutable o al archivo es incorrecta.

Solución:

# Verifica que el server corre correctamente
python server.py
# Si ves errores, arregla primero el server

# Usa rutas absolutas
claude mcp add my-server \
  $(which python) \
  $(pwd)/server.py

# Verifica la configuración
claude mcp list

"RuntimeError: asyncio event loop is already running"

Causa: Estás ejecutando el server dentro de un contexto que ya tiene un event loop (como Jupyter notebook).

Solución:

# En vez de mcp.run(), usa:
import asyncio

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

"El tool aparece pero no funciona correctamente"

Causa: El docstring o los type hints están mal definidos.

Solución:

@mcp.tool()
async def my_tool(param: str) -> str:
    """Descripción clara de lo que hace el tool.

    Esta descripción es lo que ve el modelo para decidir
    cuándo usar el tool. Hazla descriptiva.
    """
    return f"Resultado: {param}"

Resumen

En esta cápsula aprendiste:

  • Instalar el SDK con pip install "mcp[cli]" en un virtual environment
  • Crear un MCP server con FastMCP en pocas líneas
  • FastMCP automatiza el registro de tools, resources, y prompts via decoradores
  • Los decoradores (@mcp.tool(), @mcp.resource(), @mcp.prompt()) son la interfaz principal
  • El nombre, descripción, y schema se infieren de la función Python
  • mcp dev server.py abre MCP Inspector para testing rápido
  • Conectar a Claude Code con claude mcp add usando rutas absolutas
  • El setup Python es más simple que TypeScript: sin build step, sin config files adicionales

Próxima cápsula: Tools y Resources en Python — implementaciones avanzadas con Pydantic, validación de inputs complejos, y las diferencias idiomáticas con TypeScript.


Recursos adicionales

  1. MCP Python SDK — Getting Started — Quickstart oficial
  2. FastMCP Documentation — Referencia del helper
  3. Python venv Documentation — Virtual environments en Python
  4. MCP Inspector — Herramienta de debugging visual
  5. Claude Code — MCP Configuration — Cómo configurar MCP servers en Claude Code
  6. Pydantic v2 Documentation — Referencia de Pydantic (instalado con el SDK)

Siguiente cápsula: Implementar tools y resources con decoradores Python, validación con Pydantic, y cómo el enfoque Python difiere del TypeScript.