Senado BR MCP

by sidneybissoli

Not rated
GitHub

About

Brazilian Federal Senate open data over MCP — 90 tools across the legislative process, Senate administration (CEAPS expenses, payroll, contracts) and the e-Cidadania portal. Cloudflare Workers, Streamable HTTP, no auth. Responses in pt-BR.

Details

Author
sidneybissoli
Categories
Search, Other

Setup

Install Senado BR MCP in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/sidneybissoli/senado-br-mcp-cloudflare

Follow the installation instructions in the repository README, then restart your MCP client.

Apublic, hostedMCP server that gives AI assistants live, structured access toBrazilian Senate open datano installation, no account, no API key. Point your MCP client at the hosted endpoint and start asking about senators, bills, votes, expenses, and more. It runs on Cloudflare Workers over Streamable HTTP.

It exposes67 tools,4 prompts, and5 resourcesacross two domains:

- Legislative— senators; bills and their tramitation; votes; committees; plenary sessions, results and presidential vetoes; party-bloc voting orientation; speeches and stenographic transcripts; blocs and leadership; federal legislation; and citizen participation via the e-Cidadania portal.
- Administrative— CEAPS parliamentary-quota expenses; housing allowance; civil servants and payroll; overtime; interns; procurement contracts and biddings; outsourced staff; petty-cash funds; and budget execution.

Data comes from three official sources — thelegislative open-data API, theadministrative open-data API, and the e-Cidadania portal. All tool responses are in Portuguese (pt-BR). SeeCHANGELOG.mdfor release history.

Point a client at the endpoint and ask in plain language — English or Portuguese:

- "How did São Paulo's senators vote in the most recent floor votes?"senado_search_votacoes
- "Show the legislative progress of PEC 45/2019 (a constitutional amendment proposal)."senado_buscar_materias+senado_obter_materia
- "How much was spent on the CEAPS parliamentary allowance in 2024, broken down by expense type?"senado_ceaps

The answers come live from the Senate's official open-data APIs — exact figures with provenance, not numbers guessed from training data.

This is aremote, hosted, open-accessserver. To use it, point any MCP client at the Streamable HTTP endpoint —no install, no account, no API key, no configuration:

For OpenAI Apps SDK submission and review, the Worker also exposes a curated MCP surface:

https://senado.sidneybissoli.com/mcp/openai-app-v2

This endpoint intentionally keeps the full public MCP server intact at/mcp, but limits tool discovery to 27 high-signal, intent-oriented tools for ChatGPT app use./mcp/openai-appremains available as a legacy alias, but new ChatGPT app configurations should use/mcp/openai-app-v2so clients fetch the current tool schema. The tools still call the same handlers and return the same provenance envelope; only the advertised surface is narrower. Any ChatGPT app listing should present this as an independent open-data research app, not as an official Senate, OpenAI or ChatGPT connector.

For ChatGPT Apps, those 27 tools also advertise a shared MCP Apps UI template atui://senado-br-mcp/openai-app-dashboard-v2.html. The self-contained widget renders the returnedstructuredContentas a compact dashboard with metrics, main records, and source/provenance, without adding another model-visible data tool.

- Privacy policy:https://senado.sidneybissoli.com/privacy
- Terms of use:https://senado.sidneybissoli.com/terms

For clients that launch MCP servers as a command — and for one-command setup — use themcp-remotebridge.No build, no config, no key:

npx -y mcp-remote https://senado.sidneybissoli.com/mcp

- One-click (LobeHub):open theserver pageand clickInstall.
- Native remote URL(Claude Desktop/Code and other Streamable-HTTP clients): see
Connecting MCP Clients.

Everything belowArchitecture(Prerequisites, Setup, Deploy) isonly for optionally self-hosting your own instance— it isnotrequired to use this public server.

Prefer not to route queries through a third-party host (e.g. a newsroom policy)? Thesame serveralso runs as alocal stdio processthat talksdirectly to the official government APIs— same 67 tools, same provenance envelope, no Cloudflare in the loop. This is the npm/stdio channel, published assenado-br-mcp.

Point a command-based client (Claude Desktop/Code, etc.) at the package — npm fetches and runs it, no clone or build:

