senado-br-mcp 3.3.0 β†’ 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +891 -810
  2. package/README.pt-BR.md +829 -788
  3. package/dist/app-surface.js +122 -0
  4. package/dist/dataset/dictionary.js +94 -0
  5. package/dist/dataset/harmonize.js +32 -0
  6. package/dist/dataset/provenance.js +56 -0
  7. package/dist/dataset/release.js +164 -0
  8. package/dist/dataset/schema.js +572 -0
  9. package/dist/legal.js +114 -0
  10. package/dist/observability/origin.js +36 -0
  11. package/dist/openai-app-widget.js +455 -0
  12. package/dist/openai-domain-verification.js +10 -0
  13. package/dist/scraper/ecidadania.js +207 -33
  14. package/dist/scraper/pipeline.js +24 -4
  15. package/dist/server.js +22 -9
  16. package/dist/status.js +7 -0
  17. package/dist/throttle/upstream.js +2 -1
  18. package/dist/tools/comissoes.js +82 -31
  19. package/dist/tools/composicao.js +76 -33
  20. package/dist/tools/contratacoes.js +69 -26
  21. package/dist/tools/discursos.js +48 -15
  22. package/dist/tools/ecidadania.js +71 -23
  23. package/dist/tools/legislacao.js +46 -36
  24. package/dist/tools/materias.js +65 -16
  25. package/dist/tools/orcamento-senado.js +130 -10
  26. package/dist/tools/orcamento.js +82 -36
  27. package/dist/tools/plenario.js +61 -34
  28. package/dist/tools/processos.js +107 -19
  29. package/dist/tools/referencia.js +3 -4
  30. package/dist/tools/senadores-admin.js +109 -20
  31. package/dist/tools/senadores.js +58 -12
  32. package/dist/tools/servidores.js +254 -18
  33. package/dist/tools/supridos.js +150 -1
  34. package/dist/tools/taquigrafia.js +32 -9
  35. package/dist/tools/votacao-comissao.js +52 -29
  36. package/dist/tools/votacoes.js +42 -7
  37. package/dist/utils/estatisticas.js +134 -0
  38. package/dist/utils/placar.js +34 -0
  39. package/dist/utils/provenance.js +67 -23
  40. package/dist/utils/untrusted.js +0 -0
  41. package/dist/utils/upstream-parse.js +103 -0
  42. package/dist/utils/validation.js +53 -0
  43. package/dist/version.js +2 -1
  44. package/package.json +30 -7
