senado-br-mcp 3.17.0 → 3.18.1
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.
- package/README.md +13 -1
- package/dist/dataset/schema.js +4 -4
- package/dist/scraper/ecidadania.js +38 -0
- package/dist/tools/ecidadania.js +21 -6
- package/dist/version.js +1 -1
- package/package.json +22 -2
package/README.md
CHANGED
|
@@ -35,6 +35,18 @@ Point a client at the endpoint and ask in plain language — English or Portugue
|
|
|
35
35
|
|
|
36
36
|
The answers come live from the Senate's official open-data APIs — exact figures with provenance, not numbers guessed from training data.
|
|
37
37
|
|
|
38
|
+
## Compared with the alternatives
|
|
39
|
+
|
|
40
|
+
Other Brazilian MCP servers also touch the Senate — [mcp-brasil](https://github.com/Mcp-Brasil/mcp-brasil) and [mcp-dados-br](https://github.com/lucianoon/mcp-dados-br) are multi-source aggregators in which the Senate is one source among many. The difference is coverage and how you run it. Measured in each one's source code on 2026-10-11:
|
|
41
|
+
|
|
42
|
+
| | Senate tools | Covers | Where it runs |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| **senado-br-mcp** (this) | 69 | legislative, administrative (CEAPS, staff, contracts, budget) and e-Cidadania, with provenance on every answer | hosted, no install or key; or `npx` |
|
|
45
|
+
| [mcp-brasil](https://github.com/Mcp-Brasil/mcp-brasil) 0.14.0 | 26 | legislative (senators, bills, floor votes, committees, agenda) — plus dozens of other government sources | local or self-hosted (Python) |
|
|
46
|
+
| [mcp-dados-br](https://github.com/lucianoon/mcp-dados-br) 0.6.0 | 3 | sitting senators, bill list, vote tally — plus IBGE, BCB, INMET, Chamber of Deputies | local (Python) |
|
|
47
|
+
|
|
48
|
+
**Pick an aggregator** when the question crosses the Senate with the Chamber of Deputies, the electoral court (TSE) or the Transparency Portal. **Pick this server** for expenses (CEAPS), staff, contracts, budget, speeches, committee votes, vetoes or e-Cidadania — areas the others don't cover. Full per-area table, side-by-side examples and how to re-measure (in Portuguese): [`docs/comparativo-alternativas.md`](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/blob/master/docs/comparativo-alternativas.md).
|
|
49
|
+
|
|
38
50
|
## Use it (hosted — no setup)
|
|
39
51
|
|
|
40
52
|
This is a **remote, hosted, open-access** server. To use it, point any MCP client at the Streamable
|
|
@@ -581,7 +593,7 @@ Each release ships one NDJSON resource per entity (one `HarmonizedRecord` per li
|
|
|
581
593
|
| Resource (`*.ndjson`) | Grain | Key variables | Source(s) |
|
|
582
594
|
|---|---|---|---|
|
|
583
595
|
| `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 |
|
|
584
|
-
| `ideias` | 1 legislative idea (~113.7k) | `titulo`, `apoios`, `status`, **`dataPublicacao
|
|
596
|
+
| `ideias` | 1 legislative idea (~113.7k) | `titulo`, `apoios`, `status`, **`dataPublicacao`ⁿ** (the *support deadline* — the only date the portal publishes; creation ≈ deadline − 120 days, see the dictionary), **`autorUf`ⁿ**, **`descricao`ⁿ**, **`plConvertido`ⁿ**, `url`, `firstSeenAt` | `pesquisaideia` listing + **detail** (`visualizacaoideia`) via a resumable backfillⁿ |
|
|
585
597
|
| `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**ᶜ |
|
|
586
598
|
| **`eventos_comentarios`ⁿ** | 1 comment (comment-level) | `eventoId`, `comentarioId`, `uf`, `texto`, `data`, `hora`, `momentoVideoUrl`, `convidadoAssociado` | AJAX fragment `ajaxcolecaocomentarioaudiencia?audienciaId=` |
|
|
587
599
|
| `consultas_votos` | 1 matéria (historical acervo) | `materia`, `ementa`, `autoria`, `votosSim`/`votosNao`/`totalVotos`, `votosPorUf`, `status`, `url`, `referencePeriod` | Arquimedes CSV `Proposições-com-votos.csv` (re-ingested weekly) |
|
package/dist/dataset/schema.js
CHANGED
|
@@ -191,11 +191,11 @@ const ideiasSchema = {
|
|
|
191
191
|
{
|
|
192
192
|
name: "dataPublicacao",
|
|
193
193
|
type: "date",
|
|
194
|
-
description: "Data limite para atingir os 20.000 apoios (única âncora temporal upstream da ideia).",
|
|
194
|
+
description: "Data limite para atingir os 20.000 apoios (única âncora temporal upstream da ideia). NÃO é a data de criação: o nome é histórico, mantido pela compatibilidade do esquema.",
|
|
195
195
|
sourceEndpoint: EP_IDEIAS_DETALHE,
|
|
196
|
-
sourceField: "página de detalhe: bloco \"Data limite\" (DD/MM/AAAA)",
|
|
197
|
-
operationalization: "extractDate sobre a data-limite exibida no detalhe (visualizacaoideia) → ISO YYYY-MM-DD; lido no crawl de detalhe. Reaberto na v2 (era null por design na v1).",
|
|
198
|
-
caveat: "Detail-only (v2): null nas ideias ainda não enriquecidas pelo backfill de detalhe (censura à esquerda residual nesses campos até o backfill completar).",
|
|
196
|
+
sourceField: "página de detalhe: bloco \"Data limite para receber 20.000 apoios\" (DD/MM/AAAA)",
|
|
197
|
+
operationalization: "extractDate sobre a data-limite exibida no detalhe (visualizacaoideia) → ISO YYYY-MM-DD; lido no crawl de detalhe. Reaberto na v2 (era null por design na v1). Data provável de criação = esta data − 120 dias (regra dos 4 meses do portal; medida exata em 4 ideias convertidas de 2017-2020 contra a base de Pereira, UFPE 2024) — derivação do leitor, não variável do dataset.",
|
|
198
|
+
caveat: "Em ideia aberta esta data está no futuro (é o fim do prazo de apoios). Detail-only (v2): null nas ideias ainda não enriquecidas pelo backfill de detalhe (censura à esquerda residual nesses campos até o backfill completar).",
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
name: "autorUf",
|
|
@@ -253,6 +253,44 @@ export async function obterConsultaInternal(id) {
|
|
|
253
253
|
linkMateria: null,
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
|
+
// ── Ideias ─────────────────────────────────────────────────────────────
|
|
257
|
+
/**
|
|
258
|
+
* Prazo de apoios de uma ideia, em dias. O portal diz que "as ideias ficam abertas por 4 meses"
|
|
259
|
+
* (`/comofuncionaideia`) e publica UMA data só, a "Data limite para receber 20.000 apoios" — é ela
|
|
260
|
+
* que o campo `dataPublicacao` guarda (nome histórico; ver `AVISO_DATAS_IDEIA`). Medido em
|
|
261
|
+
* 09/10/2026 contra a data de proposição de 4 ideias convertidas (base de Pereira, UFPE 2024,
|
|
262
|
+
* github.com/theus0307/dissertacao): a data limite é SEMPRE a proposição + 120 dias exatos —
|
|
263
|
+
* SUG 49/2017 12/09/2017 → 10/01/2018; SUG 43/2019 14/10/2019 → 11/02/2020; SUG 49/2019
|
|
264
|
+
* 31/07/2019 → 28/11/2019; SUG 21/2020 02/06/2020 → 30/09/2020. "4 meses" de calendário erraria
|
|
265
|
+
* em 2 a 3 dias em todas.
|
|
266
|
+
*/
|
|
267
|
+
export const IDEIA_PRAZO_APOIOS_DIAS = 120;
|
|
268
|
+
/**
|
|
269
|
+
* Aviso que acompanha as datas de ideia na saída das tools. Vai JUNTO do dado, não só na descrição
|
|
270
|
+
* da tool: a descrição é lida uma vez, o dado é lido a cada resposta, e a data limite de uma ideia
|
|
271
|
+
* aberta cai no futuro — sem o aviso o leitor conclui que o servidor errou.
|
|
272
|
+
*/
|
|
273
|
+
export const AVISO_DATAS_IDEIA = "dataPublicacao é a data limite para receber os 20.000 apoios, a única data que o portal publica (o nome do campo é histórico); " +
|
|
274
|
+
"por isso cai no futuro em ideia aberta. dataCriacaoEstimada = data limite menos 120 dias, calculada pelo servidor " +
|
|
275
|
+
"(regra dos 4 meses do portal, medida em 4 ideias de 2017-2020): é estimativa, não dado da fonte.";
|
|
276
|
+
/**
|
|
277
|
+
* Data provável de criação da ideia: data limite (ISO) − `IDEIA_PRAZO_APOIOS_DIAS`. Aritmética em UTC
|
|
278
|
+
* sobre o dia civil (sem fuso, sem horário de verão). Data ausente ou inválida → null, nunca uma data
|
|
279
|
+
* inventada (o `Date` "corrige" 31/02 para 03/03; aqui isso é rejeitado).
|
|
280
|
+
*/
|
|
281
|
+
export function estimarCriacaoIdeia(dataLimite) {
|
|
282
|
+
if (!dataLimite)
|
|
283
|
+
return null;
|
|
284
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dataLimite);
|
|
285
|
+
if (!m)
|
|
286
|
+
return null;
|
|
287
|
+
const [ano, mes, dia] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
288
|
+
const t = Date.UTC(ano, mes - 1, dia);
|
|
289
|
+
const d = new Date(t);
|
|
290
|
+
if (Number.isNaN(t) || d.getUTCFullYear() !== ano || d.getUTCMonth() !== mes - 1 || d.getUTCDate() !== dia)
|
|
291
|
+
return null;
|
|
292
|
+
return new Date(t - IDEIA_PRAZO_APOIOS_DIAS * 86_400_000).toISOString().slice(0, 10);
|
|
293
|
+
}
|
|
256
294
|
/**
|
|
257
295
|
* Canonical `IdeiaResumo` builder — single source of the object's field order, so the JSON payload
|
|
258
296
|
* (and `contentHash`) is byte-identical across the writers into `ecidadania_current`: the 2h highlight
|
package/dist/tools/ecidadania.js
CHANGED
|
@@ -16,9 +16,9 @@ import { provenanceEcidadania, provenanceArquimedesVotos, resultWithProvenance }
|
|
|
16
16
|
import { tagUntrustedFields, tagUntrustedList, neutralizeUntrustedText, sanitizeScrapedText } from "../utils/untrusted.js";
|
|
17
17
|
import { CACHE_ON_DEMAND } from "../types.js";
|
|
18
18
|
import { resolveList, writeDetalheThrough } from "../scraper/store.js";
|
|
19
|
-
import { ECIDADANIA_BASE, parseBrNum, extractId, normalizeEcidadaniaUrl, stripHtml, extractDate, extractTime, buildConsultaResumo, listarConsultasInternal, obterConsultaInternal, listarIdeiasInternal, obterIdeiaInternal, listarEventosInternal, obterEventoInternal, } from "../scraper/ecidadania.js";
|
|
19
|
+
import { ECIDADANIA_BASE, parseBrNum, extractId, normalizeEcidadaniaUrl, stripHtml, extractDate, extractTime, buildConsultaResumo, listarConsultasInternal, obterConsultaInternal, listarIdeiasInternal, obterIdeiaInternal, listarEventosInternal, obterEventoInternal, estimarCriacaoIdeia, AVISO_DATAS_IDEIA, IDEIA_PRAZO_APOIOS_DIAS, } from "../scraper/ecidadania.js";
|
|
20
20
|
// Re-export the scraper's pure/IO helpers so existing unit tests keep importing them from here.
|
|
21
|
-
export { ECIDADANIA_BASE, parseBrNum, extractId, normalizeEcidadaniaUrl, stripHtml, extractDate, extractTime, buildConsultaResumo, listarConsultasInternal, obterConsultaInternal, listarIdeiasInternal, obterIdeiaInternal, listarEventosInternal, obterEventoInternal, };
|
|
21
|
+
export { estimarCriacaoIdeia, AVISO_DATAS_IDEIA, IDEIA_PRAZO_APOIOS_DIAS, ECIDADANIA_BASE, parseBrNum, extractId, normalizeEcidadaniaUrl, stripHtml, extractDate, extractTime, buildConsultaResumo, listarConsultasInternal, obterConsultaInternal, listarIdeiasInternal, obterIdeiaInternal, listarEventosInternal, obterEventoInternal, };
|
|
22
22
|
/** Format an integer with pt-BR thousands separators ("253804" → "253.804").
|
|
23
23
|
* Avoids toLocaleString, whose Intl data is not guaranteed on Workers (OBS-12). */
|
|
24
24
|
export function formatIntBR(n) {
|
|
@@ -163,7 +163,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
163
163
|
}
|
|
164
164
|
});
|
|
165
165
|
// G5. senado_ecidadania_listar_ideias
|
|
166
|
-
server.tool("senado_ecidadania_listar_ideias", "Lista ideias legislativas propostas por cidadãos no e-Cidadania — **conjunto completo** (corpus persistido em D1; ideias em andamento atualizadas diariamente; ~118 mil ideias, incluindo encerradas e convertidas em proposição). Retorna `{ count, ideias }`, cada ideia com `id`, `titulo`, `apoios`, `status` (`aberta`/`encerrada`/`convertida`/`removida` — `removida` = retirada do portal, que não a publica mais; o corpus guarda o último estado conhecido)
|
|
166
|
+
server.tool("senado_ecidadania_listar_ideias", "Lista ideias legislativas propostas por cidadãos no e-Cidadania — **conjunto completo** (corpus persistido em D1; ideias em andamento atualizadas diariamente; ~118 mil ideias, incluindo encerradas e convertidas em proposição). Retorna `{ count, ideias, avisoDatas }`, cada ideia com `id`, `titulo`, `apoios`, `status` (`aberta`/`encerrada`/`convertida`/`removida` — `removida` = retirada do portal, que não a publica mais; o corpus guarda o último estado conhecido), `url` e os campos lidos do detalhe quando o backfill já passou por ela (`dataPublicacao`, `autorUf`, `descricao`, `plConvertido`; `null` até lá). **Datas:** `dataPublicacao` é a DATA LIMITE para receber os 20.000 apoios — a única data que o portal publica; o nome do campo é histórico — e por isso cai no futuro em ideia aberta; `dataCriacaoEstimada` (= data limite − 120 dias, a regra dos 4 meses do portal) é calculada pelo servidor e é estimativa, não dado da fonte; `avisoDatas` repete isso na resposta. Aceita filtro por `status` e `limite` (padrão 20). Para um ranking das mais apoiadas, ordene por apoios (`ordenarPor: \"apoios\"`, `ordem: \"desc\"`). Para o detalhe completo de uma ideia (texto, autor, se virou projeto de lei) chame `senado_ecidadania_obter_ideia` com o `id`.", {
|
|
167
167
|
status: z.enum(["aberta", "encerrada", "convertida", "removida", "todas"]).optional().describe("Filtrar por status (`removida` = retirada do portal)"),
|
|
168
168
|
ordenarPor: z.enum(["apoios", "data", "comentarios"]).optional().describe("Campo para ordenação (apoios é o disponível no corpus; data/comentarios só no detalhe)"),
|
|
169
169
|
ordem: z.enum(["asc", "desc"]).optional().describe("Ordem de ordenação"),
|
|
@@ -181,19 +181,34 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
181
181
|
const limite = params.limite ?? 20;
|
|
182
182
|
const offset = ((params.pagina ?? 1) - 1) * limite;
|
|
183
183
|
arr = arr.slice(offset, offset + limite);
|
|
184
|
-
|
|
184
|
+
// Derivado na SAÍDA, nunca no corpus: `IdeiaResumo` é o payload persistido (hash estável).
|
|
185
|
+
const ideias = arr.map((i) => ({ ...i, dataCriacaoEstimada: estimarCriacaoIdeia(i.dataPublicacao) }));
|
|
186
|
+
return resultWithProvenance({
|
|
187
|
+
count: ideias.length,
|
|
188
|
+
ideias: tagUntrustedList("ideias", ideias),
|
|
189
|
+
avisoDatas: AVISO_DATAS_IDEIA,
|
|
190
|
+
meta,
|
|
191
|
+
}, provLista("/principalideia", "ideias", meta));
|
|
185
192
|
}
|
|
186
193
|
catch (e) {
|
|
187
194
|
return ecidadaniaError(e);
|
|
188
195
|
}
|
|
189
196
|
});
|
|
190
197
|
// G6. senado_ecidadania_obter_ideia
|
|
191
|
-
server.tool("senado_ecidadania_obter_ideia", "Obtém o detalhe de uma ideia legislativa do e-Cidadania. Retorna um objeto com `id`, `titulo`, `descricao` (texto completo, truncado em ~2000 caracteres), `apoios`, `dataPublicacao`, `status`, `autor`, `url` e `plConvertido` (sigla/número quando virou projeto de lei). O campo `comentarios` vem `null`: a página de ideia não possui recurso de comentários. Obtenha o `id` antes via `senado_ecidadania_listar_ideias`.", { id: z.number().int().positive().describe("ID da ideia legislativa") }, async (params) => {
|
|
198
|
+
server.tool("senado_ecidadania_obter_ideia", "Obtém o detalhe de uma ideia legislativa do e-Cidadania. Retorna um objeto com `id`, `titulo`, `descricao` (texto completo, truncado em ~2000 caracteres), `apoios`, `dataPublicacao`, `dataCriacaoEstimada`, `avisoDatas`, `status`, `autor`, `url` e `plConvertido` (sigla/número quando virou projeto de lei). **Datas:** `dataPublicacao` é a DATA LIMITE para receber os 20.000 apoios — a única data que o portal publica; o nome do campo é histórico — e por isso cai no futuro em ideia aberta; `dataCriacaoEstimada` (= data limite − 120 dias, a regra dos 4 meses do portal) é calculada pelo servidor e é estimativa, não dado da fonte. O campo `comentarios` vem `null`: a página de ideia não possui recurso de comentários. Obtenha o `id` antes via `senado_ecidadania_listar_ideias`.", { id: z.number().int().positive().describe("ID da ideia legislativa") }, async (params) => {
|
|
192
199
|
try {
|
|
193
200
|
const { value: r, fetchedAt } = await cachedFetchWithMeta("ecidadania_ideia", { id: params.id }, CACHE_ON_DEMAND, () => obterIdeiaInternal(params.id));
|
|
194
201
|
writeDetalheThrough(db, ctx, "ideias", params.id, r);
|
|
195
202
|
// OBS-9: idea pages have no comments resource — serve null instead of a spurious 0.
|
|
196
|
-
|
|
203
|
+
// Datas: `dataCriacaoEstimada` e `avisoDatas` entram SÓ na resposta (não no write-through acima,
|
|
204
|
+
// que guarda o que a página diz).
|
|
205
|
+
const ideia = r;
|
|
206
|
+
const detalhe = {
|
|
207
|
+
...r,
|
|
208
|
+
dataCriacaoEstimada: estimarCriacaoIdeia(ideia.dataPublicacao),
|
|
209
|
+
comentarios: null,
|
|
210
|
+
avisoDatas: AVISO_DATAS_IDEIA,
|
|
211
|
+
};
|
|
197
212
|
return resultWithProvenance(tagUntrustedFields("ideias", detalhe), provDetalhe(r, `/visualizacaoideia?id=${params.id}`, `ideia=${params.id}`, fetchedAt));
|
|
198
213
|
}
|
|
199
214
|
catch (e) {
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "senado-br-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.1",
|
|
4
4
|
"description": "MCP server for Brazilian Senate open data — runs hosted on Cloudflare Workers or locally via npx (stdio)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"mcp-server",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"senado",
|
|
10
|
+
"senado-federal",
|
|
11
|
+
"brazilian-senate",
|
|
12
|
+
"dados-abertos",
|
|
13
|
+
"open-data",
|
|
14
|
+
"brasil",
|
|
15
|
+
"brazil",
|
|
16
|
+
"legislativo",
|
|
17
|
+
"votacoes",
|
|
18
|
+
"materias-legislativas",
|
|
19
|
+
"ceaps",
|
|
20
|
+
"cota-parlamentar",
|
|
21
|
+
"e-cidadania",
|
|
22
|
+
"transparencia",
|
|
23
|
+
"cloudflare-workers"
|
|
24
|
+
],
|
|
5
25
|
"license": "MIT",
|
|
6
26
|
"type": "module",
|
|
7
27
|
"mcpName": "io.github.SidneyBissoli/senado-br-mcp-cloudflare",
|
|
@@ -61,7 +81,7 @@
|
|
|
61
81
|
"@cloudflare/workers-types": "^5.20260830.1",
|
|
62
82
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
63
83
|
"@sbissoli/mcp-evals": "^0.2.1",
|
|
64
|
-
"@sbissoli/mcp-surface": "^0.
|
|
84
|
+
"@sbissoli/mcp-surface": "^0.6.0",
|
|
65
85
|
"@types/node": "^26.5.1",
|
|
66
86
|
"agents": "^0.26.0",
|
|
67
87
|
"exceljs": "^4.4.0",
|