Skip to content

Servidor MCP

Futuros incluye un servidor Model Context Protocol (MCP) para que cualquier agente externo — Claude Code, Claude Desktop, Cursor, o su propio cliente — pueda consultar el mismo corpus citado de desarrollo de LATAM que usa el chat de Futuros. Cada cifra que devuelve una herramienta lleva un citation_id; las herramientas solo leen datos horneados y computan de forma determinista — sin escrituras, sin llamadas ocultas a un LLM.

  • Endpoint: POST https://futuros.xyz/api/mcp
  • Transporte: Streamable HTTP (JSON-RPC 2.0)
  • Versión de protocolo: 2025-06-18
  • Identidad del servidor: futuros-corpus v1.0.0 — "Futuros — LATAM governed-data corpus"
  • Auth: ninguna (corpus público de solo lectura)
  • Compuerta de despliegue: MCP_ENABLED debe valer "true". Falla cerrado: sin la variable, cada POST devuelve 403 {"error":"mcp_disabled"} — un despliegue nuevo o un .env vacío no expone el endpoint por accidente. GET ?health=1 sigue respondiendo y reporta el estado en su campo enabled.
  • Estado: sin estado — cada petición es autocontenida
  • Métodos: initialize, ping, tools/list, tools/call
  • Tope de cuerpo: 256 KB, aplicado antes de parsear y antes de cobrar el límite de tasa; por encima, 413 con -32700. Un lote legítimo de MAX_BATCH mensajes son unos pocos KB.
  • Límite de tasa: 60 mensajes/minuto por IP de cliente, cobrado una vez por mensaje JSON-RPC contenido — un lote de N cuesta N unidades, así que agrupar no multiplica el presupuesto. Las respuestas 429 traen una cabecera Retry-After. Entre instancias (respaldado por Upstash) cuando está configurado; si no, en memoria por instancia.

Semántica de transporte

El servidor implementa el mínimo de Streamable HTTP requerido para un servidor de herramientas sin estado:

Método en /api/mcpComportamiento
POSTJSON-RPC 2.0 — un solo mensaje o un lote (arreglo JSON). Devuelve el objeto de respuesta, o un arreglo para un lote.
POST (solo notificaciones)HTTP 202 Accepted, cuerpo vacío (sin id → sin respuesta).
OPTIONSPreflight CORS → 204.
GET ?health=1200{ ok, service: "mcp", enabled, transport: "streamable-http", max_batch: 20 }, una sonda barata para monitores de uptime. ok/enabled reflejan MCP_ENABLED, así que la sonda distingue "caído" de "deshabilitado a propósito".
GET (a secas)405 — servidor sin estado, sin stream SSE iniciado por el servidor.

CORS está totalmente abierto (Access-Control-Allow-Origin: *, métodos POST, OPTIONS, cabeceras permitidas Content-Type, Mcp-Session-Id, Mcp-Protocol-Version), así que los agentes basados en navegador pueden llamarlo entre orígenes.

Códigos de error JSON-RPC usados: -32700 error de parseo, -32600 petición vacía/inválida (incluido un lote sobre el tope de 20 mensajes), -32601 método no encontrado / GET, -32602 herramienta desconocida, -32000 límite de tasa, -32603 error interno — dentro de un lote, una excepción lanzada por la herramienta de un mensaje se vuelve un -32603 ligado al id de ese mensaje, con aislamiento por mensaje: los mensajes hermanos igual se responden, y el endpoint nunca devuelve un 500 que no sea JSON-RPC.

Ciclo de vida JSON-RPC

Lo que atraviesa un POST, en orden (api/mcp.tsserver/mcp/handler.ts → el ejecutor compartido de herramientas del corpus):

Clasificación de mensajes (handleMcpMessage):

Mensaje entranteTratamiento
No es un objeto JSON (cadena, número, null, arreglo anidado)-32600 con id: null
Tiene id pero jsonrpc !== "2.0" o method no es una cadena-32600 ligado a ese id
Sin id (una notificación) — cualquier método, incluso uno que lanza excepciónprocesado, nunca respondido
method que empieza con notifications/ (incluso con id)sin respuesta
Método desconocido con id-32601
tools/call que nombra una herramienta fuera de la lista expuesta de 15-32602
La herramienta lanza excepción a mitad de llamada-32603 ligado al id de ese mensaje; los hermanos no se afectan; si no hay id recuperable la respuesta se descarta (regla de notificación)

