senado-br-mcp 3.21.0 → 3.22.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.
package/dist/dataset/schema.js
CHANGED
|
@@ -114,16 +114,16 @@ const consultasSchema = {
|
|
|
114
114
|
sourceEndpoint: EP_CONSULTAS_DETALHE,
|
|
115
115
|
sourceField: "página de detalhe: <b>Autoria:</b> <span>…</span>",
|
|
116
116
|
operationalization: "Lido da página de detalhe (visualizacaomateria) no crawl enriquecido; null quando o detalhe não expõe. Nome MANTIDO (informação pública por função — ver postura de privacidade).",
|
|
117
|
-
caveat: "Detail-only (v2): populado pelo crawl de detalhe; null nas linhas ainda não enriquecidas.",
|
|
117
|
+
caveat: "Detail-only (v2): populado pelo crawl de detalhe; null nas linhas ainda não enriquecidas. Até a 3.21.0 o crawl só preencheu as ~3,4 mil consultas do primeiro lote: as demais ficaram null com a autoria publicada na página e são relidas desde então, até o teto por execução.",
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: "relator",
|
|
121
121
|
type: "string",
|
|
122
|
-
description: "Relator(a) da matéria (agente público).",
|
|
122
|
+
description: "Relator(a) da matéria (agente público). Sempre null: a página da consulta não publica relatoria.",
|
|
123
123
|
sourceEndpoint: EP_CONSULTAS_DETALHE,
|
|
124
|
-
sourceField: "página de detalhe: <b>Relator(a):</b> <span>…</span>",
|
|
125
|
-
operationalization: "
|
|
126
|
-
caveat: "
|
|
124
|
+
sourceField: "página de detalhe: <b>Relator(a):</b> <span>…</span> (rótulo que a página não traz)",
|
|
125
|
+
operationalization: "O leitor procura o rótulo Relator(a) na página de detalhe (visualizacaomateria), mas a página publica só Autoria e Ementa: 0 de 8.145 consultas e 0 de 40.548 versões do histórico tiveram valor, e 42 páginas reais de anos e situações diferentes (medição de 18/10/2026) não traziam o rótulo. O relator da matéria está na API legislativa (/processo/relatoria), fora deste corpus.",
|
|
126
|
+
caveat: "Campo mantido por compatibilidade do formato; não use como dado. Vazio não significa matéria sem relator.",
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
name: "status",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* semântica deste servidor, medida contra a fonte). O pacote CLASSIFICA; este
|
|
17
17
|
* módulo DECIDE. A classe `UpstreamError` local, as mensagens e o `transport`
|
|
18
18
|
* continuam os mesmos: `errorFrom`, `ehReuniaoInexistente`, `classifyError` e o
|
|
19
|
-
*
|
|
19
|
+
* repórter do contrato noturno (`@sbissoli/mcp-live-source`, pelo `cause`) leem todos eles.
|
|
20
20
|
*
|
|
21
21
|
* POLÍTICA (`UPSTREAM_POLICY`) — medida em 27/09/2026 com `curl` e o User-Agent
|
|
22
22
|
* do portfólio: a API legislativa responde em 0,3–1 s (`/senador/lista/atual`
|
|
@@ -93,8 +93,17 @@ export class UpstreamError extends Error {
|
|
|
93
93
|
* quem sabe mais passa explícito (rota inexistente é `defeito`, corpo vazio
|
|
94
94
|
* é `nao_encontrado`).
|
|
95
95
|
*/
|
|
96
|
-
classe
|
|
97
|
-
|
|
96
|
+
classe,
|
|
97
|
+
/**
|
|
98
|
+
* `cause`: o erro do PACOTE (`@sbissoli/mcp-upstream`) que este erro local
|
|
99
|
+
* traduz. É por ele que o repórter do contrato noturno
|
|
100
|
+
* (`@sbissoli/mcp-live-source`, via `entregaDaOrigem`) decide se a origem
|
|
101
|
+
* entregou algo medível — pelo `kind`, nunca pela frase. Sem `cause`, toda
|
|
102
|
+
* falha traduzida seria "alheia à origem" (medido em 22/10/2026: um 502 nas
|
|
103
|
+
* três tentativas caía em "other" e zerava o disjuntor).
|
|
104
|
+
*/
|
|
105
|
+
opcoes = {}) {
|
|
106
|
+
super(message, opcoes.cause === undefined ? undefined : { cause: opcoes.cause });
|
|
98
107
|
this.status = status;
|
|
99
108
|
this.retryable = retryable;
|
|
100
109
|
this.transport = transport;
|
|
@@ -376,7 +385,7 @@ export async function upstreamFetch(path, params = {}, baseUrl, options = {}) {
|
|
|
376
385
|
if (!text.trim()) {
|
|
377
386
|
// `nao_encontrado`, como a frase já dava ("vazi"): um corpo vazio é como o
|
|
378
387
|
// `/comissao/reuniao/{codigo}` responde a código inexistente.
|
|
379
|
-
throw new UpstreamError(`[${path}] Resposta upstream vazia`, 502, true, false, "nao_encontrado");
|
|
388
|
+
throw new UpstreamError(`[${path}] Resposta upstream vazia`, 502, true, false, "nao_encontrado", { cause: corpoInesperado(url.toString(), response.status, text) });
|
|
380
389
|
}
|
|
381
390
|
if (options.text)
|
|
382
391
|
return text;
|
|
@@ -384,7 +393,7 @@ export async function upstreamFetch(path, params = {}, baseUrl, options = {}) {
|
|
|
384
393
|
return JSON.parse(text);
|
|
385
394
|
}
|
|
386
395
|
catch {
|
|
387
|
-
throw new UpstreamError(`[${path}] Resposta upstream não é JSON válido`, 502, false);
|
|
396
|
+
throw new UpstreamError(`[${path}] Resposta upstream não é JSON válido`, 502, false, false, undefined, { cause: corpoInesperado(url.toString(), response.status, text.slice(0, 2048)) });
|
|
388
397
|
}
|
|
389
398
|
}
|
|
390
399
|
/**
|
|
@@ -394,28 +403,40 @@ export async function upstreamFetch(path, params = {}, baseUrl, options = {}) {
|
|
|
394
403
|
* `transport` alimenta o disjuntor do contrato noturno.
|
|
395
404
|
*/
|
|
396
405
|
export function traduzirErro(err, path) {
|
|
406
|
+
// O erro do pacote vai em `cause`: é por ele (pelo `kind`) que o repórter do
|
|
407
|
+
// contrato noturno sabe se a origem entregou algo medível (3.21.2).
|
|
408
|
+
const cause = { cause: err };
|
|
397
409
|
switch (err.kind) {
|
|
398
410
|
case "timeout":
|
|
399
411
|
case "aborted":
|
|
400
|
-
return new UpstreamError(`[${path}] Timeout na requisição upstream (${UPSTREAM_TIMEOUT_MS / 1000}s)`, 504, true, true);
|
|
412
|
+
return new UpstreamError(`[${path}] Timeout na requisição upstream (${UPSTREAM_TIMEOUT_MS / 1000}s)`, 504, true, true, undefined, cause);
|
|
401
413
|
case "network": {
|
|
402
|
-
const
|
|
403
|
-
const detalhe =
|
|
404
|
-
return new UpstreamError(`[${path}] Erro de rede: ${detalhe || "desconhecido"}`, 502, true, true);
|
|
414
|
+
const raiz = err.cause;
|
|
415
|
+
const detalhe = raiz instanceof Error ? raiz.message : typeof raiz === "string" ? raiz : "";
|
|
416
|
+
return new UpstreamError(`[${path}] Erro de rede: ${detalhe || "desconhecido"}`, 502, true, true, undefined, cause);
|
|
405
417
|
}
|
|
406
418
|
case "rate_limited":
|
|
407
|
-
return new UpstreamError(`[${path}] Upstream retornou 429`, 429, true);
|
|
419
|
+
return new UpstreamError(`[${path}] Upstream retornou 429`, 429, true, false, undefined, cause);
|
|
408
420
|
case "http_5xx": {
|
|
409
421
|
const status = err.status ?? 502;
|
|
410
422
|
return status === 503
|
|
411
|
-
? new UpstreamError(`[${path}] Upstream retornou 503`, 503, true)
|
|
412
|
-
: new UpstreamError(`[${path}] Upstream retornou HTTP ${status}`, status, true);
|
|
423
|
+
? new UpstreamError(`[${path}] Upstream retornou 503`, 503, true, false, undefined, cause)
|
|
424
|
+
: new UpstreamError(`[${path}] Upstream retornou HTTP ${status}`, status, true, false, undefined, cause);
|
|
413
425
|
}
|
|
414
426
|
case "http_4xx":
|
|
415
427
|
case "not_found":
|
|
416
|
-
return new UpstreamError(`[${path}] Upstream retornou HTTP ${err.status ?? 404}`, err.status ?? 404, false);
|
|
428
|
+
return new UpstreamError(`[${path}] Upstream retornou HTTP ${err.status ?? 404}`, err.status ?? 404, false, false, undefined, cause);
|
|
417
429
|
case "malformed_body":
|
|
418
430
|
// Não acontece no modo `response` (o corpo é lido por `upstreamFetch`).
|
|
419
|
-
return new UpstreamError(`[${path}] Resposta upstream não é JSON válido`, 502, false);
|
|
431
|
+
return new UpstreamError(`[${path}] Resposta upstream não é JSON válido`, 502, false, false, undefined, cause);
|
|
420
432
|
}
|
|
421
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* O corpo chegou, mas não é o esperado (vazio, ou não é JSON). Para o repórter do
|
|
436
|
+
* contrato noturno isso é `malformed_body` da origem — veredito "ambíguo": ou o
|
|
437
|
+
* formato mudou, ou a origem serviu a página de erro num pedido bom; repetir decide.
|
|
438
|
+
* Lido AQUI, depois da ida, porque o modo `response` do pacote não lê o corpo.
|
|
439
|
+
*/
|
|
440
|
+
function corpoInesperado(url, status, body) {
|
|
441
|
+
return new PkgUpstreamError({ url, kind: "malformed_body", status, retryable: false, transport: false, attempts: 1, body });
|
|
442
|
+
}
|
package/dist/tools/ecidadania.js
CHANGED
|
@@ -239,7 +239,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
239
239
|
}
|
|
240
240
|
});
|
|
241
241
|
// G2. senado_ecidadania_obter_consulta
|
|
242
|
-
server.tool("senado_ecidadania_obter_consulta", "Obtém o detalhe de uma consulta pública específica do e-Cidadania. Retorna um objeto com `id`, `materia`, `ementa`, `votosSim`/`votosNao`/`totalVotos`, `percentualSim`/`percentualNao`, `status`, `autor`, `relator`, `url`. O portal não publica o período da consulta: `dataAbertura`/`dataEncerramento` (e `comissao`/`linkMateria`) vêm sempre `null` — a consulta abre quando a matéria entra em tramitação e encerra quando sai, refletido em `status`. O campo `comentarios` vem `null`: a página de consulta não possui recurso de comentários. Obtenha o `id` antes via `senado_ecidadania_listar_consultas` ou `senado_ecidadania_consultas_analise`.", { id: z.number().int().positive().describe("ID da consulta pública") }, async (params) => {
|
|
242
|
+
server.tool("senado_ecidadania_obter_consulta", "Obtém o detalhe de uma consulta pública específica do e-Cidadania. Retorna um objeto com `id`, `materia`, `ementa`, `votosSim`/`votosNao`/`totalVotos`, `percentualSim`/`percentualNao`, `status`, `autor`, `relator`, `url`. O portal não publica o período da consulta: `dataAbertura`/`dataEncerramento` (e `comissao`/`linkMateria`) vêm sempre `null` — a consulta abre quando a matéria entra em tramitação e encerra quando sai, refletido em `status`. O campo `comentarios` vem `null`: a página de consulta não possui recurso de comentários. O campo `relator` vem `null`: a página da consulta publica só autoria e ementa, não a relatoria — o relator da matéria sai de `senado_obter_materia` com o mesmo número (`codigoMateria` = `id`). Obtenha o `id` antes via `senado_ecidadania_listar_consultas` ou `senado_ecidadania_consultas_analise`.", { id: z.number().int().positive().describe("ID da consulta pública") }, async (params) => {
|
|
243
243
|
try {
|
|
244
244
|
const { value: r, fetchedAt } = await cachedFetchWithMeta("ecidadania_consulta", { id: params.id }, CACHE_ON_DEMAND, () => obterConsultaInternal(params.id));
|
|
245
245
|
writeDetalheThrough(db, ctx, "consultas", params.id, r);
|
|
@@ -123,7 +123,7 @@ export function estatisticasCeaps(filtrado, opts) {
|
|
|
123
123
|
}
|
|
124
124
|
export function registerSenadoresAdminTools(server, admBaseUrl) {
|
|
125
125
|
// O1. senado_ceaps
|
|
126
|
-
server.tool("senado_ceaps", "Despesas da Cota para Exercício da Atividade Parlamentar (CEAPS) dos senadores em um ano. Para perguntas de **maior/menor/média/mediana/distribuição/ranking** ('quem gastou mais CEAPS', 'gasto mediano', 'distribuição das despesas') use `estatisticas=true`: computa min/máx/média/mediana/desvio/percentis sobre TODAS as despesas filtradas e devolve `top`/`bottom` (padrão 10) com identificadores — os modos agregados só somam por grupo e não revelam a distribuição nem o extremo individual. Sem `agruparPor` → `distribuicao` das despesas individuais + `top`/`bottom`; com `agruparPor` (`senador`/`tipo`/`mes`/`fornecedor`) → `grupos[]` ranqueados por soma decrescente (`grupos[0]` = maior gastador), cada um com sua mini-distribuição. Sem `estatisticas`: nos modos agregados (`por-senador`/`por-tipo`/`por-mes`/`por-fornecedor`, padrão `por-senador`) traz `agregado[]` ordenado por `total` desc com `chave`, `total` e `despesas` (contagem); em `modo='detalhe'` traz `despesas[]` (mês, data, senador, tipoDespesa, fornecedor, cnpjCpf, valor). Filtre por `mes`, `codSenador`, `nomeSenador`, `tipoDespesa` ou `fornecedor` (busca parcial); `limite` cap 100 com `aviso` ao truncar. Obtenha `codSenador` via `senado_listar_senadores`.", {
|
|
126
|
+
server.tool("senado_ceaps", "Despesas da Cota para Exercício da Atividade Parlamentar (CEAPS) dos senadores em um ano. A fonte também publica lançamentos NEGATIVOS, sem rótulo próprio (a maioria em passagens aéreas, com jeito de crédito ou estorno): eles entram nas somas e totais como desconto e, nas estatísticas, aparecem como mínimo e no `bottom` — não leia um valor negativo como erro nem como 'a menor despesa'. Para perguntas de **maior/menor/média/mediana/distribuição/ranking** ('quem gastou mais CEAPS', 'gasto mediano', 'distribuição das despesas') use `estatisticas=true`: computa min/máx/média/mediana/desvio/percentis sobre TODAS as despesas filtradas e devolve `top`/`bottom` (padrão 10) com identificadores — os modos agregados só somam por grupo e não revelam a distribuição nem o extremo individual. Sem `agruparPor` → `distribuicao` das despesas individuais + `top`/`bottom`; com `agruparPor` (`senador`/`tipo`/`mes`/`fornecedor`) → `grupos[]` ranqueados por soma decrescente (`grupos[0]` = maior gastador), cada um com sua mini-distribuição. Sem `estatisticas`: nos modos agregados (`por-senador`/`por-tipo`/`por-mes`/`por-fornecedor`, padrão `por-senador`) traz `agregado[]` ordenado por `total` desc com `chave`, `total` e `despesas` (contagem); em `modo='detalhe'` traz `despesas[]` (mês, data, senador, tipoDespesa, fornecedor, cnpjCpf, valor). Filtre por `mes`, `codSenador`, `nomeSenador`, `tipoDespesa` ou `fornecedor` (busca parcial); `limite` cap 100 com `aviso` ao truncar. Obtenha `codSenador` via `senado_listar_senadores`.", {
|
|
127
127
|
ano: z.number().int().min(2008).max(2100).describe("Ano das despesas"),
|
|
128
128
|
modo: z.enum(["por-senador", "por-tipo", "por-mes", "por-fornecedor", "detalhe"]).optional().default("por-senador").describe("Agregação ou detalhe (padrão: por-senador). Ignorado quando estatisticas=true"),
|
|
129
129
|
estatisticas: z.boolean().optional().default(false).describe("Computa estatísticas (min/máx/média/mediana/percentis) + ranking top/bottom sobre todas as despesas filtradas. Use para 'quem gastou mais/menos', 'gasto médio/mediano', 'distribuição', 'ranking'"),
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "senado-br-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.0",
|
|
4
4
|
"description": "MCP server for Brazilian Senate open data — runs hosted on Cloudflare Workers or locally via npx (stdio)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -74,13 +74,14 @@
|
|
|
74
74
|
"@sbissoli/mcp-provenance": "^0.4.0",
|
|
75
75
|
"@sbissoli/mcp-search": "^0.9.0",
|
|
76
76
|
"@sbissoli/mcp-stats": "^0.3.0",
|
|
77
|
-
"@sbissoli/mcp-upstream": "^0.
|
|
77
|
+
"@sbissoli/mcp-upstream": "^0.5.1",
|
|
78
78
|
"zod": "^4.5.4"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@cloudflare/workers-types": "^5.20260830.1",
|
|
82
82
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
83
83
|
"@sbissoli/mcp-evals": "^0.2.1",
|
|
84
|
+
"@sbissoli/mcp-live-source": "^0.1.1",
|
|
84
85
|
"@sbissoli/mcp-surface": "^0.6.0",
|
|
85
86
|
"@types/node": "^26.5.1",
|
|
86
87
|
"agents": "^0.28.0",
|