senado-br-mcp 3.11.0 → 3.12.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 +1 -1
- package/dist/call-shape.js +62 -0
- package/dist/data/estrutura-organizacional.js +9 -21
- package/dist/instrument.js +4 -2
- package/dist/scraper/ecidadania.js +11 -5
- package/dist/server.js +8 -8
- package/dist/throttle/upstream.js +16 -3
- package/dist/tools/comissoes.js +5 -5
- package/dist/tools/contratacoes.js +5 -5
- package/dist/tools/deep-research.js +5 -0
- package/dist/tools/ecidadania.js +4 -10
- package/dist/tools/estrutura.js +1 -1
- package/dist/tools/legislacao.js +2 -2
- package/dist/tools/materias.js +1 -1
- package/dist/tools/plenario.js +1 -1
- package/dist/tools/processos.js +2 -2
- package/dist/tools/resolver-reuniao.js +2 -0
- package/dist/tools/servidores.js +1 -1
- package/dist/tools/votacao-comissao.js +3 -3
- package/dist/tools/votacoes.js +1 -1
- package/dist/utils/validation.js +31 -5
- package/dist/version.js +1 -1
- package/package.json +6 -4
- package/README.pt-BR.md +0 -887
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
[](https://senado.sidneybissoli.com/status)
|
|
16
16
|
|
|
17
|
-
🇧🇷 [Leia em Português](
|
|
17
|
+
🇧🇷 [Leia em Português](LEIA-ME.md)
|
|
18
18
|
|
|
19
19
|
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.
|
|
20
20
|
|
package/dist/call-shape.js
CHANGED
|
@@ -171,8 +171,70 @@ export function classifyThrown(error) {
|
|
|
171
171
|
error instanceof SyntaxError) {
|
|
172
172
|
return "defeito";
|
|
173
173
|
}
|
|
174
|
+
const declarada = classeDeclarada(error);
|
|
175
|
+
if (declarada)
|
|
176
|
+
return declarada;
|
|
174
177
|
return classifyError(error instanceof Error ? error.message : String(error));
|
|
175
178
|
}
|
|
179
|
+
const CLASSES = new Set(["contrato", "nao_encontrado", "fonte", "defeito", "outro"]);
|
|
180
|
+
function ehClasse(x) {
|
|
181
|
+
return typeof x === "string" && CLASSES.has(x);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A classe com que o erro NASCEU (`UpstreamError.classe`, o erro do
|
|
185
|
+
* e-Cidadania), ou `undefined` quando ele não declara nenhuma — aí vale a
|
|
186
|
+
* frase, como sempre.
|
|
187
|
+
*/
|
|
188
|
+
export function classeDeclarada(error) {
|
|
189
|
+
const c = error?.classe;
|
|
190
|
+
return ehClasse(c) ? c : undefined;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Onde um resultado de erro leva a classe decidida pelo TIPO da exceção.
|
|
194
|
+
*
|
|
195
|
+
* Por que existe. Medido em 30/09/2026, rodando este classificador sobre as
|
|
196
|
+
* mensagens de falha da origem: "Erro de rede: ..." (base legislativa), o 429
|
|
197
|
+
* do nosso próprio balde ("Taxa de requisições excedida") e, no e-Cidadania,
|
|
198
|
+
* "falha de rede ao acessar" e "retornou HTTP 429/400/403" caíam em `outro`.
|
|
199
|
+
* O tipo da falha existia no `catch` e só o texto chegava ao `instrumentTool`.
|
|
200
|
+
*
|
|
201
|
+
* O mesmo defeito de fundo foi consertado no bcb-br-mcp (#45), no
|
|
202
|
+
* ilo-mcp-server (#24), no uis-mcp-server (#22), no ibge-br-mcp (#62) e no
|
|
203
|
+
* medical-terminologies-mcp (#58). O conserto não reescreve frase nem mexe em
|
|
204
|
+
* regex: a classe viaja AO LADO do texto, nesta chave-símbolo não enumerável,
|
|
205
|
+
* que o `JSON.stringify` não vê — o que o cliente recebe não muda.
|
|
206
|
+
* `instrumentTool` a lê antes de cair na frase.
|
|
207
|
+
*/
|
|
208
|
+
export const CLASSE_DO_ERRO = Symbol.for("br.com.sidneybissoli.mcp/classe-do-erro");
|
|
209
|
+
/** A classe anexada a um resultado de erro, ou `undefined` quando não há. */
|
|
210
|
+
export function classeAnexada(result) {
|
|
211
|
+
if (!result || typeof result !== "object")
|
|
212
|
+
return undefined;
|
|
213
|
+
const c = result[CLASSE_DO_ERRO];
|
|
214
|
+
return ehClasse(c) ? c : undefined;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Anexa ao resultado, fora do fio, a classe que o erro declara — ou `defeito`,
|
|
218
|
+
* quando é bug nosso (`TypeError` & cia., como em `classifyThrown`). Devolve o
|
|
219
|
+
* próprio resultado.
|
|
220
|
+
*
|
|
221
|
+
* Medido em 30/09/2026 na varredura da frota: os ~60 `catch` que passam por
|
|
222
|
+
* `errorFrom` engolem a exceção antes do hook, e um "Cannot read properties of
|
|
223
|
+
* undefined" saía `outro`. A falha de rede crua da undici também é `TypeError`
|
|
224
|
+
* ("fetch failed"); toda ida passa por throttle/upstream.ts, que a tipa — a
|
|
225
|
+
* exclusão é guarda, para uma ida crua futura não virar `defeito`.
|
|
226
|
+
*/
|
|
227
|
+
export function anexarClasse(result, error) {
|
|
228
|
+
const classe = classeDeclarada(error) ?? (ehBugNosso(error) ? "defeito" : undefined);
|
|
229
|
+
if (classe !== undefined)
|
|
230
|
+
Object.defineProperty(result, CLASSE_DO_ERRO, { value: classe, enumerable: false });
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
function ehBugNosso(error) {
|
|
234
|
+
if (error instanceof TypeError)
|
|
235
|
+
return error.message !== "fetch failed";
|
|
236
|
+
return error instanceof RangeError || error instanceof ReferenceError || error instanceof SyntaxError;
|
|
237
|
+
}
|
|
176
238
|
/**
|
|
177
239
|
* Classe de um erro JSON-RPC do PROTOCOLO, pelo código antes da mensagem.
|
|
178
240
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const ESTRUTURA_ORGANIZACIONAL = {
|
|
2
|
-
"extraidoEm": "2026-
|
|
2
|
+
"extraidoEm": "2026-10-01T12:13:07.708Z",
|
|
3
3
|
"fonteUrl": "https://www12.senado.leg.br/institucional/estrutura/orgaosenado",
|
|
4
|
-
"total":
|
|
4
|
+
"total": 821,
|
|
5
5
|
"orgaos": [
|
|
6
6
|
{
|
|
7
7
|
"cod": -2072346465,
|
|
@@ -3129,12 +3129,6 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
3129
3129
|
"nome": "Escritório de Apoio Nº 01 do Senador Chico Rodrigues",
|
|
3130
3130
|
"codSuperior": 10057
|
|
3131
3131
|
},
|
|
3132
|
-
{
|
|
3133
|
-
"cod": 10156,
|
|
3134
|
-
"sigla": null,
|
|
3135
|
-
"nome": "Escritório de Apoio Nº 01 do Senador Angelo Coronel",
|
|
3136
|
-
"codSuperior": 10046
|
|
3137
|
-
},
|
|
3138
3132
|
{
|
|
3139
3133
|
"cod": 10157,
|
|
3140
3134
|
"sigla": null,
|
|
@@ -4631,7 +4625,7 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
4631
4625
|
},
|
|
4632
4626
|
{
|
|
4633
4627
|
"cod": 10749,
|
|
4634
|
-
"sigla":
|
|
4628
|
+
"sigla": "CSADVOSF",
|
|
4635
4629
|
"nome": "Conselho Superior da Advocacia do Senado Federal",
|
|
4636
4630
|
"codSuperior": 67
|
|
4637
4631
|
},
|
|
@@ -4887,18 +4881,6 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
4887
4881
|
"nome": "Gabinete da Liderança do Avante",
|
|
4888
4882
|
"codSuperior": 109
|
|
4889
4883
|
},
|
|
4890
|
-
{
|
|
4891
|
-
"cod": 10847,
|
|
4892
|
-
"sigla": "GSHKLANN",
|
|
4893
|
-
"nome": "Gabinete do Senador Hermes Klann",
|
|
4894
|
-
"codSuperior": 108
|
|
4895
|
-
},
|
|
4896
|
-
{
|
|
4897
|
-
"cod": 10850,
|
|
4898
|
-
"sigla": null,
|
|
4899
|
-
"nome": "Escritório de Apoio Nº 01 do Senador Hermes Klann",
|
|
4900
|
-
"codSuperior": 10847
|
|
4901
|
-
},
|
|
4902
4884
|
{
|
|
4903
4885
|
"cod": 10857,
|
|
4904
4886
|
"sigla": null,
|
|
@@ -4940,6 +4922,12 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
4940
4922
|
"sigla": null,
|
|
4941
4923
|
"nome": "Escritório de Apoio Nº 01 do Senador Sargento Reginauro",
|
|
4942
4924
|
"codSuperior": 10862
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
"cod": 10866,
|
|
4928
|
+
"sigla": "GSRBRAZ",
|
|
4929
|
+
"nome": "Gabinete do Senador Renzo Braz",
|
|
4930
|
+
"codSuperior": 108
|
|
4943
4931
|
}
|
|
4944
4932
|
]
|
|
4945
4933
|
};
|
package/dist/instrument.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* (SELF_HEADER, value = the SELF_MARKER Wrangler secret).
|
|
30
30
|
*/
|
|
31
31
|
import { resolveDesfecho } from "./envelope.js";
|
|
32
|
-
import { classifyError, classifyThrown, errorText, paramNames } from "./call-shape.js";
|
|
32
|
+
import { classeAnexada, classifyError, classifyThrown, errorText, paramNames } from "./call-shape.js";
|
|
33
33
|
import { incr, incrTool } from "./metrics.js";
|
|
34
34
|
import { callCache, cacheClass } from "./observability/call-context.js";
|
|
35
35
|
import { withUpstreamCall } from "./throttle/upstream.js";
|
|
@@ -80,8 +80,10 @@ export function instrumentTool(name, cb, analytics, tag, gravados) {
|
|
|
80
80
|
const result = await withUpstreamCall(() => callCache.run(stats, () => Promise.resolve(cb(...args))));
|
|
81
81
|
isError =
|
|
82
82
|
typeof result === "object" && result !== null && result.isError === true;
|
|
83
|
+
// A classe que o handler anexou pelo TIPO do erro vence a frase; a frase
|
|
84
|
+
// fica para o erro que nasceu sem tipo. Ver CLASSE_DO_ERRO.
|
|
83
85
|
if (isError)
|
|
84
|
-
classe = classifyError(errorText(result));
|
|
86
|
+
classe = classeAnexada(result) ?? classifyError(errorText(result));
|
|
85
87
|
return result;
|
|
86
88
|
}
|
|
87
89
|
catch (e) {
|
|
@@ -20,8 +20,14 @@ export const ECIDADANIA_BASE = "https://www12.senado.leg.br/ecidadania";
|
|
|
20
20
|
* (not `upstreamFetch`), so transient conditions are classified here: 5xx / 429 /
|
|
21
21
|
* timeout / network → retryable; 4xx → not.
|
|
22
22
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* `classe`, quando dada, é a classe de telemetria decidida pelo TIPO da falha
|
|
25
|
+
* (ver `CLASSE_DO_ERRO` em src/call-shape.ts). Medido em 30/09/2026: pela
|
|
26
|
+
* frase, "falha de rede ao acessar" e "retornou HTTP 429/400/403" caíam em
|
|
27
|
+
* `outro`. Sem ela, vale a frase, como sempre.
|
|
28
|
+
*/
|
|
29
|
+
function ecidadaniaFetchError(message, retryable, classe) {
|
|
30
|
+
return Object.assign(new Error(message), classe ? { retryable, classe } : { retryable });
|
|
25
31
|
}
|
|
26
32
|
const isTransientStatus = (status) => status >= 500 || status === 429;
|
|
27
33
|
/**
|
|
@@ -83,14 +89,14 @@ async function idaEcidadania(path, headers, rotulo) {
|
|
|
83
89
|
switch (e.kind) {
|
|
84
90
|
case "timeout":
|
|
85
91
|
case "aborted":
|
|
86
|
-
throw ecidadaniaFetchError(`${rotulo}: timeout (${UPSTREAM_TIMEOUT_MS / 1000}s) ao acessar ${path}`, true);
|
|
92
|
+
throw ecidadaniaFetchError(`${rotulo}: timeout (${UPSTREAM_TIMEOUT_MS / 1000}s) ao acessar ${path}`, true, "fonte");
|
|
87
93
|
case "network": {
|
|
88
94
|
const causa = e.cause instanceof Error ? e.cause.message : String(e.cause ?? "desconhecido");
|
|
89
|
-
throw ecidadaniaFetchError(`${rotulo}: falha de rede ao acessar ${path} (${causa})`, true);
|
|
95
|
+
throw ecidadaniaFetchError(`${rotulo}: falha de rede ao acessar ${path} (${causa})`, true, "fonte");
|
|
90
96
|
}
|
|
91
97
|
default: {
|
|
92
98
|
const status = e.status ?? 502;
|
|
93
|
-
throw ecidadaniaFetchError(`${rotulo} retornou HTTP ${status} para ${path}`, isTransientStatus(status));
|
|
99
|
+
throw ecidadaniaFetchError(`${rotulo} retornou HTTP ${status} para ${path}`, isTransientStatus(status), status === 404 ? "nao_encontrado" : "fonte");
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
}
|
package/dist/server.js
CHANGED
|
@@ -50,14 +50,14 @@ import { announceServedVersions } from "./discover.js";
|
|
|
50
50
|
* `.strict()` publica `additionalProperties: false` e faz o SDK responder
|
|
51
51
|
* `Unrecognized key: "<nome>"`, que NOMEIA a chave.
|
|
52
52
|
*
|
|
53
|
-
* `search` e `fetch`
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Vale para TODAS, `search` e `fetch` inclusive. Até a 3.11.0 as duas ficavam
|
|
54
|
+
* abertas por serem contrato da OpenAI; desde o `@sbissoli/mcp-search` 0.9.0
|
|
55
|
+
* o próprio contrato do portfólio é estrito (`z.strictObject`), e as duas
|
|
56
|
+
* entram por este funil (registerDeepResearchToolsSenado passa a shape) —
|
|
57
|
+
* sem o `.strict()` aqui, a estrição do pacote se perderia na passagem.
|
|
56
58
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const objeto = z.object(shape);
|
|
60
|
-
return TOOLS_DE_CONTRATO_ALHEIO.has(name) ? objeto : objeto.strict();
|
|
59
|
+
function esquemaDeEntrada(shape) {
|
|
60
|
+
return z.object(shape).strict();
|
|
61
61
|
}
|
|
62
62
|
export function createServer(env, ctx, options = {}) {
|
|
63
63
|
const toolProfile = options.toolProfile ?? "full";
|
|
@@ -102,7 +102,7 @@ export function createServer(env, ctx, options = {}) {
|
|
|
102
102
|
return registerTool(name, {
|
|
103
103
|
title: titleForTool(name),
|
|
104
104
|
description,
|
|
105
|
-
inputSchema: esquemaDeEntrada(
|
|
105
|
+
inputSchema: esquemaDeEntrada(shape),
|
|
106
106
|
outputSchema: outputSchema,
|
|
107
107
|
annotations: {
|
|
108
108
|
title: titleForTool(name),
|
|
@@ -84,13 +84,24 @@ export class UpstreamError extends Error {
|
|
|
84
84
|
* cannot carry it — 502 is used for both a real Bad Gateway and a body
|
|
85
85
|
* that failed to parse.
|
|
86
86
|
*/
|
|
87
|
-
transport = false
|
|
87
|
+
transport = false,
|
|
88
|
+
/**
|
|
89
|
+
* Classe de telemetria pelo TIPO, não pela frase (ver `CLASSE_DO_ERRO` em
|
|
90
|
+
* src/call-shape.ts). Medido em 30/09/2026: pela frase, "Erro de rede: ..."
|
|
91
|
+
* e o 429 do nosso próprio balde ("Taxa de requisições excedida") caíam em
|
|
92
|
+
* `outro`. Padrão: 404 é ausência respondida, o resto é a fonte falhando;
|
|
93
|
+
* quem sabe mais passa explícito (rota inexistente é `defeito`, corpo vazio
|
|
94
|
+
* é `nao_encontrado`).
|
|
95
|
+
*/
|
|
96
|
+
classe) {
|
|
88
97
|
super(message);
|
|
89
98
|
this.status = status;
|
|
90
99
|
this.retryable = retryable;
|
|
91
100
|
this.transport = transport;
|
|
92
101
|
this.name = "UpstreamError";
|
|
102
|
+
this.classe = classe ?? (status === 404 ? "nao_encontrado" : "fonte");
|
|
93
103
|
}
|
|
104
|
+
classe;
|
|
94
105
|
}
|
|
95
106
|
/**
|
|
96
107
|
* Frase-chave da mensagem de rota inexistente. Vive numa constante porque
|
|
@@ -269,7 +280,7 @@ export async function upstreamFetch(path, params = {}, baseUrl, options = {}) {
|
|
|
269
280
|
log("upstream", path, 404, Date.now() - startTime, err.attempts - 1);
|
|
270
281
|
const corpo404 = await err.response.text();
|
|
271
282
|
if (rotaInexistente(corpo404)) {
|
|
272
|
-
throw new UpstreamError(`[${path}] ${MSG_ROTA_INEXISTENTE} — o caminho montado não existe na API do Senado.`, 404, false);
|
|
283
|
+
throw new UpstreamError(`[${path}] ${MSG_ROTA_INEXISTENTE} — o caminho montado não existe na API do Senado.`, 404, false, false, "defeito");
|
|
273
284
|
}
|
|
274
285
|
if (options.on404 === "empty")
|
|
275
286
|
return [];
|
|
@@ -301,7 +312,9 @@ export async function upstreamFetch(path, params = {}, baseUrl, options = {}) {
|
|
|
301
312
|
incr("upstreamCalls");
|
|
302
313
|
log("upstream", path, response.status, latency, call.attempts - attemptsBefore - 1);
|
|
303
314
|
if (!text.trim()) {
|
|
304
|
-
|
|
315
|
+
// `nao_encontrado`, como a frase já dava ("vazi"): um corpo vazio é como o
|
|
316
|
+
// `/comissao/reuniao/{codigo}` responde a código inexistente.
|
|
317
|
+
throw new UpstreamError(`[${path}] Resposta upstream vazia`, 502, true, false, "nao_encontrado");
|
|
305
318
|
}
|
|
306
319
|
if (options.text)
|
|
307
320
|
return text;
|
package/dist/tools/comissoes.js
CHANGED
|
@@ -175,7 +175,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
175
175
|
const secao = params.secao ?? "resumo";
|
|
176
176
|
const codigo = await resolveComissaoCodigo(sigla, baseUrl);
|
|
177
177
|
if (!codigo)
|
|
178
|
-
return toolError(`Comissão com sigla "${sigla}" não encontrada
|
|
178
|
+
return toolError(`Comissão com sigla "${sigla}" não encontrada.`, "nao_encontrado");
|
|
179
179
|
if (secao === "membros") {
|
|
180
180
|
const membrosPath = `/composicao/comissao/${codigo}`;
|
|
181
181
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_membros_comissao", { codigo, ativas: "S" }, CACHE_SEMI_STATIC,
|
|
@@ -199,7 +199,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
199
199
|
}
|
|
200
200
|
const { path: resumoPath, fetchedAt, colegiado } = await fetchComissaoColegiado(codigo, baseUrl);
|
|
201
201
|
if (!colegiado)
|
|
202
|
-
return toolError("Dados da comissão não encontrados.");
|
|
202
|
+
return toolError("Dados da comissão não encontrados.", "nao_encontrado");
|
|
203
203
|
const prov = provenanceFor("SENADO_LEGIS", baseUrl, resumoPath, {
|
|
204
204
|
dataset_id: `comissao=${sigla}; codigo=${codigo}`, retrieved_at: fetchedAt,
|
|
205
205
|
});
|
|
@@ -330,7 +330,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
330
330
|
const sigla = (params.sigla || "").toUpperCase();
|
|
331
331
|
if (!sigla) {
|
|
332
332
|
return toolError("Informe `codigoReuniao`, ou `sigla` da comissão (com `data`, opcional). " +
|
|
333
|
-
"Sem nenhum dos dois não há o que detalhar.",
|
|
333
|
+
"Sem nenhum dos dois não há o que detalhar.", "contrato");
|
|
334
334
|
}
|
|
335
335
|
const hoje = new Date();
|
|
336
336
|
const inicio = new Date(hoje);
|
|
@@ -348,7 +348,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
348
348
|
}));
|
|
349
349
|
const escolha = escolherReuniao(candidatas, sigla, params.data ? di : `${di}–${df}`);
|
|
350
350
|
if (escolha.tipo !== "codigo")
|
|
351
|
-
return toolError(escolha.mensagem,
|
|
351
|
+
return toolError(escolha.mensagem, escolha.classe);
|
|
352
352
|
codigo = escolha.codigo;
|
|
353
353
|
}
|
|
354
354
|
const reuniaoPath = `/comissao/reuniao/${codigo}`;
|
|
@@ -405,7 +405,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
405
405
|
// maioria dos endpoints e mandava o agente repetir uma chamada que
|
|
406
406
|
// nunca ia funcionar.
|
|
407
407
|
if (ehReuniaoInexistente(e)) {
|
|
408
|
-
return toolError(mensagemCodigoInexistente(params.codigoReuniao ?? 0),
|
|
408
|
+
return toolError(mensagemCodigoInexistente(params.codigoReuniao ?? 0), "nao_encontrado");
|
|
409
409
|
}
|
|
410
410
|
return errorFrom(e, "Reunião de comissão não encontrada");
|
|
411
411
|
}
|
|
@@ -205,12 +205,12 @@ export function registerContratacoesTools(server, admBaseUrl) {
|
|
|
205
205
|
let path;
|
|
206
206
|
if (params.secao === "aditivos") {
|
|
207
207
|
if (tipo !== "contratos")
|
|
208
|
-
return toolError("A seção 'aditivos' só existe para tipo=contratos.");
|
|
208
|
+
return toolError("A seção 'aditivos' só existe para tipo=contratos.", "contrato");
|
|
209
209
|
path = `/contratacoes/contratos/${params.id}/aditivos`;
|
|
210
210
|
}
|
|
211
211
|
else if (params.secao === "acionamentos") {
|
|
212
212
|
if (tipo !== "atas_registro_preco")
|
|
213
|
-
return toolError("A seção 'acionamentos' só existe para tipo=atas_registro_preco.");
|
|
213
|
+
return toolError("A seção 'acionamentos' só existe para tipo=atas_registro_preco.", "contrato");
|
|
214
214
|
path = `/contratacoes/atas_registro_preco/${params.id}/acionamentos`;
|
|
215
215
|
}
|
|
216
216
|
else {
|
|
@@ -229,7 +229,7 @@ export function registerContratacoesTools(server, admBaseUrl) {
|
|
|
229
229
|
const existe = await paiExiste(tipo, params.id, admBaseUrl);
|
|
230
230
|
if (existe === false) {
|
|
231
231
|
return toolError(`Não existe ${tipo} com id ${params.id}. Obtenha o id em ` +
|
|
232
|
-
(tipo === "contratos" ? "senado_contratos" : "senado_contratacoes_lista") + ".");
|
|
232
|
+
(tipo === "contratos" ? "senado_contratos" : "senado_contratacoes_lista") + ".", "nao_encontrado");
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
const limite = params.limite ?? 100;
|
|
@@ -261,7 +261,7 @@ export function registerContratacoesTools(server, admBaseUrl) {
|
|
|
261
261
|
objetoContains: params.objeto,
|
|
262
262
|
});
|
|
263
263
|
if (Object.keys(qp).length === 0) {
|
|
264
|
-
return toolError("Informe 'numero' ou 'objeto' para a busca.");
|
|
264
|
+
return toolError("Informe 'numero' ou 'objeto' para a busca.", "contrato");
|
|
265
265
|
}
|
|
266
266
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_licitacoes", qp, CACHE_SEMI_STATIC, () => admFetch("/contratacoes/licitacoes", qp, admBaseUrl));
|
|
267
267
|
const todos = ensureArray(response);
|
|
@@ -329,7 +329,7 @@ export function registerContratacoesTools(server, admBaseUrl) {
|
|
|
329
329
|
}, async (params) => {
|
|
330
330
|
try {
|
|
331
331
|
if (!params.nome && !params.cnpj) {
|
|
332
|
-
return toolError("Informe 'nome' ou 'cnpj' para a busca.");
|
|
332
|
+
return toolError("Informe 'nome' ou 'cnpj' para a busca.", "contrato");
|
|
333
333
|
}
|
|
334
334
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_empresas_contratadas", {}, CACHE_SEMI_STATIC, () => admFetchLarge("/contratacoes/empresas", {}, admBaseUrl));
|
|
335
335
|
let empresas = ensureArray(response);
|
|
@@ -39,6 +39,7 @@ import { provenanceExtras, provenanceFor } from "../utils/provenance.js";
|
|
|
39
39
|
import { CACHE_SEMI_STATIC } from "../types.js";
|
|
40
40
|
import { extractParlamentares, fetchSenadorDetalhe, parseSenadorResumo } from "./senadores.js";
|
|
41
41
|
import { RootNotFoundError } from "../utils/upstream-parse.js";
|
|
42
|
+
import { classifyThrown } from "../call-shape.js";
|
|
42
43
|
import { fetchComissaoColegiado, parseComissaoItem, parseComissaoResumo } from "./comissoes.js";
|
|
43
44
|
import { UFS } from "./referencia.js";
|
|
44
45
|
export { DEEP_RESEARCH_TOOLS };
|
|
@@ -286,6 +287,10 @@ export function capturarDeepResearchTools(baseUrl) {
|
|
|
286
287
|
corpus: "Brazilian Federal Senate open data (senators in office and active committees of the Senate and the National Congress)",
|
|
287
288
|
richTools: "the `senado_*` tools",
|
|
288
289
|
limit: DEEP_RESEARCH_LIMIT,
|
|
290
|
+
// A classe de uma EXCEÇÃO pelo tipo (mcp-search 0.8.0): a declarada
|
|
291
|
+
// (`error.classe`) o pacote lê primeiro; `TypeError` nosso -> `defeito`.
|
|
292
|
+
// Ela viaja no resultado (chave-símbolo da frota) até o hook do shim.
|
|
293
|
+
classifyThrown,
|
|
289
294
|
});
|
|
290
295
|
const porNome = (name) => {
|
|
291
296
|
const reg = capturados.find((c) => c.name === name);
|
package/dist/tools/ecidadania.js
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { cachedFetchWithMeta } from "../cache/manager.js";
|
|
14
|
-
import {
|
|
14
|
+
import { errorFrom } from "../utils/validation.js";
|
|
15
15
|
import { provenanceEcidadania, provenanceArquimedesVotos, resultWithProvenance } from "../utils/provenance.js";
|
|
16
16
|
import { tagUntrustedFields, tagUntrustedList, neutralizeUntrustedText, sanitizeScrapedText } from "../utils/untrusted.js";
|
|
17
|
-
import { logger } from "../utils/logger.js";
|
|
18
17
|
import { CACHE_ON_DEMAND } from "../types.js";
|
|
19
18
|
import { resolveList, writeDetalheThrough } from "../scraper/store.js";
|
|
20
19
|
import { ECIDADANIA_BASE, parseBrNum, extractId, normalizeEcidadaniaUrl, stripHtml, extractDate, extractTime, buildConsultaResumo, listarConsultasInternal, obterConsultaInternal, listarIdeiasInternal, obterIdeiaInternal, listarEventosInternal, obterEventoInternal, } from "../scraper/ecidadania.js";
|
|
@@ -80,14 +79,9 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
80
79
|
return provenanceEcidadania(alvo, { dataset_id, retrieved_at: fetchedAt });
|
|
81
80
|
}
|
|
82
81
|
function ecidadaniaError(e) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: false;
|
|
87
|
-
// Emit the same structured tool_error log the other tools get via errorFrom(); log the raw
|
|
88
|
-
// message (no suffix), but keep the reassuring suffix in the user-facing tool error.
|
|
89
|
-
logger.error("tool_error", { message: msg, retryable });
|
|
90
|
-
return toolError(`${msg}. As demais funcionalidades (senadores, matérias, votações) continuam operacionais.`, retryable);
|
|
82
|
+
// Mesmo caminho de toda tool (errorFrom: log cru, classe pelo TIPO da exceção via
|
|
83
|
+
// anexarClasse); o sufixo tranquilizador vai só na mensagem que o cliente lê.
|
|
84
|
+
return errorFrom(e, "Erro ao acessar e-Cidadania", ". As demais funcionalidades (senadores, matérias, votações) continuam operacionais.");
|
|
91
85
|
}
|
|
92
86
|
// G1. senado_ecidadania_listar_consultas
|
|
93
87
|
server.tool("senado_ecidadania_listar_consultas", "Lista consultas públicas do e-Cidadania (conjunto completo das **abertas** — toda matéria em tramitação, ~7,7 mil), em que cidadãos votam sim/não. Retorna `{ count, consultas }`, cada consulta com `id`, `materia`, `ementa`, `votosSim`/`votosNao`/`totalVotos`, `percentualSim`/`percentualNao`, `status` e `url`. Toda consulta entra como `aberta`; quando a matéria sai de tramitação ela passa a `encerrada` (o conjunto `encerrada`/`todas` cresce com o tempo). Consultas encerradas antes da 1ª ingestão não são capturadas. Aceita `limite` (padrão 20). Para o detalhe de uma consulta chame `senado_ecidadania_obter_consulta` com o `id`; para recortes analíticos (consenso/polarização) use `senado_ecidadania_consultas_analise`.", {
|
package/dist/tools/estrutura.js
CHANGED
|
@@ -49,7 +49,7 @@ export function registerEstruturaTools(server) {
|
|
|
49
49
|
const dica = sugestoes.length
|
|
50
50
|
? ` Você quis dizer: ${sugestoes.map((s) => (s.sigla ? `${s.sigla} (${s.nome})` : s.nome)).join("; ")}?`
|
|
51
51
|
: "";
|
|
52
|
-
return toolError(`Unidade '${params.unidade}' não encontrada na estrutura organizacional.${dica}
|
|
52
|
+
return toolError(`Unidade '${params.unidade}' não encontrada na estrutura organizacional.${dica}`, "nao_encontrado");
|
|
53
53
|
}
|
|
54
54
|
const caminho = ancestrais(indice, alvo.cod).reverse().map(formaOrgao); // da cúpula até o superior imediato
|
|
55
55
|
// Subárvore com nível relativo (BFS a partir do alvo; nível 0 = o próprio alvo, omitido da lista).
|
package/dist/tools/legislacao.js
CHANGED
|
@@ -66,7 +66,7 @@ export function registerLegislacaoTools(server, baseUrl) {
|
|
|
66
66
|
data: params.data,
|
|
67
67
|
});
|
|
68
68
|
if (Object.keys(qp).length === 0) {
|
|
69
|
-
return toolError("É obrigatório informar pelo menos um parâmetro de busca.");
|
|
69
|
+
return toolError("É obrigatório informar pelo menos um parâmetro de busca.", "contrato");
|
|
70
70
|
}
|
|
71
71
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_buscar_legislacao", qp, CACHE_ON_DEMAND, () => upstreamFetch("/legislacao/lista", qp, baseUrl));
|
|
72
72
|
const normas = digArrayRoot(response, [["ListaDocumento", "documentos", "documento"]], "senado_buscar_legislacao").map(parseLegislacaoResumo);
|
|
@@ -89,7 +89,7 @@ export function registerLegislacaoTools(server, baseUrl) {
|
|
|
89
89
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_obter_legislacao", { codigo: params.codigo }, CACHE_ON_DEMAND, () => upstreamFetch(path, {}, baseUrl));
|
|
90
90
|
const docs = digArrayRoot(response, [["DetalheDocumento", "documentos", "documento"]], "senado_obter_legislacao");
|
|
91
91
|
if (docs.length === 0)
|
|
92
|
-
return toolError("Norma não encontrada.");
|
|
92
|
+
return toolError("Norma não encontrada.", "nao_encontrado");
|
|
93
93
|
const prov = provenanceFor("SENADO_LEGIS", baseUrl, path, {
|
|
94
94
|
dataset_id: `norma=${params.codigo}`, retrieved_at: fetchedAt,
|
|
95
95
|
});
|
package/dist/tools/materias.js
CHANGED
|
@@ -177,7 +177,7 @@ export function registerMateriasTools(server, baseUrl) {
|
|
|
177
177
|
dataFimApresentacao: df,
|
|
178
178
|
});
|
|
179
179
|
if (Object.keys(qp).length === 0) {
|
|
180
|
-
return toolError("É obrigatório informar pelo menos um critério de busca.");
|
|
180
|
+
return toolError("É obrigatório informar pelo menos um critério de busca.", "contrato");
|
|
181
181
|
}
|
|
182
182
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_buscar_materias", qp, CACHE_ON_DEMAND, () => upstreamFetch("/processo", qp, baseUrl));
|
|
183
183
|
let todos = ensureArray(response).map(parseProcessoResumo);
|
package/dist/tools/plenario.js
CHANGED
|
@@ -235,7 +235,7 @@ export function registerPlenarioTools(server, baseUrl) {
|
|
|
235
235
|
path = `/plenario/votacao/orientacaoBancada/${params.data}`;
|
|
236
236
|
}
|
|
237
237
|
else {
|
|
238
|
-
return toolError("Informe 'data' ou o período 'dataInicio'/'dataFim'.");
|
|
238
|
+
return toolError("Informe 'data' ou o período 'dataInicio'/'dataFim'.", "contrato");
|
|
239
239
|
}
|
|
240
240
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_orientacao_bancada", { path }, CACHE_ON_DEMAND, () => upstreamFetch(path, {}, baseUrl));
|
|
241
241
|
const r = response;
|
package/dist/tools/processos.js
CHANGED
|
@@ -217,7 +217,7 @@ export function registerProcessosTools(server, baseUrl) {
|
|
|
217
217
|
dataFimApresentacao: ensureISODate(params.dataFimApresentacao),
|
|
218
218
|
});
|
|
219
219
|
if (Object.keys(qp).length === 0) {
|
|
220
|
-
return toolError("É obrigatório informar pelo menos um parâmetro de busca.");
|
|
220
|
+
return toolError("É obrigatório informar pelo menos um parâmetro de busca.", "contrato");
|
|
221
221
|
}
|
|
222
222
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_search_processos", qp, CACHE_ON_DEMAND, () => upstreamFetch("/processo", qp, baseUrl));
|
|
223
223
|
const todos = ensureArray(response).map(parseProcessoResumo);
|
|
@@ -322,7 +322,7 @@ export function registerProcessosTools(server, baseUrl) {
|
|
|
322
322
|
mapper = parseEmendaProcesso;
|
|
323
323
|
}
|
|
324
324
|
if (Object.keys(qp).length === 0) {
|
|
325
|
-
return toolError("Informe pelo menos um filtro (idProcesso, codigoMateria ou período).");
|
|
325
|
+
return toolError("Informe pelo menos um filtro (idProcesso, codigoMateria ou período).", "contrato");
|
|
326
326
|
}
|
|
327
327
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_processo_detalhe", { secao: params.secao, ...qp }, CACHE_ON_DEMAND, () => upstreamFetch(path, qp, baseUrl));
|
|
328
328
|
const todos = ensureArray(response).map(mapper);
|
|
@@ -27,6 +27,7 @@ export function escolherReuniao(reunioes, sigla, periodo) {
|
|
|
27
27
|
if (reunioes.length === 0) {
|
|
28
28
|
return {
|
|
29
29
|
tipo: "nenhuma",
|
|
30
|
+
classe: "nao_encontrado",
|
|
30
31
|
mensagem: `Nenhuma reunião da comissão ${sigla} em ${periodo}. Confira a sigla com ` +
|
|
31
32
|
`senado_listar_comissoes ou amplie o período com senado_reunioes_comissao.`,
|
|
32
33
|
};
|
|
@@ -39,6 +40,7 @@ export function escolherReuniao(reunioes, sigla, periodo) {
|
|
|
39
40
|
.join("; ");
|
|
40
41
|
return {
|
|
41
42
|
tipo: "ambigua",
|
|
43
|
+
classe: "contrato",
|
|
42
44
|
mensagem: `${reunioes.length} reuniões da comissão ${sigla} em ${periodo}. Repita informando ` +
|
|
43
45
|
`codigoReuniao com uma destas: ${lista}${reunioes.length > 10 ? "; …" : ""}`,
|
|
44
46
|
};
|
package/dist/tools/servidores.js
CHANGED
|
@@ -365,7 +365,7 @@ export function registerServidoresTools(server, admBaseUrl) {
|
|
|
365
365
|
const dica = sugestoes.length
|
|
366
366
|
? ` Você quis dizer: ${sugestoes.map((s) => (s.sigla ? `${s.sigla} (${s.nome})` : s.nome)).join("; ")}?`
|
|
367
367
|
: "";
|
|
368
|
-
return toolError(`Unidade '${params.subordinadasA}' não encontrada na estrutura organizacional.${dica}
|
|
368
|
+
return toolError(`Unidade '${params.subordinadasA}' não encontrada na estrutura organizacional.${dica}`, "nao_encontrado");
|
|
369
369
|
}
|
|
370
370
|
const { sob, naoClassificados, afastadosOuEmTransito } = particionarPorUnidade(lista, indice, alvo.cod);
|
|
371
371
|
// A classificação cruza a folha (fonte administrativa) com o organograma (fonte institucional).
|
|
@@ -82,7 +82,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
|
|
|
82
82
|
const por = params.por ?? "comissao";
|
|
83
83
|
if (por === "senador") {
|
|
84
84
|
if (!params.codigoSenador)
|
|
85
|
-
return toolError("Para por=senador, informe 'codigoSenador'.");
|
|
85
|
+
return toolError("Para por=senador, informe 'codigoSenador'.", "contrato");
|
|
86
86
|
const qp = buildParams({
|
|
87
87
|
comissao: params.comissao?.toUpperCase(),
|
|
88
88
|
dataInicio: params.dataInicio,
|
|
@@ -101,7 +101,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
|
|
|
101
101
|
}
|
|
102
102
|
if (por === "materia") {
|
|
103
103
|
if (!params.sigla || !params.numero || !params.ano) {
|
|
104
|
-
return toolError("Para por=materia, informe 'sigla', 'numero' e 'ano'.");
|
|
104
|
+
return toolError("Para por=materia, informe 'sigla', 'numero' e 'ano'.", "contrato");
|
|
105
105
|
}
|
|
106
106
|
const sigla = params.sigla.toUpperCase();
|
|
107
107
|
const qp = buildParams({
|
|
@@ -121,7 +121,7 @@ export function registerVotacaoComissaoTools(server, baseUrl) {
|
|
|
121
121
|
}
|
|
122
122
|
// por === "comissao" (padrão)
|
|
123
123
|
if (!params.siglaComissao)
|
|
124
|
-
return toolError("Para por=comissao, informe 'siglaComissao'.");
|
|
124
|
+
return toolError("Para por=comissao, informe 'siglaComissao'.", "contrato");
|
|
125
125
|
const qp = buildParams({
|
|
126
126
|
dataInicio: params.dataInicio,
|
|
127
127
|
dataFim: params.dataFim,
|
package/dist/tools/votacoes.js
CHANGED
|
@@ -168,7 +168,7 @@ export function registerVotacoesTools(server, baseUrl) {
|
|
|
168
168
|
? " Confira o ano e o código em senado_search_votacoes."
|
|
169
169
|
: " Se a votação é anterior, informe o `ano`; ou obtenha o código em" +
|
|
170
170
|
" senado_search_votacoes. O `codigoVotacao` de senado_orientacao_bancada" +
|
|
171
|
-
" pertence a outro espaço de numeração e não é aceito aqui."));
|
|
171
|
+
" pertence a outro espaço de numeração e não é aceito aqui."), "nao_encontrado");
|
|
172
172
|
}
|
|
173
173
|
catch (e) {
|
|
174
174
|
return errorFrom(e, "Votação não encontrada");
|
package/dist/utils/validation.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Shared validation helpers and response builders. */
|
|
2
2
|
import { incr } from "../metrics.js";
|
|
3
3
|
import { logger } from "./logger.js";
|
|
4
|
+
import { anexarClasse, CLASSE_DO_ERRO } from "../call-shape.js";
|
|
4
5
|
/** Actionable next-step guidance, derived from whether retrying can help. */
|
|
5
6
|
function defaultHint(isRetryable) {
|
|
6
7
|
return isRetryable
|
|
@@ -13,8 +14,11 @@ function defaultHint(isRetryable) {
|
|
|
13
14
|
* override). The same payload is also returned as `structuredContent` so clients can parse
|
|
14
15
|
* errors deterministically — symmetric with toolResult() — the permissive outputSchema is
|
|
15
16
|
* skipped on isError results anyway, and a plain object satisfies it regardless.
|
|
17
|
+
*
|
|
18
|
+
* Sem classe: é o miolo comum de `toolError` (classe declarada) e `errorFrom` (classe
|
|
19
|
+
* pelo tipo da exceção). Não exportado de propósito — ver `toolError`.
|
|
16
20
|
*/
|
|
17
|
-
|
|
21
|
+
function envelopeDeErro(message, isRetryable, hint) {
|
|
18
22
|
incr("toolErrors");
|
|
19
23
|
const payload = { error: message, retryable: isRetryable, hint: hint ?? defaultHint(isRetryable) };
|
|
20
24
|
return {
|
|
@@ -24,17 +28,39 @@ export function toolError(message, isRetryable = false, hint) {
|
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
|
-
*
|
|
31
|
+
* Erro que o HANDLER decide, com a classe de telemetria DECLARADA por quem o escreve.
|
|
32
|
+
*
|
|
33
|
+
* Por que a classe é obrigatória. Até 30/09/2026 este helper não anexava classe nenhuma, e
|
|
34
|
+
* o hook de `instrumentTool` caía na frase (`classifyError`). A frase ecoa argumento do
|
|
35
|
+
* chamador: "Nenhuma reunião da comissão X em <período>" saía `outro`, e "N reuniões …
|
|
36
|
+
* Repita informando codigoReuniao" também — a primeira é ausência respondida, a segunda é
|
|
37
|
+
* pedido ambíguo. Quem escreve a mensagem SABE o que ela significa; com o parâmetro
|
|
38
|
+
* obrigatório, o compilador cobra isso de toda chamada nova. A classe viaja fora do fio
|
|
39
|
+
* (chave-símbolo não enumerável `CLASSE_DO_ERRO`): o que o cliente recebe não muda.
|
|
40
|
+
*
|
|
41
|
+
* - `contrato`: parâmetro que falta, combinação proibida, pedido ambíguo;
|
|
42
|
+
* - `nao_encontrado`: a origem respondeu, e respondeu que não existe.
|
|
43
|
+
*/
|
|
44
|
+
export function toolError(message, classe, isRetryable = false, hint) {
|
|
45
|
+
const r = envelopeDeErro(message, isRetryable, hint);
|
|
46
|
+
Object.defineProperty(r, CLASSE_DO_ERRO, { value: classe, enumerable: false });
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build an error result from any caught value, preserving UpstreamError's retryable flag.
|
|
28
51
|
* Avoids the repetitive `e instanceof Error ? e.message : "..."` pattern.
|
|
29
|
-
*
|
|
52
|
+
* `sufixo` is appended to the user-facing message only (the log keeps the raw message).
|
|
30
53
|
*/
|
|
31
|
-
export function errorFrom(e, fallbackMessage) {
|
|
54
|
+
export function errorFrom(e, fallbackMessage, sufixo = "") {
|
|
32
55
|
const message = e instanceof Error ? e.message : fallbackMessage;
|
|
33
56
|
const retryable = e instanceof Error && "retryable" in e && typeof e.retryable === "boolean"
|
|
34
57
|
? e.retryable
|
|
35
58
|
: false;
|
|
36
59
|
logger.error("tool_error", { message, retryable });
|
|
37
|
-
|
|
60
|
+
// A classe de telemetria vai pelo TIPO do erro, fora do fio (ver
|
|
61
|
+
// CLASSE_DO_ERRO em src/call-shape.ts); sem ela, vale a frase — último
|
|
62
|
+
// recurso, só para exceção que não declara classe.
|
|
63
|
+
return anexarClasse(envelopeDeErro(message + sufixo, retryable), e);
|
|
38
64
|
}
|
|
39
65
|
export function toolResult(data) {
|
|
40
66
|
// Tools advertise an outputSchema (a permissive object schema), so the SDK requires a
|
package/dist/version.js
CHANGED