package/README.md CHANGED
@@ -1,810 +1,891 @@
1
- # Senado Brasil MCP Server
2
-
3
- ![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white)
4
- ![MCP](https://img.shields.io/badge/MCP-Streamable%20HTTP-1f6feb)
5
- ![Tools](https://img.shields.io/badge/tools-66-2ea44f)
6
- [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io)
7
- [![LobeHub](https://lobehub.com/badge/mcp/sidneybissoli-senado-br-mcp-cloudflare)](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare)
8
- [![smithery badge](https://smithery.ai/badge/sidneybissoli/senado-br-mcp-cloudflare)](https://smithery.ai/servers/sidneybissoli/senado-br-mcp-cloudflare)
9
- [![GitHub stars](https://img.shields.io/github/stars/SidneyBissoli/senado-br-mcp-cloudflare?style=flat&logo=github)](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare)
10
- [![GitHub Sponsors](https://img.shields.io/github/sponsors/SidneyBissoli?logo=githubsponsors&label=Sponsor&color=db61a2)](https://github.com/sponsors/SidneyBissoli)
11
- [![License: MIT](https://img.shields.io/github/license/SidneyBissoli/senado-br-mcp-cloudflare)](LICENSE)
12
- [![Status](https://img.shields.io/website?url=https%3A%2F%2Fsenado.sidneybissoli.com%2Fhealth&up_message=online&down_message=offline&label=status)](https://senado.sidneybissoli.com/status)
13
-
14
- πŸ‡§πŸ‡· [Leia em PortuguΓͺs](README.pt-BR.md)
15
-
16
- A **public, hosted** MCP server that gives AI assistants live, structured access to **Brazilian Senate open data** β€” **no 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.
17
-
18
- It exposes **66 tools**, **4 prompts**, and **5 resources** across two domains:
19
-
20
- - **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.
21
- - **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.
22
-
23
- Data comes from three official sources β€” the [legislative open-data API](https://legis.senado.leg.br/dadosabertos/), the [administrative open-data API](https://adm.senado.gov.br/adm-dadosabertos/swagger-ui/index.html), and the e-Cidadania portal. All tool responses are in Portuguese (pt-BR). See [CHANGELOG.md](CHANGELOG.md) for release history.
24
-
25
- ## See it in action
26
-
27
- Point a client at the endpoint and ask, in plain Portuguese:
28
-
29
- - *"Como os senadores de SΓ£o Paulo votaram nas ΓΊltimas votaΓ§Γ΅es do plenΓ‘rio?"* β†’ `senado_search_votacoes`
30
- - *"Mostre a tramitaΓ§Γ£o da PEC 45/2019."* β†’ `senado_buscar_materias` + `senado_obter_materia`
31
- - *"Quanto foi gasto com a cota parlamentar (CEAPS) em 2024, por tipo de despesa?"* β†’ `senado_ceaps`
32
-
33
- The answers come live from the Senate's official open-data APIs β€” exact figures with provenance, not numbers guessed from training data.
34
-
35
- ## Use it (hosted β€” no setup)
36
-
37
- This is a **remote, hosted, open-access** server. To use it, point any MCP client at the Streamable
38
- HTTP endpoint β€” **no install, no account, no API key, no configuration**:
39
-
40
- ```
41
- https://senado.sidneybissoli.com/mcp
42
- ```
43
-
44
- ### Install (any client)
45
-
46
- For clients that launch MCP servers as a command β€” and for one-command setup β€” use the
47
- [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge. **No build, no config, no key:**
48
-
49
- ```bash
50
- npx -y mcp-remote https://senado.sidneybissoli.com/mcp
51
- ```
52
-
53
- - **One-click (LobeHub):** open the [server page](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare) and click **Install**.
54
- - **Native remote URL** (Claude Desktop/Code and other Streamable-HTTP clients): see [Connecting MCP Clients](#connecting-mcp-clients).
55
-
56
- Everything below *Architecture* (Prerequisites, Setup, Deploy) is **only for optionally self-hosting your
57
- own instance** β€” it is **not** required to use this public server.
58
-
59
- ## Run locally (npx Β· stdio)
60
-
61
- Prefer not to route queries through a third-party host (e.g. a newsroom policy)? The **same server**
62
- also runs as a **local stdio process** that talks **directly to the official government APIs** β€” same
63
- 66 tools, same provenance envelope, no Cloudflare in the loop. This is the npm/stdio channel, published
64
- as [`senado-br-mcp`](https://www.npmjs.com/package/senado-br-mcp).
65
-
66
- Point a command-based client (Claude Desktop/Code, etc.) at the package β€” npm fetches and runs it,
67
- no clone or build:
68
-
69
- ```json
70
- {
71
- "mcpServers": {
72
- "senado-br": {
73
- "command": "npx",
74
- "args": ["-y", "senado-br-mcp"]
75
- }
76
- }
77
- }
78
- ```
79
-
80
- To run it directly or hack on it, use the source checkout instead:
81
-
82
- ```bash
83
- git clone https://github.com/SidneyBissoli/senado-br-mcp-cloudflare
84
- cd senado-br-mcp-cloudflare
85
- npm install
86
- npm run build
87
- node dist/cli.js # serves MCP over stdio (Ctrl+C to stop)
88
- ```
89
-
90
- **Parity with the hosted server:** the legislative and administrative tools are **identical** (same
91
- upstream APIs, same throttle/cache/provenance) β€” locally the L1 Cloudflare cache is a no-op, but the L0
92
- in-memory cache still works, so results are the same. The **only** difference is the e-Cidadania
93
- list/corpus tools: without D1 they fall back to a live scrape of the ~5 REST highlights, flagged via
94
- `meta.fonte` / `possivelDesatualizacao`; the detail tools (`obter_*`) are identical. Logs go to
95
- **stderr** β€” stdout carries only the JSON-RPC protocol stream.
96
-
97
- ## Architecture
98
-
99
- - **Runtime:** Cloudflare Workers (ESM)
100
- - **Transport:** Streamable HTTP (MCP spec 2025-03-26) via `createMcpHandler` from `agents/mcp`
101
- - **Protocol:** MCP over JSON-RPC β€” single `/mcp` endpoint handles POST, GET, DELETE
102
- - **SDK:** `@modelcontextprotocol/sdk` 1.26.0+ (per-request McpServer instances)
103
- - **Validation:** Zod schemas for all tool inputs
104
- - **Caching:** 2-layer (L0 memory + L1 Cache API) with SHA-256 keying
105
- - **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](#e-cidadania-d1-backed-cron-refreshed))
106
- - **Rate limiting:** Token bucket β€” global (8 req/s) + per-client (2 req/s)
107
- - **Upstream throttle:** Max 6 concurrent requests, 10s timeout, retry with exponential backoff
108
- - **Auth:** Optional Bearer token (set the `API_KEY` secret; open access when unset). Constant-time comparison.
109
- - **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
110
- - **Liveness:** Runs on Cloudflare's own global network behind a custom domain β€” no third-party host that can go dark. Public `/health` and `/status` (version + last-deploy id/timestamp) make uptime and the current build verifiable; the **status** badge above pings the live endpoint
111
- - **Tests:** Vitest unit tests for parsers, helpers, cache, throttle, and auth
112
-
113
- ## Self-hosting (optional)
114
-
115
- > **Not needed to use the server** β€” it is already hosted at `https://senado.sidneybissoli.com/mcp`
116
- > (open access). Follow this section only if you want to run your **own** private instance.
117
-
118
- ### Prerequisites
119
-
120
- - Node.js 18+
121
- - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) v4+
122
- - Cloudflare account
123
-
124
- ### Setup
125
-
126
- #### 1. Install dependencies
127
-
128
- ```bash
129
- npm install
130
- ```
131
-
132
- #### 2. Create KV namespace
133
-
134
- ```bash
135
- # Create the KV namespace
136
- wrangler kv namespace create CACHE_KV
137
-
138
- # Note the ID from the output, e.g.:
139
- # { binding = "CACHE_KV", id = "abc123..." }
140
- ```
141
-
142
- #### 3. Configure wrangler.toml
143
-
144
- Replace the placeholder KV namespace ID:
145
-
146
- ```toml
147
- [[kv_namespaces]]
148
- binding = "CACHE_KV"
149
- id = "YOUR_KV_NAMESPACE_ID_HERE"
150
- ```
151
-
152
- Optionally set `ALLOWED_ORIGIN` to restrict CORS:
153
-
154
- ```toml
155
- [vars]
156
- ALLOWED_ORIGIN = "https://your-app.example.com"
157
- ```
158
-
159
- The e-Cidadania pipeline needs a **D1 database** and a **Cron Trigger** (both already declared in `wrangler.toml` β€” replace the database ID):
160
-
161
- ```toml
162
- [[d1_databases]]
163
- binding = "ECIDADANIA_DB"
164
- database_name = "senado-ecidadania"
165
- database_id = "YOUR_D1_DATABASE_ID_HERE"
166
-
167
- [triggers]
168
- crons = ["0 */2 * * *"]
169
- ```
170
-
171
- Create the database (paste the returned ID above) and apply the schema:
172
-
173
- ```bash
174
- npx wrangler d1 create senado-ecidadania
175
- npx wrangler d1 migrations apply senado-ecidadania --remote
176
- ```
177
-
178
- The list tools fall back to live scraping when D1 is empty, so the server works before the first Cron run.
179
-
180
- #### 4. (Optional) Enable authentication
181
-
182
- ```bash
183
- wrangler secret put API_KEY
184
- # Clients must then send: Authorization: Bearer <key>
185
- # When API_KEY is not set, the server is open access.
186
- ```
187
-
188
- #### 5. Local development
189
-
190
- ```bash
191
- npm run dev
192
- # Dev server runs locally on port 8787 (local only).
193
- # The public MCP endpoint is https://senado.sidneybissoli.com/mcp
194
- ```
195
-
196
- #### 6. Tests and typecheck
197
-
198
- ```bash
199
- npm test # run all tests once
200
- npm run test:watch # watch mode
201
- npm run typecheck # tsc --noEmit
202
- ```
203
-
204
- #### 7. Deploy
205
-
206
- ```bash
207
- npm run deploy
208
- # Serves at https://senado.sidneybissoli.com (custom domain) and
209
- # https://senado-br-mcp.sidneybissoli.workers.dev (workers.dev fallback)
210
- ```
211
-
212
- ## Endpoints
213
-
214
- | Path | Methods | Description |
215
- |------|---------|-------------|
216
- | `/mcp` | POST, GET, DELETE, OPTIONS | MCP Streamable HTTP endpoint (managed by `createMcpHandler`) |
217
- | `/health` | GET | Health check β€” returns `ok` (always public) |
218
- | `/status` | GET | JSON: `status`, `version`, and last-deploy metadata (`deploy.id`/`tag`/`timestamp`) β€” liveness + current build, no MCP handshake needed (always public) |
219
- | `/metrics` | GET | JSON counters: requests, tool calls, cache hits/misses, upstream calls/retries/errors, auth failures (always public) |
220
-
221
- ## MCP Request Examples
222
-
223
- All requests go to `POST /mcp` with JSON-RPC 2.0 format.
224
-
225
- ### List available tools
226
-
227
- ```json
228
- {
229
- "jsonrpc": "2.0",
230
- "id": 1,
231
- "method": "tools/list"
232
- }
233
- ```
234
-
235
- ### Call a tool β€” List senators from SP
236
-
237
- ```json
238
- {
239
- "jsonrpc": "2.0",
240
- "id": 2,
241
- "method": "tools/call",
242
- "params": {
243
- "name": "senado_listar_senadores",
244
- "arguments": {
245
- "uf": "SP",
246
- "emExercicio": true
247
- }
248
- }
249
- }
250
- ```
251
-
252
- ### Call a tool β€” Search bills by keyword
253
-
254
- ```json
255
- {
256
- "jsonrpc": "2.0",
257
- "id": 3,
258
- "method": "tools/call",
259
- "params": {
260
- "name": "senado_buscar_materias",
261
- "arguments": {
262
- "palavraChave": "inteligΓͺncia artificial",
263
- "tramitando": true
264
- }
265
- }
266
- }
267
- ```
268
-
269
- ### Call a tool β€” Get recent plenary votes
270
-
271
- ```json
272
- {
273
- "jsonrpc": "2.0",
274
- "id": 4,
275
- "method": "tools/call",
276
- "params": {
277
- "name": "senado_search_votacoes",
278
- "arguments": {
279
- "dias": 7
280
- }
281
- }
282
- }
283
- ```
284
-
285
- ### Call a tool β€” Most popular citizen ideas
286
-
287
- ```json
288
- {
289
- "jsonrpc": "2.0",
290
- "id": 5,
291
- "method": "tools/call",
292
- "params": {
293
- "name": "senado_ecidadania_listar_ideias",
294
- "arguments": {
295
- "ordenarPor": "apoios",
296
- "ordem": "desc",
297
- "status": "aberta"
298
- }
299
- }
300
- }
301
- ```
302
-
303
- ## Upstream API Endpoints
304
-
305
- The server consumes two classes of upstream endpoints from the Senado API:
306
-
307
- ### Legacy endpoints (`.json` suffix, PascalCase responses)
308
-
309
- Used by Groups A, E, F, H, I, J, K, L, M, N. The `.json` suffix is appended automatically by `upstream.ts`. None of these is marked deprecated upstream.
310
-
311
- | Upstream path | Used by |
312
- |---------------|---------|
313
- | `/senador/lista/atual` | `senado_listar_senadores` |
314
- | `/senador/lista/legislatura/{legislatura}` | `senado_listar_senadores` (param `legislatura`) |
315
- | `/senador/{codigo}` | `senado_obter_senador` |
316
- | `/senador/{codigo}/licencas`, `/comissoes`, `/cargos`, `/historicoAcademico`, `/filiacoes`, `/profissao` | `senado_senador_historico` (`tipo` enum) |
317
- | `/senador/afastados` | `senado_senadores_afastados` |
318
- | `/senador/{codigo}/apartes` | `senado_discursos_senador` (`tipo=apartes`) |
319
- | `/comissao/lista/colegiados` | `senado_listar_comissoes` (+ sigla-to-code resolution) |
320
- | `/comissao/{codigo}` | `senado_obter_comissao` (`secao=resumo`; numeric code, not sigla) |
321
- | `/composicao/comissao/{codigo}` | `senado_obter_comissao` (`secao=membros`) |
322
- | `/comissao/agenda/{data}` | `senado_agenda_comissoes` |
323
- | `/comissao/agenda/{dataInicio}/{dataFim}` | `senado_reunioes_comissao` |
324
- | `/comissao/reuniao/{codigoReuniao}` | `senado_reuniao_comissao` |
325
- | `/comissao/cpi/{sigla}/requerimentos` | `senado_requerimentos_cpi` (empty body = no requests) |
326
- | `/materia/distribuicao/autoria`, `/distribuicao/relatoria/{sigla}` | `senado_distribuicao_materias` |
327
- | `/plenario/agenda/dia/{data}`, `/agenda/mes/{data}`, `/agenda/cn/...` | `senado_agenda_plenario` |
328
- | `/plenario/resultado/{data}`, `/resultado/cn/{data}`, `/resultado/mes/{data}` | `senado_resultado_plenario` |
329
- | `/plenario/resultado/veto/{codigo}` (+ `/materia/`, `/dispositivo/`) | `senado_resultado_veto` |
330
- | `/plenario/votacao/orientacaoBancada/{data}` (+ perΓ­odo) | `senado_orientacao_bancada` |
331
- | `/plenario/encontro/{codigo}` (+ `/pauta`, `/resultado`, `/resumo`) | `senado_encontro_plenario` |
332
- | `/plenario/tiposSessao`, `/lista/tiposComparecimento`, `/lista/legislaturas` | `senado_tabelas_plenario` |
333
- | `/materia/vetos/{ano}`, `/vetos/aposrcn`, `/vetos/antesrcn`, `/vetos/encerrados` | `senado_vetos` |
334
- | `/taquigrafia/notas/{sessao\|reuniao}/{id}` | `senado_notas_taquigraficas` |
335
- | `/taquigrafia/videos/{sessao\|reuniao}/{id}` | `senado_videos_taquigrafia` |
336
- | `/senador/{codigo}/discursos` | `senado_discursos_senador` |
337
- | `/plenario/lista/discursos/{dataInicio}/{dataFim}` | `senado_discursos_plenario` |
338
- | `/discurso/texto-integral/{codigo}` | `senado_discurso_texto` (plain text, fetched directly) |
339
- | `/senador/lista/tiposUsoPalavra` | `senado_tabelas_referencia` (`tabela=tipos-uso-palavra`) |
340
- | `/composicao/lista/blocos` | `senado_listar_blocos` |
341
- | `/composicao/bloco/{codigo}` | `senado_obter_bloco` |
342
- | `/composicao/lideranca` | `senado_liderancas` |
343
- | `/composicao/mesaSF` | `senado_mesa` (`casa=senado`) |
344
- | `/composicao/mesaCN` | `senado_mesa` (`casa=congresso`) |
345
- | `/orcamento/lista` | `senado_orcamento_parlamentar` (`tipo=emendas`) |
346
- | `/orcamento/oficios` | `senado_orcamento_parlamentar` (`tipo=oficios`) |
347
- | `/legislacao/lista` | `senado_buscar_legislacao` |
348
- | `/legislacao/{codigo}` | `senado_obter_legislacao` |
349
- | `/legislacao/tiposNorma` | `senado_tabelas_referencia` (`tabela=tipos-norma`) |
350
- | `/votacaoComissao/comissao/{sigla}` | `senado_votacao_comissao` (`por=comissao`) |
351
- | `/votacaoComissao/parlamentar/{codigo}` | `senado_votacao_comissao` (`por=senador`) |
352
- | `/votacaoComissao/materia/{sigla}/{numero}/{ano}` | `senado_votacao_comissao` (`por=materia`) |
353
- | `/autor/lista/atual` | `senado_autores_atuais` |
354
-
355
- ### v3 endpoints (flat JSON arrays/objects, camelCase)
356
-
357
- Used by Groups B, C, D. Dates must be in **ISO format** (`YYYY-MM-DD`) β€” tools accept `YYYYMMDD` and convert. The `codigoMateria` query param bridges legacy matΓ©ria codes to v3 processes.
358
-
359
- | Upstream path | Used by |
360
- |---------------|---------|
361
- | `/votacao` | `senado_obter_votacao`, `senado_search_votacoes`, `senado_votos_materia`, `senado_votacoes_senador` |
362
- | `/processo` | `senado_search_processos`, `senado_buscar_materias` |
363
- | `/processo/{id}` | `senado_obter_processo`, `senado_obter_materia` (`secao=detalhe`/`tramitacao`) |
364
- | `/processo/documento` | `senado_obter_materia` (`secao=textos`) |
365
- | `/processo/emenda` | `senado_processo_detalhe` (`secao=emendas`) |
366
- | `/processo/relatoria` | `senado_processo_detalhe` (`secao=relatorias`), `senado_obter_materia` (rapporteur) |
367
- | `/processo/prazo` | `senado_processo_detalhe` (`secao=prazos`) |
368
- | `/processo/{siglas,assuntos,classes,destinos,entes,tipos-*}` | `senado_tabelas_processo` (12 reference tables) |
369
-
370
- ### Administrative API (adm.senado.gov.br/adm-dadosabertos, flat snake_case JSON)
371
-
372
- Used by Groups O, P, Q, R via `admFetch` (no `.json` suffix; HTTP 404 treated as empty collection). Base URL configurable via `SENADO_ADM_BASE_URL`.
373
-
374
- | Upstream path | Used by |
375
- |---------------|---------|
376
- | `/api/v1/senadores/despesas_ceaps/{ano}` | `senado_ceaps` (~10 MB/year, cached + aggregated in-Worker) |
377
- | `/api/v1/senadores/{auxilio-moradia,escritorios,aposentados}` | `senado_senadores_admin` (`tipo` enum) |
378
- | `/api/v1/servidores/servidores/{ativos,efetivos,comissionados,inativos}` | `senado_servidores` |
379
- | `/api/v1/servidores/remuneracoes/{ano}/{mes}` | `senado_remuneracoes_servidores` (~5.5 MB/month) |
380
- | `/api/v1/servidores/horas-extras/{ano}/{mes}` | `senado_horas_extras` |
381
- | `/api/v1/servidores/quantitativos/*`, `/previsao-aposentadoria`, `/api/v1/senadores/quantitativos/senadores` | `senado_pessoal_tabelas` (quantitativos) |
382
- | `/api/v1/servidores/{estagiarios,pensionistas,lotacoes,cargos}` | `senado_pessoal_tabelas` (listas nominais) |
383
- | `/api/v1/contratacoes/contratos` (+ `/{id}/aditivos`) | `senado_contratos`, `senado_contratacao_detalhe` |
384
- | `/api/v1/contratacoes/{tipo}/{id}/{itens,pagamentos,garantias}` | `senado_contratacao_detalhe` |
385
- | `/api/v1/contratacoes/licitacoes` | `senado_licitacoes` |
386
- | `/api/v1/contratacoes/terceirizados` | `senado_terceirizados` |
387
- | `/api/v1/contratacoes/empresas` | `senado_empresas_contratadas` (~13 MB, requires filter) |
388
- | `/api/v1/contratacoes/{atas_registro_preco,notas_empenho,menores_aprendizes}` | `senado_contratacoes_lista` |
389
- | `/api/v1/supridos/{ano}` (+ atosConcessao, empenhos, movimentacoes, transacoes) | `senado_suprimento_fundos` |
390
- | `senado.gov.br/bi-arqs/Arquimedes/Financeiro/{Despesa,Receitas}SenadoDadosAbertos.json` | `senado_execucao_orcamentaria` (daily JSON feeds, Brazilian decimal strings normalized) |
391
-
392
- ### e-Cidadania (D1-backed, Cron-refreshed)
393
-
394
- The e-Cidadania **list** data is persisted in a **D1 database** (`ecidadania_current/_history/_scrape_runs`, discriminated by `entidade`) and read from there instead of being scraped on every call. **Two cadences** write into it:
395
-
396
- - a **weekly off-Worker GitHub Action** owns the **full corpus** of each entity (see below) β€” the source of truth;
397
- - an in-Worker **Cron Trigger** (`0 */2 * * *`, `src/scraper/pipeline.ts β†’ refreshEcidadania`) does only a **targeted metric splice** of the ~5 REST highlights per live entity (`restcolecaomaismateria/ideia/audiencia` β€” votos/comentΓ‘rios/apoios), recorded as `ok-metrica` so it never re-breaks the corpus baseline and never touches the long tail.
398
-
399
- Both writers build payloads through the canonical `buildXResumo` builders + shared `contentHash`, so their rows are byte-identical. Each write:
400
-
401
- - **upserts** `ecidadania_current` (one row per item β€” what the tools read),
402
- - **appends** `ecidadania_history` only when an item's `content_hash` changes (time-series-ready),
403
- - records each run in `ecidadania_scrape_runs`.
404
-
405
- An **anomaly guard** (`src/scraper/anomaly.ts`, `classifyRun`) ensures a failed or anomalous corpus run (zero rows, or fewer than `ECIDADANIA_CORPUS_MIN_PCT`% of the last good run) **never overwrites** the last good state.
406
-
407
- The **list / analysis tools** (`listar_*`, `consultas_analise`, `sugerir_tema_enquete`, `consultas_votos`) read from D1 via `resolveList` (`src/scraper/store.ts`): D1-first. Because every entity is now a full corpus, a stale corpus is served from D1 **flagged** (`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 uses `ECIDADANIA_CORPUS_STALE_MAX_MIN` (~10 days). Every list response carries an additive `meta` (`fonte`, `lastScrapedAt`, `possivelDesatualizacao`) so callers always see the data's real age and never get stale data silently.
408
-
409
- The **detail tools** (`obter_*`) stay **live** (HTML scraped with CSS-class-targeted regex) for freshness, and write their richer payload through to `ecidadania_detalhe` fire-and-forget (deduped by `content_hash`), so detail history accrues without adding latency to the response.
410
-
411
- #### Full-corpus ingestion (off-Worker, weekly)
412
-
413
- All four e-Cidadania entities are full corpora owned by the weekly Action (`.github/workflows/ingest-ecidadania.yml`), each with its own `scripts/ingest-ecidadania/index-*.ts` orchestrator emitting batched `out-*.sql` the apply step bulk-loads:
414
-
415
- - **`consultas`** β€” open consultations (detailed below).
416
- - **`eventos`** β€” audiΓͺncias/eventos from the `principalaudiencia?p=N` HTML listing; status comes straight from the listing block (no `/processo` bridge).
417
- - **`ideias`** β€” ideias legislativas (~150k) from `pesquisaideia?situacao=N&p=M`, crawled **per `situacao` bucket** (the listing has no inline status) and emitted in ~10k-statement batches.
418
- - **`consultas_votos`** β€” a separate **historical** acervo of votes-by-UF parsed from the ~33 MB Arquimedes CSV (`ProposiΓ§Γ΅es-com-votos.csv`), aggregated to one record per matΓ©ria with a `votosPorUf` breakdown. The CSV's "dados atualizados atΓ©" stamp becomes the provenance `reference_period`; it is excluded from the row hash (`consultaVotoCore`) so a weekly stamp bump on these frozen votes doesn't churn `_history`. `STATUS ATUAL` is uniformly "Descontinuado", hence archival, not a migration of the open consultations. Served by `senado_ecidadania_consultas_votos` with provenance pointing at the CSV (`ECIDADANIA_ARQUIMEDES`).
419
-
420
- The `consultas` job is the reference implementation:
421
-
422
- `consultas` covers the **full set of OPEN consultations** β€” every matter currently in tramitaΓ§Γ£o (~7.7k), not just the ~5 highlights. Confirmed on the first run: the `pesquisamateria` listing is **in-tramitaΓ§Γ£o-only**, so closed/historical consultations are **not** captured by this source (a pre-ingestion historical backfill is out of scope). Three settled design decisions:
423
-
424
- 1. **Decoupled ingestion.** The open set is acquired by an **off-Worker TypeScript job** (`scripts/ingest-ecidadania/`, run by a weekly 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 and **bulk-loads D1**; the Worker only reads. The brittle, long crawl is kept out of the request/Cron path.
425
- 2. **Status from `/processo`, not HTML.** A consultation runs from presentation until the end of tramitação, so `status` is a function of the matter: **aberta ⟺ the `codigoMateria` is in the `/processo` `tramitando=S` set**, derived from robust JSON (never scraped). Every consultation enters as `aberta` (the listing only yields in-tramitação matters); on each **complete** run the job re-derives status for **all stored rows** by `/processo` membership (not by listing-absence, which can be transient), so a consultation whose matter leaves tramitação flips to `encerrada`. The `encerrada`/`todas` sets therefore grow over time; consultations that closed **before** the first ingestion aren't captured (out of scope). The list/analysis tools default to `status: aberta`.
426
- 3. **Two reconciled cadences (one shared writer contract).** The job **reuses** `contentHash` + the `ConsultaResumo` builder + `classifyRun` from `src/scraper/`, so its rows are byte-identical to the Cron's. The weekly job owns the long tail; the **2h Cron** keeps the ~5 hot/open highlights fresh via a *targeted metric splice* (recorded as `ok-metrica`, bypassing the corpus `classifyRun` baseline). Corpus freshness (`possivelDesatualizacao`) is computed from the last `status='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.
427
-
428
- Write guards on the load: an **incomplete crawl** (any page failed) or an incomplete `/processo` status universe writes only an `erro` run row; even a complete crawl is rejected by a **catastrophic floor** (`ECIDADANIA_CORPUS_MIN_PCT`, default 80% of the last good corpus) to guard against a degraded page β€” overridable with `--force` / `INGEST_FORCE=1` for a legitimate large shrink. Run weekly via the Action, or manually:
429
-
430
- ```bash
431
- CLOUDFLARE_API_TOKEN=… npm run ingest:ecidadania # writes scripts/ingest-ecidadania/out.sql
432
- npx wrangler d1 execute senado-ecidadania --remote --file=scripts/ingest-ecidadania/out.sql
433
- ```
434
-
435
- ## Caching
436
-
437
- ### Layer architecture
438
-
439
- | Layer | Storage | Scope | TTL range | Purpose |
440
- |-------|---------|-------|-----------|---------|
441
- | **L0** | In-memory `Map` | Per-isolate | 30-300s | Ultra-fast, eliminates redundant requests within a Worker isolate |
442
- | **L1** | Cloudflare Cache API (`caches.default`) | Per-colo (PoP) | 60-600s | Shared across requests at the same edge location |
443
- | **L2** | KV (optional) | Global | Variable | Reserved for rare, low-write data |
444
-
445
- ### Cache categories
446
-
447
- | Category | L0 TTL | L1 TTL | Used for |
448
- |----------|--------|--------|----------|
449
- | **STATIC** | 300s | 600s | Legislation types, static reference |
450
- | **SEMI_STATIC** | 120s | 300s | Party list, UF list, committee details |
451
- | **DYNAMIC** | 30s | 60s | Agendas, recent votes, meeting lists |
452
- | **ON_DEMAND** | 30s | 120s | Specific bill/senator/vote lookups |
453
-
454
- ### POST caching approach
455
-
456
- MCP uses POST for all `tools/call` requests. Caching POST responses is not natively supported by the Cache API, which requires GET requests. The solution:
457
-
458
- 1. **Hash parameters** β€” Tool name + sorted parameters are hashed with SHA-256
459
- 2. **Synthetic GET key** β€” A synthetic URL `https://senado-br-mcp.internal/__cache/{tool}/{hash}` is constructed
460
- 3. **Cache API match/put** β€” The synthetic GET URL is used with `caches.default.match()` and `caches.default.put()`, allowing standard Cache API operations on POST-originated data
461
-
462
- This caching happens at the **tool level** (inside each tool's callback), not at the MCP transport level.
463
-
464
- ## Provenance
465
-
466
- **Every tool** attaches a **provenance envelope** so 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). The envelope lives in `structuredContent.provenance` (parseable by clients, validated by the tool's output schema) and is mirrored as a one-line source footer in the text content for clients that only render text β€” the data JSON itself is **not** duplicated with the envelope, to keep the per-response token cost low (a fixed β‰ˆ170 chars).
467
-
468
- Coverage spans all four upstream sources, each with its own `source`/`attribution`/`license` (in `src/utils/provenance.ts`):
469
-
470
- - **Senado Federal β€” Dados Abertos (Legislativo)** β€” `legis.senado.leg.br/dadosabertos`
471
- - **Senado Federal β€” Dados Abertos (Administrativo)** β€” `adm.senado.gov.br/adm-dadosabertos`
472
- - **Senado Federal β€” ExecuΓ§Γ£o OrΓ§amentΓ‘ria e Financeira** β€” Arquimedes/Financeiro feed at `senado.gov.br`
473
- - **Senado Federal β€” Portal e-Cidadania** β€” `www12.senado.leg.br/ecidadania`
474
-
475
- Fields (per response β€” one tool, one source):
476
-
477
- | Field | Meaning |
478
- |-------|---------|
479
- | `source` | Official source name (e.g. *Senado Federal β€” Dados Abertos (Legislativo)*) |
480
- | `source_url` | Canonical endpoint/item URL consulted (e.g. `…/processo/{id}`) |
481
- | `dataset_id` | Item/series identifier (e.g. `codigoMateria=137808`) |
482
- | `reference_period` | Vintage/competΓͺncia of the data (e.g. `2024-03-15`, `2019`) |
483
- | `retrieved_at` | ISO-8601 of the **upstream extraction** β€” carried through the cache, so it reflects when the data was actually fetched, not the build or the cache-hit time |
484
- | `attribution` | Ready-to-use citation string |
485
- | `license` | Source terms (Dados Abertos do Senado Federal) |
486
-
487
- `retrieved_at` fidelity is provided by the cache layer (`cachedFetchWithMeta`), which persists the fetch timestamp alongside the value, so it reflects the real upstream extraction even on a cache hit. Two exceptions report an honest live timestamp instead: the e-Cidadania **list** tools (read from D1) use the corpus's `lastScrapedAt` β€” the true age of the stored data β€” while e-Cidadania **detail** tools, scraped live, use the fetch time and a level-3 canonical item URL.
488
-
489
- Coverage is **universal**: all 66 tools carry the envelope (verify with `grep -c 'resultWithProvenance(' src/tools/*.ts`). The **βŠ•** marks in the inventory below denote the original pilot tools (votes, bills, processes); the envelope now extends to every tool, so the marks are historical.
490
-
491
- ## Tool Inventory
492
-
493
- ### Group H β€” Reference/Metadata (1 tool)
494
-
495
- | Tool | Description |
496
- |------|-------------|
497
- | `senado_tabelas_referencia` | Tabelas de referΓͺncia via `tabela` enum: tipos-materia, partidos, ufs, legislatura-atual, tipos-norma, tipos-uso-palavra |
498
-
499
- ### Group A β€” Senators (5 tools)
500
-
501
- | Tool | Description |
502
- |------|-------------|
503
- | `senado_listar_senadores` | Lista senadores em exercΓ­cio/por legislatura, com filtros `nome` (busca parcial sem acento), `uf` e `partido` |
504
- | `senado_obter_senador` | Detalhe biogrΓ‘fico de um senador: bio, mandatos, partido, contato |
505
- | `senado_votacoes_senador` βŠ• | Como um senador votou em cada matΓ©ria (via v3 `/votacao`) |
506
- | `senado_senador_historico` | HistΓ³rico funcional via `tipo` enum: licencas, comissoes, cargos, historico-academico, filiacoes, profissoes |
507
- | `senado_senadores_afastados` | Senadores atualmente afastados (fora de exercΓ­cio) |
508
-
509
- ### Group B β€” Bills/Matters (2 tools, v3 backend)
510
-
511
- | Tool | Description |
512
- |------|-------------|
513
- | `senado_buscar_materias` βŠ• | Busca matΓ©rias por tipo, nΓΊmero, ano, palavra-chave, autor ou tramitaΓ§Γ£o (via v3 `/processo`) |
514
- | `senado_obter_materia` βŠ• | Dados de uma matΓ©ria via `secao` enum: detalhe (situaΓ§Γ£o/relator), tramitacao (histΓ³rico) ou textos (documentos) |
515
-
516
- ### Group C β€” Processes (5 tools)
517
-
518
- | Tool | Description |
519
- |------|-------------|
520
- | `senado_search_processos` βŠ• | Busca processos legislativos (complementar Γ  busca de matΓ©rias) |
521
- | `senado_obter_processo` βŠ• | Detalhes completos de um processo legislativo especΓ­fico |
522
- | `senado_processo_detalhe` | Aspecto de um processo via `secao` enum: emendas, relatorias ou prazos |
523
- | `senado_autores_atuais` | Parlamentares autores de processos em tramitaΓ§Γ£o, ordenados por produΓ§Γ£o |
524
- | `senado_tabelas_processo` | 12 tabelas de referΓͺncia (siglas, assuntos, classes, tipos-*) via `tabela` enum |
525
-
526
- ### Group D β€” Votes (3 tools)
527
-
528
- | Tool | Description |
529
- |------|-------------|
530
- | `senado_obter_votacao` βŠ• | Detalhes de uma votaΓ§Γ£o com votos nominais. Aceita `codigoVotacao` (codigoSessao da sessΓ£o plenΓ‘ria). |
531
- | `senado_votos_materia` βŠ• | VotaΓ§Γ΅es de uma matΓ©ria (via v3 `/votacao?codigoMateria`), com votos nominais opcionais |
532
- | `senado_search_votacoes` βŠ• | Busca/listagem flexΓ­vel de votaΓ§Γ΅es do plenΓ‘rio por `dias`, perΓ­odo, processo, matΓ©ria ou senador |
533
-
534
- ### Group E β€” Committees (7 tools)
535
-
536
- | Tool | Description |
537
- |------|-------------|
538
- | `senado_listar_comissoes` | Lista comissΓ΅es (colegiados) ativas, filtrΓ‘veis por tipo |
539
- | `senado_obter_comissao` | Dados de uma comissΓ£o via `secao` enum: resumo (mesa/totais) ou membros (composiΓ§Γ£o). Resolve sigla para cΓ³digo internamente. |
540
- | `senado_reunioes_comissao` | ReuniΓ΅es de uma comissΓ£o num perΓ­odo (lida com intervalos entre anos) |
541
- | `senado_agenda_comissoes` | Agenda de reuniΓ΅es de todas as comissΓ΅es numa data |
542
- | `senado_reuniao_comissao` | Detalhe completo de uma reuniΓ£o: partes, itens, convidados, resultados, links pauta/ata |
543
- | `senado_requerimentos_cpi` | Requerimentos protocolados numa CPI em atividade, paginados |
544
- | `senado_distribuicao_materias` | EstatΓ­sticas de carga por senador numa comissΓ£o: autoria ou relatoria |
545
-
546
- ### Group F β€” Plenary (7 tools)
547
-
548
- | Tool | Description |
549
- |------|-------------|
550
- | `senado_agenda_plenario` | Plenary schedule β€” by day, month or Congress (escopo dia/mes/cn) |
551
- | `senado_resultado_plenario` | Session results: items deliberated, opinions, outcomes (SF/CN/month) |
552
- | `senado_orientacao_bancada` | Party leadership voting instructions per vote, with tallies |
553
- | `senado_vetos` | Presidential vetoes by year or tramitation status |
554
- | `senado_resultado_veto` | Nominal veto vote results (by veto, vetoed bill or device) |
555
- | `senado_encontro_plenario` | Legislative session detail, agenda items, results or summary |
556
- | `senado_tabelas_plenario` | Session types, attendance types, legislatures list |
557
-
558
- ### Group G β€” e-Cidadania (9 tools)
559
-
560
- | Tool | Description |
561
- |------|-------------|
562
- | `senado_ecidadania_listar_consultas` | Consultas pΓΊblicas (conjunto completo das **abertas** β€” matΓ©rias em tramitaΓ§Γ£o) com votaΓ§Γ£o sim/nΓ£o; filtro `status` (padrΓ£o `aberta`) |
563
- | `senado_ecidadania_obter_consulta` | Detalhe de uma consulta: votos, autor, relator, comentΓ‘rios |
564
- | `senado_ecidadania_consultas_analise` | Analisa o conjunto completo de consultas **abertas** via `modo` (consenso/polarizada); `status` padrΓ£o `aberta` |
565
- | `senado_ecidadania_listar_ideias` | Ideias legislativas de cidadΓ£os; ranking das mais apoiadas via `ordenarPor: apoios` |
566
- | `senado_ecidadania_obter_ideia` | Detalhe de uma ideia: texto, apoios, status de conversΓ£o em projeto |
567
- | `senado_ecidadania_listar_eventos` | Eventos interativos (audiΓͺncias, sabatinas, lives); ranking dos mais comentados via `ordenarPor` |
568
- | `senado_ecidadania_obter_evento` | Detalhe de um evento: pauta, convidados, link de vΓ­deo |
569
- | `senado_ecidadania_sugerir_tema_enquete` | Sugere temas para enquete mensal a partir de critΓ©rios configurΓ‘veis |
570
- | `senado_ecidadania_consultas_votos` | Acervo **histΓ³rico** de votos das consultas com quebra **por UF** (CSV Arquimedes); ranking por `total`/`sim`/`nao`, filtro `uf`/`materia` |
571
-
572
- ### Group I β€” Speeches (3 tools)
573
-
574
- | Tool | Description |
575
- |------|-------------|
576
- | `senado_discursos_senador` | Pronunciamentos de um senador via `tipo` enum: discursos (prΓ³prios) ou apartes (intervenΓ§Γ΅es) |
577
- | `senado_discursos_plenario` | Todos os discursos em plenΓ‘rio num intervalo de datas |
578
- | `senado_discurso_texto` | Texto integral de um pronunciamento/discurso especΓ­fico |
579
-
580
- ### Group J β€” Blocs & Leadership (4 tools)
581
-
582
- | Tool | Description |
583
- |------|-------------|
584
- | `senado_listar_blocos` | Blocos parlamentares do Senado e seus partidos membros |
585
- | `senado_obter_bloco` | Detalhes de um bloco parlamentar especΓ­fico |
586
- | `senado_liderancas` | LideranΓ§as do Senado/Congresso (lΓ­deres, vice-lΓ­deres), filtrΓ‘veis |
587
- | `senado_mesa` | Membros da Mesa Diretora via `casa` enum: senado (Mesa do SF) ou congresso (Mesa do CN) |
588
-
589
- ### Group K β€” Budget (1 tool)
590
-
591
- | Tool | Description |
592
- |------|-------------|
593
- | `senado_orcamento_parlamentar` | Dados de emendas orΓ§amentΓ‘rias via `tipo` enum: emendas (lotes) ou oficios (ofΓ­cios de apoio) |
594
-
595
- ### Group L β€” Federal Law (2 tools)
596
-
597
- | Tool | Description |
598
- |------|-------------|
599
- | `senado_buscar_legislacao` | Busca normas jurΓ­dicas federais por tipo, nΓΊmero, ano ou data (ao menos um obrigatΓ³rio) |
600
- | `senado_obter_legislacao` | Detalhes de uma norma jurΓ­dica federal especΓ­fica |
601
-
602
- ### Group M β€” Committee Voting (1 tool)
603
-
604
- | Tool | Description |
605
- |------|-------------|
606
- | `senado_votacao_comissao` | VotaΓ§Γ΅es em comissΓ΅es via `por` enum: comissao, senador ou materia; perΓ­odo opcional |
607
-
608
- ### Group N β€” Taquigrafia (2 tools)
609
-
610
- | Tool | Description |
611
- |------|-------------|
612
- | `senado_notas_taquigraficas` | Official transcripts of plenary sessions or committee meetings β€” summary mode with excerpts, full-text mode paginated in blocks, speaker filter |
613
- | `senado_videos_taquigrafia` | Video/audio units per session or meeting, with speaker and media links |
614
-
615
- ### Group O β€” Senadores/Administrativo (2 tools)
616
-
617
- | Tool | Description |
618
- |------|-------------|
619
- | `senado_ceaps` | CEAPS parliamentary quota expenses by year β€” aggregated by senator, expense type, month or supplier, or itemized detail; filters by senator/month/type/supplier |
620
- | `senado_senadores_admin` | Dados administrativos dos senadores via `tipo` enum: auxilio-moradia, escritorios-apoio ou aposentados |
621
-
622
- ### Group P β€” Servidores / GestΓ£o de Pessoas (4 tools)
623
-
624
- | Tool | Description |
625
- |------|-------------|
626
- | `senado_servidores` | Civil servants by status (active/effective/commissioned/inactive), filterable by name, unit, position |
627
- | `senado_remuneracoes_servidores` | Monthly payroll β€” summary by payroll type or per-person composition with computed gross |
628
- | `senado_horas_extras` | Overtime payments by month with totals |
629
- | `senado_pessoal_tabelas` | Tabelas de pessoal via `tabela` enum: quantitativos (pessoal, cargos-funcoes, previsao-aposentadoria, senadores) e listas (estagiarios, pensionistas, lotacoes, cargos) |
630
-
631
- ### Group Q β€” ContrataΓ§Γ΅es (6 tools)
632
-
633
- | Tool | Description |
634
- |------|-------------|
635
- | `senado_contratos` | Contracts with server-side filters: supplier, CNPJ, year, number, object, labor |
636
- | `senado_contratacao_detalhe` | Items, payments, guarantees, amendments or activations of a contract/ata/empenho |
637
- | `senado_licitacoes` | Biddings by number or object text |
638
- | `senado_terceirizados` | Outsourced collaborators by name, company or unit |
639
- | `senado_empresas_contratadas` | Companies contracting with the Senate (requires name/CNPJ filter) |
640
- | `senado_contratacoes_lista` | Price-registration atas, commitment notes, young apprentices |
641
-
642
- ### Group R β€” Suprimento de Fundos (1 tool)
643
-
644
- | Tool | Description |
645
- |------|-------------|
646
- | `senado_suprimento_fundos` | Petty-cash advances by year: recipients, concession acts, commitments, movements, card transactions |
647
-
648
- ### Group S β€” OrΓ§amento do Senado (1 tool)
649
-
650
- | Tool | Description |
651
- |------|-------------|
652
- | `senado_execucao_orcamentaria` | Budget execution since 2013 (allocation, committed/settled/paid) and own revenues since 2012 (forecast vs collected) β€” aggregated by year, action, expense group, source or revenue origin |
653
-
654
- **Total: 66 tools**
655
-
656
- ### Prompts (4)
657
-
658
- Reusable pt-BR workflow templates (MCP `prompts` capability), defined in `src/prompts.ts`:
659
-
660
- | Prompt | Args | What it guides |
661
- | --- | --- | --- |
662
- | `senado_gastos_senador` | `senador`, `ano` | Resolve o senador e agrega/detalha despesas CEAPS. |
663
- | `senado_tramitacao_materia` | `sigla`, `numero`, `ano` | ObtΓ©m situaΓ§Γ£o atual + histΓ³rico de tramitaΓ§Γ£o da matΓ©ria. |
664
- | `senado_votos_senador` | `senador`, `periodo?` | Lista os votos nominais do senador no perΓ­odo. |
665
- | `senado_panorama_ecidadania` | β€” | Consolida consultas (consenso/polarizaΓ§Γ£o), ideias e eventos populares. |
666
-
667
- ### Resources (5)
668
-
669
- Static context documents/tables (MCP `resources` capability), defined in `src/resources.ts`:
670
-
671
- | URI | Type | Content |
672
- | --- | --- | --- |
673
- | `senado://guia` | markdown | VisΓ£o geral e qual ferramenta usar por objetivo. |
674
- | `senado://catalogo` | markdown | As 66 ferramentas agrupadas por domΓ­nio. |
675
- | `senado://glossario` | markdown | Siglas e termos do Senado (PEC, CEAPS, CCJ, RCN…). |
676
- | `senado://tabelas/tipos-materia` | json | Tipos de proposiΓ§Γ£o (sigla/nome/descriΓ§Γ£o). |
677
- | `senado://tabelas/ufs` | json | As 27 unidades federativas. |
678
-
679
- ## Project Structure
680
-
681
- ```
682
- src/
683
- β”œβ”€β”€ index.ts # Worker entrypoint (fetch handler + scheduled/Cron handler)
684
- β”œβ”€β”€ server.ts # McpServer factory (creates per-request instance)
685
- β”œβ”€β”€ auth.ts # Optional Bearer token auth (constant-time compare)
686
- β”œβ”€β”€ metrics.ts # In-memory counters served at /metrics
687
- β”œβ”€β”€ types.ts # Env, cache categories, safeguard constants
688
- β”œβ”€β”€ cache/
689
- β”‚ β”œβ”€β”€ l0-memory.ts # In-memory Map cache with TTL + LRU eviction
690
- β”‚ β”œβ”€β”€ l1-cache-api.ts # Cloudflare Cache API wrapper (synthetic GET keys)
691
- β”‚ └── manager.ts # Cache orchestrator (L0 β†’ L1 β†’ upstream)
692
- β”œβ”€β”€ throttle/
693
- β”‚ β”œβ”€β”€ token-bucket.ts # Token bucket rate limiter (global + per-client)
694
- β”‚ └── upstream.ts # Upstream fetch with concurrency limit, retry, timeout
695
- β”œβ”€β”€ scraper/
696
- β”‚ β”œβ”€β”€ ecidadania.ts # Isolated e-Cidadania scraper (REST lists + regex HTML detail; buildConsultaResumo)
697
- β”‚ β”œβ”€β”€ pipeline.ts # 2h Cron: targeted highlight metric splice (consultas/eventos/ideias); corpora owned by the weekly jobs
698
- β”‚ β”œβ”€β”€ anomaly.ts # Run classification (anomalous run never overwrites current)
699
- β”‚ └── store.ts # D1 reads (resolveList + per-entity staleness, lastGoodRunAt) + detail write-through
700
- β”œβ”€β”€ instrument.ts # Per-tool call telemetry (in-memory + Analytics Engine)
701
- β”œβ”€β”€ utils/
702
- β”‚ β”œβ”€β”€ logger.ts # Structured JSON logging
703
- β”‚ └── validation.ts # toolResult, toolError, errorFrom, buildParams, ensureArray helpers
704
- └── tools/
705
- β”œβ”€β”€ referencia.ts # Group H β€” 1 reference/metadata tool
706
- β”œβ”€β”€ senadores.ts # Group A β€” 5 senator tools
707
- β”œβ”€β”€ materias.ts # Group B β€” 2 bill/matter tools (v3 backend)
708
- β”œβ”€β”€ processos.ts # Group C β€” 5 process tools
709
- β”œβ”€β”€ votacoes.ts # Group D β€” 3 vote tools
710
- β”œβ”€β”€ comissoes.ts # Group E β€” 7 committee tools
711
- β”œβ”€β”€ plenario.ts # Group F β€” 7 plenary tools
712
- β”œβ”€β”€ ecidadania.ts # Group G β€” 8 e-Cidadania tools (read from D1; see scraper/)
713
- β”œβ”€β”€ discursos.ts # Group I β€” 3 speech tools
714
- β”œβ”€β”€ composicao.ts # Group J β€” 4 bloc/leadership tools
715
- β”œβ”€β”€ orcamento.ts # Group K β€” 1 budget tool
716
- β”œβ”€β”€ legislacao.ts # Group L β€” 2 federal law tools
717
- β”œβ”€β”€ votacao-comissao.ts # Group M β€” 1 committee voting tool
718
- β”œβ”€β”€ taquigrafia.ts # Group N β€” 2 stenographic record tools
719
- β”œβ”€β”€ senadores-admin.ts # Group O β€” 2 admin senator tools (CEAPS, housing)
720
- β”œβ”€β”€ servidores.ts # Group P β€” 4 personnel tools
721
- β”œβ”€β”€ contratacoes.ts # Group Q β€” 6 procurement tools
722
- β”œβ”€β”€ supridos.ts # Group R β€” 1 petty-cash tool
723
- └── orcamento-senado.ts # Group S β€” 1 budget execution tool
724
- scripts/
725
- └── ingest-ecidadania/ # Off-Worker weekly full-corpus consultas ingestion (run via `npm run ingest:ecidadania`)
726
- β”œβ”€β”€ index.ts # Orchestrator: crawl β†’ status (/processo) β†’ normalize β†’ guards β†’ out.sql
727
- β”œβ”€β”€ listing.ts # Pure listing parser (parseConsultaListingPage, findLastPage)
728
- β”œβ”€β”€ status.ts # tramitando=S set from /processo β†’ aberta/encerrada (deriveStatus)
729
- β”œβ”€β”€ restatus.ts # Linger fix: re-status stored rows by /processo membership (close zombies)
730
- β”œβ”€β”€ http.ts # Polite fetch (retry/backoff) for the unattended crawl
731
- β”œβ”€β”€ d1.ts # D1 pre-reads (existing meta, payloads, last good rows) via wrangler
732
- └── sql.ts # out.sql generation (mirrors SQL.upsert/SQL.history; reuses SyncRecord)
733
- .github/workflows/ # publish-mcp.yml (registry) + ingest-ecidadania.yml (weekly D1 corpus load)
734
- migrations/ # D1 schema (0001 tables, 0002 indexes) for the e-Cidadania pipeline
735
- tests/ # Vitest unit tests mirroring src/ (parsers, cache, throttle, auth, scraper,
736
- # pipeline/anomaly/store, listing/sql/highlights, plus e-Cidadania contract tests)
737
- ```
738
-
739
- ## Environment Variables
740
-
741
- | Variable | Required | Default | Description |
742
- |----------|----------|---------|-------------|
743
- | `SENADO_BASE_URL` | No | `https://legis.senado.leg.br/dadosabertos` | Legislative API base URL |
744
- | `SENADO_ADM_BASE_URL` | No | `https://adm.senado.gov.br/adm-dadosabertos` | Administrative API base URL |
745
- | `ALLOWED_ORIGIN` | No | `*` | CORS allowed origin |
746
- | `API_KEY` | No (secret) | β€” | When set, requires `Authorization: Bearer <key>` on all requests except `/health`, `/metrics`, and CORS preflight |
747
- | `CACHE_KV` | Yes (binding) | β€” | KV namespace for L2 cache |
748
- | `ECIDADANIA_DB` | Yes (binding) | β€” | D1 database for the e-Cidadania pipeline (list persistence + history) |
749
- | `ECIDADANIA_CORPUS_STALE_MAX_MIN` | No | `14400` | Staleness window (minutes, ~10d) for the weekly full corpora (all e-Cidadania entities) β€” served flagged, never collapsed to highlights |
750
- | `ECIDADANIA_CORPUS_MIN_PCT` | No | `80` | Catastrophic floor for the off-Worker corpus jobs: a complete crawl/parse below this % of the last good corpus is rejected |
751
- | `CLOUDFLARE_API_TOKEN` | No (secret) | β€” | GitHub Actions secret (D1 edit scope) for the weekly corpus ingestion job; not used by the Worker |
752
- | `CLOUDFLARE_ACCOUNT_ID` | No (Actions var) | β€” | GitHub Actions repo variable so wrangler skips `/memberships` account auto-discovery (a D1-scoped token can't read it); required alongside `CLOUDFLARE_API_TOKEN` in the ingestion job |
753
- | `SENADO_ANALYTICS` | No (binding) | β€” | Analytics Engine dataset for per-tool call telemetry |
754
-
755
- ## Connecting MCP Clients
756
-
757
- This is a **remote** server (Streamable HTTP, no install, open access) β€” point any MCP client at
758
- `https://senado.sidneybissoli.com/mcp`. Besides 66 tools, it exposes **prompts** (ready-made pt-BR
759
- workflows: `senado_gastos_senador`, `senado_tramitacao_materia`, `senado_votos_senador`,
760
- `senado_panorama_ecidadania`) and **resources** (`senado://guia`, `senado://catalogo`,
761
- `senado://glossario`, `senado://tabelas/tipos-materia`, `senado://tabelas/ufs`).
762
-
763
- ### One-click (LobeHub)
764
-
765
- Install from the LobeHub marketplace β€” open the
766
- [server page](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare) and click **Install**
767
- (it pre-fills the remote endpoint, no config needed).
768
-
769
- ### Claude Desktop / Claude Code
770
-
771
- Add to your MCP configuration:
772
-
773
- ```json
774
- {
775
- "mcpServers": {
776
- "senado-br": {
777
- "url": "https://senado.sidneybissoli.com/mcp"
778
- }
779
- }
780
- }
781
- ```
782
-
783
- For command-based clients (or any client without native remote support), use the `mcp-remote` bridge:
784
-
785
- ```json
786
- {
787
- "mcpServers": {
788
- "senado-br": {
789
- "command": "npx",
790
- "args": ["-y", "mcp-remote", "https://senado.sidneybissoli.com/mcp"]
791
- }
792
- }
793
- }
794
- ```
795
-
796
- ### MCP Inspector
797
-
798
- ```bash
799
- npx @modelcontextprotocol/inspector https://senado.sidneybissoli.com/mcp
800
- ```
801
-
802
- ## License
803
-
804
- MIT
805
-
806
- ## Credits
807
-
808
- Icon: *"Amanhecer no Congresso Nacional"* β€” photograph of the Brazilian National
809
- Congress, used under a Creative Commons license. (If you are the author, open an
810
- issue so we can add full attribution / the license link.)
1
+ # Senado Brasil MCP Server
2
+
3
+ ![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white)
4
+ ![MCP](https://img.shields.io/badge/MCP-Streamable%20HTTP-1f6feb)
5
+ ![Tools](https://img.shields.io/badge/tools-66-2ea44f)
6
+ [![CI](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml)
7
+ [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io)
8
+ [![LobeHub](https://lobehub.com/badge/mcp/sidneybissoli-senado-br-mcp-cloudflare)](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare)
9
+ [![smithery badge](https://smithery.ai/badge/sidneybissoli/senado-br-mcp-cloudflare)](https://smithery.ai/servers/sidneybissoli/senado-br-mcp-cloudflare)
10
+ [![senado-br-mcp-cloudflare MCP server](https://glama.ai/mcp/servers/SidneyBissoli/senado-br-mcp-cloudflare/badges/score.svg)](https://glama.ai/mcp/servers/SidneyBissoli/senado-br-mcp-cloudflare)
11
+ [![GitHub stars](https://img.shields.io/github/stars/SidneyBissoli/senado-br-mcp-cloudflare?style=flat&logo=github)](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare)
12
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/SidneyBissoli?logo=githubsponsors&label=Sponsor&color=db61a2)](https://github.com/sponsors/SidneyBissoli)
13
+ [![License: MIT](https://img.shields.io/github/license/SidneyBissoli/senado-br-mcp-cloudflare)](LICENSE)
14
+ [![Status](https://img.shields.io/website?url=https%3A%2F%2Fsenado.sidneybissoli.com%2Fhealth&up_message=online&down_message=offline&label=status)](https://senado.sidneybissoli.com/status)
15
+
16
+ πŸ‡§πŸ‡· [Leia em PortuguΓͺs](README.pt-BR.md)
17
+
18
+ A **public, hosted** MCP server that gives AI assistants live, structured access to **Brazilian Senate open data** β€” **no 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.
19
+
20
+ It exposes **66 tools**, **4 prompts**, and **5 resources** across two domains:
21
+
22
+ - **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.
23
+ - **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.
24
+
25
+ Data comes from three official sources β€” the [legislative open-data API](https://legis.senado.leg.br/dadosabertos/), the [administrative open-data API](https://adm.senado.gov.br/adm-dadosabertos/swagger-ui/index.html), and the e-Cidadania portal. All tool responses are in Portuguese (pt-BR). See [CHANGELOG.md](CHANGELOG.md) for release history.
26
+
27
+ ## See it in action
28
+
29
+ Point a client at the endpoint and ask in plain language β€” English or Portuguese:
30
+
31
+ - *"How did SΓ£o Paulo's senators vote in the most recent floor votes?"* β†’ `senado_search_votacoes`
32
+ - *"Show the legislative progress of PEC 45/2019 (a constitutional amendment proposal)."* β†’ `senado_buscar_materias` + `senado_obter_materia`
33
+ - *"How much was spent on the CEAPS parliamentary allowance in 2024, broken down by expense type?"* β†’ `senado_ceaps`
34
+
35
+ The answers come live from the Senate's official open-data APIs β€” exact figures with provenance, not numbers guessed from training data.
36
+
37
+ ## Use it (hosted β€” no setup)
38
+
39
+ This is a **remote, hosted, open-access** server. To use it, point any MCP client at the Streamable
40
+ HTTP endpoint β€” **no install, no account, no API key, no configuration**:
41
+
42
+ ```
43
+ https://senado.sidneybissoli.com/mcp
44
+ ```
45
+
46
+ ### OpenAI / ChatGPT app surface
47
+
48
+ For OpenAI Apps SDK submission and review, the Worker also exposes a curated MCP surface:
49
+
50
+ ```
51
+ https://senado.sidneybissoli.com/mcp/openai-app-v2
52
+ ```
53
+
54
+ This endpoint intentionally keeps the full public MCP server intact at `/mcp`, but limits tool discovery
55
+ to 25 high-signal, intent-oriented tools for ChatGPT app use. `/mcp/openai-app` remains available as a
56
+ legacy alias, but new ChatGPT app configurations should use `/mcp/openai-app-v2` so clients fetch the
57
+ current tool schema. The tools still call the same handlers and return the same provenance envelope; only
58
+ the advertised surface is narrower. Any ChatGPT app listing should present this as an independent open-data
59
+ research app, not as an official Senate, OpenAI or ChatGPT connector.
60
+
61
+ For ChatGPT Apps, those 25 tools also advertise a shared MCP Apps UI template at
62
+ `ui://senado-br-mcp/openai-app-dashboard-v2.html`. The self-contained widget renders the returned
63
+ `structuredContent` as a compact dashboard with metrics, main records, and source/provenance, without
64
+ adding another model-visible data tool.
65
+
66
+ Public legal URLs for app review:
67
+
68
+ - Privacy policy: `https://senado.sidneybissoli.com/privacy`
69
+ - Terms of use: `https://senado.sidneybissoli.com/terms`
70
+
71
+ ### Install (any client)
72
+
73
+ For clients that launch MCP servers as a command β€” and for one-command setup β€” use the
74
+ [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge. **No build, no config, no key:**
75
+
76
+ ```bash
77
+ npx -y mcp-remote https://senado.sidneybissoli.com/mcp
78
+ ```
79
+
80
+ - **One-click (LobeHub):** open the [server page](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare) and click **Install**.
81
+ - **Native remote URL** (Claude Desktop/Code and other Streamable-HTTP clients): see [Connecting MCP Clients](#connecting-mcp-clients).
82
+
83
+ Everything below *Architecture* (Prerequisites, Setup, Deploy) is **only for optionally self-hosting your
84
+ own instance** β€” it is **not** required to use this public server.
85
+
86
+ ## Run locally (npx Β· stdio)
87
+
88
+ Prefer not to route queries through a third-party host (e.g. a newsroom policy)? The **same server**
89
+ also runs as a **local stdio process** that talks **directly to the official government APIs** β€” same
90
+ 66 tools, same provenance envelope, no Cloudflare in the loop. This is the npm/stdio channel, published
91
+ as [`senado-br-mcp`](https://www.npmjs.com/package/senado-br-mcp).
92
+
93
+ Point a command-based client (Claude Desktop/Code, etc.) at the package β€” npm fetches and runs it,
94
+ no clone or build:
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "senado-br": {
100
+ "command": "npx",
101
+ "args": ["-y", "senado-br-mcp"]
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ To run it directly or hack on it, use the source checkout instead:
108
+
109
+ ```bash
110
+ git clone https://github.com/SidneyBissoli/senado-br-mcp-cloudflare
111
+ cd senado-br-mcp-cloudflare
112
+ npm install
113
+ npm run build
114
+ node dist/cli.js # serves MCP over stdio (Ctrl+C to stop)
115
+ ```
116
+
117
+ **Parity with the hosted server:** the legislative and administrative tools are **identical** (same
118
+ upstream APIs, same throttle/cache/provenance) β€” locally the L1 Cloudflare cache is a no-op, but the L0
119
+ in-memory cache still works, so results are the same. The **only** difference is the e-Cidadania
120
+ list/corpus tools: without D1 they fall back to a live scrape of the ~5 REST highlights, flagged via
121
+ `meta.fonte` / `possivelDesatualizacao`; the detail tools (`obter_*`) are identical. Logs go to
122
+ **stderr** β€” stdout carries only the JSON-RPC protocol stream.
123
+
124
+ ## Agent Skill (optional)
125
+
126
+ This repo bundles a Claude [Agent Skill](https://platform.claude.com/docs/en/docs/agents-and-tools/agent-skills/overview)
127
+ at [`.claude/skills/senado-br/`](.claude/skills/senado-br/SKILL.md) that teaches Claude **when** to reach for
128
+ this server and **how** to use its 66 tools well — a themed tool map, common question→tool playbooks, the
129
+ provenance contract, and gotchas (dates, the `codigoMateria` bridge, e-Cidadania's open-set listing, pagination).
130
+ It points back to the server's own `senado://catalogo` / `senado://guia` resources rather than duplicating them.
131
+
132
+ Claude Code auto-discovers it when you work in this repo. To use it elsewhere, copy
133
+ `.claude/skills/senado-br/` into your `~/.claude/skills/`, or zip the folder and upload it in claude.ai
134
+ (Settings β†’ Features). The skill assumes the `senado-br` MCP server is connected (hosted or via npx).
135
+
136
+ ## Architecture
137
+
138
+ - **Runtime:** Cloudflare Workers (ESM)
139
+ - **Transport:** Streamable HTTP (MCP spec 2025-03-26) via `createMcpHandler` from `agents/mcp`
140
+ - **Protocol:** MCP over JSON-RPC β€” `/mcp` handles the full public server; `/mcp/openai-app-v2` exposes a curated 25-tool profile plus a shared MCP Apps widget for OpenAI app review/submission (`/mcp/openai-app` remains as a legacy alias)
141
+ - **SDK:** `@modelcontextprotocol/sdk` 1.26.0+ (per-request McpServer instances)
142
+ - **Validation:** Zod schemas for all tool inputs
143
+ - **Caching:** 2-layer (L0 memory + L1 Cache API) with SHA-256 keying
144
+ - **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](#e-cidadania-d1-backed-cron-refreshed))
145
+ - **Rate limiting:** Token bucket β€” global (8 req/s) + per-client (2 req/s)
146
+ - **Upstream throttle:** Max 6 concurrent requests, 10s timeout, retry with exponential backoff
147
+ - **Auth:** Optional Bearer token (set the `API_KEY` secret; open access when unset). Constant-time comparison.
148
+ - **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
149
+ - **Liveness:** Runs on Cloudflare's own global network behind a custom domain β€” no third-party host that can go dark. Public `/health` and `/status` (version + last-deploy id/timestamp) make uptime and the current build verifiable; the **status** badge above pings the live endpoint
150
+ - **Tests:** Vitest unit tests for parsers, helpers, cache, throttle, and auth
151
+
152
+ ## Self-hosting (optional)
153
+
154
+ > **Not needed to use the server** β€” it is already hosted at `https://senado.sidneybissoli.com/mcp`
155
+ > (open access). Follow this section only if you want to run your **own** private instance.
156
+
157
+ ### Prerequisites
158
+
159
+ - Node.js 20+
160
+ - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) v4+
161
+ - Cloudflare account
162
+
163
+ ### Setup
164
+
165
+ #### 1. Install dependencies
166
+
167
+ ```bash
168
+ npm install
169
+ ```
170
+
171
+ #### 2. Create KV namespace
172
+
173
+ ```bash
174
+ # Create the KV namespace
175
+ wrangler kv namespace create CACHE_KV
176
+
177
+ # Note the ID from the output, e.g.:
178
+ # { binding = "CACHE_KV", id = "abc123..." }
179
+ ```
180
+
181
+ #### 3. Configure wrangler.toml
182
+
183
+ Replace the placeholder KV namespace ID:
184
+
185
+ ```toml
186
+ [[kv_namespaces]]
187
+ binding = "CACHE_KV"
188
+ id = "YOUR_KV_NAMESPACE_ID_HERE"
189
+ ```
190
+
191
+ Optionally set `ALLOWED_ORIGIN` to restrict CORS:
192
+
193
+ ```toml
194
+ [vars]
195
+ ALLOWED_ORIGIN = "https://your-app.example.com"
196
+ ```
197
+
198
+ The e-Cidadania pipeline needs a **D1 database** and a **Cron Trigger** (both already declared in `wrangler.toml` β€” replace the database ID):
199
+
200
+ ```toml
201
+ [[d1_databases]]
202
+ binding = "ECIDADANIA_DB"
203
+ database_name = "senado-ecidadania"
204
+ database_id = "YOUR_D1_DATABASE_ID_HERE"
205
+
206
+ [triggers]
207
+ crons = ["0 */2 * * *"]
208
+ ```
209
+
210
+ Create the database (paste the returned ID above) and apply the schema:
211
+
212
+ ```bash
213
+ npx wrangler d1 create senado-ecidadania
214
+ npx wrangler d1 migrations apply senado-ecidadania --remote
215
+ ```
216
+
217
+ The list tools fall back to live scraping when D1 is empty, so the server works before the first Cron run.
218
+
219
+ #### 4. (Optional) Enable authentication
220
+
221
+ ```bash
222
+ wrangler secret put API_KEY
223
+ # Clients must then send: Authorization: Bearer <key>
224
+ # When API_KEY is not set, the server is open access.
225
+ ```
226
+
227
+ #### 5. Local development
228
+
229
+ ```bash
230
+ npm run dev
231
+ # Dev server runs locally on port 8787 (local only).
232
+ # The public MCP endpoint is https://senado.sidneybissoli.com/mcp
233
+ ```
234
+
235
+ #### 6. Tests and typecheck
236
+
237
+ ```bash
238
+ npm test # run all tests once
239
+ npm run test:watch # watch mode
240
+ npm run typecheck # tsc --noEmit
241
+ ```
242
+
243
+ #### 7. Deploy
244
+
245
+ ```bash
246
+ npm run deploy
247
+ # Serves at https://senado.sidneybissoli.com (custom domain) and
248
+ # https://senado-br-mcp.sidneybissoli.workers.dev (workers.dev fallback)
249
+ ```
250
+
251
+ ## Endpoints
252
+
253
+ | Path | Methods | Description |
254
+ |------|---------|-------------|
255
+ | `/mcp` | POST, GET, DELETE, OPTIONS | MCP Streamable HTTP endpoint (managed by `createMcpHandler`) |
256
+ | `/health` | GET | Health check β€” returns `ok` (always public) |
257
+ | `/status` | GET | JSON: `status`, `version`, and last-deploy metadata (`deploy.id`/`tag`/`timestamp`) β€” liveness + current build, no MCP handshake needed (always public) |
258
+ | `/metrics` | GET | JSON counters: requests, tool calls, cache hits/misses, upstream calls/retries/errors, auth failures (always public) |
259
+
260
+ ## MCP Request Examples
261
+
262
+ All requests go to `POST /mcp` with JSON-RPC 2.0 format.
263
+
264
+ ### List available tools
265
+
266
+ ```json
267
+ {
268
+ "jsonrpc": "2.0",
269
+ "id": 1,
270
+ "method": "tools/list"
271
+ }
272
+ ```
273
+
274
+ ### Call a tool β€” List senators from SP
275
+
276
+ ```json
277
+ {
278
+ "jsonrpc": "2.0",
279
+ "id": 2,
280
+ "method": "tools/call",
281
+ "params": {
282
+ "name": "senado_listar_senadores",
283
+ "arguments": {
284
+ "uf": "SP",
285
+ "emExercicio": true
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+ ### Call a tool β€” Search bills by keyword
292
+
293
+ ```json
294
+ {
295
+ "jsonrpc": "2.0",
296
+ "id": 3,
297
+ "method": "tools/call",
298
+ "params": {
299
+ "name": "senado_buscar_materias",
300
+ "arguments": {
301
+ "palavraChave": "inteligΓͺncia artificial",
302
+ "tramitando": true
303
+ }
304
+ }
305
+ }
306
+ ```
307
+
308
+ ### Call a tool β€” Get recent plenary votes
309
+
310
+ ```json
311
+ {
312
+ "jsonrpc": "2.0",
313
+ "id": 4,
314
+ "method": "tools/call",
315
+ "params": {
316
+ "name": "senado_search_votacoes",
317
+ "arguments": {
318
+ "dias": 7
319
+ }
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### Call a tool β€” Most popular citizen ideas
325
+
326
+ ```json
327
+ {
328
+ "jsonrpc": "2.0",
329
+ "id": 5,
330
+ "method": "tools/call",
331
+ "params": {
332
+ "name": "senado_ecidadania_listar_ideias",
333
+ "arguments": {
334
+ "ordenarPor": "apoios",
335
+ "ordem": "desc",
336
+ "status": "aberta"
337
+ }
338
+ }
339
+ }
340
+ ```
341
+
342
+ ## Upstream API Endpoints
343
+
344
+ The server consumes two classes of upstream endpoints from the Senado API:
345
+
346
+ ### Legacy endpoints (`.json` suffix, PascalCase responses)
347
+
348
+ Used by Groups A, E, F, H, I, J, K, L, M, N. The `.json` suffix is appended automatically by `upstream.ts`. None of these is marked deprecated upstream.
349
+
350
+ | Upstream path | Used by |
351
+ |---------------|---------|
352
+ | `/senador/lista/atual` | `senado_listar_senadores` |
353
+ | `/senador/lista/legislatura/{legislatura}` | `senado_listar_senadores` (param `legislatura`) |
354
+ | `/senador/{codigo}` (+ `/mandatos`) | `senado_obter_senador` (biografia + mandatos via chamada extra) |
355
+ | `/senador/{codigo}/licencas`, `/comissoes`, `/cargos`, `/historicoAcademico`, `/filiacoes`, `/profissao` | `senado_senador_historico` (`tipo` enum) |
356
+ | `/senador/afastados` | `senado_senadores_afastados` |
357
+ | `/senador/{codigo}/apartes` | `senado_discursos_senador` (`tipo=apartes`) |
358
+ | `/comissao/lista/colegiados` | `senado_listar_comissoes` (+ sigla-to-code resolution) |
359
+ | `/comissao/{codigo}` | `senado_obter_comissao` (`secao=resumo`; numeric code, not sigla) |
360
+ | `/composicao/comissao/{codigo}` (+ `?ativas=S`) | `senado_obter_comissao` (`secao=membros`) |
361
+ | `/comissao/agenda/{data}` | `senado_agenda_comissoes` |
362
+ | `/comissao/agenda/{dataInicio}/{dataFim}` | `senado_reunioes_comissao` |
363
+ | `/comissao/reuniao/{codigoReuniao}` | `senado_reuniao_comissao` |
364
+ | `/comissao/cpi/{sigla}/requerimentos` | `senado_requerimentos_cpi` (upstream often empty even for active CPIs β€” empty result carries an `aviso`) |
365
+ | `/materia/distribuicao/autoria`, `/distribuicao/relatoria/{sigla}` | `senado_distribuicao_materias` |
366
+ | `/plenario/agenda/dia/{data}`, `/agenda/mes/{data}`, `/agenda/cn/...` | `senado_agenda_plenario` |
367
+ | `/plenario/resultado/{data}`, `/resultado/cn/{data}`, `/resultado/mes/{data}` | `senado_resultado_plenario` |
368
+ | `/plenario/resultado/veto/{codigo}` (+ `/materia/`, `/dispositivo/`) | `senado_resultado_veto` |
369
+ | `/plenario/votacao/orientacaoBancada/{data}` (+ perΓ­odo) | `senado_orientacao_bancada` |
370
+ | `/plenario/encontro/{codigo}` (+ `/pauta`, `/resultado`, `/resumo`) | `senado_encontro_plenario` |
371
+ | `/plenario/tiposSessao`, `/lista/tiposComparecimento`, `/lista/legislaturas` | `senado_tabelas_plenario` |
372
+ | `/materia/vetos/{ano}`, `/vetos/aposrcn`, `/vetos/antesrcn`, `/vetos/encerrados` | `senado_vetos` |
373
+ | `/taquigrafia/notas/{sessao\|reuniao}/{id}` | `senado_notas_taquigraficas` |
374
+ | `/taquigrafia/videos/{sessao\|reuniao}/{id}` | `senado_videos_taquigrafia` |
375
+ | `/senador/{codigo}/discursos` | `senado_discursos_senador` |
376
+ | `/plenario/lista/discursos/{dataInicio}/{dataFim}` | `senado_discursos_plenario` |
377
+ | `/discurso/texto-integral/{codigo}` | `senado_discurso_texto` (plain text, fetched directly) |
378
+ | `/senador/lista/tiposUsoPalavra` | `senado_tabelas_referencia` (`tabela=tipos-uso-palavra`) |
379
+ | `/composicao/lista/blocos` | `senado_listar_blocos` |
380
+ | `/composicao/bloco/{codigo}` | `senado_obter_bloco` |
381
+ | `/composicao/lideranca` | `senado_liderancas` |
382
+ | `/composicao/mesaSF` | `senado_mesa` (`casa=senado`) |
383
+ | `/composicao/mesaCN` | `senado_mesa` (`casa=congresso`) |
384
+ | `/orcamento/lista` | `senado_orcamento_parlamentar` (`tipo=emendas`) |
385
+ | `/orcamento/oficios` | `senado_orcamento_parlamentar` (`tipo=oficios`) |
386
+ | `/legislacao/lista` | `senado_buscar_legislacao` |
387
+ | `/legislacao/{codigo}` | `senado_obter_legislacao` |
388
+ | `/legislacao/tiposNorma` | `senado_tabelas_referencia` (`tabela=tipos-norma`) |
389
+ | `/votacaoComissao/comissao/{sigla}` | `senado_votacao_comissao` (`por=comissao`) |
390
+ | `/votacaoComissao/parlamentar/{codigo}` | `senado_votacao_comissao` (`por=senador`) |
391
+ | `/votacaoComissao/materia/{sigla}/{numero}/{ano}` | `senado_votacao_comissao` (`por=materia`) |
392
+ | `/autor/lista/atual` | `senado_autores_atuais` |
393
+
394
+ ### v3 endpoints (flat JSON arrays/objects, camelCase)
395
+
396
+ Used by Groups B, C, D. Dates must be in **ISO format** (`YYYY-MM-DD`) β€” tools accept `YYYYMMDD` and convert. The `codigoMateria` query param bridges legacy matΓ©ria codes to v3 processes.
397
+
398
+ | Upstream path | Used by |
399
+ |---------------|---------|
400
+ | `/votacao` | `senado_obter_votacao`, `senado_search_votacoes`, `senado_votos_materia`, `senado_votacoes_senador` |
401
+ | `/processo` | `senado_search_processos`, `senado_buscar_materias` |
402
+ | `/processo/{id}` | `senado_obter_processo`, `senado_obter_materia` (`secao=detalhe`/`tramitacao`) |
403
+ | `/processo/documento` | `senado_obter_materia` (`secao=textos`) |
404
+ | `/processo/emenda` | `senado_processo_detalhe` (`secao=emendas`) |
405
+ | `/processo/relatoria` | `senado_processo_detalhe` (`secao=relatorias`), `senado_obter_materia` (rapporteur) |
406
+ | `/processo/prazo` | `senado_processo_detalhe` (`secao=prazos`) |
407
+ | `/processo/{siglas,assuntos,classes,destinos,entes,tipos-*}` | `senado_tabelas_processo` (12 reference tables) |
408
+
409
+ ### Administrative API (adm.senado.gov.br/adm-dadosabertos, flat snake_case JSON)
410
+
411
+ Used by Groups O, P, Q, R via `admFetch` (no `.json` suffix; HTTP 404 treated as empty collection). Base URL configurable via `SENADO_ADM_BASE_URL`.
412
+
413
+ | Upstream path | Used by |
414
+ |---------------|---------|
415
+ | `/api/v1/senadores/despesas_ceaps/{ano}` | `senado_ceaps` (~10 MB/year, cached + aggregated in-Worker) |
416
+ | `/api/v1/senadores/{auxilio-moradia,escritorios,aposentados}` | `senado_senadores_admin` (`tipo` enum) |
417
+ | `/api/v1/servidores/servidores/{ativos,efetivos,comissionados,inativos}` | `senado_servidores` |
418
+ | `/api/v1/servidores/remuneracoes/{ano}/{mes}` | `senado_remuneracoes_servidores` (~5.5 MB/month) |
419
+ | `/api/v1/servidores/horas-extras/{ano}/{mes}` | `senado_horas_extras` |
420
+ | `/api/v1/servidores/quantitativos/*`, `/previsao-aposentadoria`, `/api/v1/senadores/quantitativos/senadores` | `senado_pessoal_tabelas` (quantitativos) |
421
+ | `/api/v1/servidores/{estagiarios,pensionistas,lotacoes,cargos}` | `senado_pessoal_tabelas` (listas nominais) |
422
+ | `/api/v1/contratacoes/contratos` (+ `/{id}/aditivos`) | `senado_contratos`, `senado_contratacao_detalhe` |
423
+ | `/api/v1/contratacoes/{tipo}/{id}/{itens,pagamentos,garantias}` | `senado_contratacao_detalhe` |
424
+ | `/api/v1/contratacoes/licitacoes` | `senado_licitacoes` |
425
+ | `/api/v1/contratacoes/terceirizados` | `senado_terceirizados` |
426
+ | `/api/v1/contratacoes/empresas` | `senado_empresas_contratadas` (~13 MB, requires filter) |
427
+ | `/api/v1/contratacoes/{atas_registro_preco,notas_empenho,menores_aprendizes}` | `senado_contratacoes_lista` |
428
+ | `/api/v1/supridos/{ano}` (+ atosConcessao, empenhos, movimentacoes, transacoes) | `senado_suprimento_fundos` |
429
+ | `senado.gov.br/bi-arqs/Arquimedes/Financeiro/{Despesa,Receitas}SenadoDadosAbertos.json` | `senado_execucao_orcamentaria` (daily JSON feeds, Brazilian decimal strings normalized) |
430
+
431
+ ### e-Cidadania (D1-backed, Cron-refreshed)
432
+
433
+ The e-Cidadania **list** data is persisted in a **D1 database** (`ecidadania_current/_history/_scrape_runs`, discriminated by `entidade`; plus `ecidadania_comentarios` for the audiΓͺncia comment level and `ecidadania_detalhe_cursor` for the resumable detail backfill β€” added in schema v2) and read from there instead of being scraped on every call. **Three cadences** write into it:
434
+
435
+ - a **daily off-Worker GitHub Action** owns the **full corpus** of the three live entities (`consultas`, `eventos`, `ideias`; see below) β€” the source of truth. Daily (not weekly) because the first-seen series `MIN(scraped_at)` is the only measurable entry-rhythm signal and every skipped day permanently shortens it (ROADMAP Etapa 2, decisΓ£o D3);
436
+ - a **weekly integrity-check Action** (`.github/workflows/verify-consultas-votos.yml`) for the frozen `consultas_votos` acervo β€” it re-hashes the CSV and, if unchanged, records an `ok` run row **without** re-applying the ~15k upserts; if it diverges from the registered vintage the job fails to alert (see below);
437
+ - an in-Worker **Cron Trigger** (`0 */2 * * *`, `src/scraper/pipeline.ts β†’ refreshEcidadania`) does only a **targeted metric splice** of the ~5 REST highlights per live entity (`restcolecaomaismateria/ideia/audiencia` β€” votos/comentΓ‘rios/apoios), recorded as `ok-metrica` so it never re-breaks the corpus baseline and never touches the long tail. In v2 the eventos splice **preserves** the corpus's canonical comment count (the daily crawl is the source of truth for `comentarios`, so the splice can't ping-pong it against the degraded REST count).
438
+
439
+ Both writers build payloads through the canonical `buildXResumo` builders + shared `contentHash`, so their rows are byte-identical. Each write:
440
+
441
+ - **upserts** `ecidadania_current` (one row per item β€” what the tools read),
442
+ - **appends** `ecidadania_history` only when an item's `content_hash` changes (time-series-ready),
443
+ - records each run in `ecidadania_scrape_runs`.
444
+
445
+ An **anomaly guard** (`src/scraper/anomaly.ts`, `classifyRun`) ensures a failed or anomalous corpus run (zero rows, or fewer than `ECIDADANIA_CORPUS_MIN_PCT`% of the last good run) **never overwrites** the last good state.
446
+
447
+ The **list / analysis tools** (`listar_*`, `consultas_analise`, `sugerir_tema_enquete`, `consultas_votos`) read from D1 via `resolveList` (`src/scraper/store.ts`): D1-first. Because every entity is now a full corpus, a stale corpus is served from D1 **flagged** (`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 uses `ECIDADANIA_CORPUS_STALE_MAX_MIN` (~10 days). Every list response carries an additive `meta` (`fonte`, `lastScrapedAt`, `possivelDesatualizacao`) so callers always see the data's real age and never get stale data silently.
448
+
449
+ The **detail tools** (`obter_*`) stay **live** (HTML scraped with CSS-class-targeted regex) for freshness, and write their richer payload through to `ecidadania_detalhe` fire-and-forget (deduped by `content_hash`), so detail history accrues without adding latency to the response.
450
+
451
+ #### Full-corpus ingestion (off-Worker)
452
+
453
+ The three live e-Cidadania corpora are owned by the **daily** Action (`.github/workflows/ingest-ecidadania.yml`), each with its own `scripts/ingest-ecidadania/index-*.ts` orchestrator emitting batched `out-*.sql` the apply step bulk-loads:
454
+
455
+ - **`consultas`** β€” open consultations (detailed below). In v2 each crawled matter is also **enriched from its detail page** (`visualizacaomateria`) for `autoria`/`relator`; those are immutable, so only rows not yet enriched are fetched.
456
+ - **`eventos`** β€” audiΓͺncias/eventos from the `principalaudiencia?p=N` HTML listing; status comes straight from the listing block (no `/processo` bridge). In v2 every event is **enriched from its detail page** (canonical `data`/`hora` + `comissaoNomeCompleto`/`local`/`descricao`/`pauta`/`convidados`/`videoUrl`) and its **AJAX comment fragment** (canonical count + one `ecidadania_comentarios` row per comment, diffed against the stored hashes and emitted as `out-eventos-comentarios-*.sql`).
457
+ - **`ideias`** β€” ideias legislativas (~113.7k) from `pesquisaideia?situacao=N&p=M`, crawled **per `situacao` bucket** (the listing has no inline status) and emitted in ~10k-statement batches. In v2 the listing crawl **preserves** the immutable detail fields, and a **separate resumable backfill** (`index-ideias-detalhe.ts`, run via `ingest:ecidadania:ideias-detalhe`) fills them a chunk per run β€” because ~113.7k detail fetches don't fit one Action, it persists a cursor in `ecidadania_detalhe_cursor` and wraps around at the end.
458
+
459
+ The fourth entity, **`consultas_votos`**, is a separate **historical** acervo of votes-by-UF parsed from the ~33 MB Arquimedes CSV (`ProposiΓ§Γ΅es-com-votos.csv`), aggregated to one record per matΓ©ria with a `votosPorUf` breakdown. The CSV's "dados atualizados atΓ©" stamp becomes the provenance `reference_period`; it is excluded from the row hash (`consultaVotoCore`) so a stamp bump on these frozen votes doesn't churn `_history`. `STATUS ATUAL` is uniformly "Descontinuado", hence archival, not a migration of the open consultations. Served by `senado_ecidadania_consultas_votos` with provenance pointing at the CSV (`ECIDADANIA_ARQUIMEDES`). Because it is a **frozen single-vintage acervo** (no time series β€” ROADMAP Etapa 2, decisΓ£o D1), it is **excluded from the daily job** and instead gets a **weekly integrity-check** Action (`.github/workflows/verify-consultas-votos.yml`, `INGEST_CONSULTAS_VOTOS_VERIFY=1`): it downloads + re-hashes the CSV against the registered vintage; if identical it records an `ok` run row and re-applies nothing; if it diverges (any matΓ©ria hash changed, or the count moved) it records an `anomalo` run row and the job **fails**, so the "acervo congelado" premise is never silently overwritten β€” re-ingesting a legitimately new vintage is an explicit `force` dispatch (`INGEST_FORCE=1`).
460
+
461
+ The `consultas` job is the reference implementation:
462
+
463
+ `consultas` covers the **full set of OPEN consultations** β€” every matter currently in tramitaΓ§Γ£o (~7.7k), not just the ~5 highlights. Confirmed on the first run: the `pesquisamateria` listing is **in-tramitaΓ§Γ£o-only**, so closed/historical consultations are **not** captured by this source (a pre-ingestion historical backfill is out of scope). Three settled design decisions:
464
+
465
+ 1. **Decoupled ingestion.** The open set is acquired by an **off-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 and **bulk-loads D1**; the Worker only reads. The brittle, long crawl is kept out of the request/Cron path.
466
+ 2. **Status from `/processo`, not HTML.** A consultation runs from presentation until the end of tramitação, so `status` is a function of the matter: **aberta ⟺ the `codigoMateria` is in the `/processo` `tramitando=S` set**, derived from robust JSON (never scraped). Every consultation enters as `aberta` (the listing only yields in-tramitação matters); on each **complete** run the job re-derives status for **all stored rows** by `/processo` membership (not by listing-absence, which can be transient), so a consultation whose matter leaves tramitação flips to `encerrada`. The `encerrada`/`todas` sets therefore grow over time; consultations that closed **before** the first ingestion aren't captured (out of scope). The list/analysis tools default to `status: aberta`.
467
+ 3. **Two reconciled cadences (one shared writer contract).** The job **reuses** `contentHash` + the `ConsultaResumo` builder + `classifyRun` from `src/scraper/`, so its rows are byte-identical to the Cron's. The daily job owns the long tail; the **2h Cron** keeps the ~5 hot/open highlights fresh via a *targeted metric splice* (recorded as `ok-metrica`, bypassing the corpus `classifyRun` baseline). Corpus freshness (`possivelDesatualizacao`) is computed from the last `status='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.
468
+
469
+ Write guards on the load: an **incomplete crawl** (any page failed) or an incomplete `/processo` status universe writes only an `erro` run row; even a complete crawl is rejected by a **catastrophic floor** (`ECIDADANIA_CORPUS_MIN_PCT`, default 80% of the last good corpus) to guard against a degraded page β€” overridable with `--force` / `INGEST_FORCE=1` for a legitimate large shrink. Run daily via the Action, or manually:
470
+
471
+ ```bash
472
+ CLOUDFLARE_API_TOKEN=… npm run ingest:ecidadania # writes scripts/ingest-ecidadania/out.sql
473
+ npx wrangler d1 execute senado-ecidadania --remote --file=scripts/ingest-ecidadania/out.sql
474
+ ```
475
+
476
+ ## Caching
477
+
478
+ ### Layer architecture
479
+
480
+ | Layer | Storage | Scope | TTL range | Purpose |
481
+ |-------|---------|-------|-----------|---------|
482
+ | **L0** | In-memory `Map` | Per-isolate | 30-300s | Ultra-fast, eliminates redundant requests within a Worker isolate |
483
+ | **L1** | Cloudflare Cache API (`caches.default`) | Per-colo (PoP) | 60-600s | Shared across requests at the same edge location |
484
+ | **L2** | KV (optional) | Global | Variable | Reserved for rare, low-write data |
485
+
486
+ ### Cache categories
487
+
488
+ | Category | L0 TTL | L1 TTL | Used for |
489
+ |----------|--------|--------|----------|
490
+ | **STATIC** | 300s | 600s | Legislation types, static reference |
491
+ | **SEMI_STATIC** | 120s | 300s | Party list, UF list, committee details |
492
+ | **DYNAMIC** | 30s | 60s | Agendas, recent votes, meeting lists |
493
+ | **ON_DEMAND** | 30s | 120s | Specific bill/senator/vote lookups |
494
+
495
+ ### POST caching approach
496
+
497
+ MCP uses POST for all `tools/call` requests. Caching POST responses is not natively supported by the Cache API, which requires GET requests. The solution:
498
+
499
+ 1. **Hash parameters** β€” Tool name + sorted parameters are hashed with SHA-256
500
+ 2. **Synthetic GET key** β€” A synthetic URL `https://senado-br-mcp.internal/__cache/{tool}/{hash}` is constructed
501
+ 3. **Cache API match/put** β€” The synthetic GET URL is used with `caches.default.match()` and `caches.default.put()`, allowing standard Cache API operations on POST-originated data
502
+
503
+ This caching happens at the **tool level** (inside each tool's callback), not at the MCP transport level.
504
+
505
+ ## Provenance
506
+
507
+ **Every tool** attaches a **provenance envelope** so 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). The envelope lives in `structuredContent.provenance` (parseable by clients, validated by the tool's output schema) and is mirrored as a one-line source footer in the text content for clients that only render text β€” the data JSON itself is **not** duplicated with the envelope, to keep the per-response token cost low (a fixed β‰ˆ170 chars).
508
+
509
+ Coverage spans all four upstream sources, each with its own `source`/`citation`/`license` (in `src/utils/provenance.ts`):
510
+
511
+ - **Senado Federal β€” Dados Abertos (Legislativo)** β€” `legis.senado.leg.br/dadosabertos`
512
+ - **Senado Federal β€” Dados Abertos (Administrativo)** β€” `adm.senado.gov.br/adm-dadosabertos`
513
+ - **Senado Federal β€” ExecuΓ§Γ£o OrΓ§amentΓ‘ria e Financeira** β€” Arquimedes/Financeiro feed at `senado.gov.br`
514
+ - **Senado Federal β€” Portal e-Cidadania** β€” `www12.senado.leg.br/ecidadania`
515
+
516
+ Fields (per response β€” one tool, one source):
517
+
518
+ | Field | Meaning |
519
+ |-------|---------|
520
+ | `source` | Official source name (e.g. *Senado Federal β€” Dados Abertos (Legislativo)*) |
521
+ | `source_url` | Canonical endpoint/item URL consulted (e.g. `…/processo/{id}`) |
522
+ | `dataset_id` | Item/series identifier (e.g. `codigoMateria=137808`) |
523
+ | `reference_period` | Vintage/competΓͺncia of the data (e.g. `2024-03-15`, `2019`) |
524
+ | `retrieved_at` | ISO-8601 of the **upstream extraction** β€” carried through the cache, so it reflects when the data was actually fetched, not the build or the cache-hit time |
525
+ | `citation` | Ready-to-use citation string (human-readable) |
526
+ | `license` | Source terms (Dados Abertos do Senado Federal) |
527
+ | `field_sources` | *(optional)* per-field provenance β€” present only when a single response merges fields from more than one upstream endpoint (see below) |
528
+
529
+ In addition to the `provenance` envelope, `structuredContent` carries a top-level **`attribution`** list β€” the distinct source URLs behind the response. This mirrors the naming proposed in [modelcontextprotocol#711](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/711) (where `attribution` is a list of source references at the response level), so the server stays forward-compatible if that RFC lands; the richer `provenance` object remains this server's own extension.
530
+
531
+ **Field-level granularity.** Most tools are single-source, so one envelope suffices. The few that merge slices in one response fill `provenance.field_sources` β€” a list of `{ fields, source_url, retrieved_at, … }` attributing specific output fields to their real origin. Example: `senado_obter_materia` `secao=detalhe` fuses `/processo/{id}` (the top-level source) with the `ementa` from `/processo` and the `relator` from `/processo/relatoria`, each carrying its own `retrieved_at`.
532
+
533
+ `retrieved_at` fidelity is provided by the cache layer (`cachedFetchWithMeta`), which persists the fetch timestamp alongside the value, so it reflects the real upstream extraction even on a cache hit. Two exceptions report an honest live timestamp instead: the e-Cidadania **list** tools (read from D1) use the corpus's `lastScrapedAt` β€” the true age of the stored data β€” while e-Cidadania **detail** tools, scraped live, use the fetch time and a level-3 canonical item URL. The only path that falls back to the build-time default is the in-code static reference catalog (`senado_tabelas_referencia` `tipos-materia`), which has no upstream extraction instant.
534
+
535
+ Coverage is **universal**: all 66 tools carry the envelope (verify with `grep -c 'resultWithProvenance(' src/tools/*.ts`). The **βŠ•** marks in the inventory below denote the original pilot tools (votes, bills, processes); the envelope now extends to every tool, so the marks are historical.
536
+
537
+ ## Citable dataset (e-Cidadania participation)
538
+
539
+ Beyond the live server, this project publishes a **frozen, versioned, citable dataset** of the e-Cidadania participation layer (public consultations, legislative ideas, interactive events + their comments, historical votes by state) β€” the layer the R package `congressbr` never covered. Each value carries a per-field provenance envelope (`{ value, sourceEndpoint, sourceField, retrievedAt, license, schemaVersion }`); the data license (Dados Abertos do Senado Federal) is kept **separate** from the code license (MIT).
540
+
541
+ - **How to cite** β€” [`CITATION.cff`](CITATION.cff) (dataset; cite the version-DOI of the snapshot you used, the concept-DOI for the dataset across versions).
542
+ - **What's in each release** β€” [`CHANGELOG-dataset.md`](CHANGELOG-dataset.md) (cumulative, append-only; binds each release to its `schemaVersion`).
543
+ - **Variable dictionary & field provenance** β€” [`docs/dataset-dictionary.md`](docs/dataset-dictionary.md) (generated from `src/dataset/schema.ts`, the single source of truth).
544
+ - **Data license** β€” [`LICENSE-DATA.md`](LICENSE-DATA.md).
545
+ - **Cutting a release** (freeze β†’ checksums β†’ GitHub Release β†’ Zenodo DOI) β€” [`docs/release-runbook.md`](docs/release-runbook.md); machinery in `src/dataset/`, `scripts/build-dataset/`, and `.github/workflows/release-dataset.yml`.
546
+
547
+ ### Inventory β€” schema v2 (`schemaVersion` `2.0.0`)
548
+
549
+ Each release ships one NDJSON resource per entity (one `HarmonizedRecord` per line: identity + a provenance envelope per field), plus a `datapackage.json` manifest and a copy of the dictionary. Five resources:
550
+
551
+ | Resource (`*.ndjson`) | Grain | Key variables | Source(s) |
552
+ |---|---|---|---|
553
+ | `consultas` | 1 public consultation (matéria) | `materia`, `ementa`, `votosSim`/`votosNao`/`totalVotos`, `percentual*`, **`autoria`ⁿ**, **`relator`ⁿ**, `status`, `url`, `firstSeenAt` | `pesquisamateria` listing + **detail** (`visualizacaomateria`)ⁿ + `/processo?tramitando=S` for status |
554
+ | `ideias` | 1 legislative idea (~113.7k) | `titulo`, `apoios`, `status`, **`dataPublicacao`ⁿ**, **`autorUf`ⁿ**, **`descricao`ⁿ**, **`plConvertido`ⁿ**, `url`, `firstSeenAt` | `pesquisaideia` listing + **detail** (`visualizacaoideia`) via a resumable backfillⁿ |
555
+ | `eventos` | 1 interactive event (audiΓͺncia) | `titulo`, **`data`ᢜ**, **`hora`ᢜ**, `comissao`, **`comissaoNomeCompleto`ⁿ**, **`local`ⁿ**, **`descricao`ⁿ**, **`pauta`ⁿ**, **`convidados`ⁿ**, **`videoUrl`ⁿ**, **`comentarios`ᢜ**, `status`, `url`, `firstSeenAt` | `principalaudiencia` listing + **detail** (`visualizacaoaudiencia`)ⁿ + **AJAX comment fragment**ᢜ |
556
+ | **`eventos_comentarios`ⁿ** | 1 comment (comment-level) | `eventoId`, `comentarioId`, `uf`, `texto`, `data`, `hora`, `momentoVideoUrl`, `convidadoAssociado` | AJAX fragment `ajaxcolecaocomentarioaudiencia?audienciaId=` |
557
+ | `consultas_votos` | 1 matΓ©ria (historical acervo) | `materia`, `ementa`, `autoria`, `votosSim`/`votosNao`/`totalVotos`, `votosPorUf`, `status`, `url`, `referencePeriod` | Arquimedes CSV `ProposiΓ§Γ΅es-com-votos.csv` (single vintage) |
558
+
559
+ **ⁿ = new/reopened in v2 Β· ᢜ = source corrected to the canonical one in v2.** What v2 (`2.0.0`) changed β€” the ingestion moved from **listing-only** to **listing + detail (+ AJAX comments for events)**:
560
+
561
+ - **Eventos corrected & enriched.** `data`/`hora` now come from the detail page (canonical β€” the [estudo A3](docs/estudo-a3-reconciliacao-eventos.md) found the listing 57% divergent on `hora`), plus six new detail fields (`comissaoNomeCompleto`, `local`, `descricao`, `pauta`, `convidados`, `videoUrl`). `comentarios` is now the **canonical AJAX count** (the listing count was `0`-spurious in 82% of events, capturing only ~6.7% of engagement).
562
+ - **New comment-level resource** `eventos_comentarios` β€” one row per audiΓͺncia comment, the participation signal nobody else publishes versioned.
563
+ - **Detail-only fields reopened** for `ideias` (`dataPublicacao`, `autorUf`, `descricao`, `plConvertido`) and `consultas` (`autoria`, `relator`) β€” previously always `null` by design.
564
+ - **Privacy posture by data origin.** Citizen content (audiΓͺncia comments, idea authors) keeps **UF only β€” never the name**, discarded at the parser; public agents (consulta authorship/rapporteur, event guests) keep the name (public by function). See `docs/schema-v2-inventario.md` for the field-by-field rationale (approved target).
565
+
566
+ The frozen NDJSON is **not** committed (built from the sovereign D1 corpus on demand); a tagged `dataset-v*` release attaches the tarball + `SHA256SUMS` + `release.json` and archives them on Zenodo.
567
+
568
+ ## Tool Inventory
569
+
570
+ ### Group H β€” Reference/Metadata (1 tool)
571
+
572
+ | Tool | Description |
573
+ |------|-------------|
574
+ | `senado_tabelas_referencia` | Tabelas de referΓͺncia via `tabela` enum: tipos-materia, partidos, ufs, legislatura-atual, tipos-norma, tipos-uso-palavra |
575
+
576
+ ### Group A β€” Senators (5 tools)
577
+
578
+ | Tool | Description |
579
+ |------|-------------|
580
+ | `senado_listar_senadores` | Lista senadores em exercΓ­cio/por legislatura, com filtros `nome` (busca parcial sem acento), `uf` e `partido` |
581
+ | `senado_obter_senador` | Detalhe biogrΓ‘fico de um senador: bio, mandatos, partido, contato |
582
+ | `senado_votacoes_senador` βŠ• | Como um senador votou em cada matΓ©ria (via v3 `/votacao`) |
583
+ | `senado_senador_historico` | HistΓ³rico funcional via `tipo` enum: licencas, comissoes, cargos, historico-academico, filiacoes, profissoes |
584
+ | `senado_senadores_afastados` | Senadores atualmente afastados (fora de exercΓ­cio) |
585
+
586
+ ### Group B β€” Bills/Matters (2 tools, v3 backend)
587
+
588
+ | Tool | Description |
589
+ |------|-------------|
590
+ | `senado_buscar_materias` βŠ• | Busca matΓ©rias por tipo, nΓΊmero, ano, palavra-chave, autor ou tramitaΓ§Γ£o (via v3 `/processo`) |
591
+ | `senado_obter_materia` βŠ• | Dados de uma matΓ©ria via `secao` enum: detalhe (situaΓ§Γ£o/relator), tramitacao (histΓ³rico) ou textos (documentos) |
592
+
593
+ ### Group C β€” Processes (5 tools)
594
+
595
+ | Tool | Description |
596
+ |------|-------------|
597
+ | `senado_search_processos` βŠ• | Busca processos legislativos (complementar Γ  busca de matΓ©rias) |
598
+ | `senado_obter_processo` βŠ• | Detalhes completos de um processo legislativo especΓ­fico |
599
+ | `senado_processo_detalhe` | Aspecto de um processo via `secao` enum: emendas, relatorias ou prazos |
600
+ | `senado_autores_atuais` | Parlamentares autores de processos em tramitaΓ§Γ£o, ordenados por produΓ§Γ£o |
601
+ | `senado_tabelas_processo` | 12 tabelas de referΓͺncia (siglas, assuntos, classes, tipos-*) via `tabela` enum |
602
+
603
+ ### Group D β€” Votes (3 tools)
604
+
605
+ | Tool | Description |
606
+ |------|-------------|
607
+ | `senado_obter_votacao` βŠ• | Detalhes de uma votaΓ§Γ£o com votos nominais. Aceita `codigoVotacao` (codigoSessao da sessΓ£o plenΓ‘ria). |
608
+ | `senado_votos_materia` βŠ• | VotaΓ§Γ΅es de uma matΓ©ria (via v3 `/votacao?codigoMateria`), com votos nominais opcionais |
609
+ | `senado_search_votacoes` βŠ• | Busca/listagem flexΓ­vel de votaΓ§Γ΅es do plenΓ‘rio por `dias`, perΓ­odo, processo, matΓ©ria ou senador |
610
+
611
+ ### Group E β€” Committees (7 tools)
612
+
613
+ | Tool | Description |
614
+ |------|-------------|
615
+ | `senado_listar_comissoes` | Lista comissΓ΅es (colegiados) ativas, filtrΓ‘veis por tipo |
616
+ | `senado_obter_comissao` | Dados de uma comissΓ£o via `secao` enum: resumo (mesa/totais) ou membros (composiΓ§Γ£o). Resolve sigla para cΓ³digo internamente. |
617
+ | `senado_reunioes_comissao` | ReuniΓ΅es de uma comissΓ£o num perΓ­odo (lida com intervalos entre anos) |
618
+ | `senado_agenda_comissoes` | Agenda de reuniΓ΅es de todas as comissΓ΅es numa data |
619
+ | `senado_reuniao_comissao` | Detalhe completo de uma reuniΓ£o: partes, itens, convidados, resultados, links pauta/ata |
620
+ | `senado_requerimentos_cpi` | Requerimentos protocolados numa CPI em atividade, paginados (upstream costuma vir vazio mesmo para CPIs ativas; retorno vazio traz `aviso`) |
621
+ | `senado_distribuicao_materias` | EstatΓ­sticas de carga por senador numa comissΓ£o: autoria ou relatoria |
622
+
623
+ ### Group F β€” Plenary (7 tools)
624
+
625
+ | Tool | Description |
626
+ |------|-------------|
627
+ | `senado_agenda_plenario` | Plenary schedule β€” by day, month or Congress (escopo dia/mes/cn) |
628
+ | `senado_resultado_plenario` | Session results: items deliberated, opinions, outcomes (SF/CN/month) |
629
+ | `senado_orientacao_bancada` | Party leadership voting instructions per vote, with tallies |
630
+ | `senado_vetos` | Presidential vetoes by year or tramitation status |
631
+ | `senado_resultado_veto` | Nominal veto vote results (by veto, vetoed bill or device) |
632
+ | `senado_encontro_plenario` | Legislative session detail, agenda items, results or summary |
633
+ | `senado_tabelas_plenario` | Session types, attendance types, legislatures list |
634
+
635
+ ### Group G β€” e-Cidadania (9 tools)
636
+
637
+ | Tool | Description |
638
+ |------|-------------|
639
+ | `senado_ecidadania_listar_consultas` | Consultas pΓΊblicas (conjunto completo das **abertas** β€” matΓ©rias em tramitaΓ§Γ£o) com votaΓ§Γ£o sim/nΓ£o; filtro `status` (padrΓ£o `aberta`) |
640
+ | `senado_ecidadania_obter_consulta` | Detalhe de uma consulta: votos, autor, relator, comentΓ‘rios |
641
+ | `senado_ecidadania_consultas_analise` | Analisa o conjunto completo de consultas **abertas** via `modo` (consenso/polarizada); `status` padrΓ£o `aberta` |
642
+ | `senado_ecidadania_listar_ideias` | Ideias legislativas de cidadΓ£os; ranking das mais apoiadas via `ordenarPor: apoios` |
643
+ | `senado_ecidadania_obter_ideia` | Detalhe de uma ideia: texto, apoios, status de conversΓ£o em projeto |
644
+ | `senado_ecidadania_listar_eventos` | Eventos interativos (audiΓͺncias, sabatinas, lives); ranking dos mais comentados via `ordenarPor` |
645
+ | `senado_ecidadania_obter_evento` | Detalhe de um evento: pauta, convidados, link de vΓ­deo |
646
+ | `senado_ecidadania_sugerir_tema_enquete` | Sugere temas para enquete mensal a partir de critΓ©rios configurΓ‘veis |
647
+ | `senado_ecidadania_consultas_votos` | Acervo **histΓ³rico** de votos das consultas com quebra **por UF** (CSV Arquimedes); ranking por `total`/`sim`/`nao`, filtro `uf`/`materia` |
648
+
649
+ ### Group I β€” Speeches (3 tools)
650
+
651
+ | Tool | Description |
652
+ |------|-------------|
653
+ | `senado_discursos_senador` | Pronunciamentos de um senador via `tipo` enum: discursos (prΓ³prios) ou apartes (intervenΓ§Γ΅es) |
654
+ | `senado_discursos_plenario` | Todos os discursos em plenΓ‘rio num intervalo de datas |
655
+ | `senado_discurso_texto` | Texto integral de um pronunciamento/discurso especΓ­fico |
656
+
657
+ ### Group J β€” Blocs & Leadership (4 tools)
658
+
659
+ | Tool | Description |
660
+ |------|-------------|
661
+ | `senado_listar_blocos` | Blocos parlamentares do Senado e seus partidos membros |
662
+ | `senado_obter_bloco` | Detalhes de um bloco parlamentar especΓ­fico |
663
+ | `senado_liderancas` | LideranΓ§as do Senado/Congresso (lΓ­deres, vice-lΓ­deres), filtrΓ‘veis |
664
+ | `senado_mesa` | Membros da Mesa Diretora via `casa` enum: senado (Mesa do SF) ou congresso (Mesa do CN) |
665
+
666
+ ### Group K β€” Budget (1 tool)
667
+
668
+ | Tool | Description |
669
+ |------|-------------|
670
+ | `senado_orcamento_parlamentar` | Emendas parlamentares ao orΓ§amento via `tipo` enum: emendas (lotes por autor) ou oficios (indicaΓ§Γ£o de destino β€” filtrΓ‘vel por `ano` da emenda, paginado, `incluirEmendas` opcional) |
671
+
672
+ ### Group L β€” Federal Law (2 tools)
673
+
674
+ | Tool | Description |
675
+ |------|-------------|
676
+ | `senado_buscar_legislacao` | Busca normas jurΓ­dicas federais por tipo, nΓΊmero, ano ou data (ao menos um obrigatΓ³rio) |
677
+ | `senado_obter_legislacao` | Detalhes de uma norma jurΓ­dica federal especΓ­fica |
678
+
679
+ ### Group M β€” Committee Voting (1 tool)
680
+
681
+ | Tool | Description |
682
+ |------|-------------|
683
+ | `senado_votacao_comissao` | VotaΓ§Γ΅es em comissΓ΅es via `por` enum: comissao, senador ou materia; perΓ­odo opcional |
684
+
685
+ ### Group N β€” Taquigrafia (2 tools)
686
+
687
+ | Tool | Description |
688
+ |------|-------------|
689
+ | `senado_notas_taquigraficas` | Official transcripts of plenary sessions or committee meetings β€” summary mode with excerpts, full-text mode paginated in blocks, speaker filter |
690
+ | `senado_videos_taquigrafia` | Video/audio units per session or meeting, with speaker and media links |
691
+
692
+ ### Group O β€” Senadores/Administrativo (2 tools)
693
+
694
+ | Tool | Description |
695
+ |------|-------------|
696
+ | `senado_ceaps` | CEAPS parliamentary quota expenses by year β€” aggregated by senator, expense type, month or supplier, or itemized detail; `estatisticas=true` returns whole-set distribution stats (min/max/mean/median/percentiles) + top/bottom ranking, or a group ranking by total spend via `agruparPor` (senator/type/month/supplier) with `topN`; filters by senator/month/type/supplier |
697
+ | `senado_senadores_admin` | Dados administrativos dos senadores via `tipo` enum: auxilio-moradia, escritorios-apoio ou aposentados |
698
+
699
+ ### Group P β€” Servidores / GestΓ£o de Pessoas (4 tools)
700
+
701
+ | Tool | Description |
702
+ |------|-------------|
703
+ | `senado_servidores` | Civil servants by status (active/effective/commissioned/inactive), filterable by name, unit, position |
704
+ | `senado_remuneracoes_servidores` | Monthly payroll β€” summary by payroll type or per-person composition with computed gross; `estatisticas=true` returns whole-payroll stats (min/max/mean/median/percentiles) + top/bottom ranking, with `campo`, `consolidarPorServidor`, `agruparPor` and `topN` |
705
+ | `senado_horas_extras` | Overtime payments by month with totals; `estatisticas=true` returns whole-set distribution stats (min/max/mean/median/percentiles) + top/bottom ranking, or a per-servant ranking by summed value via `agruparPor` (name/competΓͺncia), with `topN` |
706
+ | `senado_pessoal_tabelas` | Tabelas de pessoal via `tabela` enum: quantitativos (pessoal, cargos-funcoes, previsao-aposentadoria, senadores) e listas (estagiarios, pensionistas, lotacoes, cargos) |
707
+
708
+ ### Group Q β€” ContrataΓ§Γ΅es (6 tools)
709
+
710
+ | Tool | Description |
711
+ |------|-------------|
712
+ | `senado_contratos` | Contracts filtered in-Worker over the full base (accent-insensitive): supplier, CNPJ, year, number, object, labor |
713
+ | `senado_contratacao_detalhe` | Items, payments, guarantees, amendments or activations of a contract/ata/empenho |
714
+ | `senado_licitacoes` | Biddings by number or object text |
715
+ | `senado_terceirizados` | Outsourced collaborators by name, company or unit |
716
+ | `senado_empresas_contratadas` | Companies contracting with the Senate (requires name/CNPJ filter) |
717
+ | `senado_contratacoes_lista` | Price-registration atas, commitment notes, young apprentices |
718
+
719
+ ### Group R β€” Suprimento de Fundos (1 tool)
720
+
721
+ | Tool | Description |
722
+ |------|-------------|
723
+ | `senado_suprimento_fundos` | Petty-cash advances by year: recipients, concession acts, commitments, movements, card transactions; `estatisticas=true` (tipo transacoes/empenhos/atos-concessao) returns whole-set distribution stats (min/max/mean/median/percentiles) + top/bottom ranking, or a ranking by summed value via `agruparPor` (e.g. supplier), with contextual `campo` and `topN` |
724
+
725
+ ### Group S β€” OrΓ§amento do Senado (1 tool)
726
+
727
+ | Tool | Description |
728
+ |------|-------------|
729
+ | `senado_execucao_orcamentaria` | Budget execution since 2013 (allocation, committed/settled/paid) and own revenues since 2012 (forecast vs collected) β€” aggregated by year, action, expense group, source or revenue origin; `estatisticas=true` returns whole-set distribution stats (min/max/mean/median/percentiles) + top/bottom ranking, or a group ranking by summed `campo` via `agruparPor`, with `campo` (default paid / collected) and `topN` |
730
+
731
+ **Total: 66 tools**
732
+
733
+ ### Prompts (4)
734
+
735
+ Reusable pt-BR workflow templates (MCP `prompts` capability), defined in `src/prompts.ts`:
736
+
737
+ | Prompt | Args | What it guides |
738
+ | --- | --- | --- |
739
+ | `senado_gastos_senador` | `senador`, `ano` | Resolve o senador e agrega/detalha despesas CEAPS. |
740
+ | `senado_tramitacao_materia` | `sigla`, `numero`, `ano` | ObtΓ©m situaΓ§Γ£o atual + histΓ³rico de tramitaΓ§Γ£o da matΓ©ria. |
741
+ | `senado_votos_senador` | `senador`, `periodo?` | Lista os votos nominais do senador no perΓ­odo. |
742
+ | `senado_panorama_ecidadania` | β€” | Consolida consultas (consenso/polarizaΓ§Γ£o), ideias e eventos populares. |
743
+
744
+ ### Resources (5)
745
+
746
+ Static context documents/tables (MCP `resources` capability), defined in `src/resources.ts`:
747
+
748
+ | URI | Type | Content |
749
+ | --- | --- | --- |
750
+ | `senado://guia` | markdown | VisΓ£o geral e qual ferramenta usar por objetivo. |
751
+ | `senado://catalogo` | markdown | As 66 ferramentas agrupadas por domΓ­nio. |
752
+ | `senado://glossario` | markdown | Siglas e termos do Senado (PEC, CEAPS, CCJ, RCN…). |
753
+ | `senado://tabelas/tipos-materia` | json | Tipos de proposiΓ§Γ£o (sigla/nome/descriΓ§Γ£o). |
754
+ | `senado://tabelas/ufs` | json | As 27 unidades federativas. |
755
+
756
+ ## Project Structure
757
+
758
+ ```
759
+ src/
760
+ β”œβ”€β”€ index.ts # Worker entrypoint (fetch handler + scheduled/Cron handler)
761
+ β”œβ”€β”€ server.ts # McpServer factory (creates per-request instance)
762
+ β”œβ”€β”€ auth.ts # Optional Bearer token auth (constant-time compare)
763
+ β”œβ”€β”€ metrics.ts # In-memory counters served at /metrics
764
+ β”œβ”€β”€ types.ts # Env, cache categories, safeguard constants
765
+ β”œβ”€β”€ cache/
766
+ β”‚ β”œβ”€β”€ l0-memory.ts # In-memory Map cache with TTL + LRU eviction
767
+ β”‚ β”œβ”€β”€ l1-cache-api.ts # Cloudflare Cache API wrapper (synthetic GET keys)
768
+ β”‚ └── manager.ts # Cache orchestrator (L0 β†’ L1 β†’ upstream)
769
+ β”œβ”€β”€ throttle/
770
+ β”‚ β”œβ”€β”€ token-bucket.ts # Token bucket rate limiter (global + per-client)
771
+ β”‚ └── upstream.ts # Upstream fetch with concurrency limit, retry, timeout
772
+ β”œβ”€β”€ scraper/
773
+ β”‚ β”œβ”€β”€ ecidadania.ts # Isolated e-Cidadania scraper (REST lists + regex HTML detail; buildConsultaResumo)
774
+ β”‚ β”œβ”€β”€ pipeline.ts # 2h Cron: targeted highlight metric splice (consultas/eventos/ideias); corpora owned by the off-Worker jobs
775
+ β”‚ β”œβ”€β”€ anomaly.ts # Run classification (anomalous run never overwrites current)
776
+ β”‚ └── store.ts # D1 reads (resolveList + per-entity staleness, lastGoodRunAt) + detail write-through
777
+ β”œβ”€β”€ instrument.ts # Per-tool call telemetry (in-memory + Analytics Engine)
778
+ β”œβ”€β”€ utils/
779
+ β”‚ β”œβ”€β”€ logger.ts # Structured JSON logging
780
+ β”‚ └── validation.ts # toolResult, toolError, errorFrom, buildParams, ensureArray helpers
781
+ └── tools/
782
+ β”œβ”€β”€ referencia.ts # Group H β€” 1 reference/metadata tool
783
+ β”œβ”€β”€ senadores.ts # Group A β€” 5 senator tools
784
+ β”œβ”€β”€ materias.ts # Group B β€” 2 bill/matter tools (v3 backend)
785
+ β”œβ”€β”€ processos.ts # Group C β€” 5 process tools
786
+ β”œβ”€β”€ votacoes.ts # Group D β€” 3 vote tools
787
+ β”œβ”€β”€ comissoes.ts # Group E β€” 7 committee tools
788
+ β”œβ”€β”€ plenario.ts # Group F β€” 7 plenary tools
789
+ β”œβ”€β”€ ecidadania.ts # Group G β€” 8 e-Cidadania tools (read from D1; see scraper/)
790
+ β”œβ”€β”€ discursos.ts # Group I β€” 3 speech tools
791
+ β”œβ”€β”€ composicao.ts # Group J β€” 4 bloc/leadership tools
792
+ β”œβ”€β”€ orcamento.ts # Group K β€” 1 budget tool
793
+ β”œβ”€β”€ legislacao.ts # Group L β€” 2 federal law tools
794
+ β”œβ”€β”€ votacao-comissao.ts # Group M β€” 1 committee voting tool
795
+ β”œβ”€β”€ taquigrafia.ts # Group N β€” 2 stenographic record tools
796
+ β”œβ”€β”€ senadores-admin.ts # Group O β€” 2 admin senator tools (CEAPS, housing)
797
+ β”œβ”€β”€ servidores.ts # Group P β€” 4 personnel tools
798
+ β”œβ”€β”€ contratacoes.ts # Group Q β€” 6 procurement tools
799
+ β”œβ”€β”€ supridos.ts # Group R β€” 1 petty-cash tool
800
+ └── orcamento-senado.ts # Group S β€” 1 budget execution tool
801
+ scripts/
802
+ └── ingest-ecidadania/ # Off-Worker full-corpus consultas ingestion (run via `npm run ingest:ecidadania`)
803
+ β”œβ”€β”€ index.ts # Orchestrator: crawl β†’ status (/processo) β†’ normalize β†’ guards β†’ out.sql
804
+ β”œβ”€β”€ listing.ts # Pure listing parser (parseConsultaListingPage, findLastPage)
805
+ β”œβ”€β”€ status.ts # tramitando=S set from /processo β†’ aberta/encerrada (deriveStatus)
806
+ β”œβ”€β”€ restatus.ts # Linger fix: re-status stored rows by /processo membership (close zombies)
807
+ β”œβ”€β”€ http.ts # Polite fetch (retry/backoff) for the unattended crawl
808
+ β”œβ”€β”€ d1.ts # D1 pre-reads (existing meta, payloads, last good rows) via wrangler
809
+ β”œβ”€β”€ verify.ts # consultas_votos weekly integrity-check verdict (verifyAcervoIntegrity)
810
+ └── sql.ts # out.sql generation (mirrors SQL.upsert/SQL.history; reuses SyncRecord)
811
+ .github/workflows/ # ingest-ecidadania.yml (daily D1 corpus load), verify-consultas-votos.yml
812
+ # (weekly frozen-acervo integrity check), publish-mcp.yml (registry),
813
+ # usage-report.yml (monthly Analytics report), deprecate-registry.yml
814
+ # (all pinned to current Node 24 action majors β€” see each YAML for exact versions)
815
+ migrations/ # D1 schema (0001 tables, 0002 indexes, 0003 comment level + detail cursor) for the e-Cidadania pipeline
816
+ tests/ # Vitest unit tests mirroring src/ (parsers, cache, throttle, auth, scraper,
817
+ # pipeline/anomaly/store, listing/sql/highlights, plus e-Cidadania contract tests)
818
+ ```
819
+
820
+ ## Environment Variables
821
+
822
+ | Variable | Required | Default | Description |
823
+ |----------|----------|---------|-------------|
824
+ | `SENADO_BASE_URL` | No | `https://legis.senado.leg.br/dadosabertos` | Legislative API base URL |
825
+ | `SENADO_ADM_BASE_URL` | No | `https://adm.senado.gov.br/adm-dadosabertos` | Administrative API base URL |
826
+ | `ALLOWED_ORIGIN` | No | `*` | CORS allowed origin |
827
+ | `API_KEY` | No (secret) | β€” | When set, requires `Authorization: Bearer <key>` on all requests except `/health`, `/metrics`, and CORS preflight |
828
+ | `CACHE_KV` | Yes (binding) | β€” | KV namespace for L2 cache |
829
+ | `ECIDADANIA_DB` | Yes (binding) | β€” | D1 database for the e-Cidadania pipeline (list persistence + history) |
830
+ | `ECIDADANIA_CORPUS_STALE_MAX_MIN` | No | `14400` | Staleness window (minutes, ~10d) for the off-Worker full corpora (all e-Cidadania entities) β€” served flagged, never collapsed to highlights |
831
+ | `ECIDADANIA_CORPUS_MIN_PCT` | No | `80` | Catastrophic floor for the off-Worker corpus jobs: a complete crawl/parse below this % of the last good corpus is rejected |
832
+ | `CLOUDFLARE_API_TOKEN` | No (secret) | β€” | GitHub Actions secret (D1 edit scope) for the corpus ingestion / integrity-check jobs; not used by the Worker |
833
+ | `CLOUDFLARE_ACCOUNT_ID` | No (Actions var) | β€” | GitHub Actions repo variable so wrangler skips `/memberships` account auto-discovery (a D1-scoped token can't read it); required alongside `CLOUDFLARE_API_TOKEN` in the ingestion job |
834
+ | `SENADO_ANALYTICS` | No (binding) | β€” | Analytics Engine dataset for per-tool call telemetry |
835
+
836
+ ## Connecting MCP Clients
837
+
838
+ This is a **remote** server (Streamable HTTP, no install, open access) β€” point any MCP client at
839
+ `https://senado.sidneybissoli.com/mcp`. Besides 66 tools, it exposes **prompts** (ready-made pt-BR
840
+ workflows: `senado_gastos_senador`, `senado_tramitacao_materia`, `senado_votos_senador`,
841
+ `senado_panorama_ecidadania`) and **resources** (`senado://guia`, `senado://catalogo`,
842
+ `senado://glossario`, `senado://tabelas/tipos-materia`, `senado://tabelas/ufs`).
843
+
844
+ ### One-click (LobeHub)
845
+
846
+ Install from the LobeHub marketplace β€” open the
847
+ [server page](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare) and click **Install**
848
+ (it pre-fills the remote endpoint, no config needed).
849
+
850
+ ### Claude Desktop / Claude Code
851
+
852
+ Add to your MCP configuration:
853
+
854
+ ```json
855
+ {
856
+ "mcpServers": {
857
+ "senado-br": {
858
+ "url": "https://senado.sidneybissoli.com/mcp"
859
+ }
860
+ }
861
+ }
862
+ ```
863
+
864
+ For command-based clients (or any client without native remote support), use the `mcp-remote` bridge:
865
+
866
+ ```json
867
+ {
868
+ "mcpServers": {
869
+ "senado-br": {
870
+ "command": "npx",
871
+ "args": ["-y", "mcp-remote", "https://senado.sidneybissoli.com/mcp"]
872
+ }
873
+ }
874
+ }
875
+ ```
876
+
877
+ ### MCP Inspector
878
+
879
+ ```bash
880
+ npx @modelcontextprotocol/inspector https://senado.sidneybissoli.com/mcp
881
+ ```
882
+
883
+ ## License
884
+
885
+ MIT
886
+
887
+ ## Credits
888
+
889
+ Icon: *"Amanhecer no Congresso Nacional"* β€” photograph of the Brazilian National
890
+ Congress, used under a Creative Commons license. (If you are the author, open an
891
+ issue so we can add full attribution / the license link.)