{ "mcpServers": { "senado-br": { "command": "npx", "args": ["-y", "senado-br-mcp"] } } }

To run it directly or hack on it, use the source checkout instead:

git clone https://github.com/SidneyBissoli/senado-br-mcp-cloudflare cd senado-br-mcp-cloudflare npm install npm run build node dist/cli.js # serves MCP over stdio (Ctrl+C to stop)

Parity with the hosted server:the legislative and administrative tools areidentical(same upstream APIs, same throttle/cache/provenance) — locally the L1 Cloudflare cache is a no-op, but the L0 in-memory cache still works, so results are the same. Theonlydifference is the e-Cidadania list/corpus tools: without D1 they fall back to a live scrape of the ~5 REST highlights, flagged viameta.fonte/possivelDesatualizacao; the detail tools (obter_) are identical. Logs go tostderr— stdout carries only the JSON-RPC protocol stream.

This repo bundles a ClaudeAgent Skillat.claude/skills/senado-br/that teaches Claudewhento reach for this server andhowto use its 67 tools well — a themed tool map, common question→tool playbooks, the provenance contract, and gotchas (dates, thecodigoMateriabridge, e-Cidadania's open-set listing, pagination). It points back to the server's ownsenado://catalogo/senado://guiaresources rather than duplicating them.

Claude Code auto-discovers it when you work in this repo. To use it elsewhere, copy.claude/skills/senado-br/into your~/.claude/skills/, or zip the folder and upload it in claude.ai (Settings → Features). The skill assumes thesenado-brMCP server is connected (hosted or via npx).

- Runtime:Cloudflare Workers (ESM)
- Transport:Streamable HTTP (MCP spec 2025-03-26) viacreateMcpHandlerfromagents/mcp
- Protocol:MCP over JSON-RPC —/mcphandles the full public server;/mcp/openai-app-v2exposes a curated 27-tool profile plus a shared MCP Apps widget for OpenAI app review/submission (/mcp/openai-appremains as a legacy alias)
- SDK:@modelcontextprotocol/sdk1.26.0+ (per-request McpServer instances)
- Validation:Zod schemas for all tool inputs
- Caching:2-layer (L0 memory + L1 Cache API) with SHA-256 keying
- e-Cidadania store:D1 database refreshed by a Cron Trigger (every 2h) — list tools read from D1 with a live-scrape fallback and a staleness flag; detail tools stay live with write-through (see
e-Cidadania)
- Rate limiting:Token bucket — global (8 req/s) + per-client (2 req/s)
- Upstream throttle:Max 6 concurrent requests, 10s timeout, retry with exponential backoff
- Auth:Optional Bearer token (set theAPI_KEYsecret; open access when unset). Constant-time comparison.
- Observability:Structured JSON logging + in-memory counters at/metrics; per-tool call telemetry (selection, error rate, cache-vs-live) in Cloudflare Analytics Engine, PII-free
- Liveness:Runs on Cloudflare's own global network behind a custom domain — no third-party host that can go dark. Public/healthand/status(version + last-deploy id/timestamp) make uptime and the current build verifiable; thestatusbadge above pings the live endpoint
- Tests:Vitest unit tests for parsers, helpers, cache, throttle, and auth

Not needed to use the server— it is already hosted athttps://senado.sidneybissoli.com/mcp(open access). Follow this section only if you want to run yourownprivate instance.

- Node.js 20+
-
Wrangler CLIv4+
- Cloudflare account

# Create the KV namespace wrangler kv namespace create CACHE_KV # Note the ID from the output, e.g.: # { binding = "CACHE_KV", id = "abc123..." }

Replace the placeholder KV namespace ID:

[[kv_namespaces]] binding = "CACHE_KV" id = "YOUR_KV_NAMESPACE_ID_HERE"

Optionally setALLOWED_ORIGINto restrict CORS:

[vars] ALLOWED_ORIGIN = "https://your-app.example.com"

The e-Cidadania pipeline needs aD1 databaseand aCron Trigger(both already declared inwrangler.toml— replace the database ID):

[[d1_databases]] binding = "ECIDADANIA_DB" database_name = "senado-ecidadania" database_id = "YOUR_D1_DATABASE_ID_HERE" [triggers] crons = ["0 /2   "]

Create the database (paste the returned ID above) and apply the schema:

npx wrangler d1 create senado-ecidadania npx wrangler d1 migrations apply senado-ecidadania --remote

The list tools fall back to live scraping when D1 is empty, so the server works before the first Cron run.

wrangler secret put API_KEY # Clients must then send: Authorization: Bearer <key> # When API_KEY is not set, the server is open access.
npm run dev # Dev server runs locally on port 8787 (local only). # The public MCP endpoint is https://senado.sidneybissoli.com/mcp
npm test # run all tests once npm run test:watch # watch mode npm run typecheck # tsc --noEmit
npm run deploy # Serves at https://senado.sidneybissoli.com (custom domain) and # https://senado-br-mcp.sidneybissoli.workers.dev (workers.dev fallback)

All requests go toPOST /mcpwith JSON-RPC 2.0 format.

{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "senado_listar_senadores", "arguments": { "uf": "SP", "emExercicio": true } } }
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "senado_buscar_materias", "arguments": { "palavraChave": "inteligência artificial", "tramitando": true } } }
{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "senado_search_votacoes", "arguments": { "dias": 7 } } }

