GPT Diffusion

MCP: conectar agentes a APIs REST reales (tutorial práctico)

2026-05-22 · Tutoriales #agentes#mcp#api#tutorial#python#fastmcp

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:

  1. Buscar repos de un usuario con list_repos.
  2. Buscar issues con search_issues.
  3. 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:

  1. Escribe una función Python.
  2. Ponle @mcp.tool().
  3. 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.


Fuentes

Cargando comentarios...