Los mensajes de un lote se ejecutan secuencialmente, en orden del arreglo, y las respuestas vuelven en el mismo orden (solo las peticiones — las notificaciones no aportan nada). Un cuerpo de solo notificaciones produce por tanto un conjunto de respuestas vacío → HTTP 202 sin cuerpo. Cada invocación corre bajo un maxDuration de función de 30 s.

Mecánica del límite de tasa — el limitador corre antes del handler:

  • Clave: mcp:<client-ip>, donde la IP es x-real-ip / la entrada más a la derecha de x-forwarded-for — nunca la más a la izquierda, que es falsificable.
  • El tope de tamaño de lote se verifica antes de cobrar, así que un arreglo sobredimensionado es un único -32600 barato que consume cero presupuesto. Cada mensaje contenido cobra entonces una unidad; si algún cobro dispara el límite, toda la petición recibe 429 con el mayor Retry-After observado (las unidades cobradas quedan consumidas).
  • Backend: un viaje de ida y vuelta con pipeline INCR + EXPIRE … NX + PTTL a Upstash/Vercel-KV cuando UPSTASH_REDIS_REST_* / KV_REST_API_* está configurado (techo entre instancias); si no, un limitador en memoria acotado (tope duro de 50.000 entradas, barridos cada 30 s, expulsión del más antiguo primero). Cualquier error del store cae en silencio a memoria — el limitador nunca lanza excepción, así que un store inestable degrada a límite por instancia en vez de dar 500s.

Adónde van las lecturas de las herramientas. El handler construye un FetchDataSource apuntado al propio origen del deployment, resuelto por selfOrigin(): prefiere la URL de deployment que fija la plataforma, y solo cae a las cabeceras x-forwarded-proto / x-forwarded-host fuera de Vercel (desarrollo local). El orden importa y es una defensa, no un detalle: como esa lectura es la verdad de base detrás de cada respuesta de herramienta, un x-forwarded-host o un host falsificado no puede reapuntar el fetch del servidor a un host elegido por un atacante. Una llamada de herramienta lee así el mismo JSON de public/data cacheado en el CDN que usa la web app — sin base de datos, sin un tercer host, y las lecturas repetidas dentro de una invocación golpean una caché LRU.

Normalización de argumentos. Antes del despacho, el ejecutor reconcilia los argumentos de país y pilar contra el catálogo real: "Brasil"BRA, "mex"MEX, "educación"educacion. Puramente correctivo — los valores no resolubles pasan sin tocar, y las herramientas de texto libre o direccionadas por dataset no se ven afectadas.

Conectar un cliente

Claude Code

bash
claude mcp add --transport http futuros https://futuros.xyz/api/mcp

Cursor

En .cursor/mcp.json (por proyecto) o ~/.cursor/mcp.json (global):

jsonc
{
  "mcpServers": {
    "futuros": { "url": "https://futuros.xyz/api/mcp" }
  }
}

Claude Desktop / cualquier cliente con archivo de config

Añada a la config de su cliente MCP (el claude_desktop_config.json de Claude Desktop, o equivalente). Un cliente nativo con soporte HTTP:

jsonc
{
  "mcpServers": {
    "futuros": {
      "type": "http",
      "url": "https://futuros.xyz/api/mcp"
    }
  }
}

Para un cliente que solo habla stdio, use el puente mcp-remote:

jsonc
{
  "mcpServers": {
    "futuros": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://futuros.xyz/api/mcp"]
    }
  }
}

En initialize el servidor devuelve su cadena instructions, que le dice al agente cómo conducir el corpus: resolver una métrica nombrada en palabras con resolve_metric, luego llamar get_parameter_data / compare_countries / compute; cada cifra lleva un citation_id; si el corpus carece de una cifra, las herramientas lo dicen en vez de inventarla.

Herramientas