Call a tool — Most popular citizen ideas

{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "senado_ecidadania_listar_ideias", "arguments": { "ordenarPor": "apoios", "ordem": "desc", "status": "aberta" } } }

The server consumes two classes of upstream endpoints from the Senado API:

Legacy endpoints (.jsonsuffix, PascalCase responses)

Used by Groups A, E, F, H, I, J, K, L, M, N. The.jsonsuffix is appended automatically byupstream.ts. None of these is marked deprecated upstream.

v3 endpoints (flat JSON arrays/objects, camelCase)

Used by Groups B, C, D. Dates must be inISO format(YYYY-MM-DD) — tools acceptYYYYMMDDand convert. ThecodigoMateriaquery param bridges legacy matéria codes to v3 processes.

Administrative API (adm.senado.gov.br/adm-dadosabertos, flat snake_case JSON)

Used by Groups O, P, Q, R viaadmFetch(no.jsonsuffix; HTTP 404 treated as empty collection). Base URL configurable viaSENADO_ADM_BASE_URL.

The e-Cidadanialistdata is persisted in aD1 database(ecidadania_current/_history/_scrape_runs, discriminated byentidade; plusecidadania_comentariosfor the audiência comment level andecidadania_detalhe_cursorfor the resumable detail backfill — added in schema v2) and read from there instead of being scraped on every call.Three cadenceswrite into it:

- adaily off-Worker GitHub Actionowns thefull corpusof the three live entities (consultas,eventos,ideias; see below) — the source of truth. Daily (not weekly) because the first-seen seriesMIN(scraped_at)is the only measurable entry-rhythm signal and every skipped day permanently shortens it (ROADMAP Etapa 2, decisão D3);
- aweekly ingest Action(.github/workflows/verify-consultas-votos.yml— historical filename) for theconsultas_votosacervo: the Senado republishes the Arquimedes CSV periodically (confirmed 2026-07-20), so the weekly run re-ingests the current vintage under the same anomaly guards as the other corpora (see below);
- an in-WorkerCron Trigger(0
/2 ,src/scraper/pipeline.ts → refreshEcidadania) does only atargeted metric spliceof the ~5 REST highlights per live entity (restcolecaomaismateria/ideia/audiencia— votos/comentários/apoios), recorded asok-metricaso it never re-breaks the corpus baseline and never touches the long tail. In v2 the eventos splicepreservesthe corpus's canonical comment count (the daily crawl is the source of truth forcomentarios, so the splice can't ping-pong it against the degraded REST count).

Both writers build payloads through the canonicalbuildXResumobuilders + sharedcontentHash, so their rows are byte-identical. Each write:

- upsertsecidadania_current(one row per item — what the tools read),
- appendsecidadania_historyonly when an item'scontent_hashchanges (time-series-ready),
- records each run inecidadania_scrape_runs.

Ananomaly guard(src/scraper/anomaly.ts,classifyRun) ensures a failed or anomalous corpus run (zero rows, or fewer thanECIDADANIA_CORPUS_MIN_PCT% of the last good run)never overwritesthe last good state.

