MCP Python SDK
GuΓa completa del Model Context Protocol (MCP) y su SDK de Python. Cubre la creaciΓ³n de servidores MCP con resources, tools y prompts, el transporte STDIO/SSE, y la integraciΓ³n con agentes LLM.
MCP Overview
El Model Context Protocol (MCP) es un estΓ‘ndar abierto creado por Anthropic para conectar LLMs con fuentes de datos y herramientas externas. Funciona como un βUSB-C para AIβ: una interfaz universal.
Arquitectura MCP
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Host β
β (Claude, IDE, Agent) β
β β
β βββββββββββββββββββββββ β
β β MCP Client β β
β β (inside the host) β β
β βββββββββββ¬ββββββββββββ β
ββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Protocol (JSON-RPC)
β
βββββββββΌββββββββ βββββββββββββββββ ββββββββββββββββ
β MCP Server β β MCP Server β β MCP Server β
β Database β β GitHub API β β File System β
β β β β β β
β β’ Resources β β β’ Tools β β β’ Resources β
β β’ Tools β β β’ Resources β β β’ Tools β
βββββββββββββββββ βββββββββββββββββ ββββββββββββββββ | Primitiva | Control | DescripciΓ³n |
|---|---|---|
| Resources | Application | Datos que el host puede leer (archivos, DB rows, APIs) |
| Tools | Model (LLM) | Funciones que el LLM puede invocar (queries, acciones) |
| Prompts | User | Templates de prompts pre-definidos con argumentos |
# Instalar el SDK de Python
pip install mcp
# O con extras para development
pip install "mcp[cli]" Creating an MCP Server
Un servidor MCP expone resources, tools y prompts. El SDK de Python proporciona decoradores para registrar cada primitiva de forma declarativa.
# server.py β Servidor MCP bΓ‘sico
from mcp.server.fastmcp import FastMCP
# Crear servidor
mcp = FastMCP("My DB Server")
# ββββββββββββββββββββββββββββββββββββββ
# Resources β datos que el host puede leer
# ββββββββββββββββββββββββββββββββββββββ
@mcp.resource("config://app")
def get_config() -> str:
"""ConfiguraciΓ³n actual de la aplicaciΓ³n."""
return json.dumps({
"version": "1.0.0",
"env": "production",
"features": ["auth", "cache"],
})
@mcp.resource("db://users/{user_id}")
def get_user(user_id: str) -> str:
"""Obtener datos de un usuario por ID."""
user = db.query(User).get(user_id)
return json.dumps(user.to_dict())
# ββββββββββββββββββββββββββββββββββββββ
# Tools β funciones que el LLM puede invocar
# ββββββββββββββββββββββββββββββββββββββ
@mcp.tool()
def query_database(sql: str) -> str:
"""Ejecuta una consulta SQL de solo lectura en la base de datos.
Args:
sql: Consulta SQL SELECT a ejecutar.
"""
if not sql.strip().upper().startswith("SELECT"):
raise ValueError("Solo se permiten consultas SELECT")
result = db.execute(sql)
return json.dumps(result.fetchall())
@mcp.tool()
def create_ticket(title: str, description: str, priority: str = "medium") -> str:
"""Crea un ticket de soporte en el sistema.
Args:
title: TΓtulo del ticket.
description: DescripciΓ³n detallada del problema.
priority: Prioridad (low, medium, high, critical).
"""
ticket = tickets.create(title=title, desc=description, priority=priority)
return f"Ticket {ticket.id} creado exitosamente"
# ββββββββββββββββββββββββββββββββββββββ
# Prompts β templates pre-definidos
# ββββββββββββββββββββββββββββββββββββββ
@mcp.prompt()
def analyze_table(table_name: str) -> str:
"""Genera un prompt para analizar una tabla de la DB."""
schema = db.get_schema(table_name)
return f"""Analiza la siguiente tabla de base de datos:
Tabla: {table_name}
Schema: {schema}
Genera un informe con:
1. DescripciΓ³n de cada columna
2. Posibles indices a optimizar
3. Sugerencias de normalizaciΓ³n""" Transport: STDIO & SSE
MCP soporta dos transportes: STDIO (para procesos locales) y SSE (Server-Sent Events, para servidores remotos).
# ββ STDIO Transport (desarrollo y herramientas locales) ββ
# Iniciar con: python server.py
if __name__ == "__main__":
mcp.run() # STDIO por defecto
# ββ SSE Transport (servidores remotos) ββ
# Iniciar con: python server.py --transport sse
if __name__ == "__main__":
mcp.run(transport="sse") # Inicia servidor HTTP con SSE | Transporte | Protocolo | Caso de uso |
|---|---|---|
| STDIO | stdin/stdout | Herramientas locales, integraciΓ³n con IDEs |
| SSE | HTTP + SSE | Servidores remotos, cloud deployments |
Testing con MCP Inspector
Usa mcp dev server.py para abrir el MCP Inspector,
una interfaz web donde puedes probar tus resources, tools y prompts interactivamente
antes de integrar con un host real.
Client Integration
Un MCP client se conecta a uno o mΓ‘s servidores MCP y expone sus tools al LLM. Esto permite agentic workflows donde el LLM usa tools de mΓΊltiples fuentes.
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio
async def main():
# Conectar a servidor MCP via STDIO
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Inicializar conexiΓ³n
await session.initialize()
# Listar tools disponibles
tools = await session.list_tools()
for tool in tools.tools:
print(f"Tool: {tool.name} β {tool.description}")
# Invocar una tool
result = await session.call_tool(
"query_database",
arguments={"sql": "SELECT COUNT(*) FROM users"},
)
print(f"Result: {result.content}")
# Listar resources
resources = await session.list_resources()
for res in resources.resources:
print(f"Resource: {res.uri}")
asyncio.run(main()) Advanced Patterns
Patrones avanzados para servidores MCP de producciΓ³n: contexto compartido, manejo de errores y composiciΓ³n de servidores.
Context y lifespan
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP, Context
# ββ Lifespan: inicializar recursos al iniciar ββ
@asynccontextmanager
async def lifespan(server: FastMCP):
# Inicializar conexiΓ³n de DB al arrancar
db = await create_db_pool()
try:
yield {"db": db}
finally:
await db.close()
mcp = FastMCP("Production Server", lifespan=lifespan)
# ββ Acceder al contexto en tools ββ
@mcp.tool()
async def query_users(ctx: Context, department: str) -> str:
"""Query users by department."""
db = ctx.request_context.lifespan_context["db"]
rows = await db.fetch(
"SELECT * FROM users WHERE department = $1",
department,
)
# Reportar progreso
await ctx.report_progress(1, 1)
return json.dumps([dict(r) for r in rows]) MCP en IDEs
MCP estΓ‘ integrado nativamente en Claude Desktop, VS Code (via Copilot), Cursor y otros IDEs. Tu servidor MCP funciona automΓ‘ticamente como una extensiΓ³n de las capacidades del LLM del IDE.
Multi-Server Composition
En proyectos reales, un agente necesita conectarse a mΓΊltiples servidores MCP simultΓ‘neamente: uno para la base de datos, otro para GitHub, otro para el sistema de tickets, etc. Gestionar estas conexiones requiere namespacing de tools, monitoreo de salud y configuraciΓ³n centralizada.
ComposiciΓ³n multi-servidor MCP
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Host / Agent β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Multi-Server MCP Client β β
β β β β
β β Tools disponibles (con namespace): β β
β β β’ db.query_database β’ github.create_pr β β
β β β’ db.create_ticket β’ github.list_issues β β
β β β’ slack.send_message β’ jira.create_issue β β
β ββββββ¬ββββββββββββββββ¬βββββββββββββββ¬ββββββββββββββββ β
βββββββββΌββββββββββββββββΌβββββββββββββββΌββββββββββββββββββββ
β β β
βββββββΌβββββββ βββββββΌβββββββ βββββΌβββββββββ
β DB Server β β GitHub β β Slack β
β (STDIO) β β Server β β Server β
β β β (STDIO) β β (SSE) β
ββββββββββββββ ββββββββββββββ ββββββββββββββ Cliente multi-servidor con namespacing
El principal reto de conectar mΓΊltiples servidores es evitar colisiones de nombres de tools. Si dos servidores exponen una tool llamada search, el LLM no sabrΓ‘ cuΓ‘l usar. La soluciΓ³n es prefijar cada tool con el nombre del servidor.
# multi_server_client.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from dataclasses import dataclass, field
import asyncio
import json
import logging
logger = logging.getLogger(__name__)
@dataclass
class ServerConfig:
"""ConfiguraciΓ³n de un servidor MCP."""
name: str # Nombre para namespacing (ej: "db", "github")
command: str # Comando para iniciar el servidor
args: list[str] = field(default_factory=list)
env: dict[str, str] = field(default_factory=dict)
@dataclass
class ConnectedServer:
"""Servidor MCP conectado con su sesiΓ³n activa."""
config: ServerConfig
session: ClientSession
tools: list = field(default_factory=list)
healthy: bool = True
class MultiServerMCPClient:
"""Cliente que gestiona mΓΊltiples servidores MCP con namespacing."""
def __init__(self):
self.servers: dict[str, ConnectedServer] = {}
self._contexts = [] # Para cleanup
async def connect_server(self, config: ServerConfig):
"""Conectar a un servidor MCP y registrar sus tools."""
params = StdioServerParameters(
command=config.command,
args=config.args,
env=config.env or None,
)
# Mantener contextos para que la conexiΓ³n no se cierre
stdio_ctx = stdio_client(params)
read, write = await stdio_ctx.__aenter__()
self._contexts.append(stdio_ctx)
session_ctx = ClientSession(read, write)
session = await session_ctx.__aenter__()
self._contexts.append(session_ctx)
await session.initialize()
# Obtener tools y aplicar namespace
tools_result = await session.list_tools()
namespaced_tools = []
for tool in tools_result.tools:
namespaced_tools.append({
"original_name": tool.name,
"namespaced_name": f"{config.name}.{tool.name}",
"description": f"[{config.name}] {tool.description}",
"schema": tool.inputSchema,
})
server = ConnectedServer(
config=config,
session=session,
tools=namespaced_tools,
)
self.servers[config.name] = server
logger.info(
f"Conectado a '{config.name}' β "
f"{len(namespaced_tools)} tools disponibles"
)
async def connect_all(self, configs: list[ServerConfig]):
"""Conectar a todos los servidores en paralelo."""
tasks = [self.connect_server(cfg) for cfg in configs]
results = await asyncio.gather(*tasks, return_exceptions=True)
for cfg, result in zip(configs, results):
if isinstance(result, Exception):
logger.error(f"Error conectando a '{cfg.name}': {result}")
def list_all_tools(self) -> list[dict]:
"""Listar todas las tools de todos los servidores (con namespace)."""
all_tools = []
for server in self.servers.values():
if server.healthy:
all_tools.extend(server.tools)
return all_tools
async def call_tool(self, namespaced_name: str, arguments: dict) -> str:
"""Llamar a una tool usando su nombre con namespace."""
# Parsear namespace: "db.query_database" β server="db", tool="query_database"
parts = namespaced_name.split(".", 1)
if len(parts) != 2:
raise ValueError(
f"Nombre de tool invΓ‘lido: '{namespaced_name}'. "
f"Usa formato 'servidor.tool_name'"
)
server_name, tool_name = parts
server = self.servers.get(server_name)
if not server:
raise ValueError(f"Servidor '{server_name}' no encontrado")
if not server.healthy:
raise RuntimeError(f"Servidor '{server_name}' no estΓ‘ saludable")
result = await server.session.call_tool(tool_name, arguments)
return result.content
async def check_health(self) -> dict[str, bool]:
"""Verificar salud de todos los servidores conectados."""
health = {}
for name, server in self.servers.items():
try:
# Ping simple: listar tools para verificar conexiΓ³n
await asyncio.wait_for(
server.session.list_tools(),
timeout=5.0,
)
server.healthy = True
except Exception:
server.healthy = False
logger.warning(f"Servidor '{name}' no responde")
health[name] = server.healthy
return health
async def close(self):
"""Cerrar todas las conexiones."""
for ctx in reversed(self._contexts):
try:
await ctx.__aexit__(None, None, None)
except Exception:
pass
# ββ Uso del cliente multi-servidor ββ
async def main():
client = MultiServerMCPClient()
# Configurar servidores
servers = [
ServerConfig(
name="db",
command="python",
args=["servers/db_server.py"],
env={"DATABASE_URL": "postgresql://localhost/mydb"},
),
ServerConfig(
name="github",
command="python",
args=["servers/github_server.py"],
env={"GITHUB_TOKEN": "ghp_xxxxx"},
),
ServerConfig(
name="slack",
command="python",
args=["servers/slack_server.py"],
env={"SLACK_TOKEN": "xoxb-xxxxx"},
),
]
try:
await client.connect_all(servers)
# Ver todas las tools disponibles
for tool in client.list_all_tools():
print(f" {tool['namespaced_name']}: {tool['description']}")
# Llamar tools de diferentes servidores
db_result = await client.call_tool(
"db.query_database",
{"sql": "SELECT COUNT(*) FROM users"},
)
print(f"DB: {db_result}")
gh_result = await client.call_tool(
"github.list_issues",
{"repo": "myorg/myrepo", "state": "open"},
)
print(f"GitHub: {gh_result}")
# Health check
health = await client.check_health()
print(f"Health: {health}")
finally:
await client.close()
asyncio.run(main()) ConfiguraciΓ³n via archivo JSON
Para facilitar la gestiΓ³n de mΓΊltiples servidores, usa un archivo de configuraciΓ³n JSON. Este mismo formato es compatible con Claude Desktop y otros hosts MCP.
{
"mcpServers": {
"database": {
"command": "python",
"args": ["servers/db_server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb"
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"],
"env": {}
},
"slack": {
"command": "python",
"args": ["servers/slack_server.py"],
"env": {
"SLACK_BOT_TOKEN": "${SLACK_TOKEN}",
"SLACK_TEAM_ID": "T01234567"
}
},
"jira": {
"command": "python",
"args": ["servers/jira_server.py"],
"env": {
"JIRA_URL": "https://mycompany.atlassian.net",
"JIRA_TOKEN": "${JIRA_TOKEN}"
}
}
}
} Cargador de configuraciΓ³n
# config_loader.py
import json
import os
from pathlib import Path
def load_mcp_config(config_path: str = "mcp_servers.json") -> list[ServerConfig]:
"""Cargar configuraciΓ³n de servidores MCP desde archivo JSON."""
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"Archivo de configuraciΓ³n no encontrado: {config_path}")
with open(path) as f:
raw = json.load(f)
configs = []
for name, server_cfg in raw.get("mcpServers", {}).items():
# Resolver variables de entorno en env values
resolved_env = {}
for key, value in server_cfg.get("env", {}).items():
if value.startswith("${") and value.endswith("}"):
env_var = value[2:-1]
resolved_env[key] = os.environ.get(env_var, "")
else:
resolved_env[key] = value
configs.append(ServerConfig(
name=name,
command=server_cfg["command"],
args=server_cfg.get("args", []),
env=resolved_env,
))
return configs
# ββ Uso con el cliente multi-servidor ββ
async def main_from_config():
client = MultiServerMCPClient()
configs = load_mcp_config("mcp_servers.json")
await client.connect_all(configs)
print(f"Conectado a {len(client.servers)} servidores MCP")
# Health check periΓ³dico
health = await client.check_health()
for server, is_healthy in health.items():
status = "OK" if is_healthy else "DOWN"
print(f" [{status}] {server}") Monitoreo de salud con reconexiΓ³n automΓ‘tica
# health_monitor.py
import asyncio
import logging
logger = logging.getLogger(__name__)
class MCPHealthMonitor:
"""Monitor que verifica la salud de servidores MCP y reconecta si es necesario."""
def __init__(self, client: MultiServerMCPClient, check_interval: float = 30.0):
self.client = client
self.check_interval = check_interval
self._running = False
async def start(self):
"""Iniciar monitoreo en background."""
self._running = True
while self._running:
health = await self.client.check_health()
for server_name, is_healthy in health.items():
if not is_healthy:
logger.warning(f"Servidor '{server_name}' caΓdo β intentando reconectar")
try:
config = self.client.servers[server_name].config
await self.client.connect_server(config)
logger.info(f"Servidor '{server_name}' reconectado exitosamente")
except Exception as e:
logger.error(f"ReconexiΓ³n de '{server_name}' fallΓ³: {e}")
await asyncio.sleep(self.check_interval)
def stop(self):
self._running = False
# ββ Integrar monitoreo con el cliente ββ
async def main_with_monitoring():
client = MultiServerMCPClient()
configs = load_mcp_config("mcp_servers.json")
await client.connect_all(configs)
# Iniciar health monitor en background
monitor = MCPHealthMonitor(client, check_interval=30.0)
monitor_task = asyncio.create_task(monitor.start())
try:
# Tu lΓ³gica de agente aquΓ...
tools = client.list_all_tools()
print(f"{len(tools)} tools disponibles de {len(client.servers)} servidores")
# El monitor sigue corriendo en background
await asyncio.sleep(3600) # Mantener vivo
finally:
monitor.stop()
await client.close() | PatrΓ³n | Problema que resuelve | ImplementaciΓ³n |
|---|---|---|
| Namespacing | ColisiΓ³n de nombres de tools entre servidores | Prefijar con servidor.tool_name |
| Health monitoring | Servidores que caen sin aviso | Ping periΓ³dico + reconexiΓ³n automΓ‘tica |
| Config file | Hardcodear servidores en el cΓ³digo | JSON compatible con Claude Desktop |
| Env resolution | Secrets en archivos de configuraciΓ³n | Variables ${VAR} resueltas desde entorno |
| Parallel connect | Lentitud al conectar muchos servidores | asyncio.gather para conexiones concurrentes |
Compatibilidad con Claude Desktop
El formato JSON de configuraciΓ³n es idΓ©ntico al que usa Claude Desktop en
~/Library/Application Support/Claude/claude_desktop_config.json (macOS) o
%APPDATA%\Claude\claude_desktop_config.json (Windows). Si tu agente usa el mismo
formato, los desarrolladores pueden reutilizar su configuraciΓ³n existente sin cambios.