MCP: conectar agentes a APIs REST reales (tutorial práctico)
TL;DR
- Usa FastMCP (Python) para envolver cualquier API REST como herramienta MCP.
- Dos ejemplos reales: GitHub API (listar repos) y Nominatim (geocoding).
- Transporte Streamable HTTP para producción, sin stdin.
- Configuración para Claude Desktop, OpenClaw y cualquier host MCP.
- El agente decide cuándo llamar la API; tú solo defines la interfaz.
Tiempo estimado: 30–45 minutos.
El problema
Tienes un agente que necesita interactuar con APIs externas: GitHub, Stripe, una API interna… La solución naive es pegar requests.get() en el prompt y rezar. Funciona hasta que el modelo alucina endpoints, mezcla parámetros, o rompe el parsing de la respuesta.
MCP resuelve esto definiendo un contrato formal: el agente descubre qué herramientas existen, qué parámetros aceptan, y qué devuelven. Sin pegar documentación en el prompt. Sin祈祷.
Si ya leíste nuestro tutorial de MCP con herramientas locales, este artículo es el siguiente paso: en lugar de herramientas simuladas, conectamos APIs reales.
Qué necesitas
- Python 3.11+
pip install mcp httpx- Una terminal
- Opcional: cuenta de GitHub (para el ejemplo de la API de GitHub)
Paso 1: Instalar FastMCP
FastMCP es la capa de alto nivel del SDK oficial de MCP para Python. Evita que escribas boilerplate de servidor a mano:
pip install "mcp[cli]" httpx
Verifica:
mcp --version
# mcp, version 1.x.x
Paso 2: Servidor MCP — GitHub API
Vamos a crear un servidor que expone dos herramientas: listar repositorios de un usuario y buscar issues.
Crea github_mcp_server.py:
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("github-api", json_response=True)
@mcp.tool()
async def list_repos(username: str, limit: int = 10) -> str:
"""Lista repositorios públicos de un usuario de GitHub.
Devuelve nombre, descripción, estrellas y lenguaje principal.
Usa 'limit' para controlar cuántos resultados (máx 30)."""
limit = min(limit, 30)
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.github.com/users/{username}/repos",
params={"per_page": limit, "sort": "updated"},
headers={"Accept": "application/vnd.github.v3+json"}
)
resp.raise_for_status()
repos = resp.json()
lines = []
for r in repos:
lines.append(
f"- {r['name']} (⭐{r['stargazers_count']}) "
f"[{r.get('language', 'N/A')}]: {r.get('description', '')}"
)
return "\n".join(lines) if lines else "No se encontraron repos."
@mcp.tool()
async def search_issues(query: str, repo: str = "") -> str:
"""Busca issues en GitHub. Parámetros:
- query: texto de búsqueda (requerido)
- repo: filtrar por repo en formato 'owner/repo' (opcional)
Devuelve título, estado y URL de cada issue."""
q = query
if repo:
q = f"repo:{repo} {q}"
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.github.com/search/issues",
params={"q": q, "per_page": 5},
headers={"Accept": "application/vnd.github.v3+json"}
)
resp.raise_for_status()
data = resp.json()
items = data.get("items", [])
lines = []
for i in items:
state = "🟢 abierto" if i["state"] == "open" else "🔴 cerrado"
lines.append(f"- [{state}] {i['title']}\n {i['html_url']}")
return "\n\n".join(lines) if lines else "Sin resultados."
if __name__ == "__main__":
# Producción: Streamable HTTP en puerto 8000
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)
Notas sobre el código
json_response=True: devuelve respuestas estructuradas (el agente las parsea mejor que texto plano).httpx.AsyncClient: llamadas async, no bloquea el event loop del servidor MCP.raise_for_status(): si la API falla, el error se propaga al agente como fallo de herramienta.- Límites explícitos:
limit = min(limit, 30)previene que el agente pida miles de resultados.
Paso 3: Probar el servidor
Modo desarrollo (stdio)
Para pruebas rápidas con stdin:
# Cambia la última línea a:
mcp.run(transport="stdio")
Prueba con el CLI inspector:
mcp dev github_mcp_server.py
Esto abre un inspector web donde puedes invocar herramientas directamente.
Modo producción (Streamable HTTP)
Deja la última línea como está (streamable-http). Arranca el servidor:
python github_mcp_server.py
El servidor escucha en http://127.0.0.1:8000/mcp. Verifica con curl:
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
Si devuelve JSON con capabilities, funciona.
Paso 4: Conectar al agente
Claude Desktop
Edita ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) o el equivalente en tu OS:
{
"mcpServers": {
"github-api": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}
Reinicia Claude Desktop. El agente ahora tiene acceso a list_repos y search_issues.
OpenClaw
En tu openclaw.config.yaml:
agent:
name: GitHubAgent
model: zai/glm-4.5-air
tools:
- mcp:
url: http://127.0.0.1:8000/mcp
maxIterations: 15
Cualquier host MCP genérico
El endpoint Streamable HTTP es estándar. Cualquier cliente MCP que soporte el transporte puede conectarse. No hay vendor lock-in.
Paso 5: Añadir una segunda API — geocoding con Nominatim
MCP brilla cuando compones múltiples APIs. Añadamos geocoding (Nominatim/OpenStreetMap, sin API key):
@mcp.tool()
async def geocode(address: str) -> str:
"""Convierte una dirección en coordenadas (lat, lon) usando Nominatim.
Devuelve latitud, longitud y nombre display.
Ejemplo: geocode('Oviedo, Asturias')"""
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://nominatim.openstreetmap.org/search",
params={"q": address, "format": "json", "limit": 1},
headers={"User-Agent": "mcp-demo/1.0"}
)
resp.raise_for_status()
data = resp.json()
if not data:
return "Dirección no encontrada."
r = data[0]
return f"{r['display_name']}\nCoordenadas: {r['lat']}, {r['lon']}"
Ahora el agente puede:
- Buscar repos de un usuario con
list_repos. - Buscar issues con
search_issues. - Geolocalizar direcciones con
geocode.
Y tú no escribiste ningún prompt especial para enseñarle a usar estas APIs. Las descripciones de las herramientas bastan.
El patrón general
Cualquier API REST sigue el mismo patrón:
1. Define la función Python con type hints claros.
2. Describe qué hace y qué devuelve en la docstring.
3. Haz la llamada HTTP dentro (con httpx).
4. Devuelve texto estructurado.
5. Decora con @mcp.tool().
La regla de oro: una función = una herramienta = una acción de la API. No hagas herramientas que hagan cinco cosas. Los agentes eligen mejor cuando cada herramienta tiene responsabilidad única.
Problemas comunes
El agente no usa la herramienta: revisa la docstring. Si la descripción es vaga ("procesa datos"), el agente no sabe cuándo invocarla. Sé explícito: "Lista repositorios públicos de un usuario de GitHub. Devuelve nombre, estrellas y lenguaje."
Error 403/429 de la API: la mayoría de APIs sin autenticación tienen rate limits. Añade headers de autenticación (Authorization: Bearer ...) y controla reintentos con httpx (transport=httpx.AsyncHTTPTransport(retries=2)).
El agente alucina parámetros: el inputSchema que genera FastMCP desde los type hints suele ser suficiente. Pero si el parámetro tiene restricciones (enums, rangos), usa Literal o Annotated con validación:
from typing import Literal
@mcp.tool()
async def search_issues(
query: str,
state: Literal["open", "closed", "all"] = "open"
) -> str:
Transporte stdio vs Streamable HTTP: stdio es para desarrollo local (el host lanza tu proceso). Streamable HTTP es para producción (el servidor corre独立mente, múltiples clientes se conectan). No uses stdio en producción.
Siguientes pasos
- Añade autenticación OAuth 2.0 para APIs que lo requieran (Stripe, Google, etc.).
- Implementa Resources para datos contextuales de solo lectura (config, documentación).
- Separa APIs en servidores MCP distintos (uno por dominio) siguiendo el patrón microservicio.
- Añade logging estructurado para monitorizar qué herramientas usa el agente y con qué frecuencia.
- Lee la especificación oficial para entender prompts, resources y capacidades avanzadas.
Conclusión
MCP convierte el problema de “¿cómo mi agente habla con esta API?” en algo con una respuesta estándar:
- Escribe una función Python.
- Ponle
@mcp.tool(). - El agente la descubre y la usa.
Sin pegar docs en prompts. Sin parsers frágiles. Sin vendor lock-in.
El ecosistema ya tiene 13.000+ servidores públicos (Smithery, MCPHub). Antes de escribir el tuyo, busca si ya existe. Pero si necesitas conectar una API interna o específica, este tutorial te da el patrón en 50 líneas de Python.