Thelist / analysis tools(listar_,consultas_analise,sugerir_tema_enquete,consultas_votos) read from D1 viaresolveList(src/scraper/store.ts): D1-first. Because every entity is now a full corpus, a stale corpus is served from D1flagged(possivelDesatualizacao: true) rather than collapsing to the ~5-item live highlights (the original coverage bug); the live scrape is reserved for an empty D1 (cold start, before the first weekly run). Staleness usesECIDADANIA_CORPUS_STALE_MAX_MIN(~10 days). Every list response carries an additivemeta(fonte,lastScrapedAt,possivelDesatualizacao) so callers always see the data's real age and never get stale data silently.

Thedetail tools(obter_) staylive(HTML scraped with CSS-class-targeted regex) for freshness, and write their richer payload through toecidadania_detalhefire-and-forget (deduped bycontent_hash), so detail history accrues without adding latency to the response.

The three live e-Cidadania corpora are owned by thedailyAction (.github/workflows/ingest-ecidadania.yml), each with its ownscripts/ingest-ecidadania/index-.tsorchestrator emitting batchedout-.sqlthe apply step bulk-loads:

- consultas— open consultations (detailed below). In v2 each crawled matter is alsoenriched from its detail page(visualizacaomateria) forautoria/relator; those are immutable, so only rows not yet enriched are fetched.
- eventos— audiências/eventos from theprincipalaudiencia?p=NHTML listing; status comes straight from the listing block (no/processobridge). In v2 every event isenriched from its detail page(canonicaldata/hora+comissaoNomeCompleto/local/descricao/pauta/convidados/videoUrl) and itsAJAX comment fragment(canonical count + oneecidadania_comentariosrow per comment, diffed against the stored hashes and emitted asout-eventos-comentarios-
.sql).
- ideias— ideias legislativas (~113.7k) frompesquisaideia?situacao=N&p=M, crawledpersituacaobucket(the listing has no inline status) and emitted in ~10k-statement batches. In v2 the listing crawlpreservesthe immutable detail fields, and aseparate resumable backfill(index-ideias-detalhe.ts, run viaingest:ecidadania:ideias-detalhe) fills them a chunk per run — because ~113.7k detail fetches don't fit one Action, it persists a cursor inecidadania_detalhe_cursorand wraps around at the end.

The fourth entity,consultas_votos, is a separatehistoricalacervo of votes-by-UF parsed from the ~33 MB Arquimedes CSV (Proposições-com-votos.csv), aggregated to one record per matéria with avotosPorUfbreakdown. The CSV's "dados atualizados até" stamp becomes the provenancedata_vintage; it is excluded from the row hash (consultaVotoCore) so a re-ingest with unchanged votes doesn't churn_history.STATUS ATUALis uniformly "Descontinuado", hence archival, not a migration of the open consultations. Served bysenado_ecidadania_consultas_votoswith provenance pointing at the CSV (ECIDADANIA_ARQUIMEDES). It isexcluded from the daily joband owned by its ownweekly ingestAction (.github/workflows/verify-consultas-votos.yml— the filename keeps the historicalverify-prefix): the acervo was originally treated as a frozen single vintage (ROADMAP Etapa 2, decisão D1) and the weekly run onlyverifiedit, but on 2026-07-20 the Senado republished the CSV as a fresh vintage (+43 matérias, 648 updated), so the scheduled run now re-ingests the current vintage under the standard anomaly guards (empty/truncated CSV and the catastrophic floor still fail without writing;forcedispatch overrides the floor). The script's verify mode (INGEST_CONSULTAS_VOTOS_VERIFY=1/--verify) remains available as an on-demand integrity check.

Theconsultasjob is the reference implementation:

