senado-br-mcp 3.21.1 → 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/throttle/upstream.js +35 -14
- package/dist/tools/senadores-admin.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -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
|
+
}
|
|
@@ -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",
|