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 hintname: 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
| Paso | TypeScript | Python |
|---|---|---|
| Inicializar proyecto | npm init -y + tsconfig.json | python -m venv .venv |
| Instalar SDK | npm install @modelcontextprotocol/sdk zod | pip install "mcp[cli]" |
| Archivo de config | tsconfig.json + package.json | Ninguno necesario |
| Paso de build | tsc | No hay build |
| Ejecutar | node dist/index.js | python server.py |
| Inspector | npx @modelcontextprotocol/inspector | mcp 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"(deldef) - 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"] } unites 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:
- Verifica que la ruta al Python del venv es correcta
- Verifica que el server corre sin errores:
python server.py(debería quedarse esperando sin output de error) - Revisa los logs de Claude Code:
claude mcp listpara ver el estado - 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 palabrasreverse_text: recibe un texto, lo retorna al revésto_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://timeque retorne la fecha y hora actual - Un resource
system://platformque retorne información del sistema operativo - Un tool
run_commandque 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 pointsrc/tools/notes.py— tools para crear y listar notassrc/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:
- Regístralo en Claude Code con
claude mcp add - Verifica con
/mcpque aparece - Usa al menos un tool y un resource desde Claude Code
- 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
FastMCPen 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.pyabre MCP Inspector para testing rápido- Conectar a Claude Code con
claude mcp addusando 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
- MCP Python SDK — Getting Started — Quickstart oficial
- FastMCP Documentation — Referencia del helper
- Python venv Documentation — Virtual environments en Python
- MCP Inspector — Herramienta de debugging visual
- Claude Code — MCP Configuration — Cómo configurar MCP servers en Claude Code
- 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.