senado-br-mcp 3.18.0 → 3.18.2
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 +14 -2
- package/dist/tools/comissoes.js +37 -9
- package/dist/tools/deep-research.js +3 -6
- package/dist/tools/ecidadania.js +72 -8
- package/dist/tools/materias.js +13 -4
- package/dist/tools/senadores.js +48 -9
- package/dist/tools/servidores.js +7 -3
- package/dist/tools/supridos.js +54 -10
- package/dist/utils/provenance.js +19 -0
- 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
|
|
@@ -552,13 +564,13 @@ Fields of the `concise` block (per response — one tool, one source; keys in th
|
|
|
552
564
|
| `citation` | Ready-to-use citation string (human-readable) |
|
|
553
565
|
| `license` | Source terms (Dados Abertos do Senado Federal) |
|
|
554
566
|
|
|
555
|
-
The canonical model behind the block also carries `dataset.id` (item/series identifier, e.g. `codigoMateria=137808`)
|
|
567
|
+
The canonical model behind the block also carries `dataset.id` (item/series identifier, e.g. `codigoMateria=137808`) and `api_version`; those are validated on every build and inform `attribution` (below), but are not part of the `concise` projection. Per-field `field_sources` are (contract v1.2, since 3.17.0), on the responses that merge reads.
|
|
556
568
|
|
|
557
569
|
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.
|
|
558
570
|
|
|
559
571
|
**Out-of-band mirror in `_meta`.** The same `provenance` and `attribution` are also mirrored on the result's `_meta` under namespaced keys (`com.sidneybissoli.senado/provenance` and `com.sidneybissoli.senado/attribution`). The MCP spec keeps `_meta` for metadata *about* a result that should not steer the model, which is where the still-incubating trust/attribution work from #711 (split into an experimental extensions track, not yet in core) points; mirroring there gives audit and UI consumers the provenance without reading the model-facing data channel, at no model-token cost, while `structuredContent` keeps it visible so the model can cite the source. The mirror survives the ChatGPT-app profile minimizer (which only strips `structuredContent.meta`).
|
|
560
572
|
|
|
561
|
-
**Field-level granularity.** Most tools are single-source, so one envelope suffices. The few that merge
|
|
573
|
+
**Field-level granularity.** Most tools are single-source, so one envelope suffices. The few that merge reads in one response fill `field_sources` — a list of `{ fields, source_url, dataset_id, data_vintage, retrieved_at, served_from_cache }` attributing specific output fields to their real origin, and the block's `retrieved_at` is the oldest of them. `served_from_cache` is `true`/`false` for reads through the server cache and `null` where the notion does not apply (the bundled org-chart snapshot, the e-Cidadania corpus in D1). The merging tools (since 3.18.2): `senado_obter_materia` `secao=detalhe` (`/processo/{id}` + `ementa` from `/processo` + `relator` from `/processo/relatoria`), `senado_obter_senador` and the Deep Research `fetch` of a senator (biography + `mandatos`/`emExercicio` from `/senador/{codigo}/mandatos`), `senado_reunioes_comissao` over a window that crosses the year (one read per year), `senado_servidores` with `subordinadasA` (payroll + org-chart snapshot), `senado_suprimento_fundos` statistics on `atos-concessao` (acts + beneficiary names from the supridos registry), `senado_ecidadania_obter_evento` (live detail + `comentarios` from the corpus) and `senado_ecidadania_sugerir_tema_enquete` (consultas and ideias corpora). **Only a read that becomes a field of the answer is a sub-source**: a read that only resolves or checks an identifier (a committee acronym → its code, the parent check in `senado_contratacao_detalhe`) is left out, otherwise a cached lookup table would make the whole answer claim to be as old as it. `attribution` always lists every distinct underlying `source_url`.
|
|
562
574
|
|
|
563
575
|
`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.
|
|
564
576
|
|
package/dist/tools/comissoes.js
CHANGED
|
@@ -17,7 +17,7 @@ import { upstreamFetch, UpstreamError } from "../throttle/upstream.js";
|
|
|
17
17
|
import { toolError, errorFrom, ensureArray, safeInt, toBool, normalizeText } from "../utils/validation.js";
|
|
18
18
|
import { digArrayRoot } from "../utils/upstream-parse.js";
|
|
19
19
|
import { escolherReuniao, ehReuniaoInexistente, mensagemCodigoInexistente } from "./resolver-reuniao.js";
|
|
20
|
-
import { provenanceFor, resultWithProvenance } from "../utils/provenance.js";
|
|
20
|
+
import { leituraDoCache, provenanceFor, resultWithProvenance } from "../utils/provenance.js";
|
|
21
21
|
import { CACHE_SEMI_STATIC, CACHE_DYNAMIC, CACHE_ON_DEMAND } from "../types.js";
|
|
22
22
|
export function formatDateYMD(d) {
|
|
23
23
|
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
|
|
@@ -125,6 +125,35 @@ export async function fetchComissaoColegiado(codigo, baseUrl) {
|
|
|
125
125
|
const colegiado = ensureArray(r?.ComissoesCongressoNacional?.Colegiados?.Colegiado)[0];
|
|
126
126
|
return { path, fetchedAt, colegiado };
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Proveniência de `senado_reunioes_comissao`. A fonte não aceita janela que cruza o ano, então
|
|
130
|
+
* a tool lê um recorte por ano, cada um numa entrada de cache própria, e junta as reuniões.
|
|
131
|
+
* Com um recorte só, o bloco é o desse recorte. Com dois, cada um vira sub-fonte das MESMAS
|
|
132
|
+
* linhas (`reunioes`, `count`) com a sua janela e o seu instante, e o topo é o mais antigo
|
|
133
|
+
* (adaptador, contrato §3) — até a 3.18.1 o topo era sempre o do 1º recorte e o instante do
|
|
134
|
+
* 2º era descartado, mesmo quando ele estava em cache havia mais tempo.
|
|
135
|
+
*/
|
|
136
|
+
export function provenanceReunioesComissao(baseUrl, sigla, di, df, partes) {
|
|
137
|
+
const dataset_id = `comissao=${sigla}`;
|
|
138
|
+
const [primeira] = partes;
|
|
139
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
140
|
+
return provenanceFor("SENADO_LEGIS", baseUrl, `/comissao/agenda/${di}/${df}`, {
|
|
141
|
+
dataset_id,
|
|
142
|
+
reference_period: `${di}/${df}`,
|
|
143
|
+
retrieved_at: primeira.fetchedAt,
|
|
144
|
+
...(partes.length > 1
|
|
145
|
+
? {
|
|
146
|
+
field_sources: partes.map((p) => ({
|
|
147
|
+
fields: ["reunioes", "count"],
|
|
148
|
+
source_url: `${base}/comissao/agenda/${p.dataInicio}/${p.dataFim}`,
|
|
149
|
+
dataset_id,
|
|
150
|
+
reference_period: `${p.dataInicio}/${p.dataFim}`,
|
|
151
|
+
...leituraDoCache(p),
|
|
152
|
+
})),
|
|
153
|
+
}
|
|
154
|
+
: {}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
128
157
|
export function registerComissoesTools(server, baseUrl) {
|
|
129
158
|
// E1. senado_listar_comissoes
|
|
130
159
|
server.tool("senado_listar_comissoes", "Lista comissões (colegiados) ativas do Senado, com filtros por `tipo` (permanente, temporaria, cpi, mista) e `ativa`. Retorna `{ count, comissoes }`, cada item com `codigo`, `sigla`, `nome`, `tipo`, `casa` e `ativa`. O endpoint só traz comissões ativas, logo `ativa=false` resulta em lista vazia. Use para descobrir a `sigla` exigida por `senado_obter_comissao` e `senado_reunioes_comissao`.", {
|
|
@@ -229,17 +258,20 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
229
258
|
const yearDi = parseInt(di.slice(0, 4));
|
|
230
259
|
const yearDf = parseInt(df.slice(0, 4));
|
|
231
260
|
let todas = [];
|
|
232
|
-
let
|
|
261
|
+
let partes;
|
|
233
262
|
if (yearDi === yearDf) {
|
|
234
263
|
const res = await cachedFetchWithMeta("senado_reunioes_comissao", { sigla, dataInicio: di, dataFim: df }, CACHE_DYNAMIC, () => upstreamFetch(`/comissao/agenda/${di}/${df}`, {}, baseUrl));
|
|
235
|
-
|
|
264
|
+
partes = [{ dataInicio: di, dataFim: df, ...res }];
|
|
236
265
|
todas = ensureArray(res.value?.AgendaReuniao?.reunioes?.reuniao);
|
|
237
266
|
}
|
|
238
267
|
else {
|
|
239
268
|
// Split: [di..yearDi-12-31] + [yearDf-01-01..df]
|
|
240
269
|
const r1 = await cachedFetchWithMeta("senado_reunioes_comissao_p1", { sigla, dataInicio: di, dataFim: `${yearDi}1231` }, CACHE_DYNAMIC, () => upstreamFetch(`/comissao/agenda/${di}/${yearDi}1231`, {}, baseUrl));
|
|
241
270
|
const r2 = await cachedFetchWithMeta("senado_reunioes_comissao_p2", { sigla, dataInicio: `${yearDf}0101`, dataFim: df }, CACHE_DYNAMIC, () => upstreamFetch(`/comissao/agenda/${yearDf}0101/${df}`, {}, baseUrl));
|
|
242
|
-
|
|
271
|
+
partes = [
|
|
272
|
+
{ dataInicio: di, dataFim: `${yearDi}1231`, ...r1 },
|
|
273
|
+
{ dataInicio: `${yearDf}0101`, dataFim: df, ...r2 },
|
|
274
|
+
];
|
|
243
275
|
todas = [
|
|
244
276
|
...ensureArray(r1.value?.AgendaReuniao?.reunioes?.reuniao),
|
|
245
277
|
...ensureArray(r2.value?.AgendaReuniao?.reunioes?.reuniao),
|
|
@@ -259,11 +291,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
259
291
|
tipo: re.tipo?.descricao || null,
|
|
260
292
|
situacao: re.situacao || null,
|
|
261
293
|
}));
|
|
262
|
-
const prov =
|
|
263
|
-
dataset_id: `comissao=${sigla}`,
|
|
264
|
-
reference_period: `${di}/${df}`,
|
|
265
|
-
retrieved_at: fetchedAt,
|
|
266
|
-
});
|
|
294
|
+
const prov = provenanceReunioesComissao(baseUrl, sigla, di, df, partes);
|
|
267
295
|
return resultWithProvenance({ sigla, periodo: { dataInicio: di, dataFim: df }, count: reunioes.length, reunioes }, prov);
|
|
268
296
|
}
|
|
269
297
|
catch (e) {
|
|
@@ -37,7 +37,7 @@ import { upstreamFetch } from "../throttle/upstream.js";
|
|
|
37
37
|
import { ensureArray } from "../utils/validation.js";
|
|
38
38
|
import { provenanceExtras, provenanceFor } from "../utils/provenance.js";
|
|
39
39
|
import { CACHE_SEMI_STATIC } from "../types.js";
|
|
40
|
-
import { extractParlamentares, fetchSenadorDetalhe, parseSenadorResumo } from "./senadores.js";
|
|
40
|
+
import { extractParlamentares, fetchSenadorDetalhe, parseSenadorResumo, provenanceSenadorDetalhe } from "./senadores.js";
|
|
41
41
|
import { RootNotFoundError } from "../utils/upstream-parse.js";
|
|
42
42
|
import { classifyThrown } from "../call-shape.js";
|
|
43
43
|
import { fetchComissaoColegiado, parseComissaoItem, parseComissaoResumo } from "./comissoes.js";
|
|
@@ -214,13 +214,10 @@ function handlers(baseUrl) {
|
|
|
214
214
|
return null;
|
|
215
215
|
throw e;
|
|
216
216
|
}
|
|
217
|
-
const {
|
|
217
|
+
const { detalhe } = detalheSenador;
|
|
218
218
|
if (!detalhe.codigo)
|
|
219
219
|
return null;
|
|
220
|
-
const prov =
|
|
221
|
-
dataset_id: `codigoParlamentar=${codigo}`,
|
|
222
|
-
retrieved_at: fetchedAt,
|
|
223
|
-
});
|
|
220
|
+
const prov = provenanceSenadorDetalhe(baseUrl, codigo, detalheSenador);
|
|
224
221
|
return {
|
|
225
222
|
document: {
|
|
226
223
|
id,
|
package/dist/tools/ecidadania.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { cachedFetchWithMeta } from "../cache/manager.js";
|
|
14
14
|
import { errorFrom } from "../utils/validation.js";
|
|
15
|
-
import { provenanceEcidadania, provenanceArquimedesVotos, resultWithProvenance } from "../utils/provenance.js";
|
|
15
|
+
import { leituraDoCache, provenanceEcidadania, provenanceArquimedesVotos, resultWithProvenance, withFieldSources, } from "../utils/provenance.js";
|
|
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";
|
|
@@ -46,6 +46,68 @@ export const OBTER_CONSULTA_DATAS_AVISO = "O portal e-Cidadania não publica o p
|
|
|
46
46
|
export function buildConsultaDetalheResult(r) {
|
|
47
47
|
return { ...r, comentarios: null, aviso: OBTER_CONSULTA_DATAS_AVISO };
|
|
48
48
|
}
|
|
49
|
+
/** URL canônica do item raspado (nível 3) quando o scraper a expõe; senão o caminho de fallback. */
|
|
50
|
+
function urlDoItem(item, fallbackPath) {
|
|
51
|
+
const url = item?.url;
|
|
52
|
+
return typeof url === "string" && url.startsWith("http") ? url : fallbackPath;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sub-fonte lida do corpus em D1 (`resolveList`): o instante é o `lastScrapedAt` da meta, o
|
|
56
|
+
* mesmo que `provLista` põe no topo das listas; o D1 não é cache, então `served_from_cache`
|
|
57
|
+
* fica de fora (sai `null`).
|
|
58
|
+
*/
|
|
59
|
+
export function subFonteDoCorpus(pathOrUrl, dataset_id, meta, fields) {
|
|
60
|
+
const ts = meta?.lastScrapedAt;
|
|
61
|
+
return {
|
|
62
|
+
fields,
|
|
63
|
+
source_url: provenanceEcidadania(pathOrUrl).source_url,
|
|
64
|
+
dataset_id,
|
|
65
|
+
...(typeof ts === "string" && ts ? { retrieved_at: ts } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Proveniência de `senado_ecidadania_obter_evento`. O detalhe é raspado ao vivo (cache próprio),
|
|
70
|
+
* mas a página estática não lê a contagem de `comentarios` (carregada por AJAX): ela vem do
|
|
71
|
+
* corpus em D1, lido noutro instante. Até a 3.18.1 o topo era só o do detalhe; agora, quando o
|
|
72
|
+
* corpus dá a contagem, as duas leituras viram sub-fontes e o topo é o mais antigo (adaptador,
|
|
73
|
+
* contrato §3). Evento fora do corpus = `comentarios: null`, sem sub-fonte.
|
|
74
|
+
*/
|
|
75
|
+
export function provenanceEventoDetalhe(item, id, leitura, corpusMeta) {
|
|
76
|
+
const dataset_id = `evento=${id}`;
|
|
77
|
+
const prov = provenanceEcidadania(urlDoItem(item, `/visualizacaoaudiencia?id=${id}`), {
|
|
78
|
+
dataset_id,
|
|
79
|
+
retrieved_at: leitura.fetchedAt,
|
|
80
|
+
});
|
|
81
|
+
if (corpusMeta === undefined)
|
|
82
|
+
return prov;
|
|
83
|
+
return withFieldSources(prov, [
|
|
84
|
+
{
|
|
85
|
+
fields: Object.keys(item).filter((k) => k !== "comentarios"),
|
|
86
|
+
source_url: prov.source_url,
|
|
87
|
+
dataset_id,
|
|
88
|
+
...leituraDoCache(leitura),
|
|
89
|
+
},
|
|
90
|
+
subFonteDoCorpus("/principalaudiencia", "eventos", corpusMeta, ["comentarios"]),
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Proveniência de `senado_ecidadania_sugerir_tema_enquete`: as sugestões cruzam DOIS corpora
|
|
95
|
+
* do D1 (consultas e ideias), cada um com o seu `lastScrapedAt`. Até a 3.18.1 o topo levava só
|
|
96
|
+
* o das consultas e o das ideias era descartado; agora cada corpus é sub-fonte das mesmas
|
|
97
|
+
* contagens e o topo é o mais antigo (adaptador, contrato §3).
|
|
98
|
+
*/
|
|
99
|
+
export function provenanceSugestaoEnquete(consultasMeta, ideiasMeta) {
|
|
100
|
+
const campos = ["sugestoes", "count", "totalAnalisados", "totalQualificados"];
|
|
101
|
+
const ts = consultasMeta?.lastScrapedAt;
|
|
102
|
+
const prov = provenanceEcidadania("/principalmateria", {
|
|
103
|
+
dataset_id: "consultas+ideias",
|
|
104
|
+
retrieved_at: typeof ts === "string" && ts ? ts : undefined,
|
|
105
|
+
});
|
|
106
|
+
return withFieldSources(prov, [
|
|
107
|
+
subFonteDoCorpus("/principalmateria", "consultas", consultasMeta, campos),
|
|
108
|
+
subFonteDoCorpus("/principalideia", "ideias", ideiasMeta, campos),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
49
111
|
// ══════════════════════════════════════════════════════════════════════════
|
|
50
112
|
// Tool registration
|
|
51
113
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -74,9 +136,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
74
136
|
// Detalhes são raspados ao vivo (via cachedFetch): use a URL canônica do próprio item quando
|
|
75
137
|
// o scraper a expõe (nível 3), com o fetchedAt do cache como retrieved_at.
|
|
76
138
|
function provDetalhe(item, fallbackPath, dataset_id, fetchedAt) {
|
|
77
|
-
|
|
78
|
-
const alvo = typeof url === "string" && url.startsWith("http") ? url : fallbackPath;
|
|
79
|
-
return provenanceEcidadania(alvo, { dataset_id, retrieved_at: fetchedAt });
|
|
139
|
+
return provenanceEcidadania(urlDoItem(item, fallbackPath), { dataset_id, retrieved_at: fetchedAt });
|
|
80
140
|
}
|
|
81
141
|
function ecidadaniaError(e) {
|
|
82
142
|
// Mesmo caminho de toda tool (errorFrom: log cru, classe pelo TIPO da exceção via
|
|
@@ -266,22 +326,26 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
266
326
|
// G9. senado_ecidadania_obter_evento
|
|
267
327
|
server.tool("senado_ecidadania_obter_evento", "Obtém o detalhe completo de um evento interativo do e-Cidadania (audiência, sabatina, live). Retorna um objeto com `id`, `titulo`, `descricao`, `data`, `hora`, `comissao` e `comissaoNomeCompleto`, `local`, `status` (`agendado`/`encerrado`/`cancelado`), `comentarios`, `url`, mais `pauta` (até 15 itens), `convidados` e `videoUrl` (embed do YouTube quando houver, senão `null`) — campos não preenchidos vêm `null` e `id` inexistente retorna erro. Obtenha o `id` antes via `senado_ecidadania_listar_eventos`. Para apenas listar/rankear eventos (sem descrição/pauta/convidados) use `senado_ecidadania_listar_eventos`, não esta.", { id: z.number().int().positive().describe("Identificador do evento (campo `id` de senado_ecidadania_listar_eventos)") }, async (params) => {
|
|
268
328
|
try {
|
|
269
|
-
const
|
|
329
|
+
const leitura = await cachedFetchWithMeta("ecidadania_evento", { id: params.id }, CACHE_ON_DEMAND, () => obterEventoInternal(params.id));
|
|
330
|
+
const r = leitura.value;
|
|
270
331
|
writeDetalheThrough(db, ctx, "eventos", params.id, r);
|
|
271
332
|
// The static detail scrape can't read the AJAX-loaded comentarios count (always 0);
|
|
272
333
|
// the corpus carries the canonical count from the AJAX fragment (estudo A3 — the
|
|
273
334
|
// listing count is spurious, 0 in ~82%). Splice it in, or null when the event is not
|
|
274
335
|
// in the corpus (unknown, not a spurious 0).
|
|
275
336
|
const detalhe = { ...r };
|
|
337
|
+
let corpusMeta;
|
|
276
338
|
try {
|
|
277
|
-
const { items } = await resolveList(db, "eventos", corpusStaleMaxMin(), () => listarEventosInternal({ limite: 100 }), undefined, CORPUS_RESOLVE);
|
|
339
|
+
const { items, meta } = await resolveList(db, "eventos", corpusStaleMaxMin(), () => listarEventosInternal({ limite: 100 }), undefined, CORPUS_RESOLVE);
|
|
278
340
|
const corpusItem = items.find((e) => e.id === params.id);
|
|
279
341
|
detalhe.comentarios = corpusItem ? corpusItem.comentarios : null;
|
|
342
|
+
if (corpusItem)
|
|
343
|
+
corpusMeta = meta;
|
|
280
344
|
}
|
|
281
345
|
catch {
|
|
282
346
|
detalhe.comentarios = null;
|
|
283
347
|
}
|
|
284
|
-
return resultWithProvenance(tagUntrustedFields("eventos", detalhe),
|
|
348
|
+
return resultWithProvenance(tagUntrustedFields("eventos", detalhe), provenanceEventoDetalhe(r, params.id, leitura, corpusMeta));
|
|
285
349
|
}
|
|
286
350
|
catch (e) {
|
|
287
351
|
return ecidadaniaError(e);
|
|
@@ -352,7 +416,7 @@ export function registerECidadaniaTools(server, _baseUrl, env, ctx) {
|
|
|
352
416
|
totalQualificados: sugestoes.length,
|
|
353
417
|
sugestoes: retornadas,
|
|
354
418
|
meta: { consultas: cRes.meta, ideias: iRes.meta },
|
|
355
|
-
},
|
|
419
|
+
}, provenanceSugestaoEnquete(cRes.meta, iRes.meta));
|
|
356
420
|
}
|
|
357
421
|
catch (e) {
|
|
358
422
|
return ecidadaniaError(e);
|
package/dist/tools/materias.js
CHANGED
|
@@ -145,8 +145,15 @@ export function provenanceMateriaDetalhe(baseUrl, a) {
|
|
|
145
145
|
source_url: `${base}/processo/${a.idProcesso}`,
|
|
146
146
|
dataset_id,
|
|
147
147
|
retrieved_at: a.detalheFetchedAt,
|
|
148
|
+
served_from_cache: a.doCache?.detalhe,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
fields: ["ementa"],
|
|
152
|
+
source_url: `${base}/processo`,
|
|
153
|
+
dataset_id,
|
|
154
|
+
retrieved_at: a.resumoFetchedAt,
|
|
155
|
+
served_from_cache: a.doCache?.resumo,
|
|
148
156
|
},
|
|
149
|
-
{ fields: ["ementa"], source_url: `${base}/processo`, dataset_id, retrieved_at: a.resumoFetchedAt },
|
|
150
157
|
];
|
|
151
158
|
if (a.relatoriaFetchedAt !== undefined) {
|
|
152
159
|
fieldSources.push({
|
|
@@ -154,6 +161,7 @@ export function provenanceMateriaDetalhe(baseUrl, a) {
|
|
|
154
161
|
source_url: `${base}/processo/relatoria`,
|
|
155
162
|
dataset_id,
|
|
156
163
|
retrieved_at: a.relatoriaFetchedAt,
|
|
164
|
+
served_from_cache: a.doCache?.relatoria,
|
|
157
165
|
});
|
|
158
166
|
}
|
|
159
167
|
return provenanceFor("SENADO_LEGIS", baseUrl, `/processo/${a.idProcesso}`, {
|
|
@@ -164,12 +172,12 @@ export function provenanceMateriaDetalhe(baseUrl, a) {
|
|
|
164
172
|
});
|
|
165
173
|
}
|
|
166
174
|
async function resolveProcesso(codigoMateria, baseUrl) {
|
|
167
|
-
const { value: response, fetchedAt } = await cachedFetchWithMeta("_processo_por_materia", { codigoMateria }, CACHE_ON_DEMAND, () => upstreamFetch("/processo", { codigoMateria: String(codigoMateria) }, baseUrl));
|
|
175
|
+
const { value: response, fetchedAt, fromCache } = await cachedFetchWithMeta("_processo_por_materia", { codigoMateria }, CACHE_ON_DEMAND, () => upstreamFetch("/processo", { codigoMateria: String(codigoMateria) }, baseUrl));
|
|
168
176
|
const item = ensureArray(response)[0];
|
|
169
177
|
if (!item || !item.id) {
|
|
170
178
|
throw new Error(`Matéria ${codigoMateria} não encontrada na API de processos`);
|
|
171
179
|
}
|
|
172
|
-
return { item, fetchedAt };
|
|
180
|
+
return { item, fetchedAt, fromCache };
|
|
173
181
|
}
|
|
174
182
|
/**
|
|
175
183
|
* Zero resultado por palavra-chave não é prova de que o tema não existe: a
|
|
@@ -312,7 +320,7 @@ export function registerMateriasTools(server, baseUrl) {
|
|
|
312
320
|
cachedFetchWithMeta("_processo_relatoria", { codigoMateria: params.codigoMateria }, CACHE_ON_DEMAND, () => upstreamFetch("/processo/relatoria", { codigoMateria: String(params.codigoMateria) }, baseUrl)).catch(() => null),
|
|
313
321
|
]);
|
|
314
322
|
const resumo = resumoRes.item;
|
|
315
|
-
const { value: detalheRes, fetchedAt } = await cachedFetchWithMeta("senado_obter_materia", { idProcesso: resumo.id }, CACHE_ON_DEMAND, () => upstreamFetch(`/processo/${resumo.id}`, {}, baseUrl));
|
|
323
|
+
const { value: detalheRes, fetchedAt, fromCache } = await cachedFetchWithMeta("senado_obter_materia", { idProcesso: resumo.id }, CACHE_ON_DEMAND, () => upstreamFetch(`/processo/${resumo.id}`, {}, baseUrl));
|
|
316
324
|
const detalhe = parseProcessoDetalhe(detalheRes);
|
|
317
325
|
const relator = pickRelatorAtual(ensureArray(relatoriasRes?.value));
|
|
318
326
|
const prov = provenanceMateriaDetalhe(baseUrl, {
|
|
@@ -322,6 +330,7 @@ export function registerMateriasTools(server, baseUrl) {
|
|
|
322
330
|
detalheFetchedAt: fetchedAt,
|
|
323
331
|
resumoFetchedAt: resumoRes.fetchedAt,
|
|
324
332
|
relatoriaFetchedAt: relator ? relatoriasRes?.fetchedAt : undefined,
|
|
333
|
+
doCache: { detalhe: fromCache, resumo: resumoRes.fromCache, relatoria: relatoriasRes?.fromCache },
|
|
325
334
|
});
|
|
326
335
|
return resultWithProvenance({
|
|
327
336
|
...detalhe,
|
package/dist/tools/senadores.js
CHANGED
|
@@ -11,7 +11,7 @@ import { cachedFetchWithMeta } from "../cache/manager.js";
|
|
|
11
11
|
import { upstreamFetch } from "../throttle/upstream.js";
|
|
12
12
|
import { errorFrom, ensureArray, normalizeText, safeInt } from "../utils/validation.js";
|
|
13
13
|
import { digArrayRoot, digObjectRoot } from "../utils/upstream-parse.js";
|
|
14
|
-
import { provenanceFor, resultWithProvenance, toBrasiliaIso } from "../utils/provenance.js";
|
|
14
|
+
import { leituraDoCache, provenanceFor, resultWithProvenance, toBrasiliaIso } from "../utils/provenance.js";
|
|
15
15
|
import { CACHE_SEMI_STATIC, CACHE_DYNAMIC, CACHE_ON_DEMAND } from "../types.js";
|
|
16
16
|
export function parseSenadorResumo(parlamentar) {
|
|
17
17
|
const id = parlamentar.IdentificacaoParlamentar || parlamentar;
|
|
@@ -210,7 +210,7 @@ export function matchesNome(s, nome) {
|
|
|
210
210
|
*/
|
|
211
211
|
export async function fetchSenadorDetalhe(codigoSenador, baseUrl) {
|
|
212
212
|
const path = `/senador/${codigoSenador}`;
|
|
213
|
-
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_obter_senador", { codigo: codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(path, {}, baseUrl));
|
|
213
|
+
const { value: response, fetchedAt, fromCache } = await cachedFetchWithMeta("senado_obter_senador", { codigo: codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(path, {}, baseUrl));
|
|
214
214
|
// Ausência com HTTP 200: para um código que não existe o upstream devolve
|
|
215
215
|
// `{"DetalheParlamentar":{...Metadados}}` — envelope e metadados presentes,
|
|
216
216
|
// SEM o nó `Parlamentar` (medido em 24/09/2026: 200, 304 bytes). O
|
|
@@ -228,10 +228,14 @@ export async function fetchSenadorDetalhe(codigoSenador, baseUrl) {
|
|
|
228
228
|
// É também o único endpoint que carrega os `Exercicios`, de onde sai o
|
|
229
229
|
// `emExercicio` — lista vazia (falha do sub-endpoint) vira `null`, não `false`.
|
|
230
230
|
let mandatosBrutos = [];
|
|
231
|
+
// Leitura dos mandatos, só quando ela de fato deu os campos `mandatos`/`emExercicio`
|
|
232
|
+
// (falha = lista vazia e `null`, que não vêm de fonte nenhuma).
|
|
233
|
+
let mandatosLeitura;
|
|
231
234
|
try {
|
|
232
235
|
const mandatosPath = `/senador/${codigoSenador}/mandatos`;
|
|
233
|
-
const { value: mResp } = await cachedFetchWithMeta("senado_obter_senador_mandatos", { codigo: codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(mandatosPath, {}, baseUrl));
|
|
236
|
+
const { value: mResp, fetchedAt: mFetchedAt, fromCache: mFromCache } = await cachedFetchWithMeta("senado_obter_senador_mandatos", { codigo: codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(mandatosPath, {}, baseUrl));
|
|
234
237
|
mandatosBrutos = digArrayRoot(mResp, [["MandatoParlamentar", "Parlamentar", "Mandatos", "Mandato"]], "senado_obter_senador:mandatos");
|
|
238
|
+
mandatosLeitura = { fetchedAt: mFetchedAt, fromCache: mFromCache };
|
|
235
239
|
}
|
|
236
240
|
catch {
|
|
237
241
|
// sub-endpoint unavailable — keep the (empty) mandatos from the detail
|
|
@@ -240,6 +244,8 @@ export async function fetchSenadorDetalhe(codigoSenador, baseUrl) {
|
|
|
240
244
|
return {
|
|
241
245
|
path,
|
|
242
246
|
fetchedAt,
|
|
247
|
+
fromCache,
|
|
248
|
+
mandatosLeitura,
|
|
243
249
|
detalhe: {
|
|
244
250
|
...detalhe,
|
|
245
251
|
emExercicio: derivarEmExercicio(mandatosBrutos, hoje),
|
|
@@ -247,6 +253,42 @@ export async function fetchSenadorDetalhe(codigoSenador, baseUrl) {
|
|
|
247
253
|
},
|
|
248
254
|
};
|
|
249
255
|
}
|
|
256
|
+
/** Campos do detalhe que vêm de `/senador/{codigo}/mandatos`, não do detalhe biográfico. */
|
|
257
|
+
const CAMPOS_DOS_MANDATOS = ["mandatos", "emExercicio"];
|
|
258
|
+
/**
|
|
259
|
+
* Proveniência de `senado_obter_senador` (e do `fetch` `senador:` do Deep Research, que
|
|
260
|
+
* renderiza o mesmo registro no texto do documento). O registro funde DUAS leituras, cada
|
|
261
|
+
* uma de uma entrada de cache própria: o detalhe biográfico (`/senador/{codigo}`, topo) e os
|
|
262
|
+
* mandatos (`/senador/{codigo}/mandatos`), de onde saem `mandatos` e `emExercicio`. Até a
|
|
263
|
+
* 3.18.1 o instante dos mandatos era descartado e o topo era o do detalhe — com os mandatos
|
|
264
|
+
* em cache desde antes, o bloco dizia que `emExercicio` fora lido mais recentemente do que
|
|
265
|
+
* foi. Com as duas como sub-fontes o topo passa a ser o mais antigo (adaptador, contrato §3).
|
|
266
|
+
* Mandatos que falharam não viram sub-fonte: a resposta leva `[]` e `null`, sem fonte.
|
|
267
|
+
*/
|
|
268
|
+
export function provenanceSenadorDetalhe(baseUrl, codigoSenador, leitura) {
|
|
269
|
+
const dataset_id = `codigoParlamentar=${codigoSenador}`;
|
|
270
|
+
const topo = { dataset_id, retrieved_at: leitura.fetchedAt };
|
|
271
|
+
if (!leitura.mandatosLeitura)
|
|
272
|
+
return provenanceFor("SENADO_LEGIS", baseUrl, leitura.path, topo);
|
|
273
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
274
|
+
return provenanceFor("SENADO_LEGIS", baseUrl, leitura.path, {
|
|
275
|
+
...topo,
|
|
276
|
+
field_sources: [
|
|
277
|
+
{
|
|
278
|
+
fields: Object.keys(leitura.detalhe).filter((k) => !CAMPOS_DOS_MANDATOS.includes(k)),
|
|
279
|
+
source_url: `${base}${leitura.path}`,
|
|
280
|
+
dataset_id,
|
|
281
|
+
...leituraDoCache(leitura),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
fields: CAMPOS_DOS_MANDATOS,
|
|
285
|
+
source_url: `${base}${leitura.path}/mandatos`,
|
|
286
|
+
dataset_id,
|
|
287
|
+
...leituraDoCache(leitura.mandatosLeitura),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
250
292
|
export function registerSenadoresTools(server, baseUrl) {
|
|
251
293
|
// A1. senado_listar_senadores (filtro `nome` substitui a antiga busca por nome)
|
|
252
294
|
server.tool("senado_listar_senadores", "Use para pedidos como 'liste os senadores em exercício', 'senadores atuais', 'lista atual de senadores' ou filtros por UF/partido. Lista senadores em exercício ou de uma legislatura específica, com filtros opcionais por `nome`, `uf` e `partido`. Retorna `{ count, senadores }`, cada item com `codigo`, `nome`, `nomeCompleto`, `partido`, `uf`, `foto` e `emExercicio`, mais proveniência oficial do endpoint `/senador/lista/atual`. Use `emExercicio` (padrão `true`) ou `legislatura` para escolher o conjunto; `nome` faz correspondência parcial ignorando acentos/maiúsculas (use quando você só tem o nome e precisa do `codigo`); `uf`/`partido` filtram localmente. Use o `codigo` em `senado_obter_senador` ou `senado_votacoes_senador`. Para senadores fora de exercício veja `senado_senadores_afastados`.", {
|
|
@@ -287,12 +329,9 @@ export function registerSenadoresTools(server, baseUrl) {
|
|
|
287
329
|
codigoSenador: z.number().int().positive().describe("Código único do senador no sistema do Senado"),
|
|
288
330
|
}, async (params) => {
|
|
289
331
|
try {
|
|
290
|
-
const
|
|
291
|
-
const prov =
|
|
292
|
-
|
|
293
|
-
retrieved_at: fetchedAt,
|
|
294
|
-
});
|
|
295
|
-
return resultWithProvenance(detalhe, prov);
|
|
332
|
+
const leitura = await fetchSenadorDetalhe(params.codigoSenador, baseUrl);
|
|
333
|
+
const prov = provenanceSenadorDetalhe(baseUrl, params.codigoSenador, leitura);
|
|
334
|
+
return resultWithProvenance(leitura.detalhe, prov);
|
|
296
335
|
}
|
|
297
336
|
catch (e) {
|
|
298
337
|
return errorFrom(e, "Senador não encontrado");
|
package/dist/tools/servidores.js
CHANGED
|
@@ -343,14 +343,18 @@ export function particionarPorUnidade(lista, indice, codUnidade) {
|
|
|
343
343
|
* das duas (derivado pelo adaptador, contrato §3), e a folha entra como sub-fonte própria
|
|
344
344
|
* para que o seu instante não suma do bloco. Antes da 3.17.0 o topo era o da folha, mais
|
|
345
345
|
* novo que o snapshot — com a v1.2 ligada, a lib lançaria e a tool cairia.
|
|
346
|
+
*
|
|
347
|
+
* `folhaDoCache` é o `fromCache` da leitura da folha; o snapshot embutido não é cache nem
|
|
348
|
+
* busca, então a sub-fonte dele não diz (`served_from_cache: null`).
|
|
346
349
|
*/
|
|
347
|
-
export function provenanceServidoresSubordinadas(prov, situacao) {
|
|
350
|
+
export function provenanceServidoresSubordinadas(prov, situacao, folhaDoCache) {
|
|
348
351
|
return withFieldSources(prov, [
|
|
349
352
|
{
|
|
350
353
|
fields: ["servidores", "total", "count", "naoClassificados", "afastadosOuEmTransito"],
|
|
351
354
|
source_url: prov.source_url,
|
|
352
355
|
dataset_id: `servidores; situacao=${situacao}`,
|
|
353
356
|
retrieved_at: prov.retrieved_at,
|
|
357
|
+
served_from_cache: folhaDoCache,
|
|
354
358
|
},
|
|
355
359
|
{
|
|
356
360
|
fields: ["subordinadasA", "total", "naoClassificados"],
|
|
@@ -373,7 +377,7 @@ export function registerServidoresTools(server, admBaseUrl) {
|
|
|
373
377
|
}, async (params) => {
|
|
374
378
|
try {
|
|
375
379
|
const situacao = params.situacao ?? "ativos";
|
|
376
|
-
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_servidores", { situacao }, CACHE_SEMI_STATIC, () => admFetchLarge(`/servidores/servidores/${situacao}`, {}, admBaseUrl));
|
|
380
|
+
const { value: response, fetchedAt, fromCache } = await cachedFetchWithMeta("senado_servidores", { situacao }, CACHE_SEMI_STATIC, () => admFetchLarge(`/servidores/servidores/${situacao}`, {}, admBaseUrl));
|
|
377
381
|
let lista = ensureArray(response).map(parseServidor);
|
|
378
382
|
if (params.nome)
|
|
379
383
|
lista = lista.filter((s) => matchesFiltro(s.nome, params.nome));
|
|
@@ -398,7 +402,7 @@ export function registerServidoresTools(server, admBaseUrl) {
|
|
|
398
402
|
return toolError(`Unidade '${params.subordinadasA}' não encontrada na estrutura organizacional.${dica}`, "nao_encontrado");
|
|
399
403
|
}
|
|
400
404
|
const { sob, naoClassificados, afastadosOuEmTransito } = particionarPorUnidade(lista, indice, alvo.cod);
|
|
401
|
-
const provComEstrutura = provenanceServidoresSubordinadas(prov, situacao);
|
|
405
|
+
const provComEstrutura = provenanceServidoresSubordinadas(prov, situacao, fromCache);
|
|
402
406
|
return resultWithProvenance({
|
|
403
407
|
situacao,
|
|
404
408
|
subordinadasA: { sigla: alvo.sigla, nome: alvo.nome },
|
package/dist/tools/supridos.js
CHANGED
|
@@ -9,7 +9,7 @@ import { z } from "zod";
|
|
|
9
9
|
import { cachedFetchWithMeta } from "../cache/manager.js";
|
|
10
10
|
import { admFetch } from "../throttle/adm.js";
|
|
11
11
|
import { errorFrom, ensureArray } from "../utils/validation.js";
|
|
12
|
-
import { provenanceFor, resultWithProvenance } from "../utils/provenance.js";
|
|
12
|
+
import { leituraDoCache, provenanceFor, resultWithProvenance, withFieldSources } from "../utils/provenance.js";
|
|
13
13
|
import { computarEstatisticas, arredondarEstatisticas, arredondarEntradas, } from "../utils/estatisticas.js";
|
|
14
14
|
import { CACHE_STATIC } from "../types.js";
|
|
15
15
|
import { matchesFiltro } from "./contratacoes.js";
|
|
@@ -172,22 +172,66 @@ export function estatisticasSuprimento(lista, opts) {
|
|
|
172
172
|
/**
|
|
173
173
|
* Build a `codigo -> nome` map of supridos for a year, from the registry (tipo=supridos, ~1 MB),
|
|
174
174
|
* so `atos-concessao` statistics can identify the beneficiary by NAME instead of the internal code.
|
|
175
|
-
* Cached by year; a fetch failure degrades to an empty map (entries fall back to name = null)
|
|
175
|
+
* Cached by year; a fetch failure degrades to an empty map (entries fall back to name = null)
|
|
176
|
+
* and no `leitura` — the names then come from nowhere, so there is no sub-source to declare.
|
|
176
177
|
*/
|
|
177
178
|
export async function nomesSupridos(ano, admBaseUrl) {
|
|
178
|
-
const
|
|
179
|
+
const nomes = new Map();
|
|
179
180
|
try {
|
|
180
|
-
const { value } = await cachedFetchWithMeta("senado_suprimento_fundos_nomes", { ano }, CACHE_STATIC, () => admFetch(PATHS["supridos"](ano), {}, admBaseUrl));
|
|
181
|
+
const { value, fetchedAt, fromCache } = await cachedFetchWithMeta("senado_suprimento_fundos_nomes", { ano }, CACHE_STATIC, () => admFetch(PATHS["supridos"](ano), {}, admBaseUrl));
|
|
181
182
|
for (const s of ensureArray(value)) {
|
|
182
183
|
const cod = s?.codigo != null ? String(s.codigo) : null;
|
|
183
184
|
if (cod && s?.nome)
|
|
184
|
-
|
|
185
|
+
nomes.set(cod, String(s.nome));
|
|
185
186
|
}
|
|
187
|
+
return { nomes, leitura: { fetchedAt, fromCache } };
|
|
186
188
|
}
|
|
187
189
|
catch {
|
|
188
190
|
// best-effort enrichment; the ranking still works without names
|
|
191
|
+
return { nomes };
|
|
189
192
|
}
|
|
190
|
-
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Which fields of the `atos-concessao` statistics carry the suprido's NAME: the groups when
|
|
196
|
+
* grouping by suprido; nothing when grouping by another (valid) dimension; top/bottom otherwise
|
|
197
|
+
* (no grouping, or an invalid grouping that falls back to the row distribution).
|
|
198
|
+
*/
|
|
199
|
+
export function camposComNomeDeSuprido(agruparPor) {
|
|
200
|
+
if (agruparPor === "suprido")
|
|
201
|
+
return ["grupos"];
|
|
202
|
+
if (agruparPor != null && CHAVES_POR_TIPO["atos-concessao"][agruparPor])
|
|
203
|
+
return [];
|
|
204
|
+
return ["top", "bottom"];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Proveniência de `senado_suprimento_fundos` com `estatisticas` em atos de concessão: os atos
|
|
208
|
+
* (`prov`, topo) são cruzados com o cadastro de supridos do ano (`/supridos/{ano}`, outra
|
|
209
|
+
* entrada de cache) para identificar cada beneficiário pelo NOME. Até a 3.18.1 o instante do
|
|
210
|
+
* cadastro era descartado. Agora, quando os nomes aparecem na resposta, os atos e o cadastro
|
|
211
|
+
* viram sub-fontes e o topo é o mais antigo (adaptador, contrato §3). Sem leitura do cadastro
|
|
212
|
+
* (falhou) ou sem campo com nome (agrupado por outra dimensão), o bloco fica o dos atos.
|
|
213
|
+
*/
|
|
214
|
+
export function provenanceSuprimentoComNomes(prov, atos, ano, admBaseUrl, nomes, agruparPor) {
|
|
215
|
+
const campos = camposComNomeDeSuprido(agruparPor);
|
|
216
|
+
if (!nomes.leitura || campos.length === 0)
|
|
217
|
+
return prov;
|
|
218
|
+
return withFieldSources(prov, [
|
|
219
|
+
{
|
|
220
|
+
fields: agruparPor === "suprido" ? ["total", "totalGrupos", "grupos"] : ["total", "distribuicao", "top", "bottom"],
|
|
221
|
+
source_url: prov.source_url,
|
|
222
|
+
dataset_id: prov.dataset.id ?? undefined,
|
|
223
|
+
reference_period: String(ano),
|
|
224
|
+
retrieved_at: prov.retrieved_at,
|
|
225
|
+
served_from_cache: atos.fromCache,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
fields: campos,
|
|
229
|
+
source_url: `${admBaseUrl.replace(/\/$/, "")}/api/v1${PATHS["supridos"](ano)}`,
|
|
230
|
+
dataset_id: `suprimento; tipo=supridos; ano=${ano}`,
|
|
231
|
+
reference_period: String(ano),
|
|
232
|
+
...leituraDoCache(nomes.leitura),
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
191
235
|
}
|
|
192
236
|
export function registerSupridosTools(server, admBaseUrl) {
|
|
193
237
|
// R1. senado_suprimento_fundos
|
|
@@ -209,7 +253,7 @@ export function registerSupridosTools(server, admBaseUrl) {
|
|
|
209
253
|
try {
|
|
210
254
|
const tipo = params.tipo ?? "supridos";
|
|
211
255
|
const path = PATHS[tipo](params.ano);
|
|
212
|
-
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_suprimento_fundos", { ano: params.ano, tipo }, CACHE_STATIC, () => admFetch(path, {}, admBaseUrl));
|
|
256
|
+
const { value: response, fetchedAt, fromCache } = await cachedFetchWithMeta("senado_suprimento_fundos", { ano: params.ano, tipo }, CACHE_STATIC, () => admFetch(path, {}, admBaseUrl));
|
|
213
257
|
let lista = ensureArray(response);
|
|
214
258
|
if (params.filtro) {
|
|
215
259
|
const f = params.filtro;
|
|
@@ -234,14 +278,14 @@ export function registerSupridosTools(server, admBaseUrl) {
|
|
|
234
278
|
}, prov);
|
|
235
279
|
}
|
|
236
280
|
// atos-concessao: join the supridos registry so entries carry the beneficiary's NAME.
|
|
237
|
-
const
|
|
281
|
+
const nomes = tipo === "atos-concessao" ? await nomesSupridos(params.ano, admBaseUrl) : undefined;
|
|
238
282
|
return resultWithProvenance({
|
|
239
283
|
ano: params.ano,
|
|
240
284
|
tipo,
|
|
241
285
|
modo: "estatisticas",
|
|
242
286
|
total: lista.length,
|
|
243
|
-
...estatisticasSuprimento(lista, { tipo, campo: params.campo, agruparPor: params.agruparPor, topN: params.topN ?? 10, nomePorCodigo }),
|
|
244
|
-
}, prov);
|
|
287
|
+
...estatisticasSuprimento(lista, { tipo, campo: params.campo, agruparPor: params.agruparPor, topN: params.topN ?? 10, nomePorCodigo: nomes?.nomes }),
|
|
288
|
+
}, nomes ? provenanceSuprimentoComNomes(prov, { fromCache }, params.ano, admBaseUrl, nomes, params.agruparPor) : prov);
|
|
245
289
|
}
|
|
246
290
|
return resultWithProvenance({
|
|
247
291
|
ano: params.ano,
|
package/dist/utils/provenance.js
CHANGED
|
@@ -78,6 +78,17 @@ const toCanonicalFieldSource = (fs) => ({
|
|
|
78
78
|
dataset_id: fs.dataset_id ?? null,
|
|
79
79
|
data_vintage: fs.reference_period ?? null,
|
|
80
80
|
retrieved_at: fs.retrieved_at ?? null,
|
|
81
|
+
served_from_cache: fs.served_from_cache ?? null,
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Instante e origem de uma leitura de `cachedFetchWithMeta`, nos nomes da sub-fonte:
|
|
85
|
+
* `{ retrieved_at, served_from_cache }`. Entrada de cache sem envelope (anterior ao
|
|
86
|
+
* `fetchedAt` persistido) volta com `fromCache: true` e o instante de AGORA — o `true`
|
|
87
|
+
* continua verdadeiro; o instante é o melhor que a camada sabe.
|
|
88
|
+
*/
|
|
89
|
+
export const leituraDoCache = (m) => ({
|
|
90
|
+
retrieved_at: m.fetchedAt,
|
|
91
|
+
served_from_cache: m.fromCache,
|
|
81
92
|
});
|
|
82
93
|
/**
|
|
83
94
|
* `retrieved_at` do topo de uma resposta que funde sub-fontes: o MAIS ANTIGO entre o
|
|
@@ -237,6 +248,14 @@ export function provenanceArquimedesVotos(extra) {
|
|
|
237
248
|
* tools cuja resposta funde campos de mais de um endpoint upstream — o `source_url` de topo
|
|
238
249
|
* cobre o grosso do payload e cada `FieldSource` atribui os campos específicos à sua origem.
|
|
239
250
|
* O envelope é re-validado inteiro no contrato (build).
|
|
251
|
+
*
|
|
252
|
+
* REGRA (decisão do dono, 13/10/2026): só entra como sub-fonte a leitura que VIRA CAMPO da
|
|
253
|
+
* resposta. Leitura só de resolução ou checagem — sigla→código da comissão, o id do processo
|
|
254
|
+
* antes da tramitação, a confirmação do pai em `senado_contratacao_detalhe` — fica de fora.
|
|
255
|
+
* Se entrasse, a regra do topo = mais antigo faria a resposta inteira dizer "dados de ontem"
|
|
256
|
+
* porque a tabela de siglas está em cache desde ontem, com os campos lidos agora (aviso falso
|
|
257
|
+
* de desatualização). Custo aceito: uma tradução errada por cache velho (código recriado na
|
|
258
|
+
* fonte) não aparece no bloco — risco baixo, os códigos são estáveis.
|
|
240
259
|
*/
|
|
241
260
|
export function withFieldSources(prov, fieldSources) {
|
|
242
261
|
if (fieldSources.length === 0)
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "senado-br-mcp",
|
|
3
|
-
"version": "3.18.
|
|
3
|
+
"version": "3.18.2",
|
|
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",
|