senado-br-mcp 3.6.0 → 3.7.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/README.md +28 -9
- package/README.pt-BR.md +69 -11
- package/dist/data/estrutura-organizacional.js +26 -32
- package/dist/discovery.js +90 -0
- package/dist/landing.js +63 -4
- package/dist/legal.js +9 -1
- package/dist/resources.js +6 -5
- package/dist/server.js +4 -0
- package/dist/tool-titles.js +5 -1
- package/dist/tools/comissoes.js +31 -12
- package/dist/tools/deep-research.js +280 -0
- package/dist/tools/senadores.js +26 -16
- package/dist/utils/provenance.js +12 -0
- package/dist/version.js +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
|
-

|
|
6
6
|
[](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml)
|
|
7
7
|
[](https://registry.modelcontextprotocol.io)
|
|
8
8
|
[](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
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.
|
|
19
19
|
|
|
20
|
-
It exposes **
|
|
20
|
+
It exposes **69 tools**, **4 prompts**, and **5 resources** across two domains:
|
|
21
21
|
|
|
22
22
|
- **Legislative** — senators; bills and their tramitation; votes; committees; plenary sessions, results and presidential vetoes; party-bloc voting orientation; speeches and stenographic transcripts; blocs and leadership; federal legislation; and citizen participation via the e-Cidadania portal.
|
|
23
23
|
- **Administrative** — CEAPS parliamentary-quota expenses; housing allowance; civil servants and payroll; overtime; interns; procurement contracts and biddings; outsourced staff; petty-cash funds; and budget execution.
|
|
@@ -68,6 +68,16 @@ Public legal URLs for app review:
|
|
|
68
68
|
- Privacy policy: `https://senado.sidneybissoli.com/privacy`
|
|
69
69
|
- Terms of use: `https://senado.sidneybissoli.com/terms`
|
|
70
70
|
|
|
71
|
+
### ChatGPT (Deep Research)
|
|
72
|
+
|
|
73
|
+
ChatGPT deep research (and company knowledge, and research workflows over the Responses API) only uses an MCP server that exposes exactly `search` and `fetch` — this server does, on top of the `senado_*` tools, on the full `/mcp` surface (not on the curated app profile). Point the connector at the hosted endpoint, no key required:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
https://senado.sidneybissoli.com/mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`search` ranks the query against the senators in office and the active committees of the Senate and the National Congress and returns `{ id, title, url }` (`sen:<código>` / `com:<código>`); `fetch` returns the document as readable Markdown — the senator's biography and mandates, or the committee's summary and board — with the canonical public page (the senator's profile on www25.senado.leg.br or the committee page on legis.senado.leg.br), which is what ChatGPT cites. Both carry the same provenance block as every other tool, in `structuredContent` and `_meta` (the text channel is the contract's JSON). In ChatGPT's developer mode (Settings → Security and login → Developer mode) any tool is callable — the `senado_*` tools remain the ones to use for data.
|
|
80
|
+
|
|
71
81
|
### Install (any client)
|
|
72
82
|
|
|
73
83
|
For clients that launch MCP servers as a command — and for one-command setup — use the
|
|
@@ -87,7 +97,7 @@ own instance** — it is **not** required to use this public server.
|
|
|
87
97
|
|
|
88
98
|
Prefer not to route queries through a third-party host (e.g. a newsroom policy)? The **same server**
|
|
89
99
|
also runs as a **local stdio process** that talks **directly to the official government APIs** — same
|
|
90
|
-
|
|
100
|
+
69 tools, same provenance envelope, no Cloudflare in the loop. This is the npm/stdio channel, published
|
|
91
101
|
as [`senado-br-mcp`](https://www.npmjs.com/package/senado-br-mcp).
|
|
92
102
|
|
|
93
103
|
Point a command-based client (Claude Desktop/Code, etc.) at the package — npm fetches and runs it,
|
|
@@ -125,7 +135,7 @@ list/corpus tools: without D1 they fall back to a live scrape of the ~5 REST hig
|
|
|
125
135
|
|
|
126
136
|
This repo bundles a Claude [Agent Skill](https://platform.claude.com/docs/en/docs/agents-and-tools/agent-skills/overview)
|
|
127
137
|
at [`.claude/skills/senado-br/`](.claude/skills/senado-br/SKILL.md) that teaches Claude **when** to reach for
|
|
128
|
-
this server and **how** to use its
|
|
138
|
+
this server and **how** to use its 69 tools well — a themed tool map, common question→tool playbooks, the
|
|
129
139
|
provenance contract, and gotchas (dates, the `codigoMateria` bridge, e-Cidadania's open-set listing, pagination).
|
|
130
140
|
It points back to the server's own `senado://catalogo` / `senado://guia` resources rather than duplicating them.
|
|
131
141
|
|
|
@@ -156,7 +166,7 @@ Claude Code auto-discovers it when you work in this repo. To use it elsewhere, c
|
|
|
156
166
|
|
|
157
167
|
### Prerequisites
|
|
158
168
|
|
|
159
|
-
- Node.js
|
|
169
|
+
- Node.js 22+ (`engines.node`)
|
|
160
170
|
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) v4+
|
|
161
171
|
- Cloudflare account
|
|
162
172
|
|
|
@@ -535,7 +545,7 @@ In addition to the `provenance` envelope, `structuredContent` carries a top-leve
|
|
|
535
545
|
|
|
536
546
|
`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.
|
|
537
547
|
|
|
538
|
-
Coverage is **universal**: all
|
|
548
|
+
Coverage is **universal**: all 69 tools carry the envelope — the 67 `senado_*` tools via `resultWithProvenance(` (verify with `grep -c 'resultWithProvenance(' src/tools/*.ts`) and the two Deep Research tools via `provenanceExtras` (same block, in `structuredContent`/`_meta`, since their text channel is the contract's JSON). The **⊕** marks in the inventory below denote the original pilot tools (votes, bills, processes); the envelope now extends to every tool, so the marks are historical.
|
|
539
549
|
|
|
540
550
|
## Citable dataset (e-Cidadania participation)
|
|
541
551
|
|
|
@@ -739,7 +749,16 @@ Reads a bundled snapshot of the Senate's organizational tree (crawled from the i
|
|
|
739
749
|
|------|-------------|
|
|
740
750
|
| `senado_estrutura_organizacional` | Organizational chart (organograma) resolved for a `unidade` (sigla like `DGER` or name): returns its `caminho` (ancestors) and every subordinate unit (`subordinadas[]` — secretarias, coordenações, serviços, núcleos — with `nivel`). Pairs with `senado_servidores`'s `subordinadasA` filter, which counts/lists all servants under a whole directorate (a servant sits in a leaf serviço, so filtering `lotacao` by the parent sigla returns 0). |
|
|
741
751
|
|
|
742
|
-
|
|
752
|
+
### Group U — Deep Research (2 tools)
|
|
753
|
+
|
|
754
|
+
The [OpenAI Deep Research contract](#chatgpt-deep-research): the only two tools without the `senado_` prefix, because the names are fixed by the contract. Registered through the same shim as the others (read-only annotations, permissive `outputSchema`, per-tool telemetry) and served on `/mcp` only — the curated ChatGPT app profile does not include them. The index (senators in office + active committees, ~300 documents) is built on first use from the same two list endpoints the `senado_listar_*` tools read, and kept for 24 h.
|
|
755
|
+
|
|
756
|
+
| Tool | Description |
|
|
757
|
+
|------|-------------|
|
|
758
|
+
| `search` | Ranks the query (natural language or keywords, pt/en, accent-insensitive) against senators in office and active committees; returns up to 10 `{ id, title, url }` — `sen:<código>` with the public profile URL, `com:<código>` with the public committee page. Provenance of both lists in `structuredContent`/`_meta`. |
|
|
759
|
+
| `fetch` | Returns the document for an id from `search` as `{ id, title, text, url, metadata }`: the senator's biography and mandates (same read as `senado_obter_senador`) or the committee's summary and board (same read as `senado_obter_comissao`), as Markdown, with that read's provenance. Unknown id → error. |
|
|
760
|
+
|
|
761
|
+
**Total: 69 tools**
|
|
743
762
|
|
|
744
763
|
### Prompts (4)
|
|
745
764
|
|
|
@@ -759,7 +778,7 @@ Static context documents/tables (MCP `resources` capability), defined in `src/re
|
|
|
759
778
|
| URI | Type | Content |
|
|
760
779
|
| --- | --- | --- |
|
|
761
780
|
| `senado://guia` | markdown | Visão geral e qual ferramenta usar por objetivo. |
|
|
762
|
-
| `senado://catalogo` | markdown | As
|
|
781
|
+
| `senado://catalogo` | markdown | As 69 ferramentas agrupadas por domínio. |
|
|
763
782
|
| `senado://glossario` | markdown | Siglas e termos do Senado (PEC, CEAPS, CCJ, RCN…). |
|
|
764
783
|
| `senado://tabelas/tipos-materia` | json | Tipos de proposição (sigla/nome/descrição). |
|
|
765
784
|
| `senado://tabelas/ufs` | json | As 27 unidades federativas. |
|
|
@@ -848,7 +867,7 @@ tests/ # Vitest unit tests mirroring src/ (parsers, cache, th
|
|
|
848
867
|
## Connecting MCP Clients
|
|
849
868
|
|
|
850
869
|
This is a **remote** server (Streamable HTTP, no install, open access) — point any MCP client at
|
|
851
|
-
`https://senado.sidneybissoli.com/mcp`. Besides
|
|
870
|
+
`https://senado.sidneybissoli.com/mcp`. Besides 69 tools, it exposes **prompts** (ready-made pt-BR
|
|
852
871
|
workflows: `senado_gastos_senador`, `senado_tramitacao_materia`, `senado_votos_senador`,
|
|
853
872
|
`senado_panorama_ecidadania`) and **resources** (`senado://guia`, `senado://catalogo`,
|
|
854
873
|
`senado://glossario`, `senado://tabelas/tipos-materia`, `senado://tabelas/ufs`).
|
package/README.pt-BR.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
|
-

|
|
6
6
|
[](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml)
|
|
7
7
|
[](https://registry.modelcontextprotocol.io)
|
|
8
8
|
[](https://lobehub.com/mcp/sidneybissoli-senado-br-mcp-cloudflare)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
Um servidor MCP **público e hospedado** que dá aos assistentes de IA acesso ao vivo e estruturado aos **dados abertos do Senado Federal do Brasil** — **sem instalação, sem conta, sem chave de API**. Aponte o seu cliente MCP para o endpoint hospedado e comece a perguntar sobre senadores, matérias, votações, despesas e muito mais. Roda em Cloudflare Workers via Streamable HTTP.
|
|
18
18
|
|
|
19
|
-
Expõe **
|
|
19
|
+
Expõe **69 ferramentas**, **4 prompts** e **5 recursos** em dois domínios:
|
|
20
20
|
|
|
21
21
|
- **Legislativo** — senadores; matérias e sua tramitação; votações; comissões; sessões plenárias, resultados e vetos presidenciais; orientação de bancada nas votações; discursos e notas taquigráficas; blocos e lideranças; legislação federal; e participação cidadã pelo portal e-Cidadania.
|
|
22
22
|
- **Administrativo** — despesas da cota parlamentar (CEAPS); auxílio-moradia; servidores e remunerações; horas extras; estagiários; contratos e licitações; terceirizados; suprimento de fundos; e execução orçamentária.
|
|
@@ -50,7 +50,7 @@ Para submissao e revisao pelo OpenAI Apps SDK, o Worker tambem expoe uma superfi
|
|
|
50
50
|
https://senado.sidneybissoli.com/mcp/openai-app-v2
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Esse endpoint preserva o servidor MCP publico completo em `/mcp`, mas limita a descoberta a
|
|
53
|
+
Esse endpoint preserva o servidor MCP publico completo em `/mcp`, mas limita a descoberta a 27
|
|
54
54
|
ferramentas de alto sinal, organizadas por intencao de usuario, para uso como app do ChatGPT.
|
|
55
55
|
`/mcp/openai-app` continua disponivel como alias legado, mas novas configuracoes do app no ChatGPT
|
|
56
56
|
devem usar `/mcp/openai-app-v2` para forcar a leitura do schema atual das ferramentas. As ferramentas
|
|
@@ -58,7 +58,7 @@ continuam chamando os mesmos handlers e retornando o mesmo envelope de provenien
|
|
|
58
58
|
anunciada fica mais estreita. Qualquer listagem de ChatGPT App deve apresentar isto como um app
|
|
59
59
|
independente de pesquisa em dados abertos, nao como conector oficial do Senado, da OpenAI ou do ChatGPT.
|
|
60
60
|
|
|
61
|
-
Para ChatGPT Apps, essas
|
|
61
|
+
Para ChatGPT Apps, essas 27 ferramentas tambem anunciam um template UI compartilhado do MCP Apps em
|
|
62
62
|
`ui://senado-br-mcp/openai-app-dashboard-v2.html`. O widget e autocontido e renderiza o
|
|
63
63
|
`structuredContent` retornado como painel compacto com metricas, registros principais e fonte/proveniencia,
|
|
64
64
|
sem adicionar outra ferramenta de dados visivel ao modelo.
|
|
@@ -68,6 +68,16 @@ URLs publicas legais para revisao do app:
|
|
|
68
68
|
- Politica de privacidade: `https://senado.sidneybissoli.com/privacy`
|
|
69
69
|
- Termos de uso: `https://senado.sidneybissoli.com/terms`
|
|
70
70
|
|
|
71
|
+
### ChatGPT (Deep Research)
|
|
72
|
+
|
|
73
|
+
O deep research do ChatGPT (e o company knowledge, e os workflows de pesquisa da API Responses) só usa um servidor MCP que exponha exatamente `search` e `fetch` — este servidor expõe, além das ferramentas `senado_*`, na superfície completa `/mcp` (não no perfil curado do app). Aponte o conector para o endpoint hospedado, sem chave:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
https://senado.sidneybissoli.com/mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`search` ranqueia a consulta contra os senadores em exercício e as comissões ativas do Senado e do Congresso Nacional e devolve `{ id, title, url }` (`sen:<código>` / `com:<código>`); `fetch` devolve o documento em Markdown legível — a biografia e os mandatos do senador, ou o resumo e a mesa da comissão — com a página pública canônica (o perfil do senador em www25.senado.leg.br ou a página da comissão em legis.senado.leg.br), que é o que o ChatGPT cita. As duas carregam o mesmo bloco de proveniência das outras ferramentas, em `structuredContent` e `_meta` (o canal de texto é o JSON do contrato). No modo desenvolvedor do ChatGPT (Settings → Security and login → Developer mode) qualquer ferramenta é chamável — as `senado_*` continuam sendo as ferramentas para dados.
|
|
80
|
+
|
|
71
81
|
### Instalação (qualquer cliente)
|
|
72
82
|
|
|
73
83
|
Para clientes que iniciam servidores MCP como um comando — e para uma configuração de um único comando — use a
|
|
@@ -87,7 +97,7 @@ própria instância** — **não** é necessário para usar este servidor públi
|
|
|
87
97
|
|
|
88
98
|
Prefere não rotear suas consultas por um servidor de terceiros (ex.: política de uma redação)? O **mesmo
|
|
89
99
|
servidor** também roda como um **processo local stdio** que bate **direto nas APIs oficiais do governo** —
|
|
90
|
-
as mesmas
|
|
100
|
+
as mesmas 69 ferramentas, o mesmo envelope de proveniência, sem Cloudflare no caminho. É o canal npm/stdio,
|
|
91
101
|
publicado como [`senado-br-mcp`](https://www.npmjs.com/package/senado-br-mcp).
|
|
92
102
|
|
|
93
103
|
Aponte um cliente baseado em comando (Claude Desktop/Code, etc.) para o pacote — o npm baixa e executa,
|
|
@@ -125,7 +135,7 @@ Os logs vão para o **stderr** — o stdout carrega apenas o fluxo do protocolo
|
|
|
125
135
|
|
|
126
136
|
Este repositório inclui uma [Agent Skill](https://platform.claude.com/docs/en/docs/agents-and-tools/agent-skills/overview)
|
|
127
137
|
do Claude em [`.claude/skills/senado-br/`](.claude/skills/senado-br/SKILL.md) que ensina o Claude **quando**
|
|
128
|
-
recorrer a este servidor e **como** usar bem as
|
|
138
|
+
recorrer a este servidor e **como** usar bem as 69 ferramentas — um mapa de ferramentas por tema, playbooks de
|
|
129
139
|
pergunta→ferramenta, o contrato de proveniência e os gotchas (datas, a ponte `codigoMateria`, a listagem
|
|
130
140
|
só-abertas do e-Cidadania, paginação). Ela aponta para os recursos `senado://catalogo` / `senado://guia` do
|
|
131
141
|
próprio servidor em vez de duplicá-los.
|
|
@@ -138,7 +148,7 @@ O Claude Code a descobre automaticamente ao trabalhar neste repositório. Para u
|
|
|
138
148
|
|
|
139
149
|
- **Runtime:** Cloudflare Workers (ESM)
|
|
140
150
|
- **Transporte:** Streamable HTTP (spec MCP 2025-03-26) via `createMcpHandler` de `agents/mcp`
|
|
141
|
-
- **Protocolo:** MCP sobre JSON-RPC — `/mcp` serve o catalogo publico completo; `/mcp/openai-app-v2` expoe um perfil curado de
|
|
151
|
+
- **Protocolo:** MCP sobre JSON-RPC — `/mcp` serve o catalogo publico completo; `/mcp/openai-app-v2` expoe um perfil curado de 27 ferramentas mais um widget MCP Apps compartilhado para revisao/submissao como app OpenAI (`/mcp/openai-app` permanece como alias legado)
|
|
142
152
|
- **SDK:** `@modelcontextprotocol/sdk` 1.26.0+ (instâncias de McpServer por requisição)
|
|
143
153
|
- **Validação:** schemas Zod para todas as entradas das ferramentas
|
|
144
154
|
- **Cache:** 2 camadas (L0 memória + L1 Cache API) com chaveamento SHA-256
|
|
@@ -156,7 +166,7 @@ O Claude Code a descobre automaticamente ao trabalhar neste repositório. Para u
|
|
|
156
166
|
|
|
157
167
|
### Pré-requisitos
|
|
158
168
|
|
|
159
|
-
- Node.js
|
|
169
|
+
- Node.js 22+ (`engines.node`)
|
|
160
170
|
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) v4+
|
|
161
171
|
- Conta Cloudflare
|
|
162
172
|
|
|
@@ -507,6 +517,37 @@ Campos do bloco `concise` (por resposta — uma ferramenta, uma fonte): `source`
|
|
|
507
517
|
|
|
508
518
|
A cobertura abrange as quatro fontes upstream: **Dados Abertos Legislativo** (`legis.senado.leg.br`), **Dados Abertos Administrativo** (`adm.senado.gov.br`), **Execução Orçamentária** (feed Arquimedes/Financeiro em `senado.gov.br`) e **Portal e-Cidadania** (`www12.senado.leg.br/ecidadania`). Nas listas do e-Cidadania (lidas do D1) o `retrieved_at` é o `lastScrapedAt` do corpus — a idade real do dado; nos detalhes (raspados ao vivo) é o instante da chamada, com a URL canônica do item. A fidelidade do `retrieved_at` vem da camada de cache (`cachedFetchWithMeta`), que persiste o timestamp junto ao valor.
|
|
509
519
|
|
|
520
|
+
## Conjunto de dados citável (participação no e-Cidadania)
|
|
521
|
+
|
|
522
|
+
Além do servidor ao vivo, este projeto publica um **conjunto de dados congelado, versionado e citável** da camada de participação do e-Cidadania (consultas públicas, ideias legislativas, eventos interativos + seus comentários, votos históricos por estado) — a camada que o pacote R `congressbr` nunca cobriu. Cada valor carrega um envelope de procedência por campo (`{ value, sourceEndpoint, sourceField, retrievedAt, license, schemaVersion }`); a licença dos dados (Dados Abertos do Senado Federal) é mantida **separada** da licença do código (MIT).
|
|
523
|
+
|
|
524
|
+
- **Como citar** — [`CITATION.cff`](CITATION.cff) (conjunto de dados; cite o DOI da versão do snapshot que você usou, e o DOI de conceito para o conjunto através das versões).
|
|
525
|
+
- **O que vai em cada release** — [`CHANGELOG-dataset.md`](CHANGELOG-dataset.md) (cumulativo, só acrescenta; amarra cada release ao seu `schemaVersion`).
|
|
526
|
+
- **Dicionário de variáveis e procedência de campo** — [`docs/dataset-dictionary.md`](docs/dataset-dictionary.md) (gerado de `src/dataset/schema.ts`, a fonte única de verdade).
|
|
527
|
+
- **Licença dos dados** — [`LICENSE-DATA.md`](LICENSE-DATA.md).
|
|
528
|
+
- **Como cortar uma release** (congelar → checksums → GitHub Release → DOI no Zenodo) — [`docs/release-runbook.md`](docs/release-runbook.md); a maquinaria está em `src/dataset/`, `scripts/build-dataset/` e `.github/workflows/release-dataset.yml`.
|
|
529
|
+
|
|
530
|
+
### Inventário — schema v2 (`schemaVersion` `2.0.0`)
|
|
531
|
+
|
|
532
|
+
Cada release entrega um recurso NDJSON por entidade (um `HarmonizedRecord` por linha: identidade + um envelope de procedência por campo), mais um manifesto `datapackage.json` e uma cópia do dicionário. Cinco recursos:
|
|
533
|
+
|
|
534
|
+
| Recurso (`*.ndjson`) | Grão | Variáveis principais | Fonte(s) |
|
|
535
|
+
|---|---|---|---|
|
|
536
|
+
| `consultas` | 1 consulta pública (matéria) | `materia`, `ementa`, `votosSim`/`votosNao`/`totalVotos`, `percentual*`, **`autoria`ⁿ**, **`relator`ⁿ**, `status`, `url`, `firstSeenAt` | listagem `pesquisamateria` + **detalhe** (`visualizacaomateria`)ⁿ + `/processo?tramitando=S` para o status |
|
|
537
|
+
| `ideias` | 1 ideia legislativa (~113,7 mil) | `titulo`, `apoios`, `status`, **`dataPublicacao`ⁿ**, **`autorUf`ⁿ**, **`descricao`ⁿ**, **`plConvertido`ⁿ**, `url`, `firstSeenAt` | listagem `pesquisaideia` + **detalhe** (`visualizacaoideia`) por um backfill retomávelⁿ |
|
|
538
|
+
| `eventos` | 1 evento interativo (audiência) | `titulo`, **`data`ᶜ**, **`hora`ᶜ**, `comissao`, **`comissaoNomeCompleto`ⁿ**, **`local`ⁿ**, **`descricao`ⁿ**, **`pauta`ⁿ**, **`convidados`ⁿ**, **`videoUrl`ⁿ**, **`comentarios`ᶜ**, `status`, `url`, `firstSeenAt` | listagem `principalaudiencia` + **detalhe** (`visualizacaoaudiencia`)ⁿ + **fragmento AJAX de comentários**ᶜ |
|
|
539
|
+
| **`eventos_comentarios`ⁿ** | 1 comentário (nível de comentário) | `eventoId`, `comentarioId`, `uf`, `texto`, `data`, `hora`, `momentoVideoUrl`, `convidadoAssociado` | fragmento AJAX `ajaxcolecaocomentarioaudiencia?audienciaId=` |
|
|
540
|
+
| `consultas_votos` | 1 matéria (acervo histórico) | `materia`, `ementa`, `autoria`, `votosSim`/`votosNao`/`totalVotos`, `votosPorUf`, `status`, `url`, `referencePeriod` | CSV do Arquimedes `Proposições-com-votos.csv` (reingerido semanalmente) |
|
|
541
|
+
|
|
542
|
+
**ⁿ = novo ou reaberto na v2 · ᶜ = fonte corrigida para a canônica na v2.** O que a v2 (`2.0.0`) mudou — a ingestão saiu de **só listagem** para **listagem + detalhe (+ comentários por AJAX nos eventos)**:
|
|
543
|
+
|
|
544
|
+
- **Eventos corrigidos e enriquecidos.** `data`/`hora` agora vêm da página de detalhe (a canônica — o [estudo A3](docs/estudo-a3-reconciliacao-eventos.md) achou a listagem divergente em 57% dos casos no `hora`), mais seis campos novos de detalhe (`comissaoNomeCompleto`, `local`, `descricao`, `pauta`, `convidados`, `videoUrl`). `comentarios` passou a ser a **contagem canônica do AJAX** (a contagem da listagem era `0` espúrio em 82% dos eventos, capturando só ~6,7% do engajamento).
|
|
545
|
+
- **Recurso novo em nível de comentário**, `eventos_comentarios` — uma linha por comentário de audiência, o sinal de participação que ninguém mais publica versionado.
|
|
546
|
+
- **Campos que só existem no detalhe foram reabertos** para `ideias` (`dataPublicacao`, `autorUf`, `descricao`, `plConvertido`) e `consultas` (`autoria`, `relator`) — antes sempre `null` por decisão de projeto.
|
|
547
|
+
- **Postura de privacidade pela origem do dado.** Conteúdo de cidadão (comentários de audiência, autores de ideia) guarda **apenas a UF — nunca o nome**, descartado já no parser; agente público (autoria/relatoria de consulta, convidados de evento) guarda o nome (público pela função). O racional campo a campo está em `docs/schema-v2-inventario.md` (alvo aprovado).
|
|
548
|
+
|
|
549
|
+
O NDJSON congelado **não** é commitado (é construído sob demanda a partir do corpus soberano no D1); uma release marcada `dataset-v*` anexa o tarball + `SHA256SUMS` + `release.json` e os arquiva no Zenodo.
|
|
550
|
+
|
|
510
551
|
## Inventário de ferramentas
|
|
511
552
|
|
|
512
553
|
### Grupo H — Referência/Metadados (1 ferramenta)
|
|
@@ -670,7 +711,24 @@ A cobertura abrange as quatro fontes upstream: **Dados Abertos Legislativo** (`l
|
|
|
670
711
|
|------|-------------|
|
|
671
712
|
| `senado_execucao_orcamentaria` | Execução orçamentária desde 2013 (dotação, empenhado/liquidado/pago) e receitas próprias desde 2012 (previsto vs. arrecadado) — agregadas por ano, ação, grupo de despesa, fonte ou origem de receita; `estatisticas=true` devolve a distribuição do conjunto (min/máx/média/mediana/percentis) + ranking top/bottom, ou um ranking de grupos por soma do `campo` via `agruparPor`, com `campo` (padrão pago / arrecadada) e `topN` |
|
|
672
713
|
|
|
673
|
-
|
|
714
|
+
### Grupo T — Estrutura Organizacional (1 ferramenta)
|
|
715
|
+
|
|
716
|
+
Lê um retrato embutido da árvore organizacional do Senado (varrida do portal institucional até o nível de serviço por `npm run ingest:estrutura`), porque a API de dados abertos só publica unidades até Secretaria e nunca liga uma unidade-folha ao pai dela. Unidades que o portal lista apenas por nome, sem página própria (os núcleos da CONLEG/CONORF), são capturadas como nós sintéticos a partir da listagem indentada da página. Órgãos do Congresso onde o cadastro de servidores registra lotações (CMO, CPCMS, CMMC) vêm de um complemento curado (`src/estrutura/complemento-cn.ts`, fonte pública: congressonacional.leg.br) sob uma raiz **Congresso Nacional** (`CN`) separada — nunca sob a árvore do Senado, de modo que `subordinadasA: "DGER"` os exclui e `subordinadasA: "CN"` os conta. Servidores registrados sob as pseudo-unidades situacionais "Servidores Afastados/em Trânsito - SF" são reportados à parte, como `afastadosOuEmTransito`, por `senado_servidores`.
|
|
717
|
+
|
|
718
|
+
| Ferramenta | Descrição |
|
|
719
|
+
|------|-------------|
|
|
720
|
+
| `senado_estrutura_organizacional` | Organograma resolvido para uma `unidade` (sigla como `DGER` ou nome): devolve o `caminho` (ancestrais) e todas as unidades subordinadas (`subordinadas[]` — secretarias, coordenações, serviços, núcleos — com `nivel`). Faz par com o filtro `subordinadasA` de `senado_servidores`, que conta/lista todos os servidores sob uma diretoria inteira (o servidor fica num serviço-folha, então filtrar `lotacao` pela sigla do pai devolve 0). |
|
|
721
|
+
|
|
722
|
+
### Grupo U — Deep Research (2 ferramentas)
|
|
723
|
+
|
|
724
|
+
O [contrato Deep Research da OpenAI](#chatgpt-deep-research): as únicas duas ferramentas sem o prefixo `senado_`, porque os nomes são fixados pelo contrato. Registradas pelo mesmo shim das outras (annotations somente-leitura, `outputSchema` permissivo, telemetria por ferramenta) e servidas só em `/mcp` — o perfil curado do app do ChatGPT não as inclui. O índice (senadores em exercício + comissões ativas, ~300 documentos) é construído no primeiro uso a partir dos mesmos dois endpoints de lista que as ferramentas `senado_listar_*` leem, e mantido por 24 h.
|
|
725
|
+
|
|
726
|
+
| Ferramenta | Descrição |
|
|
727
|
+
|------|-------------|
|
|
728
|
+
| `search` | Ranqueia a consulta (linguagem natural ou palavras-chave, pt/en, sem distinguir acentos) contra senadores em exercício e comissões ativas; devolve até 10 `{ id, title, url }` — `sen:<código>` com a URL do perfil público, `com:<código>` com a página pública da comissão. Proveniência das duas listas em `structuredContent`/`_meta`. |
|
|
729
|
+
| `fetch` | Devolve o documento de um id vindo de `search` como `{ id, title, text, url, metadata }`: a biografia e os mandatos do senador (a mesma leitura de `senado_obter_senador`) ou o resumo e a mesa da comissão (a mesma leitura de `senado_obter_comissao`), em Markdown, com a proveniência dessa leitura. Id desconhecido → erro. |
|
|
730
|
+
|
|
731
|
+
**Total: 69 ferramentas**
|
|
674
732
|
|
|
675
733
|
### Prompts (4)
|
|
676
734
|
|
|
@@ -690,7 +748,7 @@ Documentos/tabelas de contexto estáticos (capacidade MCP `resources`), definido
|
|
|
690
748
|
| URI | Tipo | Conteúdo |
|
|
691
749
|
| --- | --- | --- |
|
|
692
750
|
| `senado://guia` | markdown | Visão geral e qual ferramenta usar por objetivo. |
|
|
693
|
-
| `senado://catalogo` | markdown | As
|
|
751
|
+
| `senado://catalogo` | markdown | As 69 ferramentas agrupadas por domínio. |
|
|
694
752
|
| `senado://glossario` | markdown | Siglas e termos do Senado (PEC, CEAPS, CCJ, RCN…). |
|
|
695
753
|
| `senado://tabelas/tipos-materia` | json | Tipos de proposição (sigla/nome/descrição). |
|
|
696
754
|
| `senado://tabelas/ufs` | json | As 27 unidades federativas. |
|
|
@@ -774,7 +832,7 @@ tests/ # Testes unitários Vitest espelhando src/ (parsers, c
|
|
|
774
832
|
## Conectando clientes MCP
|
|
775
833
|
|
|
776
834
|
Este é um servidor **remoto** (Streamable HTTP, sem instalação, acesso aberto) — aponte qualquer cliente MCP para
|
|
777
|
-
`https://senado.sidneybissoli.com/mcp`. Além das
|
|
835
|
+
`https://senado.sidneybissoli.com/mcp`. Além das 69 ferramentas, ele expõe **prompts** (workflows prontos em pt-BR:
|
|
778
836
|
`senado_gastos_senador`, `senado_tramitacao_materia`, `senado_votos_senador`,
|
|
779
837
|
`senado_panorama_ecidadania`) e **recursos** (`senado://guia`, `senado://catalogo`,
|
|
780
838
|
`senado://glossario`, `senado://tabelas/tipos-materia`, `senado://tabelas/ufs`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const ESTRUTURA_ORGANIZACIONAL = {
|
|
2
|
-
"extraidoEm": "2026-
|
|
2
|
+
"extraidoEm": "2026-09-01T10:56:16.383Z",
|
|
3
3
|
"fonteUrl": "https://www12.senado.leg.br/institucional/estrutura/orgaosenado",
|
|
4
|
-
"total":
|
|
4
|
+
"total": 823,
|
|
5
5
|
"orgaos": [
|
|
6
6
|
{
|
|
7
7
|
"cod": -2072346465,
|
|
@@ -1947,12 +1947,6 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
1947
1947
|
"nome": "Ouvidoria do Senado Federal",
|
|
1948
1948
|
"codSuperior": null
|
|
1949
1949
|
},
|
|
1950
|
-
{
|
|
1951
|
-
"cod": 9426,
|
|
1952
|
-
"sigla": null,
|
|
1953
|
-
"nome": "Assessoria Técnica da Strans",
|
|
1954
|
-
"codSuperior": 8852
|
|
1955
|
-
},
|
|
1956
1950
|
{
|
|
1957
1951
|
"cod": 9430,
|
|
1958
1952
|
"sigla": null,
|
|
@@ -2217,12 +2211,6 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
2217
2211
|
"nome": "Serviço de Atendimento Eletrônico",
|
|
2218
2212
|
"codSuperior": 9600
|
|
2219
2213
|
},
|
|
2220
|
-
{
|
|
2221
|
-
"cod": 9596,
|
|
2222
|
-
"sigla": null,
|
|
2223
|
-
"nome": "Serviço de Manutenção Eletromecânica",
|
|
2224
|
-
"codSuperior": 10741
|
|
2225
|
-
},
|
|
2226
2214
|
{
|
|
2227
2215
|
"cod": 9600,
|
|
2228
2216
|
"sigla": null,
|
|
@@ -2457,12 +2445,6 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
2457
2445
|
"nome": "Serviço de Pesquisa e Análise",
|
|
2458
2446
|
"codSuperior": 9807
|
|
2459
2447
|
},
|
|
2460
|
-
{
|
|
2461
|
-
"cod": 9810,
|
|
2462
|
-
"sigla": null,
|
|
2463
|
-
"nome": "Observatório da Mulher Contra a Violência",
|
|
2464
|
-
"codSuperior": 9807
|
|
2465
|
-
},
|
|
2466
2448
|
{
|
|
2467
2449
|
"cod": 9868,
|
|
2468
2450
|
"sigla": "IFI",
|
|
@@ -4785,18 +4767,6 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
4785
4767
|
"nome": "Serviço de Suporte aos Sistemas de Saúde",
|
|
4786
4768
|
"codSuperior": 10620
|
|
4787
4769
|
},
|
|
4788
|
-
{
|
|
4789
|
-
"cod": 10801,
|
|
4790
|
-
"sigla": "GSBBONET",
|
|
4791
|
-
"nome": "Gabinete do Senador Bruno Bonetti",
|
|
4792
|
-
"codSuperior": 108
|
|
4793
|
-
},
|
|
4794
|
-
{
|
|
4795
|
-
"cod": 10803,
|
|
4796
|
-
"sigla": null,
|
|
4797
|
-
"nome": "Escritório de Apoio Nº 01 do Senador Bruno Bonetti",
|
|
4798
|
-
"codSuperior": 10801
|
|
4799
|
-
},
|
|
4800
4770
|
{
|
|
4801
4771
|
"cod": 10806,
|
|
4802
4772
|
"sigla": "GSRACIOL",
|
|
@@ -4946,6 +4916,30 @@ export const ESTRUTURA_ORGANIZACIONAL = {
|
|
|
4946
4916
|
"sigla": null,
|
|
4947
4917
|
"nome": "Coordenação do Observatório da Mulher Contra a Violência",
|
|
4948
4918
|
"codSuperior": 8852
|
|
4919
|
+
},
|
|
4920
|
+
{
|
|
4921
|
+
"cod": 10860,
|
|
4922
|
+
"sigla": "GSLPEREI",
|
|
4923
|
+
"nome": "Gabinete da Senadora Lourdinha Pereira",
|
|
4924
|
+
"codSuperior": 108
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
"cod": 10861,
|
|
4928
|
+
"sigla": null,
|
|
4929
|
+
"nome": "Escritório de Apoio Nº 01 da Senadora Lourdinha Pereira",
|
|
4930
|
+
"codSuperior": 10860
|
|
4931
|
+
},
|
|
4932
|
+
{
|
|
4933
|
+
"cod": 10862,
|
|
4934
|
+
"sigla": "GSSREGIN",
|
|
4935
|
+
"nome": "Gabinete do Senador Sargento Reginauro",
|
|
4936
|
+
"codSuperior": 108
|
|
4937
|
+
},
|
|
4938
|
+
{
|
|
4939
|
+
"cod": 10863,
|
|
4940
|
+
"sigla": null,
|
|
4941
|
+
"nome": "Escritório de Apoio Nº 01 do Senador Sargento Reginauro",
|
|
4942
|
+
"codSuperior": 10862
|
|
4949
4943
|
}
|
|
4950
4944
|
]
|
|
4951
4945
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* As três coisas que um rastreador procura antes de decidir se vale a pena
|
|
3
|
+
* visitar um domínio: `robots.txt`, `sitemap.xml` e a chave do IndexNow.
|
|
4
|
+
*
|
|
5
|
+
* POR QUE ESTE ARQUIVO EXISTE. Em 31/08/2026 a landing foi reescrita para ser
|
|
6
|
+
* indexável — meta description, og:, JSON-LD, texto de verdade nos dois
|
|
7
|
+
* idiomas. E aí a pergunta certa apareceu: indexável por quem? Medido no mesmo
|
|
8
|
+
* dia, `sitemap.xml` respondia **404** nos quatro domínios do portfólio, e o
|
|
9
|
+
* `robots.txt` servido era o bloco de *content signals* que a Cloudflare injeta
|
|
10
|
+
* — 1.248 bytes que são só COMENTÁRIO, sem uma única diretiva ativa: nenhum
|
|
11
|
+
* `Sitemap:`, nenhum `Allow`. Não bloqueia nada, e também não convida nada. Os
|
|
12
|
+
* links de entrada para o domínio são finos, então esperar rastreamento
|
|
13
|
+
* espontâneo era esperar por algo que podia não acontecer — e, pior, ler o
|
|
14
|
+
* silêncio como resposta.
|
|
15
|
+
*
|
|
16
|
+
* O IndexNow é a peça que não depende de conta em painel de buscador: publica-se
|
|
17
|
+
* uma chave num arquivo do próprio domínio e avisa-se por HTTP quando uma URL
|
|
18
|
+
* muda. Bing, Yandex, Seznam e Naver honram o protocolo. A chave é PÚBLICA por
|
|
19
|
+
* desenho — ela prova posse do domínio justamente por estar servida nele.
|
|
20
|
+
*
|
|
21
|
+
* Aqui o sitemap tem DUAS URLs: a raiz e a página de termos. Privacidade fica
|
|
22
|
+
* de fora por decisão registrada em `tests/landing.test.ts` — páginas de
|
|
23
|
+
* obrigação não disputam a busca com a landing —, mas os termos descrevem o
|
|
24
|
+
* serviço e são o que um avaliador de diretório procura.
|
|
25
|
+
*/
|
|
26
|
+
import { SITE_URL } from "./landing.js";
|
|
27
|
+
/**
|
|
28
|
+
* Chave do IndexNow. É PÚBLICA por desenho: prova posse do domínio por estar
|
|
29
|
+
* servida em `/<chave>.txt`, então versioná-la não é vazamento.
|
|
30
|
+
*/
|
|
31
|
+
export const INDEXNOW_KEY = "a5286cd351e45bcfa411debef2957eaf";
|
|
32
|
+
/** Quando a superfície mudou pela última vez — vira o `lastmod` do sitemap. */
|
|
33
|
+
const ULTIMA_MUDANCA = "2026-08-31";
|
|
34
|
+
function texto(corpo, tipo = "text/plain; charset=utf-8") {
|
|
35
|
+
return new Response(corpo, {
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: { "Content-Type": tipo, "Cache-Control": "public, max-age=3600" },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `robots.txt` PRÓPRIO. Ele diz duas coisas que o injetado não dizia: que o
|
|
42
|
+
* rastreamento é liberado, e onde está o sitemap. As rotas de máquina ficam
|
|
43
|
+
* fora do índice — `/mcp` é JSON-RPC e as demais são sondas operacionais.
|
|
44
|
+
*/
|
|
45
|
+
export function robotsTxt() {
|
|
46
|
+
return [
|
|
47
|
+
"User-agent: *",
|
|
48
|
+
"Allow: /",
|
|
49
|
+
"Disallow: /mcp",
|
|
50
|
+
"Disallow: /health",
|
|
51
|
+
"Disallow: /status",
|
|
52
|
+
"Disallow: /metrics",
|
|
53
|
+
"Disallow: /.well-known/",
|
|
54
|
+
"",
|
|
55
|
+
`Sitemap: ${SITE_URL}/sitemap.xml`,
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
export function sitemapXml() {
|
|
60
|
+
const urls = [
|
|
61
|
+
{ loc: `${SITE_URL}/`, prioridade: "1.0", freq: "weekly" },
|
|
62
|
+
{ loc: `${SITE_URL}/terms`, prioridade: "0.3", freq: "yearly" },
|
|
63
|
+
];
|
|
64
|
+
const corpo = urls
|
|
65
|
+
.map((u) => ` <url>
|
|
66
|
+
<loc>${u.loc}</loc>
|
|
67
|
+
<lastmod>${ULTIMA_MUDANCA}</lastmod>
|
|
68
|
+
<changefreq>${u.freq}</changefreq>
|
|
69
|
+
<priority>${u.prioridade}</priority>
|
|
70
|
+
</url>`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
74
|
+
${corpo}
|
|
75
|
+
</urlset>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Responde as rotas de descoberta, ou `null` se o caminho não for uma delas.
|
|
80
|
+
* Chamada ANTES da auth: um rastreador não tem credencial.
|
|
81
|
+
*/
|
|
82
|
+
export function discoveryResponseForPath(pathname) {
|
|
83
|
+
if (pathname === "/robots.txt")
|
|
84
|
+
return texto(robotsTxt());
|
|
85
|
+
if (pathname === "/sitemap.xml")
|
|
86
|
+
return texto(sitemapXml(), "application/xml; charset=utf-8");
|
|
87
|
+
if (pathname === `/${INDEXNOW_KEY}.txt`)
|
|
88
|
+
return texto(INDEXNOW_KEY);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
package/dist/landing.js
CHANGED
|
@@ -7,10 +7,61 @@
|
|
|
7
7
|
import { htmlPage, CONTACT_EMAIL, PRIVACY_URL, TERMS_URL } from "./legal.js";
|
|
8
8
|
import { VERSION, USER_AGENT } from "./version.js";
|
|
9
9
|
const GITHUB_URL = "https://github.com/SidneyBissoli/senado-br-mcp-cloudflare";
|
|
10
|
+
const NPM_URL = "https://www.npmjs.com/package/senado-br-mcp";
|
|
11
|
+
export const SITE_URL = "https://senado.sidneybissoli.com";
|
|
12
|
+
const PAGE_TITLE = "Dados Abertos Senado BR MCP";
|
|
13
|
+
/**
|
|
14
|
+
* UMA frase, em português: o que o servidor serve e de qual fonte. Vira a
|
|
15
|
+
* `meta description` e o parágrafo de abertura — até ~155 caracteres, que é o
|
|
16
|
+
* que o resultado de busca mostra sem cortar.
|
|
17
|
+
*/
|
|
18
|
+
const RESUMO = "Servidor MCP dos dados abertos do Senado Federal: senadores, matérias, votações, " +
|
|
19
|
+
"comissões, e-Cidadania e dados administrativos, com fonte citada.";
|
|
20
|
+
/**
|
|
21
|
+
* Cabeçalho de DESCOBERTA. Esta é a única página do produto que não pertence a
|
|
22
|
+
* terceiro — não é o npm, não é o GitHub, não é ficha de diretório. Até
|
|
23
|
+
* 2026-08-31 ela não declarava `description`, og: nem dado estruturado: havia
|
|
24
|
+
* texto para ler, mas nada para um buscador entender.
|
|
25
|
+
*/
|
|
26
|
+
function seoHead() {
|
|
27
|
+
const jsonLd = {
|
|
28
|
+
"@context": "https://schema.org",
|
|
29
|
+
"@type": "SoftwareApplication",
|
|
30
|
+
name: PAGE_TITLE,
|
|
31
|
+
description: RESUMO,
|
|
32
|
+
url: SITE_URL,
|
|
33
|
+
applicationCategory: "DeveloperApplication",
|
|
34
|
+
operatingSystem: "Any",
|
|
35
|
+
inLanguage: "pt-BR",
|
|
36
|
+
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
|
37
|
+
license: "https://opensource.org/licenses/MIT",
|
|
38
|
+
author: { "@type": "Person", name: "Sidney Bissoli" },
|
|
39
|
+
codeRepository: GITHUB_URL,
|
|
40
|
+
};
|
|
41
|
+
return ` <meta name="description" content="${RESUMO}">
|
|
42
|
+
<link rel="canonical" href="${SITE_URL}/">
|
|
43
|
+
<meta property="og:type" content="website">
|
|
44
|
+
<meta property="og:site_name" content="${PAGE_TITLE}">
|
|
45
|
+
<meta property="og:title" content="${PAGE_TITLE}">
|
|
46
|
+
<meta property="og:description" content="${RESUMO}">
|
|
47
|
+
<meta property="og:url" content="${SITE_URL}/">
|
|
48
|
+
<meta property="og:locale" content="pt_BR">
|
|
49
|
+
<meta name="twitter:card" content="summary">
|
|
50
|
+
<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>`;
|
|
51
|
+
}
|
|
10
52
|
export function buildLandingBody() {
|
|
11
53
|
return `
|
|
12
|
-
<h1
|
|
54
|
+
<h1>${PAGE_TITLE}</h1>
|
|
13
55
|
<p class="muted">Versão ${VERSION} — serviço independente, somente leitura, não afiliado ao Senado Federal.</p>
|
|
56
|
+
<p class="lead">${RESUMO}</p>
|
|
57
|
+
|
|
58
|
+
<h2>Perguntas que ele responde</h2>
|
|
59
|
+
<ul>
|
|
60
|
+
<li>“Quais senadores estão em exercício hoje?”</li>
|
|
61
|
+
<li>“Como cada senador votou na última votação nominal do plenário?”</li>
|
|
62
|
+
<li>“Quais matérias sobre saúde foram apresentadas neste ano?”</li>
|
|
63
|
+
<li>“Quem mais gastou com a cota parlamentar (CEAPS) em 2025?”</li>
|
|
64
|
+
</ul>
|
|
14
65
|
<p>Este é um servidor <a href="https://modelcontextprotocol.io">MCP (Model Context Protocol)</a> que expõe os dados abertos do Senado Federal do Brasil — senadores, matérias, votações, comissões, e-Cidadania e dados administrativos (CEAPS, contratações, servidores, orçamento) — para assistentes de IA como Claude e ChatGPT. Código aberto sob licença MIT em <a href="${GITHUB_URL}">GitHub</a>.</p>
|
|
15
66
|
|
|
16
67
|
<h2>Identificação nos logs (User-Agent)</h2>
|
|
@@ -33,17 +84,25 @@ export function buildLandingBody() {
|
|
|
33
84
|
<li><a href="/status">/status</a> — versão e metadados do último deploy</li>
|
|
34
85
|
<li><a href="/health">/health</a> — health check</li>
|
|
35
86
|
<li><a href="${PRIVACY_URL}">Política de privacidade</a> · <a href="${TERMS_URL}">Termos de uso</a></li>
|
|
36
|
-
<li><a href="${GITHUB_URL}">Código-fonte no GitHub</a> · pacote npm <a href="
|
|
87
|
+
<li><a href="${GITHUB_URL}">Código-fonte no GitHub</a> · pacote npm <a href="${NPM_URL}"><code>senado-br-mcp</code></a></li>
|
|
37
88
|
</ul>
|
|
38
89
|
|
|
39
|
-
<
|
|
90
|
+
<section lang="en">
|
|
91
|
+
<h2>Also in English</h2>
|
|
92
|
+
<p>An independent, read-only MCP server for Brazilian Federal Senate open data — senators, bills, votes, committees, the e-Cidadania participation layer and administrative data (expenses, procurement, staff, budget), each answer carrying its source.</p>
|
|
93
|
+
<ul>
|
|
94
|
+
<li>“Which senators are currently in office?”</li>
|
|
95
|
+
<li>“How did each senator vote on the last recorded floor vote?”</li>
|
|
96
|
+
</ul>
|
|
97
|
+
<p>If you found this domain in your server logs, the requests came from this client; contact <a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a>.</p>
|
|
98
|
+
</section>
|
|
40
99
|
`;
|
|
41
100
|
}
|
|
42
101
|
export function landingResponseForPath(pathname) {
|
|
43
102
|
if (pathname !== "/") {
|
|
44
103
|
return null;
|
|
45
104
|
}
|
|
46
|
-
return new Response(htmlPage(
|
|
105
|
+
return new Response(htmlPage(PAGE_TITLE, buildLandingBody(), "pt-BR", seoHead()), {
|
|
47
106
|
status: 200,
|
|
48
107
|
headers: {
|
|
49
108
|
"Content-Type": "text/html; charset=utf-8",
|
package/dist/legal.js
CHANGED
|
@@ -5,13 +5,20 @@ export const PRIVACY_URL = `${SITE_ORIGIN}/privacy`;
|
|
|
5
5
|
export const TERMS_URL = `${SITE_ORIGIN}/terms`;
|
|
6
6
|
const UPDATED_AT = "2026-06-28";
|
|
7
7
|
export const CONTACT_EMAIL = "sbissoli76@gmail.com";
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Shell HTML das páginas públicas. `head` é injetado apenas por quem tem o que
|
|
10
|
+
* declarar: a LANDING passa aí a `meta description`, o canonical, os og: e o
|
|
11
|
+
* JSON-LD. Privacidade e Termos não passam nada — são páginas de obrigação,
|
|
12
|
+
* não superfície de produto, e não disputam a busca com a landing.
|
|
13
|
+
*/
|
|
14
|
+
export function htmlPage(title, body, lang = "en", head = "") {
|
|
9
15
|
return `<!doctype html>
|
|
10
16
|
<html lang="${lang}">
|
|
11
17
|
<head>
|
|
12
18
|
<meta charset="utf-8">
|
|
13
19
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14
20
|
<title>${title}</title>
|
|
21
|
+
${head}
|
|
15
22
|
<style>
|
|
16
23
|
:root { color-scheme: light dark; }
|
|
17
24
|
body {
|
|
@@ -28,6 +35,7 @@ export function htmlPage(title, body, lang = "en") {
|
|
|
28
35
|
p, li { font-size: 1rem; }
|
|
29
36
|
a { color: LinkText; }
|
|
30
37
|
.muted { color: color-mix(in srgb, CanvasText 72%, Canvas 28%); }
|
|
38
|
+
.lead { font-size: 1.1rem; }
|
|
31
39
|
</style>
|
|
32
40
|
</head>
|
|
33
41
|
<body>
|
package/dist/resources.js
CHANGED
|
@@ -12,7 +12,7 @@ export function buildGuia() {
|
|
|
12
12
|
return `# Senado BR MCP — guia de uso
|
|
13
13
|
|
|
14
14
|
Servidor MCP de dados abertos do Senado Federal (API legislativa, API administrativa e portal
|
|
15
|
-
e-Cidadania).
|
|
15
|
+
e-Cidadania). 69 ferramentas, respostas em pt-BR. Abaixo, por onde começar conforme o objetivo.
|
|
16
16
|
|
|
17
17
|
## Senadores
|
|
18
18
|
- \`senado_listar_senadores\` — lista/busca por nome, UF ou partido (use o filtro \`nome\` quando só tiver o nome).
|
|
@@ -51,9 +51,9 @@ e-Cidadania). 67 ferramentas, respostas em pt-BR. Abaixo, por onde começar conf
|
|
|
51
51
|
Dica: vários tools usam um parâmetro enum (\`secao\`/\`tipo\`/\`por\`/\`modo\`/\`tabela\`) para cobrir variações —
|
|
52
52
|
leia a descrição do tool para os valores aceitos.`;
|
|
53
53
|
}
|
|
54
|
-
/** Compact grouped catalog of all
|
|
54
|
+
/** Compact grouped catalog of all 69 tools. */
|
|
55
55
|
export function buildCatalogo() {
|
|
56
|
-
return `# Catálogo de ferramentas (
|
|
56
|
+
return `# Catálogo de ferramentas (69)
|
|
57
57
|
|
|
58
58
|
A — Senadores (5): senado_listar_senadores, senado_obter_senador, senado_votacoes_senador, senado_senador_historico, senado_senadores_afastados
|
|
59
59
|
B — Matérias (2): senado_buscar_materias, senado_obter_materia
|
|
@@ -74,7 +74,8 @@ P — Servidores (4): senado_servidores, senado_remuneracoes_servidores, senado_
|
|
|
74
74
|
Q — Contratações (6): senado_contratos, senado_contratacao_detalhe, senado_licitacoes, senado_terceirizados, senado_empresas_contratadas, senado_contratacoes_lista
|
|
75
75
|
R — Suprimento de fundos (1): senado_suprimento_fundos
|
|
76
76
|
S — Orçamento do Senado (1): senado_execucao_orcamentaria
|
|
77
|
-
T — Estrutura organizacional (1): senado_estrutura_organizacional
|
|
77
|
+
T — Estrutura organizacional (1): senado_estrutura_organizacional
|
|
78
|
+
U — Deep Research (2, contrato OpenAI, sem prefixo): search, fetch`;
|
|
78
79
|
}
|
|
79
80
|
/** Glossary of common Senate acronyms and terms. */
|
|
80
81
|
export function buildGlossario() {
|
|
@@ -117,7 +118,7 @@ export function registerResources(server) {
|
|
|
117
118
|
}, (uri) => ({ contents: [{ uri: uri.href, mimeType: "text/markdown", text: buildGuia() }] }));
|
|
118
119
|
server.registerResource("catalogo", "senado://catalogo", {
|
|
119
120
|
title: "Catálogo de ferramentas",
|
|
120
|
-
description: "Lista das
|
|
121
|
+
description: "Lista das 69 ferramentas agrupadas por domínio.",
|
|
121
122
|
mimeType: "text/markdown",
|
|
122
123
|
}, (uri) => ({ contents: [{ uri: uri.href, mimeType: "text/markdown", text: buildCatalogo() }] }));
|
|
123
124
|
server.registerResource("glossario", "senado://glossario", {
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerServidoresTools } from "./tools/servidores.js";
|
|
|
24
24
|
import { registerSupridosTools } from "./tools/supridos.js";
|
|
25
25
|
import { registerOrcamentoSenadoTools } from "./tools/orcamento-senado.js";
|
|
26
26
|
import { registerEstruturaTools } from "./tools/estrutura.js";
|
|
27
|
+
import { registerDeepResearchToolsSenado } from "./tools/deep-research.js";
|
|
27
28
|
import { registerPrompts } from "./prompts.js";
|
|
28
29
|
import { registerResources } from "./resources.js";
|
|
29
30
|
import { instrumentTool } from "./instrument.js";
|
|
@@ -129,6 +130,9 @@ export function createServer(env, ctx, options = {}) {
|
|
|
129
130
|
registerOrcamentoSenadoTools(host);
|
|
130
131
|
// Group T — Estrutura Organizacional (1 tool — árvore de órgãos, snapshot bundlado)
|
|
131
132
|
registerEstruturaTools(host);
|
|
133
|
+
// Group U — Deep Research (2 tools: `search`/`fetch`, o contrato OpenAI — os únicos
|
|
134
|
+
// nomes sem o prefixo `senado_`; passam pelo mesmo shim que os outros)
|
|
135
|
+
registerDeepResearchToolsSenado(host, baseUrl);
|
|
132
136
|
// MCP prompts (4 reusable pt-BR workflow templates) and resources (5 static
|
|
133
137
|
// context docs/tables) — advertised as the `prompts` and `resources` capabilities.
|
|
134
138
|
registerPrompts(server);
|
package/dist/tool-titles.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The Anthropic Connectors Directory review requires a `title` annotation on
|
|
5
5
|
* each tool (alongside `readOnlyHint`). The central shim in `server.ts` injects
|
|
6
6
|
* `annotations.title = TOOL_TITLES[name]` for each tool, so this map must stay
|
|
7
|
-
* in sync with the
|
|
7
|
+
* in sync with the 69 registered tools — the coverage test in
|
|
8
8
|
* `tests/tool-titles.test.ts` fails if a registered tool is missing an entry.
|
|
9
9
|
*
|
|
10
10
|
* Titles are short, user-facing display labels (pt-BR), not descriptions.
|
|
@@ -97,6 +97,10 @@ export const TOOL_TITLES = {
|
|
|
97
97
|
senado_execucao_orcamentaria: "Execução orçamentária do Senado",
|
|
98
98
|
// Estrutura organizacional
|
|
99
99
|
senado_estrutura_organizacional: "Estrutura organizacional",
|
|
100
|
+
// Deep Research (contrato OpenAI — os únicos nomes sem o prefixo `senado_`;
|
|
101
|
+
// os mesmos títulos padrão pt-BR da fábrica de @sbissoli/mcp-search)
|
|
102
|
+
search: "Busca para Deep Research",
|
|
103
|
+
fetch: "Documento para Deep Research",
|
|
100
104
|
};
|
|
101
105
|
/**
|
|
102
106
|
* Fallback title for a tool with no explicit entry — returns the tool name so a
|
package/dist/tools/comissoes.js
CHANGED
|
@@ -87,6 +87,21 @@ export function parseComissaoResumo(colegiado, sigla, secao) {
|
|
|
87
87
|
payload.aviso = OBTER_COMISSAO_FINALIDADE_AVISO;
|
|
88
88
|
return payload;
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Um item de `/comissao/lista/colegiados` na forma que `senado_listar_comissoes`
|
|
92
|
+
* devolve. Exportado porque o índice do Deep Research (`deep-research.ts`) lê a
|
|
93
|
+
* mesma lista e precisa da mesma projeção.
|
|
94
|
+
*/
|
|
95
|
+
export function parseComissaoItem(c) {
|
|
96
|
+
return {
|
|
97
|
+
codigo: parseInt(c.Codigo || "0"),
|
|
98
|
+
sigla: c.Sigla || "",
|
|
99
|
+
nome: c.Nome || "",
|
|
100
|
+
tipo: c.DescricaoTipoColegiado || null,
|
|
101
|
+
casa: c.SiglaCasa || null,
|
|
102
|
+
ativa: true, // this endpoint only returns active committees
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
/** Resolve a committee sigla to its numeric code via the list endpoint. */
|
|
91
106
|
export async function resolveComissaoCodigo(sigla, baseUrl) {
|
|
92
107
|
const response = await cachedFetch("senado_listar_comissoes", {}, CACHE_SEMI_STATIC, () => upstreamFetch("/comissao/lista/colegiados", {}, baseUrl));
|
|
@@ -96,6 +111,20 @@ export async function resolveComissaoCodigo(sigla, baseUrl) {
|
|
|
96
111
|
const match = comissoes.find((c) => (c.Sigla || "").toUpperCase() === upper);
|
|
97
112
|
return match ? parseInt(match.Codigo || "0") : null;
|
|
98
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* A leitura da seção `resumo` de `senado_obter_comissao` a partir do código
|
|
116
|
+
* numérico: o Colegiado cru de `/comissao/{codigo}` (ou `undefined` quando a
|
|
117
|
+
* fonte não o traz). Exportada porque o `fetch` do Deep Research
|
|
118
|
+
* (`deep-research.ts`) é este mesmo documento, com o mesmo cache — `path` é o
|
|
119
|
+
* que entra em `provenanceFor`.
|
|
120
|
+
*/
|
|
121
|
+
export async function fetchComissaoColegiado(codigo, baseUrl) {
|
|
122
|
+
const path = `/comissao/${codigo}`;
|
|
123
|
+
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_obter_comissao", { codigo }, CACHE_SEMI_STATIC, () => upstreamFetch(path, {}, baseUrl));
|
|
124
|
+
const r = response;
|
|
125
|
+
const colegiado = ensureArray(r?.ComissoesCongressoNacional?.Colegiados?.Colegiado)[0];
|
|
126
|
+
return { path, fetchedAt, colegiado };
|
|
127
|
+
}
|
|
99
128
|
export function registerComissoesTools(server, baseUrl) {
|
|
100
129
|
// E1. senado_listar_comissoes
|
|
101
130
|
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`.", {
|
|
@@ -105,14 +134,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
105
134
|
try {
|
|
106
135
|
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_listar_comissoes", {}, CACHE_SEMI_STATIC, () => upstreamFetch("/comissao/lista/colegiados", {}, baseUrl));
|
|
107
136
|
const r = response;
|
|
108
|
-
let comissoes = ensureArray(r?.ListaColegiados?.Colegiados?.Colegiado).map(
|
|
109
|
-
codigo: parseInt(c.Codigo || "0"),
|
|
110
|
-
sigla: c.Sigla || "",
|
|
111
|
-
nome: c.Nome || "",
|
|
112
|
-
tipo: c.DescricaoTipoColegiado || null,
|
|
113
|
-
casa: c.SiglaCasa || null,
|
|
114
|
-
ativa: true, // this endpoint only returns active committees
|
|
115
|
-
}));
|
|
137
|
+
let comissoes = ensureArray(r?.ListaColegiados?.Colegiados?.Colegiado).map(parseComissaoItem);
|
|
116
138
|
if (params.tipo === "mista") {
|
|
117
139
|
// No DescricaoTipoColegiado equals "mista"; the mistas (CMO, CCAI, CMMC, CMCVM,
|
|
118
140
|
// CMMIR and the CMMPVs) are identifiable by "Mista" in the name.
|
|
@@ -175,10 +197,7 @@ export function registerComissoesTools(server, baseUrl) {
|
|
|
175
197
|
});
|
|
176
198
|
return resultWithProvenance({ sigla, secao, count: membros.length, membros }, prov);
|
|
177
199
|
}
|
|
178
|
-
const resumoPath =
|
|
179
|
-
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_obter_comissao", { codigo }, CACHE_SEMI_STATIC, () => upstreamFetch(resumoPath, {}, baseUrl));
|
|
180
|
-
const r = response;
|
|
181
|
-
const colegiado = ensureArray(r?.ComissoesCongressoNacional?.Colegiados?.Colegiado)[0];
|
|
200
|
+
const { path: resumoPath, fetchedAt, colegiado } = await fetchComissaoColegiado(codigo, baseUrl);
|
|
182
201
|
if (!colegiado)
|
|
183
202
|
return toolError("Dados da comissão não encontrados.");
|
|
184
203
|
const prov = provenanceFor("SENADO_LEGIS", baseUrl, resumoPath, {
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group U — Deep Research (2 tools)
|
|
3
|
+
* search, fetch — o contrato Deep Research da OpenAI sobre o acervo do servidor.
|
|
4
|
+
* ChatGPT deep research, company knowledge e os workflows de pesquisa da API
|
|
5
|
+
* Responses exigem EXATAMENTE essas duas tools, com esses nomes — a única
|
|
6
|
+
* exceção admitida ao prefixo `senado_`.
|
|
7
|
+
*
|
|
8
|
+
* Desenho "coletor + shim `host.tool`": a fábrica de `@sbissoli/mcp-search` é
|
|
9
|
+
* apontada para um coletor que só colhe `description` e `callback`; o registro
|
|
10
|
+
* de verdade passa pelo shim de `createServer` como o dos outros 20 grupos —
|
|
11
|
+
* e ganha de graça o filtro de perfil, o título de `tool-titles.ts`, as
|
|
12
|
+
* annotations somente-leitura, o `outputSchema` permissivo único (o gate de
|
|
13
|
+
* `tests/output-contract.test.ts` exige um só; o contrato do ChatGPT lê o JSON
|
|
14
|
+
* do `content`, não exige outputSchema tipado) e o `instrumentTool`. Por isso
|
|
15
|
+
* a fábrica NÃO recebe `record` (a telemetria é do shim) nem
|
|
16
|
+
* `extendOutputSchema` (o outputSchema é do shim).
|
|
17
|
+
*
|
|
18
|
+
* O acervo: senadores em exercício (`/senador/lista/atual`, id `sen:<codigo>`)
|
|
19
|
+
* e colegiados ativos (`/comissao/lista/colegiados`, id `com:<codigo>`) —
|
|
20
|
+
* as duas listas fechadas que a API publica inteiras. Matérias ficam de fora:
|
|
21
|
+
* não há dump (só busca por query/ano). A `url` é a página PÚBLICA humana,
|
|
22
|
+
* nunca a API — é o que o ChatGPT cita: perfil do senador em www25 e a página
|
|
23
|
+
* da comissão em legis.senado.leg.br/comissoes (padrão verificado ao vivo em
|
|
24
|
+
* 03/09/2026: CAE=38, CCJ=34, CCAI=449 respondem 200; código inexistente 404).
|
|
25
|
+
*
|
|
26
|
+
* O índice vive neste módulo com TTL de 24 h (o L0 do cache, `src/cache/`, tem
|
|
27
|
+
* TTL máximo de 600 s e 500 entradas — não serve para um índice); é construído
|
|
28
|
+
* no primeiro `search` e chamadas concorrentes compartilham a construção.
|
|
29
|
+
* `fetch` reusa as leituras reais de `senado_obter_senador` e
|
|
30
|
+
* `senado_obter_comissao` (`fetchSenadorDetalhe`/`fetchComissaoColegiado`),
|
|
31
|
+
* com o mesmo cache e a mesma proveniência — que viaja em `structuredContent`
|
|
32
|
+
* e `_meta` (`provenanceExtras`), sem rodapé no texto.
|
|
33
|
+
*/
|
|
34
|
+
import { DEEP_RESEARCH_TOOLS, contractSchemas, createIndex, registerDeepResearchTools, } from "@sbissoli/mcp-search";
|
|
35
|
+
import { cachedFetchWithMeta } from "../cache/manager.js";
|
|
36
|
+
import { upstreamFetch } from "../throttle/upstream.js";
|
|
37
|
+
import { ensureArray } from "../utils/validation.js";
|
|
38
|
+
import { provenanceExtras, provenanceFor } from "../utils/provenance.js";
|
|
39
|
+
import { CACHE_SEMI_STATIC } from "../types.js";
|
|
40
|
+
import { extractParlamentares, fetchSenadorDetalhe, parseSenadorResumo } from "./senadores.js";
|
|
41
|
+
import { fetchComissaoColegiado, parseComissaoItem, parseComissaoResumo } from "./comissoes.js";
|
|
42
|
+
import { UFS } from "./referencia.js";
|
|
43
|
+
export { DEEP_RESEARCH_TOOLS };
|
|
44
|
+
/** Teto de resultados do `search` (o contrato pede uma lista curta e relevante). */
|
|
45
|
+
export const DEEP_RESEARCH_LIMIT = 10;
|
|
46
|
+
const PREFIXO_SENADOR = "sen:";
|
|
47
|
+
const PREFIXO_COMISSAO = "com:";
|
|
48
|
+
const PATH_SENADORES = "/senador/lista/atual";
|
|
49
|
+
const PATH_COLEGIADOS = "/comissao/lista/colegiados";
|
|
50
|
+
/** Página pública do perfil do senador (confirmada nas fixtures de contrato e ao vivo). */
|
|
51
|
+
export const urlPerfilSenador = (codigo) => `https://www25.senado.leg.br/web/senadores/senador/-/perfil/${codigo}`;
|
|
52
|
+
/** Página pública da comissão (verificada ao vivo em 03/09/2026 — ver cabeçalho). */
|
|
53
|
+
export const urlPaginaComissao = (codigo) => `https://legis.senado.leg.br/comissoes/comissao?codcol=${codigo}`;
|
|
54
|
+
/** O índice muda com posse, licença e criação de colegiado — nada que um dia não absorva. */
|
|
55
|
+
const INDICE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
/** Título de exibição de um senador: nome parlamentar e a bancada. */
|
|
57
|
+
export function tituloSenador(s) {
|
|
58
|
+
const bancada = [s.partido, s.uf].filter(Boolean).join("/");
|
|
59
|
+
return bancada ? `${s.nome} (${bancada})` : s.nome;
|
|
60
|
+
}
|
|
61
|
+
/** Título de exibição de uma comissão: sigla e nome. */
|
|
62
|
+
export function tituloComissao(c) {
|
|
63
|
+
return c.sigla ? `${c.sigla} — ${c.nome}` : c.nome;
|
|
64
|
+
}
|
|
65
|
+
// "senadora do mato grosso do sul" tem de achar quem é de MS: a sigla sozinha
|
|
66
|
+
// não casa com o nome do estado, então o nome entra nas keywords.
|
|
67
|
+
const NOME_UF = new Map(UFS.map((uf) => [uf.sigla, uf.nome]));
|
|
68
|
+
export function entradasSenadores(senadores) {
|
|
69
|
+
return senadores
|
|
70
|
+
.filter((s) => s.codigo > 0)
|
|
71
|
+
.map((s) => ({
|
|
72
|
+
id: `${PREFIXO_SENADOR}${s.codigo}`,
|
|
73
|
+
title: tituloSenador(s),
|
|
74
|
+
url: urlPerfilSenador(s.codigo),
|
|
75
|
+
keywords: [s.nomeCompleto, s.partido ?? "", s.uf, NOME_UF.get(s.uf) ?? "", "senador", "senadora", "parlamentar"].filter(Boolean),
|
|
76
|
+
text: `Senador(a) ${s.nomeCompleto} — ${s.partido ?? "sem partido"}/${s.uf} (${NOME_UF.get(s.uf) ?? "UF n/d"}), em exercício no Senado Federal.`,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
export function entradasComissoes(comissoes) {
|
|
80
|
+
return comissoes
|
|
81
|
+
.filter((c) => c.codigo > 0)
|
|
82
|
+
.map((c) => ({
|
|
83
|
+
id: `${PREFIXO_COMISSAO}${c.codigo}`,
|
|
84
|
+
title: tituloComissao(c),
|
|
85
|
+
url: urlPaginaComissao(c.codigo),
|
|
86
|
+
keywords: [c.sigla, c.tipo ?? "", c.casa ?? "", "comissão", "colegiado"].filter(Boolean),
|
|
87
|
+
text: `${c.nome} — ${c.tipo ?? "colegiado"} (${c.casa === "CN" ? "Congresso Nacional" : "Senado Federal"}).`,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
// Por baseUrl: os testes e o stdio podem apontar para outra base, e o índice de
|
|
91
|
+
// uma não pode servir a outra.
|
|
92
|
+
const indices = new Map();
|
|
93
|
+
const carregando = new Map();
|
|
94
|
+
async function construirIndice(baseUrl) {
|
|
95
|
+
// As mesmas chaves de cache de `senado_listar_senadores`/`senado_listar_comissoes`:
|
|
96
|
+
// quem já listou pelo tool aquece o índice, e vice-versa.
|
|
97
|
+
const [sen, com] = await Promise.all([
|
|
98
|
+
cachedFetchWithMeta("senado_listar_senadores", { path: PATH_SENADORES }, CACHE_SEMI_STATIC, () => upstreamFetch(PATH_SENADORES, {}, baseUrl)),
|
|
99
|
+
cachedFetchWithMeta("senado_listar_comissoes", {}, CACHE_SEMI_STATIC, () => upstreamFetch(PATH_COLEGIADOS, {}, baseUrl)),
|
|
100
|
+
]);
|
|
101
|
+
const senadores = extractParlamentares(sen.value).map(parseSenadorResumo);
|
|
102
|
+
const comissoes = ensureArray(com.value?.ListaColegiados?.Colegiados?.Colegiado).map(parseComissaoItem);
|
|
103
|
+
return {
|
|
104
|
+
index: createIndex([...entradasSenadores(senadores), ...entradasComissoes(comissoes)]),
|
|
105
|
+
provenance: [
|
|
106
|
+
provenanceFor("SENADO_LEGIS", baseUrl, PATH_SENADORES, { dataset_id: "lista/atual", retrieved_at: sen.fetchedAt }),
|
|
107
|
+
provenanceFor("SENADO_LEGIS", baseUrl, PATH_COLEGIADOS, { retrieved_at: com.fetchedAt }),
|
|
108
|
+
],
|
|
109
|
+
criadoEm: Date.now(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* O índice da base, construído no primeiro uso e mantido por `INDICE_TTL_MS`.
|
|
114
|
+
* Chamadas concorrentes compartilham a construção; falha não fica em cache.
|
|
115
|
+
*/
|
|
116
|
+
export async function obterIndice(baseUrl) {
|
|
117
|
+
const atual = indices.get(baseUrl);
|
|
118
|
+
if (atual && Date.now() - atual.criadoEm < INDICE_TTL_MS)
|
|
119
|
+
return atual;
|
|
120
|
+
let promessa = carregando.get(baseUrl);
|
|
121
|
+
if (!promessa) {
|
|
122
|
+
promessa = construirIndice(baseUrl)
|
|
123
|
+
.then((indice) => {
|
|
124
|
+
indices.set(baseUrl, indice);
|
|
125
|
+
return indice;
|
|
126
|
+
})
|
|
127
|
+
.finally(() => carregando.delete(baseUrl));
|
|
128
|
+
carregando.set(baseUrl, promessa);
|
|
129
|
+
}
|
|
130
|
+
return promessa;
|
|
131
|
+
}
|
|
132
|
+
/** Só para testes: esquece os índices construídos. */
|
|
133
|
+
export function limparIndices() {
|
|
134
|
+
indices.clear();
|
|
135
|
+
carregando.clear();
|
|
136
|
+
}
|
|
137
|
+
const linha = (rotulo, valor) => valor === null || valor === undefined || valor === "" ? null : `- **${rotulo}:** ${String(valor)}`;
|
|
138
|
+
export function renderizarSenador(d) {
|
|
139
|
+
const tratamento = d.sexo === "Feminino" ? "Senadora" : "Senador";
|
|
140
|
+
const cabecalho = [
|
|
141
|
+
`# ${tratamento} ${d.nome}`,
|
|
142
|
+
"",
|
|
143
|
+
linha("Nome completo", d.nomeCompleto),
|
|
144
|
+
linha("Nome civil", d.nomeCivil),
|
|
145
|
+
linha("Partido/UF", [d.partido, d.uf].filter(Boolean).join("/")),
|
|
146
|
+
linha("Nascimento", d.dataNascimento),
|
|
147
|
+
linha("Naturalidade", [d.naturalidade, d.ufNaturalidade].filter(Boolean).join("/")),
|
|
148
|
+
linha("E-mail", d.email),
|
|
149
|
+
].filter((l) => l !== null);
|
|
150
|
+
const mandatos = d.mandatos.length
|
|
151
|
+
? [
|
|
152
|
+
"",
|
|
153
|
+
"## Mandatos",
|
|
154
|
+
...d.mandatos.map((m) => `- ${m.legislatura ? `${m.legislatura}ª legislatura` : "legislatura n/d"} — ${m.uf ?? ""} ${m.participacao ?? ""}`.trimEnd() +
|
|
155
|
+
(m.dataInicio ? ` (${m.dataInicio}${m.dataFim ? ` a ${m.dataFim}` : ""})` : "")),
|
|
156
|
+
]
|
|
157
|
+
: [];
|
|
158
|
+
return [...cabecalho, ...mandatos, "", `Fonte: Senado Federal — Dados Abertos. Perfil público: ${urlPerfilSenador(d.codigo)}`].join("\n");
|
|
159
|
+
}
|
|
160
|
+
export function renderizarComissao(r) {
|
|
161
|
+
const presidente = r.presidente;
|
|
162
|
+
const vice = r.vicePresidente;
|
|
163
|
+
const pessoa = (p) => (p?.nome ? `${p.nome}${p.bancada ? ` (${p.bancada})` : ""}` : null);
|
|
164
|
+
return [
|
|
165
|
+
`# ${tituloComissao({ sigla: String(r.sigla ?? ""), nome: String(r.nome ?? "") })}`,
|
|
166
|
+
"",
|
|
167
|
+
linha("Tipo", r.tipo),
|
|
168
|
+
linha("Finalidade", r.finalidade),
|
|
169
|
+
linha("Presidência", pessoa(presidente)),
|
|
170
|
+
linha("Vice-presidência", pessoa(vice)),
|
|
171
|
+
linha("Membros", r.totalMembros ? `${r.totalMembros} (${r.titulares ?? "?"} titulares, ${r.suplentes ?? "?"} suplentes)` : null),
|
|
172
|
+
r.aviso ? `\n> ${String(r.aviso)}` : null,
|
|
173
|
+
"",
|
|
174
|
+
`Fonte: Senado Federal — Dados Abertos. Página pública: ${urlPaginaComissao(Number(r.codigo))}`,
|
|
175
|
+
]
|
|
176
|
+
.filter((l) => l !== null)
|
|
177
|
+
.join("\n");
|
|
178
|
+
}
|
|
179
|
+
// ==================== HANDLERS ====================
|
|
180
|
+
function handlers(baseUrl) {
|
|
181
|
+
async function search(query) {
|
|
182
|
+
const indice = await obterIndice(baseUrl);
|
|
183
|
+
const results = indice.index.search(query, { limit: DEEP_RESEARCH_LIMIT }).map(({ id, title, url }) => ({ id, title, url }));
|
|
184
|
+
return { results, extras: provenanceExtras(indice.provenance) };
|
|
185
|
+
}
|
|
186
|
+
async function fetch(id) {
|
|
187
|
+
if (id.startsWith(PREFIXO_SENADOR)) {
|
|
188
|
+
const codigo = Number.parseInt(id.slice(PREFIXO_SENADOR.length), 10);
|
|
189
|
+
if (!Number.isInteger(codigo) || codigo <= 0)
|
|
190
|
+
return null;
|
|
191
|
+
const { path, fetchedAt, detalhe } = await fetchSenadorDetalhe(codigo, baseUrl);
|
|
192
|
+
if (!detalhe.codigo)
|
|
193
|
+
return null;
|
|
194
|
+
const prov = provenanceFor("SENADO_LEGIS", baseUrl, path, {
|
|
195
|
+
dataset_id: `codigoParlamentar=${codigo}`,
|
|
196
|
+
retrieved_at: fetchedAt,
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
document: {
|
|
200
|
+
id,
|
|
201
|
+
title: tituloSenador(detalhe),
|
|
202
|
+
text: renderizarSenador(detalhe),
|
|
203
|
+
url: urlPerfilSenador(codigo),
|
|
204
|
+
metadata: {
|
|
205
|
+
tipo: "senador",
|
|
206
|
+
codigo,
|
|
207
|
+
partido: detalhe.partido,
|
|
208
|
+
uf: detalhe.uf,
|
|
209
|
+
mandatos: detalhe.mandatos.length,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
extras: provenanceExtras(prov),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (id.startsWith(PREFIXO_COMISSAO)) {
|
|
216
|
+
const codigo = Number.parseInt(id.slice(PREFIXO_COMISSAO.length), 10);
|
|
217
|
+
if (!Number.isInteger(codigo) || codigo <= 0)
|
|
218
|
+
return null;
|
|
219
|
+
const { path, fetchedAt, colegiado } = await fetchComissaoColegiado(codigo, baseUrl);
|
|
220
|
+
if (!colegiado)
|
|
221
|
+
return null;
|
|
222
|
+
const resumo = parseComissaoResumo(colegiado, String(colegiado.SiglaColegiado ?? ""), "resumo");
|
|
223
|
+
const prov = provenanceFor("SENADO_LEGIS", baseUrl, path, {
|
|
224
|
+
dataset_id: `comissao=${resumo.sigla}; codigo=${codigo}`,
|
|
225
|
+
retrieved_at: fetchedAt,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
document: {
|
|
229
|
+
id,
|
|
230
|
+
title: tituloComissao({ sigla: String(resumo.sigla ?? ""), nome: String(resumo.nome ?? "") }),
|
|
231
|
+
text: renderizarComissao(resumo),
|
|
232
|
+
url: urlPaginaComissao(codigo),
|
|
233
|
+
metadata: {
|
|
234
|
+
tipo: "comissao",
|
|
235
|
+
codigo,
|
|
236
|
+
sigla: resumo.sigla,
|
|
237
|
+
tipoColegiado: resumo.tipo,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
extras: provenanceExtras(prov),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return { search, fetch };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Aponta a fábrica do pacote para um coletor e devolve o que ela registrou —
|
|
249
|
+
* só `description` e `callback` interessam: título, annotations, outputSchema,
|
|
250
|
+
* perfil e telemetria são do shim.
|
|
251
|
+
*/
|
|
252
|
+
export function capturarDeepResearchTools(baseUrl) {
|
|
253
|
+
const capturados = [];
|
|
254
|
+
const coletor = {
|
|
255
|
+
registerTool: (name, config, callback) => {
|
|
256
|
+
capturados.push({ name, config, callback });
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
registerDeepResearchTools(coletor, {
|
|
260
|
+
...handlers(baseUrl),
|
|
261
|
+
corpus: "Brazilian Federal Senate open data (senators in office and active committees of the Senate and the National Congress)",
|
|
262
|
+
richTools: "the `senado_*` tools",
|
|
263
|
+
limit: DEEP_RESEARCH_LIMIT,
|
|
264
|
+
});
|
|
265
|
+
const porNome = (name) => {
|
|
266
|
+
const reg = capturados.find((c) => c.name === name);
|
|
267
|
+
if (!reg)
|
|
268
|
+
throw new Error(`fábrica do mcp-search não registrou "${name}"`);
|
|
269
|
+
return reg;
|
|
270
|
+
};
|
|
271
|
+
return { search: porNome("search"), fetch: porNome("fetch") };
|
|
272
|
+
}
|
|
273
|
+
export function registerDeepResearchToolsSenado(server, baseUrl) {
|
|
274
|
+
const { searchInputSchema, fetchInputSchema } = contractSchemas();
|
|
275
|
+
const capturados = capturarDeepResearchTools(baseUrl);
|
|
276
|
+
// U1. search
|
|
277
|
+
server.tool("search", capturados.search.config.description, searchInputSchema.shape, capturados.search.callback);
|
|
278
|
+
// U2. fetch
|
|
279
|
+
server.tool("fetch", capturados.fetch.config.description, fetchInputSchema.shape, capturados.fetch.callback);
|
|
280
|
+
}
|
package/dist/tools/senadores.js
CHANGED
|
@@ -155,6 +155,30 @@ export function matchesNome(s, nome) {
|
|
|
155
155
|
const alvo = normalizeText(nome);
|
|
156
156
|
return normalizeText(s.nomeCompleto).includes(alvo) || normalizeText(s.nome).includes(alvo);
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* A leitura de `senado_obter_senador`: detalhe biográfico + mandatos (sub-endpoint,
|
|
160
|
+
* degrada para lista vazia). Exportada porque o `fetch` do Deep Research
|
|
161
|
+
* (`deep-research.ts`) é este mesmo documento, com a mesma proveniência e o
|
|
162
|
+
* mesmo cache — `path` é o que entra em `provenanceFor`.
|
|
163
|
+
*/
|
|
164
|
+
export async function fetchSenadorDetalhe(codigoSenador, baseUrl) {
|
|
165
|
+
const path = `/senador/${codigoSenador}`;
|
|
166
|
+
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_obter_senador", { codigo: codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(path, {}, baseUrl));
|
|
167
|
+
const dados = response.DetalheParlamentar || response;
|
|
168
|
+
const detalhe = parseSenadorDetalhe(dados);
|
|
169
|
+
// The /senador/{codigo} detail carries no Mandatos; fetch the sub-endpoint.
|
|
170
|
+
// Degrade to an empty list if it fails, keeping the biographical data.
|
|
171
|
+
let mandatos = [];
|
|
172
|
+
try {
|
|
173
|
+
const mandatosPath = `/senador/${codigoSenador}/mandatos`;
|
|
174
|
+
const { value: mResp } = await cachedFetchWithMeta("senado_obter_senador_mandatos", { codigo: codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(mandatosPath, {}, baseUrl));
|
|
175
|
+
mandatos = digArrayRoot(mResp, [["MandatoParlamentar", "Parlamentar", "Mandatos", "Mandato"]], "senado_obter_senador:mandatos").map(parseMandato);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// sub-endpoint unavailable — keep the (empty) mandatos from the detail
|
|
179
|
+
}
|
|
180
|
+
return { path, fetchedAt, detalhe: { ...detalhe, mandatos } };
|
|
181
|
+
}
|
|
158
182
|
export function registerSenadoresTools(server, baseUrl) {
|
|
159
183
|
// A1. senado_listar_senadores (filtro `nome` substitui a antiga busca por nome)
|
|
160
184
|
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`.", {
|
|
@@ -195,26 +219,12 @@ export function registerSenadoresTools(server, baseUrl) {
|
|
|
195
219
|
codigoSenador: z.number().int().positive().describe("Código único do senador no sistema do Senado"),
|
|
196
220
|
}, async (params) => {
|
|
197
221
|
try {
|
|
198
|
-
const path =
|
|
199
|
-
const { value: response, fetchedAt } = await cachedFetchWithMeta("senado_obter_senador", { codigo: params.codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(path, {}, baseUrl));
|
|
200
|
-
const dados = response.DetalheParlamentar || response;
|
|
201
|
-
const detalhe = parseSenadorDetalhe(dados);
|
|
202
|
-
// The /senador/{codigo} detail carries no Mandatos; fetch the sub-endpoint.
|
|
203
|
-
// Degrade to an empty list if it fails, keeping the biographical data.
|
|
204
|
-
let mandatos = [];
|
|
205
|
-
try {
|
|
206
|
-
const mandatosPath = `/senador/${params.codigoSenador}/mandatos`;
|
|
207
|
-
const { value: mResp } = await cachedFetchWithMeta("senado_obter_senador_mandatos", { codigo: params.codigoSenador }, CACHE_ON_DEMAND, () => upstreamFetch(mandatosPath, {}, baseUrl));
|
|
208
|
-
mandatos = digArrayRoot(mResp, [["MandatoParlamentar", "Parlamentar", "Mandatos", "Mandato"]], "senado_obter_senador:mandatos").map(parseMandato);
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
// sub-endpoint unavailable — keep the (empty) mandatos from the detail
|
|
212
|
-
}
|
|
222
|
+
const { path, fetchedAt, detalhe } = await fetchSenadorDetalhe(params.codigoSenador, baseUrl);
|
|
213
223
|
const prov = provenanceFor("SENADO_LEGIS", baseUrl, path, {
|
|
214
224
|
dataset_id: `codigoParlamentar=${params.codigoSenador}`,
|
|
215
225
|
retrieved_at: fetchedAt,
|
|
216
226
|
});
|
|
217
|
-
return resultWithProvenance(
|
|
227
|
+
return resultWithProvenance(detalhe, prov);
|
|
218
228
|
}
|
|
219
229
|
catch (e) {
|
|
220
230
|
return errorFrom(e, "Senador não encontrado");
|
package/dist/utils/provenance.js
CHANGED
|
@@ -202,3 +202,15 @@ export const ATTRIBUTION_META_KEY = provenanceContext.metaKeys.attribution;
|
|
|
202
202
|
* `data` deve ser um objeto (structuredContent precisa ser objeto).
|
|
203
203
|
*/
|
|
204
204
|
export const resultWithProvenance = (data, provenance) => ({ ...provenanceContext.result(data, provenance) });
|
|
205
|
+
/**
|
|
206
|
+
* Os dois canais não-textuais do envelope (`structuredContent.provenance` +
|
|
207
|
+
* `attribution`, e o espelho em `_meta`) SEM o bloco de texto — para tools cujo
|
|
208
|
+
* `content` é ditado por um contrato externo e não pode levar rodapé (`search`/
|
|
209
|
+
* `fetch` do Deep Research, em `src/tools/deep-research.ts`, cujo texto tem de
|
|
210
|
+
* ser exatamente o JSON do contrato). Deriva do mesmo `provenanceContext.result`
|
|
211
|
+
* das outras tools, então o portão de proveniência continua único.
|
|
212
|
+
*/
|
|
213
|
+
export function provenanceExtras(provenance) {
|
|
214
|
+
const { structuredContent, _meta } = provenanceContext.result({}, provenance);
|
|
215
|
+
return { structured: structuredContent, meta: _meta };
|
|
216
|
+
}
|
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.7.0",
|
|
4
4
|
"description": "MCP server for Brazilian Senate open data — runs hosted on Cloudflare Workers or locally via npx (stdio)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=22"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"dev": "wrangler dev",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"eval:judge": "tsx scripts/eval/judge.ts",
|
|
36
36
|
"eval:report": "tsx scripts/eval/report.ts",
|
|
37
37
|
"smoke:openai-app": "node scripts/smoke-openai-app.mjs",
|
|
38
|
+
"smoke:stdio": "node scripts/smoke-stdio.mjs",
|
|
38
39
|
"ingest:ecidadania": "tsx scripts/ingest-ecidadania/index.ts",
|
|
39
40
|
"ingest:ecidadania:eventos": "tsx scripts/ingest-ecidadania/index-eventos.ts",
|
|
40
41
|
"ingest:ecidadania:ideias": "tsx scripts/ingest-ecidadania/index-ideias.ts",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
51
52
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
52
53
|
"@sbissoli/mcp-provenance": "^0.1.0",
|
|
54
|
+
"@sbissoli/mcp-search": "^0.3.0",
|
|
53
55
|
"@sbissoli/mcp-stats": "^0.2.0",
|
|
54
56
|
"zod": "^4.5.4"
|
|
55
57
|
},
|