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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white)
4
4
  ![MCP](https://img.shields.io/badge/MCP-Streamable%20HTTP-1f6feb)
5
- ![Tools](https://img.shields.io/badge/tools-67-2ea44f)
5
+ ![Tools](https://img.shields.io/badge/tools-69-2ea44f)
6
6
  [![CI](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml)
7
7
  [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io)
8
8
  [![LobeHub](https://lobehub.com/badge/mcp/sidneybissoli-senado-br-mcp-cloudflare)](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 **67 tools**, **4 prompts**, and **5 resources** across two domains:
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
- 67 tools, same provenance envelope, no Cloudflare in the loop. This is the npm/stdio channel, published
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 67 tools well — a themed tool map, common question→tool playbooks, the
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 20+
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 67 tools carry the envelope (verify with `grep -c 'resultWithProvenance(' src/tools/*.ts`). 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.
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
- **Total: 67 tools**
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 67 ferramentas agrupadas por domínio. |
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 67 tools, it exposes **prompts** (ready-made pt-BR
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
  ![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflare&logoColor=white)
4
4
  ![MCP](https://img.shields.io/badge/MCP-Streamable%20HTTP-1f6feb)
5
- ![Tools](https://img.shields.io/badge/tools-67-2ea44f)
5
+ ![Tools](https://img.shields.io/badge/tools-69-2ea44f)
6
6
  [![CI](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/SidneyBissoli/senado-br-mcp-cloudflare/actions/workflows/ci.yml)
7
7
  [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io)
8
8
  [![LobeHub](https://lobehub.com/badge/mcp/sidneybissoli-senado-br-mcp-cloudflare)](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 **67 ferramentas**, **4 prompts** e **5 recursos** em dois domínios:
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 25
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 25 ferramentas tambem anunciam um template UI compartilhado do MCP Apps em
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 67 ferramentas, o mesmo envelope de proveniência, sem Cloudflare no caminho. É o canal npm/stdio,
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 67 ferramentas — um mapa de ferramentas por tema, playbooks de
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 25 ferramentas mais um widget MCP Apps compartilhado para revisao/submissao como app OpenAI (`/mcp/openai-app` permanece como alias legado)
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 20+
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
- **Total: 67 ferramentas**
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 67 ferramentas agrupadas por domínio. |
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 67 ferramentas, ele expõe **prompts** (workflows prontos em pt-BR:
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-08-01T08:10:35.063Z",
2
+ "extraidoEm": "2026-09-01T10:56:16.383Z",
3
3
  "fonteUrl": "https://www12.senado.leg.br/institucional/estrutura/orgaosenado",
4
- "total": 824,
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>Dados Abertos Senado BR MCP</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="https://www.npmjs.com/package/senado-br-mcp"><code>senado-br-mcp</code></a></li>
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
- <p class="muted">In English: this is an independent, read-only MCP server for Brazilian Federal Senate open data. 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>
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("Dados Abertos Senado BR MCP", buildLandingBody(), "pt-BR"), {
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
- export function htmlPage(title, body, lang = "en") {
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). 67 ferramentas, respostas em pt-BR. Abaixo, por onde começar conforme o objetivo.
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 67 tools. */
54
+ /** Compact grouped catalog of all 69 tools. */
55
55
  export function buildCatalogo() {
56
- return `# Catálogo de ferramentas (67)
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 67 ferramentas agrupadas por domínio.",
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);
@@ -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 67 registered tools — the coverage test in
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
@@ -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((c) => ({
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 = `/comissao/${codigo}`;
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
+ }
@@ -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 = `/senador/${params.codigoSenador}`;
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({ ...detalhe, mandatos }, prov);
227
+ return resultWithProvenance(detalhe, prov);
218
228
  }
219
229
  catch (e) {
220
230
  return errorFrom(e, "Senador não encontrado");
@@ -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
@@ -1,3 +1,3 @@
1
1
  /** Single source of truth for the server version (kept in sync with package.json). */
2
- export const VERSION = "3.6.0";
2
+ export const VERSION = "3.7.0";
3
3
  export const USER_AGENT = `senado-br-mcp/${VERSION} (+https://senado.sidneybissoli.com; sbissoli76@gmail.com)`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senado-br-mcp",
3
- "version": "3.6.0",
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": ">=20"
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
  },