Servidor MCP
O Futuros publica um servidor Model Context Protocol (MCP) para que qualquer agente externo — Claude Code, Claude Desktop, Cursor, ou seu próprio cliente — possa consultar o mesmo corpus citado de desenvolvimento da LATAM que o chat do Futuros usa. Cada número que uma ferramenta retorna carrega um citation_id; as ferramentas apenas leem dados assados e calculam de forma determinística — sem escritas, sem chamada oculta de LLM.
- Endpoint:
POST https://futuros.xyz/api/mcp - Transporte: Streamable HTTP (JSON-RPC 2.0)
- Versão do protocolo:
2025-06-18 - Identidade do servidor:
futuros-corpusv1.0.0— "Futuros — LATAM governed-data corpus" - Auth: nenhuma (corpus público somente-leitura)
- Gate de deploy:
MCP_ENABLEDdeve valer"true". Falha fechado: sem ela, cadaPOSTdevolve403 {"error":"mcp_disabled"}— um deploy novo ou um.envvazio não expõe o endpoint por acidente.GET ?health=1continua respondendo e reporta o estado em seu campoenabled. - Estado: sem estado — cada requisição é autocontida
- Métodos:
initialize,ping,tools/list,tools/call - Teto de corpo: 256 KB, aplicado antes do parse e antes de cobrar o rate limiter; acima disso,
413com-32700. Um lote legítimo deMAX_BATCHmensagens tem poucos KB. - Rate limit: 60 mensagens/minuto por IP de cliente, cobradas uma vez por mensagem JSON-RPC contida — um batch de N custa N unidades, então agrupar não multiplica o orçamento. Respostas
429carregam um cabeçalhoRetry-After. Entre instâncias (via Upstash) quando configurado; caso contrário, em memória por instância.
Semântica de transporte
O servidor implementa o mínimo de Streamable HTTP exigido para um servidor de ferramentas sem estado:
Método em /api/mcp | Comportamento |
|---|---|
POST | JSON-RPC 2.0 — uma única mensagem ou um batch (array JSON). Retorna o objeto de resposta, ou um array para um batch. |
POST (somente notificações) | HTTP 202 Accepted, corpo vazio (sem id → sem resposta). |
OPTIONS | Preflight CORS → 204. |
GET ?health=1 | 200 — { ok, service: "mcp", enabled, transport: "streamable-http", max_batch: 20 }, uma sonda barata para monitores de uptime. ok/enabled refletem MCP_ENABLED, então a sonda distingue "caído" de "desabilitado de propósito". |
GET (puro) | 405 — servidor sem estado, sem stream SSE iniciado pelo servidor. |
O CORS é totalmente aberto (Access-Control-Allow-Origin: *, métodos POST, OPTIONS, cabeçalhos permitidos Content-Type, Mcp-Session-Id, Mcp-Protocol-Version), então agentes baseados em navegador podem chamá-lo cross-origin.
Códigos de erro JSON-RPC usados: -32700 erro de parse, -32600 requisição vazia/inválida (incluindo um batch acima do teto de 20 mensagens), -32601 método não encontrado / GET, -32602 ferramenta desconhecida, -32000 rate limit, -32603 erro interno — dentro de um batch, um throw da ferramenta de uma mensagem vira um -32603 vinculado ao id daquela mensagem, com isolamento por mensagem: as mensagens irmãs ainda são respondidas, e o endpoint nunca retorna um 500 fora do JSON-RPC.
Ciclo de vida JSON-RPC
Pelo que um POST passa, em ordem (api/mcp.ts → server/mcp/handler.ts → o executor de ferramentas de corpus compartilhado):
Classificação de mensagens (handleMcpMessage):
| Mensagem recebida | Tratamento |
|---|---|
Não é um objeto JSON (string, número, null, array aninhado) | -32600 com id: null |
Tem id mas jsonrpc !== "2.0" ou method não é string | -32600 vinculado àquele id |
Sem id (uma notificação) — qualquer método, mesmo um que lance throw | processada, nunca respondida |
method começando com notifications/ (mesmo com id) | sem resposta |
Método desconhecido com id | -32601 |
tools/call nomeando uma ferramenta fora da lista exposta de 15 ferramentas | -32602 |
| Ferramenta lança throw no meio da chamada | -32603 vinculado ao id daquela mensagem; irmãs não afetadas; se nenhum id for recuperável, a resposta é descartada (regra de notificação) |
Mensagens em um batch executam sequencialmente, na ordem do array, e as respostas voltam na mesma ordem (somente requisições — notificações não contribuem com nada). Um corpo só de notificações, portanto, produz um conjunto de respostas vazio → HTTP 202 sem corpo. Cada invocação roda sob um maxDuration de função de 30 s.
Mecânica do rate limit — o limitador roda antes do handler:
- Chave:
mcp:<client-ip>, onde o IP éx-real-ip/ a entrada mais à direita dex-forwarded-for— nunca a mais à esquerda, que é forjável. - O teto de tamanho de batch é verificado antes de cobrar, então um array grande demais é um único
-32600barato que consome zero orçamento. Cada mensagem contida cobra então uma unidade; se qualquer cobrança estourar o limite, a requisição inteira recebe429com o maiorRetry-Afterobservado (as unidades já cobradas permanecem consumidas). - Backend: uma única viagem pipelined de
INCR+EXPIRE … NX+PTTLao Upstash/Vercel-KV quandoUPSTASH_REDIS_REST_*/KV_REST_API_*está configurado (teto entre instâncias); caso contrário, um limitador em memória com fronteiras (teto rígido de 50.000 entradas, varreduras de 30 s, remoção do mais antigo primeiro). Qualquer erro do store cai silenciosamente para a memória — o limitador nunca lança throw, então um store instável degrada para limitação por instância em vez de 500s.
Para onde vão as leituras das ferramentas. O handler constrói um FetchDataSource apontado para a própria origem do deployment, resolvida por selfOrigin(): ela prefere a URL de deployment definida pela plataforma e só recorre aos cabeçalhos x-forwarded-proto / x-forwarded-host fora da Vercel (dev local). A ordem importa e é uma defesa, não um detalhe: como essa leitura é a verdade de base por trás de cada resposta de ferramenta, um x-forwarded-host ou host falsificado não pode reapontar o fetch do servidor para um host escolhido por um atacante. Uma chamada de ferramenta lê assim o mesmo JSON de public/data cacheado na CDN que o app web usa — sem banco de dados, sem terceiro host, e leituras repetidas dentro de uma invocação acertam um cache LRU.
Normalização de argumentos. Antes do dispatch, o executor reconcilia argumentos de país e pilar contra o catálogo real: "Brasil" → BRA, "mex" → MEX, "educación" → educacion. Puramente corretiva — valores não resolvíveis passam intocados, e ferramentas de texto livre / endereçadas por dataset não são afetadas.
Conectando um cliente
Claude Code
claude mcp add --transport http futuros https://futuros.xyz/api/mcpCursor
Em .cursor/mcp.json (por projeto) ou ~/.cursor/mcp.json (global):
{
"mcpServers": {
"futuros": { "url": "https://futuros.xyz/api/mcp" }
}
}Claude Desktop / qualquer cliente com arquivo de config
Adicione à config do seu cliente MCP (o claude_desktop_config.json do Claude Desktop, ou equivalente). Um cliente nativo com suporte a HTTP:
{
"mcpServers": {
"futuros": {
"type": "http",
"url": "https://futuros.xyz/api/mcp"
}
}
}Para um cliente que só fala stdio, faça ponte com mcp-remote:
{
"mcpServers": {
"futuros": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://futuros.xyz/api/mcp"]
}
}
}No initialize o servidor retorna sua string instructions, que diz ao agente como conduzir o corpus: resolva uma métrica nomeada em palavras com resolve_metric, depois chame get_parameter_data / compare_countries / compute; cada número carrega um citation_id; se o corpus não tiver um número, as ferramentas dizem isso em vez de inventar um.
Ferramentas
A exposição são 15 ferramentas de corpus somente-leitura: as 13 da allowlist do toolset do chat, mais 2 exclusivas do MCP (server/mcp/tools.ts) que o chat deliberadamente não tem — get_series (o painel país-ano completo de um indicador; o chat trunca a série aos últimos 6 pontos por orçamento de tokens) e resolve_citations (resolver citation_ids guardados sem sair do MCP para a API de arquivos). As ferramentas de web aberta (web_search, fetch_url) e a paga perplexity_search, apoiada em LLM, nunca são expostas — o endpoint não pode servir de proxy de SSRF, não pode gastar o orçamento de busca externa do deployment, e permanece restrito aos dados citados do Futuros. O design falha fechado nas duas vias: uma ferramenta nova adicionada ao toolset do chat não vaza para o MCP até ser explicitamente incluída na allowlist, e uma ferramenta exclusiva do MCP só existe se estiver definida em MCP_EXTRA_TOOLS.
Toda ferramenta também aceita um parâmetro opcional lang (es | en | pt, padrão es — a língua-fonte do corpus). Ele localiza campos de prosa (narrativas, nomes de pilotos, afirmações); números, ids e citações são independentes de língua. tools/list o anuncia explicitamente: o handler injeta o enum lang em todo input schema de objeto anunciado, para que um cliente possa descobrir que existe saída não-espanhola em vez de ter que sabê-lo; por chamada, lang é lido dos argumentos da ferramenta, e qualquer outro valor cai de volta para es.
As quinze ferramentas:
| Ferramenta | O que retorna | Parâmetros-chave |
|---|---|---|
resolve_metric | Mapeia uma métrica nomeada em palavras para um id de indicador governado (do registro de métricas) — candidatos ranqueados com id canônico, unidade, good_direction, pilar, cobertura, safra. Chame-a primeiro quando precisar de um id. | query (obrig.), pillar, limit |
get_parameter_data | Quadro completo de um pilar em um país: narrativa citada, todos os indicadores (valor/tendência/ano/citation_id), governança, correlações, grau de evidência. | parameter (obrig.), country (obrig.) |
compare_countries | Os indicadores de um pilar em 2–8 países numa só chamada, cada número citado. | parameter (obrig.), countries[] (obrig., 2–8) |
get_news | Notícias recentes curadas para um pilar+país; o id de cada item é seu citation_id. | parameter (obrig.), country (obrig.), limit |
get_regional_pulse | Agregado regional LATAM para um pilar: valor, tendência, líderes/retardatários, movimentos, contradições de fontes documentadas. | parameter (obrig.) |
compute | Um cálculo reproduzível, citado às suas células de entrada exatas. Ops: change, cagr, rank, gap_to_frontier, correlation, forecast, explain_change. | op+parameter+indicator (obrig.); depois country/countries[]/from_year/to_year/year/order/frontier/method/indicator_b… |
get_data_health | Quão frescos/confiáveis/completos os dados estão. Com parameter+country: pontuação de confiança, % de proveniência, safra, anomalias, lacunas de cobertura. Sem args: o resumo regional + a lista de trabalho de atualização. | parameter, country (ambos opcionais) |
get_discoveries | Traz proativamente as maiores anomalias e contradições de fontes, ranqueadas por magnitude, cada uma citada. | parameter, country, limit (todos opcionais) |
find_pilots | Busca no portfólio de pilotos bancáveis; retorna slug, pitch, país, maturidade, capex, base de impacto. | country, parameter, query, limit (todos opcionais) |
get_pilot | Ficha de design completa de um piloto: economia, evidência de bancabilidade, riscos, financiamento, tomadores de decisão. | slug (obrig.) |
recommend_action | A cadeia de ação incopiável para um país+pilar: piloto mais adequado → instrumentos de financiamento → tomadores de decisão ranqueados com pedidos sob medida. | country (obrig.), pillar (obrig.) |
search_corpus | Busca semântica em todo o corpus (personas, pilotos, casos, sinais, financiamento, regulação…); retorna acertos com dataset_id + chaves para buscar via get_dataset. | query (obrig.), datasets[], country, parameter, limit |
get_dataset | Acessador genérico para qualquer dataset catalogado por dataset_id + as chaves daquele dataset. | dataset_id (obrig.), country/slug/id/role/layer/status… |
get_series | Exclusiva do MCP. O painel país-ano completo de um indicador governado em 1–8 países — cada ponto anual com seu valor, mais fonte, deep-link e citation_id por país. (get_parameter_data retorna só os últimos 6 pontos como series_tail.) Países sem o dado são listados sob missing, nunca preenchidos. | parameter+indicator+countries[] (obrig.), from_year, to_year |
resolve_citations | Exclusiva do MCP. Resolve citation_ids guardados contra o registro global de citações: source, title, url, year, retrieved_at. Ids que o registro global não tem voltam sob unresolved — ou ids sintéticos por-resposta (news-*, pulse-*, civic-*…), que só existem dentro da resposta que os cunhou, ou ids cujo registro viaja inline dentro do payload de um dataset (rechame a ferramenta que retornou o id para receber seu registro). | ids[] (obrig., 1–50) |
Semântica de citações
tools/call retorna o conteúdo de ferramenta MCP como um único bloco de texto cujo payload é uma string JSON:
{ "result": { /* the tool's answer */ }, "citations": [ /* resolved sources */ ] }isError é true quando uma ferramenta reporta um problema (por exemplo, um indicador desconhecido); o payload é então {"result":{"error":"…"},"citations":[]} — a mensagem de falha viaja dentro de result.error como um erro de nível de ferramenta, não como um objeto error JSON-RPC de nível de protocolo (esses ficam reservados para os códigos -32xxx acima). Cada número dentro de result está vinculado a um citation_id que aparece em citations (ou resolve contra o citations.json público). O contrato de design: nunca exibir um número do Futuros sem sua citação. O MCP não tem nenhuma ferramenta de web ao vivo, então tudo o que uma ferramenta retorna é verificado no corpus.
Exemplo passo a passo
Liste as ferramentas, depois resolva uma métrica e busque uma comparação citada.
1 — initialize
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"}}}'{ "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
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)
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) usando o id resolvido
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"]}}}'{ "jsonrpc": "2.0", "id": 4, "result": {
"content": [ { "type": "text",
"text": "{\"result\":{ …números por país… },\"citations\":[ … ]}" } ],
"isError": false } }5 — tools/call (get_series) — o painel país-ano completo
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}}}'Retorna, por país, cada ponto anual {year, value} desde 2010 com a fonte, o deep-link e o citation_id do indicador — a série completa que get_parameter_data trunca a series_tail.
6 — tools/call (resolve_citations) — resolver um id guardado
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"]}}}'Batch — envie um array para executar várias chamadas em uma só ida e volta; a resposta é um array de resultados em ordem. Um batch é limitado a 20 mensagens (MAX_BATCH); um array maior é rejeitado com um único -32600 antes de qualquer ferramenta rodar. Cada mensagem contida cobra uma unidade do orçamento de 60/min, e um throw de ferramenta dentro de um batch produz -32603 só para aquele id — as irmãs ainda respondem.
Para downloads de arquivos planos do mesmo corpus (CSV/SDMX/XLSX), use a API Pública v1.