La exposición son 15 herramientas de corpus de solo lectura: las 13 de la lista blanca del toolset del chat, más 2 exclusivas de MCP (server/mcp/tools.ts) que el chat deliberadamente no tiene — get_series (el panel país-año completo de un indicador; el chat trunca la serie a los últimos 6 puntos por presupuesto de tokens) y resolve_citations (resolver citation_ids guardados sin salir de MCP hacia la API de archivos). Las herramientas de web abierta (web_search, fetch_url) y la paga perplexity_search, respaldada por LLM, nunca se exponen — el endpoint no puede fungir como proxy SSRF, no puede gastar el presupuesto de búsqueda externa del deployment, y queda acotado a datos citados de Futuros. El diseño falla cerrado en ambas vías: una herramienta nueva añadida al toolset del chat no se filtra a MCP hasta que se agrega explícitamente a la lista blanca, y una herramienta exclusiva de MCP solo existe si está definida en MCP_EXTRA_TOOLS.

Cada herramienta acepta además un parámetro opcional lang (es | en | pt, por defecto es — el idioma fuente del corpus). Localiza los campos de prosa (narrativas, nombres de pilotos, claims); los números, ids y citas son independientes del idioma. tools/list lo anuncia explícitamente: el handler inyecta el enum lang en el input schema de cada objeto anunciado, así un cliente puede descubrir que existe salida en otros idiomas en vez de tener que saberlo; por llamada, lang se lee de los argumentos de la herramienta, y cualquier otro valor cae de vuelta a es.

Las quince herramientas:

HerramientaQué devuelveParámetros clave
resolve_metricMapea una métrica nombrada en palabras a un id de indicador gobernado (del registro de métricas) — candidatos rankeados con id canónico, unidad, good_direction, pilar, cobertura, vintage. Llámela primero cuando necesite un id.query (req), pillar, limit
get_parameter_dataEl cuadro completo de un pilar en un país: narrativa citada, todos los indicadores (valor/tendencia/año/citation_id), gobernanza, correlaciones, grado de evidencia.parameter (req), country (req)
compare_countriesLos indicadores de un pilar en 2–8 países en una sola llamada, cada cifra citada.parameter (req), countries[] (req, 2–8)
get_newsNoticias recientes curadas para un pilar+país; el id de cada ítem es su citation_id.parameter (req), country (req), limit
get_regional_pulseAgregado regional LATAM de un pilar: valor, tendencia, líderes/rezagados, movimientos, contradicciones documentadas entre fuentes.parameter (req)
computeUn cálculo reproducible citado a sus celdas de entrada exactas. Ops: change, cagr, rank, gap_to_frontier, correlation, forecast, explain_change.op+parameter+indicator (req); luego country/countries[]/from_year/to_year/year/order/frontier/method/indicator_b
get_data_healthQué tan frescos/confiables/completos son los datos. Con parameter+country: puntaje de confianza, % de provenance, vintage, anomalías, brechas de cobertura. Sin argumentos: el resumen regional + la lista de trabajo de refresco.parameter, country (ambos opcionales)
get_discoveriesAflora proactivamente las mayores anomalías y contradicciones entre fuentes, rankeadas por magnitud, cada una citada.parameter, country, limit (todos opcionales)
find_pilotsBusca en el portafolio de pilotos bancables; devuelve slug, pitch, país, madurez, capex, base del impacto.country, parameter, query, limit (todos opcionales)
get_pilotLa ficha de diseño completa de un piloto: economía, evidencia de bancabilidad, riesgos, financiamiento, tomadores de decisión.slug (req)
recommend_actionLa cadena de acción no copiable para un país+pilar: piloto más adecuado → instrumentos de financiamiento → tomadores de decisión rankeados con pedidos a la medida.country (req), pillar (req)
search_corpusBúsqueda semántica sobre el corpus entero (personas, pilotos, casos, señales, financiamiento, regulación…); devuelve hits con dataset_id + claves para traer vía get_dataset.query (req), datasets[], country, parameter, limit
get_datasetAccesor genérico de cualquier dataset catalogado por dataset_id + las claves de ese dataset.dataset_id (req), country/slug/id/role/layer/status
get_seriesExclusiva de MCP. El panel país-año completo de un indicador gobernado en 1–8 países — cada punto anual con su valor, más fuente, deep-link y citation_id por país. (get_parameter_data devuelve solo los últimos 6 puntos como series_tail.) Los países sin el dato se listan bajo missing, nunca se rellenan.parameter+indicator+countries[] (req), from_year, to_year
resolve_citationsExclusiva de MCP. Resuelve citation_ids guardados (de una respuesta anterior o de la API pública) a su registro de fuente completo: source, title, url, year, retrieved_at. Los ids que el registro global no tiene vuelven bajo unresolved — o ids sintéticos por-respuesta (news-*, pulse-*, civic-*…), que solo existen dentro de la respuesta que los acuñó, o ids cuyo registro viaja inline dentro del payload de un dataset (vuelva a llamar la herramienta que devolvió el id para recibir su registro).ids[] (req, 1–50)

