Skip to content

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-corpus v1.0.0 — "Futuros — LATAM governed-data corpus"
  • Auth: nenhuma (corpus público somente-leitura)
  • Gate de deploy: MCP_ENABLED deve valer "true". Falha fechado: sem ela, cada POST devolve 403 {"error":"mcp_disabled"} — um deploy novo ou um .env vazio não expõe o endpoint por acidente. GET ?health=1 continua respondendo e reporta o estado em seu campo enabled.
  • 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, 413 com -32700. Um lote legítimo de MAX_BATCH mensagens 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 429 carregam um cabeçalho Retry-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/mcpComportamento
POSTJSON-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).
OPTIONSPreflight CORS → 204.
GET ?health=1200{ 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.tsserver/mcp/handler.ts → o executor de ferramentas de corpus compartilhado):

Classificação de mensagens (handleMcpMessage):

Mensagem recebidaTratamento
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 throwprocessada, 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 de x-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 -32600 barato que consome zero orçamento. Cada mensagem contida cobra então uma unidade; se qualquer cobrança estourar o limite, a requisição inteira recebe 429 com o maior Retry-After observado (as unidades já cobradas permanecem consumidas).
  • Backend: uma única viagem pipelined de INCR + EXPIRE … NX + PTTL ao Upstash/Vercel-KV quando UPSTASH_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

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

Cursor

Em .cursor/mcp.json (por projeto) ou ~/.cursor/mcp.json (global):

jsonc
{
  "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:

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

Para um cliente que só fala stdio, faça ponte com mcp-remote:

jsonc
{
  "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:

FerramentaO que retornaParâmetros-chave
resolve_metricMapeia 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_dataQuadro 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_countriesOs indicadores de um pilar em 2–8 países numa só chamada, cada número citado.parameter (obrig.), countries[] (obrig., 2–8)
get_newsNotícias recentes curadas para um pilar+país; o id de cada item é seu citation_id.parameter (obrig.), country (obrig.), limit
get_regional_pulseAgregado regional LATAM para um pilar: valor, tendência, líderes/retardatários, movimentos, contradições de fontes documentadas.parameter (obrig.)
computeUm 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_healthQuã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_discoveriesTraz proativamente as maiores anomalias e contradições de fontes, ranqueadas por magnitude, cada uma citada.parameter, country, limit (todos opcionais)
find_pilotsBusca no portfólio de pilotos bancáveis; retorna slug, pitch, país, maturidade, capex, base de impacto.country, parameter, query, limit (todos opcionais)
get_pilotFicha de design completa de um piloto: economia, evidência de bancabilidade, riscos, financiamento, tomadores de decisão.slug (obrig.)
recommend_actionA 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_corpusBusca 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_datasetAcessador genérico para qualquer dataset catalogado por dataset_id + as chaves daquele dataset.dataset_id (obrig.), country/slug/id/role/layer/status
get_seriesExclusiva 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_citationsExclusiva 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:

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

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) usando o id resolvido

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\":{ …números por país… },\"citations\":[ … ]}" } ],
    "isError": false } }

5 — tools/call (get_series) — o painel país-ano 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}}}'

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

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"]}}}'

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.

Cada número com sua fonte — a rastreabilidade é o contrato.