consultascovers thefull set of OPEN consultations— every matter currently in tramitação (~7.7k), not just the ~5 highlights. Confirmed on the first run: thepesquisamaterialisting isin-tramitação-only, so closed/historical consultations arenotcaptured by this source (a pre-ingestion historical backfill is out of scope). Three settled design decisions:
- Decoupled ingestion.The open set is acquired by anoff-Worker TypeScript job(scripts/ingest-ecidadania/, run by a daily GitHub Action —.github/workflows/ingest-ecidadania.yml) that paginates the HTML listing (pesquisamateria?p=1..N, the only full-coverage source for open consultations) for ids + vote counts andbulk-loads D1; the Worker only reads. The brittle, long crawl is kept out of the request/Cron path.
- Status from/processo, not HTML.A consultation runs from presentation until the end of tramitação, sostatusis a function of the matter:aberta ⟺ thecodigoMateriais in the/processotramitando=Sset, derived from robust JSON (never scraped). Every consultation enters asaberta(the listing only yields in-tramitação matters); on eachcompleterun the job re-derives status forall stored rowsby/processomembership (not by listing-absence, which can be transient), so a consultation whose matter leaves tramitação flips toencerrada. Theencerrada/todassets therefore grow over time; consultations that closedbeforethe first ingestion aren't captured (out of scope). The list/analysis tools default tostatus: aberta.
- Two reconciled cadences (one shared writer contract).The jobreusescontentHash+ theConsultaResumobuilder +classifyRunfromsrc/scraper/, so its rows are byte-identical to the Cron's. The daily job owns the long tail; the2h Cronkeeps the ~5 hot/open highlights fresh via atargeted metric splice(recorded asok-metrica, bypassing the corpusclassifyRunbaseline). Corpus freshness (possivelDesatualizacao) is computed from the laststatus='ok'run and uses a larger window (ECIDADANIA_CORPUS_STALE_MAX_MIN), and a stale consultas corpus is served from D1 flagged rather than collapsing back to the live highlights.

Write guards on the load: anincomplete crawl(any page failed) or an incomplete/processostatus universe writes only anerrorun row; even a complete crawl is rejected by acatastrophic floor(ECIDADANIA_CORPUS_MIN_PCT, default 80% of the last good corpus) to guard against a degraded page — overridable with--force/INGEST_FORCE=1for a legitimate large shrink. Run daily via the Action, or manually:

CLOUDFLARE_API_TOKEN=… npm run ingest:ecidadania # writes scripts/ingest-ecidadania/out.sql npx wrangler d1 execute senado-ecidadania --remote --file=scripts/ingest-ecidadania/out.sql

MCP uses POST for alltools/callrequests. Caching POST responses is not natively supported by the Cache API, which requires GET requests. The solution:
- Hash parameters— Tool name + sorted parameters are hashed with SHA-256
- Synthetic GET key— A synthetic URLhttps://senado-br-mcp.internal/__cache/{tool}/{hash}is constructed
- Cache API match/put— The synthetic GET URL is used withcaches.default.match()andcaches.default.put(), allowing standard Cache API operations on POST-originated data

This caching happens at thetool level(inside each tool's callback), not at the MCP transport level.

Every toolattaches aprovenance envelopeso a result is traceable back to its official source — provenance is treated as a first-class part of the answer, not an optional extra (the audience is journalists and political-science researchers, for whom an un-sourced figure is unusable). Sincev3.5.0the envelope implements the portfolio-wideprovenance contract v1.0(@sbissoli/mcp-provenance): the server builds and validates a full canonical model per response, and emits itsconciseprojection— a fixed 6-key block with explicitnullfor unknown fields. The block lives instructuredContent.provenance(parseable by clients; note the advertised per-tool output schema is permissive, so contract validation happens server-side at build time, in the package) and is mirrored as a compact source footer in the text content for clients that only render text — the data JSON itself isnotduplicated with the envelope, to keep the per-response token cost low.

Coverage spans all four upstream sources, each with its ownsource/citation/license(insrc/utils/provenance.ts):

- Senado Federal — Dados Abertos (Legislativo)legis.senado.leg.br/dadosabertos
- Senado Federal — Dados Abertos (Administrativo)adm.senado.gov.br/adm-dadosabertos
- Senado Federal — Execução Orçamentária e Financeira— Arquimedes/Financeiro feed atsenado.gov.br
- Senado Federal — Portal e-Cidadaniawww12.senado.leg.br/ecidadania

Fields of theconciseblock (per response — one tool, one source; keys in this fixed order,nullwhen the source does not expose the value):

The canonical model behind the block also carriesdataset.id(item/series identifier, e.g.codigoMateria=137808),api_versionand per-fieldfield_sources; those are validated on every build and informattribution(below), but are not part of theconciseprojection.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.