Semántica de citas

tools/call devuelve el contenido de herramienta MCP como un único bloque de texto cuyo payload es una cadena JSON:

json
{ "result": { /* la respuesta de la herramienta */ }, "citations": [ /* fuentes resueltas */ ] }

isError es true cuando una herramienta reporta un problema (p. ej. un indicador desconocido); el payload es entonces {"result":{"error":"…"},"citations":[]} — el mensaje de fallo viaja dentro de result.error como error de nivel herramienta, no como objeto error JSON-RPC de nivel protocolo (esos quedan reservados para los códigos -32xxx de arriba). Cada cifra numérica dentro de result está ligada a un citation_id que aparece en citations (o resuelve contra el citations.json público). El contrato de diseño: nunca mostrar una cifra de Futuros sin su cita. MCP no tiene herramienta de web en vivo en absoluto, así que todo lo que devuelve una herramienta está verificado contra el corpus.

Ejemplo completo

Listar las herramientas, luego resolver una métrica y traer una comparación citada.

1 — initialize

bash
curl -s https://futuros.xyz/api/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"demo","version":"0"}}}'
jsonc
{ "jsonrpc": "2.0", "id": 1, "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "futuros-corpus", "version": "1.0.0",
                    "title": "Futuros — LATAM governed-data corpus" },
    "instructions": "Read-only access to the Futuros corpus: …" } }

2 — tools/list

bash
curl -s https://futuros.xyz/api/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

3 — tools/call (resolve_metric)

bash
curl -s https://futuros.xyz/api/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"resolve_metric","arguments":{"query":"homicidios"}}}'

4 — tools/call (compare_countries) con el id resuelto

bash
curl -s https://futuros.xyz/api/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call",
       "params":{"name":"compare_countries",
                 "arguments":{"parameter":"seguridad","countries":["MEX","BRA","COL"]}}}'
jsonc
{ "jsonrpc": "2.0", "id": 4, "result": {
    "content": [ { "type": "text",
      "text": "{\"result\":{ …cifras por país… },\"citations\":[ … ]}" } ],
    "isError": false } }

5 — tools/call (get_series) — el panel país-año completo

bash
curl -s https://futuros.xyz/api/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call",
       "params":{"name":"get_series",
                 "arguments":{"parameter":"salud","indicator":"sp_dyn_le00_in",
                              "countries":["MEX","COL"],"from_year":2010}}}'

Devuelve, por país, cada punto anual {year, value} desde 2010 con la fuente, el deep-link y el citation_id del indicador — la serie completa que get_parameter_data trunca a series_tail.

6 — tools/call (resolve_citations) — resolver un id guardado

bash
curl -s https://futuros.xyz/api/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call",
       "params":{"name":"resolve_citations",
                 "arguments":{"ids":["wb-sp-dyn-le00-in-mex-2024"]}}}'

Lote — envíe un arreglo para correr varias llamadas en un solo viaje; la respuesta es un arreglo de resultados en orden. Un lote está topado en 20 mensajes (MAX_BATCH); un arreglo mayor se rechaza con un único -32600 antes de que corra herramienta alguna. Cada mensaje contenido cobra una unidad del presupuesto de 60/min, y una excepción de herramienta dentro de un lote produce -32603 solo para ese id — los hermanos igual responden.

Para descargas de archivos planos del mismo corpus (CSV/SDMX/XLSX), use la API pública v1.

Cada cifra con su fuente — la trazabilidad es el contrato.