br-eli-mcp 0.6.0__tar.gz

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.
Files changed (45) hide show
  1. br_eli_mcp-0.6.0/.github/workflows/release.yml +63 -0
  2. br_eli_mcp-0.6.0/.gitignore +20 -0
  3. br_eli_mcp-0.6.0/CONSTITUTION.md +75 -0
  4. br_eli_mcp-0.6.0/DISCOVERY.md +425 -0
  5. br_eli_mcp-0.6.0/LICENSE +202 -0
  6. br_eli_mcp-0.6.0/PKG-INFO +152 -0
  7. br_eli_mcp-0.6.0/README.md +121 -0
  8. br_eli_mcp-0.6.0/SOURCES.md +187 -0
  9. br_eli_mcp-0.6.0/glama.json +4 -0
  10. br_eli_mcp-0.6.0/pyproject.toml +65 -0
  11. br_eli_mcp-0.6.0/server.json +22 -0
  12. br_eli_mcp-0.6.0/src/br_eli_mcp/__init__.py +3 -0
  13. br_eli_mcp-0.6.0/src/br_eli_mcp/audit.py +94 -0
  14. br_eli_mcp-0.6.0/src/br_eli_mcp/cache.py +56 -0
  15. br_eli_mcp-0.6.0/src/br_eli_mcp/carf_client.py +138 -0
  16. br_eli_mcp-0.6.0/src/br_eli_mcp/caselaw_client.py +231 -0
  17. br_eli_mcp-0.6.0/src/br_eli_mcp/citations.py +397 -0
  18. br_eli_mcp-0.6.0/src/br_eli_mcp/client.py +92 -0
  19. br_eli_mcp-0.6.0/src/br_eli_mcp/models.py +177 -0
  20. br_eli_mcp-0.6.0/src/br_eli_mcp/norma_client.py +93 -0
  21. br_eli_mcp-0.6.0/src/br_eli_mcp/norma_text.py +112 -0
  22. br_eli_mcp-0.6.0/src/br_eli_mcp/server.py +1151 -0
  23. br_eli_mcp-0.6.0/src/br_eli_mcp/stj_client.py +174 -0
  24. br_eli_mcp-0.6.0/src/br_eli_mcp/tcu_client.py +166 -0
  25. br_eli_mcp-0.6.0/src/br_eli_mcp/text_client.py +89 -0
  26. br_eli_mcp-0.6.0/src/br_eli_mcp/tst_client.py +241 -0
  27. br_eli_mcp-0.6.0/tests/fixtures/carf_acordao_sample.json +35 -0
  28. br_eli_mcp-0.6.0/tests/fixtures/codigo_civil_normas.json +1 -0
  29. br_eli_mcp-0.6.0/tests/fixtures/stj_espelho_sample.json +26 -0
  30. br_eli_mcp-0.6.0/tests/fixtures/tcu_documento_sample.json +41 -0
  31. br_eli_mcp-0.6.0/tests/fixtures/tcu_resumidos_sample.json +355 -0
  32. br_eli_mcp-0.6.0/tests/fixtures/tst_acordao_sample.json +74 -0
  33. br_eli_mcp-0.6.0/tests/fixtures/tst_pesquisa_exact_sample.json +140 -0
  34. br_eli_mcp-0.6.0/tests/test_carf_parse_offline.py +62 -0
  35. br_eli_mcp-0.6.0/tests/test_carf_smoke.py +40 -0
  36. br_eli_mcp-0.6.0/tests/test_caselaw_smoke.py +52 -0
  37. br_eli_mcp-0.6.0/tests/test_norma_smoke.py +27 -0
  38. br_eli_mcp-0.6.0/tests/test_smoke.py +26 -0
  39. br_eli_mcp-0.6.0/tests/test_stj_parse_offline.py +46 -0
  40. br_eli_mcp-0.6.0/tests/test_stj_smoke.py +37 -0
  41. br_eli_mcp-0.6.0/tests/test_tcu_parse_offline.py +74 -0
  42. br_eli_mcp-0.6.0/tests/test_tcu_smoke.py +50 -0
  43. br_eli_mcp-0.6.0/tests/test_text_smoke.py +30 -0
  44. br_eli_mcp-0.6.0/tests/test_tst_parse_offline.py +105 -0
  45. br_eli_mcp-0.6.0/tests/test_tst_smoke.py +69 -0
@@ -0,0 +1,63 @@
1
+ name: release
2
+
3
+ # PyPI publish via classic API token (org secret PYPI_API_TOKEN) + MCP Registry.
4
+ # Trigger: push a version tag, e.g. `git tag v0.1.0 && git push origin v0.1.0`.
5
+ # NOTE: PyPI trusted-publishing (OIDC) hit a persistent invalid-publisher error
6
+ # across the whole fleet; switched to a classic token as a stopgap. Once a
7
+ # package version has published successfully once, trusted publishing can be
8
+ # re-enabled on pypi.org and this can flip back to OIDC.
9
+
10
+ on:
11
+ push:
12
+ tags:
13
+ - "v*"
14
+ workflow_dispatch: {} # manual re-run of the MCP Registry publish without bumping the package version
15
+
16
+ permissions:
17
+ contents: read
18
+ id-token: write # required for MCP Registry OIDC login
19
+
20
+ jobs:
21
+ build-and-publish-pypi:
22
+ if: github.event_name == 'push' # only on a version tag; skipped on manual dispatch
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+
32
+ - name: Install build tooling
33
+ run: python -m pip install --upgrade build
34
+
35
+ - name: Build sdist + wheel
36
+ run: python -m build
37
+
38
+ - name: Publish to PyPI (classic API token)
39
+ uses: pypa/gh-action-pypi-publish@release/v1
40
+ with:
41
+ password: ${{ secrets.PYPI_API_TOKEN }}
42
+
43
+ publish-mcp-registry:
44
+ needs: build-and-publish-pypi
45
+ # run after a successful PyPI publish, OR standalone on manual dispatch
46
+ if: always() && (github.event_name == 'workflow_dispatch' || needs.build-and-publish-pypi.result == 'success')
47
+ runs-on: ubuntu-latest
48
+ permissions:
49
+ contents: read
50
+ id-token: write # OIDC login to the MCP Registry
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+
54
+ - name: Install mcp-publisher
55
+ run: |
56
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_amd64.tar.gz" | tar xz mcp-publisher
57
+ sudo mv mcp-publisher /usr/local/bin/
58
+
59
+ - name: Login to MCP Registry (GitHub OIDC)
60
+ run: mcp-publisher login github-oidc
61
+
62
+ - name: Publish server.json to MCP Registry
63
+ run: mcp-publisher publish
@@ -0,0 +1,20 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+
12
+ # Local runtime (never source)
13
+ .matematic/
14
+ *.log
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Note: tests/fixtures/* ARE committed - public Finlex data needed for tests.
@@ -0,0 +1,75 @@
1
+ # Constitution of br-eli-mcp
2
+
3
+ Version: 0.3.0
4
+ Date: 2026-07-06
5
+ Licence: Apache-2.0
6
+
7
+ `br-eli-mcp` is an MCP server for Brazilian federal legislative data provided by the Camara dos
8
+ Deputados and the Congresso Nacional (Senado Federal). It searches and retrieves legislative-process
9
+ bills, enacted Normas Juridicas, and real article-level text with verifiable URN Lex /
10
+ stable-API-URI citations.
11
+
12
+ The 4 principles below bind every contribution to the project and every tool exposed by this MCP
13
+ server. They are inherited from the `eu-legal-mcp` line Constitution (Article IV) - a connector may
14
+ tighten them, never weaken them.
15
+
16
+ ---
17
+
18
+ ## Art. 1. Public data only
19
+
20
+ `dadosabertos.camara.leg.br`, `legis.senado.leg.br/dadosabertos`, and `normas.leg.br/api/public` are
21
+ the official, public sources of Brazilian federal legislative data provided by the Camara dos
22
+ Deputados and the Congresso Nacional. Legal status of the data: open data, keyless, no
23
+ registration - see DISCOVERY.md.
24
+
25
+ This server must not:
26
+ - transfer law-firm client personal data or pleadings to either API (both are read-only for the
27
+ source's data - we send nothing beyond search parameters).
28
+ - proxy access to other sources or private databases through this API.
29
+
30
+ ## Art. 2. Mandatory audit log
31
+
32
+ Every call to every MCP tool MUST be written to `~/.matematic/audit/br-eli-mcp.jsonl` as one JSON
33
+ line:
34
+
35
+ ```json
36
+ {"ts": "...", "tool": "...", "input_hash": "...", "output_count_or_size": N, "duration_ms": N, "status": "ok|error"}
37
+ ```
38
+
39
+ Purpose: operator accountability. `input_hash` is a SHA-256 of the normalized argument form (without
40
+ storing raw queries that could contain fragments of client pleadings). Inability to write to the audit
41
+ log = inability to run the tool (the tool returns an error, it does not silently skip). The link to AI
42
+ Act art. 12 belongs to the deploying entity - the MCP server itself is not a high-risk AI system.
43
+
44
+ ## Art. 3. Vendor neutrality
45
+
46
+ No tool may: hardcode an LLM provider; assume a specific model in prompt engineering; introduce
47
+ telemetry to commercial services. The server communicates only with `dadosabertos.camara.leg.br`,
48
+ `legis.senado.leg.br/dadosabertos`, `normas.leg.br/api/public`, and the local filesystem (cache +
49
+ audit). No authentication - all three upstream APIs are public and keyless.
50
+
51
+ ## Art. 4. URN Lex / stable-API-URI citations and a human-readable citation are mandatory
52
+
53
+ Every response from every tool MUST contain three fields:
54
+ - `lex_uri`: the canonical identifier - a real `urn:lex:br:federal:...` (from `br_get_norma`, echoed
55
+ back verbatim, never invented - e.g. `urn:lex:br:federal:lei:2002-01-10;10406`), that same URN plus
56
+ an article suffix from `br_get_norma_index` (from `br_get_norma_texto`, e.g.
57
+ `urn:lex:br:federal:lei:2002-01-10;10406!art5`), or, for bills that are not yet enacted law, the
58
+ Camara's own stable API URI (from `br_search_proposicoes` / `br_get_proposicao`).
59
+ - `human_readable_citation`: a citation in Brazilian legal convention (e.g. `"PL 2597/2024"` for a
60
+ bill, `"Codigo Civil (2002) (CC)"` for an enacted norma, `"Art. 5o"` for one article).
61
+ - `source_url`: a full URL by which this document can be retrieved independently of the MCP
62
+ (`camara.leg.br` tramitacao page or `normas.leg.br`).
63
+
64
+ Purpose: verifiability. An LLM never presents content without the ability to click the link and check
65
+ the original. The presence of these fields is a necessary, not a sufficient, condition with respect
66
+ to rules on the admissibility of evidence.
67
+
68
+ ---
69
+
70
+ ## Constitution evolution
71
+
72
+ Changes to art. 1-4 require: a SEMVER version bump (PATCH/MINOR/MAJOR), an entry in `CHANGELOG.md`,
73
+ and a package version bump in `pyproject.toml`.
74
+
75
+ First version: 2026-07-06. Author: Wieslaw Mazur / MateMatic.
@@ -0,0 +1,425 @@
1
+ # Discovery notes - Brazil
2
+
3
+ Current as of 2026-07-07 (v0.6.0). History below - earlier releases (v0.1.0,
4
+ v0.2.0, v0.5.0) each got something wrong; the corrections are kept for the
5
+ record, not because they're still live guidance.
6
+
7
+ ## v0.6.0 update - TST exact-match confirmed (v0.5.0 rejection reversed) + TCU wired in; TRF4/TRF5 and RFB rejected
8
+
9
+ Widen round, same day as v0.5.0. All probes 2026-07-07.
10
+
11
+ **TST - the v0.5.0 "unreliable_exact_match" rejection was wrong, and the fix
12
+ is exactly what v0.5.0's own notes prescribed**: a browser network trace of
13
+ the real frontend (`jurisprudencia.tst.jus.br`) submitting a search captured
14
+ the true request body of `POST jurisprudencia-backend2.tst.jus.br/rest/
15
+ pesquisa-textual/{start}/{size}`. Two fields the v0.5.0 static analysis of
16
+ the minified bundle missed turn the silently-no-oping filters into working
17
+ ones: a top-level `"orgao": "TST"` and `numeracaoUnica.orgao` defaulting to
18
+ `"5"`. Replayed from a bare httpx client: baseline ACORDAO 3,751,594
19
+ (`totalRegistros`); free text `e='"adicional de insalubridade"'` narrows to
20
+ 228,802; a fully-populated `numeracaoUnica` for a case the endpoint itself
21
+ returned (AIRR 21036-38.2019.5.04.0021) narrows to exactly 1 - the queried
22
+ case. Two more gotchas caught live: (a) `DECISAO_MONOCRATICA`, listed in the
23
+ frontend's own `config.json` and trusted by v0.5.0, is NOT a valid `tipos`
24
+ code - the backend silently ignores it and returns the full 8,483,448-doc
25
+ corpus, so `tst_client.DOC_TYPES` whitelists only the eight codes the real
26
+ frontend sends, each individually verified to change the total; (b)
27
+ `txtInteiroTeor` is often redacted to the literal string "removido no
28
+ backend" while the same record carries the full prose in `inteiroTeorHtml`
29
+ (59K chars on the sample) - the parser falls back to the HTML field,
30
+ mechanically flattened. Wired as `br_search_case_tst` / `br_get_case_tst`.
31
+
32
+ **TCU - wired in.** The open-data JSON feed
33
+ (`dados-abertos.apps.tcu.gov.br/api/acordao/recupera-acordaos`) is live but
34
+ its filter params silently no-op (ano/numero/colegiado all returned the same
35
+ newest-first page) and records carry only the sumario. The search portal's
36
+ own backend, found via the SPA bundle config plus a network trace, is the
37
+ real machine interface: `GET pesquisa.apps.tcu.gov.br/rest/publico/base/
38
+ acordao-completo/documentosResumidos?termo=...&quantidade=N&inicio=M`
39
+ (dedicated total `quantidadeEncontrada`: 525,620 unfiltered, 53,320 for
40
+ `licitação`; field-scoped `NUMACORDAO:1771 ANOACORDAO:2026` -> 3, one per
41
+ colegiado; adding `COLEGIADO:"Plenário"` -> 1) and `GET .../documento?termo=
42
+ KEY:"..."` returning the full ruling prose (`ACORDAO` deliberation,
43
+ `RELATORIO` 35K chars, `VOTO` 25K chars on the sample record). Wired as
44
+ `br_search_case_tcu` / `br_get_case_tcu`.
45
+
46
+ **TRF4 / TRF5 - rejected, `geo_restricted` (connection-level).**
47
+ `jurisprudencia.trf4.jus.br`, `juliapesquisa.trf5.jus.br` and even
48
+ `www.trf5.jus.br` never establish a TCP connection from this (EU) client -
49
+ "All connection attempts failed" across two probe rounds minutes apart,
50
+ while every other .jus.br host probed the same minute connected fine.
51
+ Consistent with country-level network blocking, not an outage. Re-check from
52
+ a BR vantage point before believing this rejection forever.
53
+
54
+ **RFB Solucoes de Consulta (sijut2consulta) - rejected, no machine API.**
55
+ `normas.receita.fazenda.gov.br/sijut2consulta/consulta.action` is a
56
+ server-rendered Struts HTML app; the search page's own markup references only
57
+ `.action` HTML endpoints (no XHR/JSON backend to lift, unlike TST/TCU).
58
+ Scrape-class source - off-principle for this keyless-JSON connector line.
59
+
60
+ **TJDFT / TJBA (state courts) - not probed this round**: the shortlist's
61
+ rule was "state courts only if the federal targets fail", and TST + TCU both
62
+ shipped. Left as explicit `todo` rows in SOURCES.md, not silent omissions.
63
+
64
+ ## v0.5.0 update - STJ + CARF ruling text wired in; TST backend found but not wired in; Planalto re-checked and still skipped
65
+
66
+ Starting point: `https://raw.githubusercontent.com/worldwidelaw/legal-sources/main/manifest.yaml`,
67
+ entries `BR/Planalto`, `BR/STJDadosAbertos`, `BR/TST`, `BR/CARF`,
68
+ `BR/QueridoDiario`. The manifest documents *that* a scraper for each source
69
+ exists in that project, not the exact live endpoint shape - every URL and
70
+ JSON field below was independently re-confirmed with a live HTTP request
71
+ against the actual host, not copied from the manifest's notes.
72
+
73
+ ### STJ Open Data Portal - CONFIRMED LIVE, WIRED IN
74
+
75
+ `https://dadosabertos.web.stj.jus.br` is a CKAN open-data portal, not a
76
+ per-case REST search API. Confirmed live:
77
+
78
+ - `GET /api/3/action/package_list` -> lists 10 chamber/section datasets, e.g.
79
+ `espelhos-de-acordaos-terceira-secao`, one per `orgao julgador` (Corte
80
+ Especial, 1a/2a Secao, 1a-6a Turma).
81
+ - `GET /api/3/action/package_show?id=<dataset>` -> lists that dataset's dated
82
+ resources: one cumulative history ZIP (~9MB, from 2022-05-08) plus ~50
83
+ monthly JSON delta files (one per month since), the newest being
84
+ `20260531.json` at the time of this check.
85
+ - Each monthly JSON file is a flat list of acordao ("espelho do acordao")
86
+ records. A live sample record (`20260531.json` from the Terceira Secao
87
+ dataset) had these keys: `id`, `numeroDocumento`, `numeroProcesso`,
88
+ `numeroRegistro`, `siglaClasse`, `descricaoClasse`, `classePadronizada`,
89
+ `nomeOrgaoJulgador`, `ministroRelator`, `dataPublicacao`, `ementa`,
90
+ `tipoDeDecisao`, `dataDecisao`, `decisao`, `jurisprudenciaCitada`, `notas`,
91
+ `informacoesComplementares`, `termosAuxiliares`, `teseJuridica`, `tema`,
92
+ `referenciasLegislativas`, `acordaosSimilares`. Both `ementa` and `decisao`
93
+ carried real multi-paragraph Portuguese prose, not a metadata stub.
94
+
95
+ **Scope, honestly stated**: the portal's own CKAN dataset notes say "espelhos
96
+ data from May 2022 onwards" - there is no public full-text API for
97
+ pre-2022 STJ decisions. This is a bulk-snapshot dataset, not a live
98
+ search-by-case-number service - `stj_client.py` downloads a bounded number
99
+ of the most recent monthly files and filters in-memory, rather than mirroring
100
+ the whole corpus, to keep this a lookup tool. Wired in as
101
+ `br_search_case_stj` / `br_get_case_stj`.
102
+
103
+ ### CARF - CONFIRMED LIVE, WIRED IN
104
+
105
+ `https://acordaos.economia.gov.br/solr/acordaos2/select` is a public, keyless
106
+ Apache Solr index. Confirmed live:
107
+
108
+ - `GET ?q=*:*&rows=1&wt=json` -> `numFound: 579226` documents.
109
+ - Exact-field query `q=numero_processo_s:"13896.904173/2008-79"` returns
110
+ exactly 1 matching document (confirmed `numFoundExact: true`).
111
+ - Live document fields: `id`, `numero_processo_s`, `numero_decisao_s`,
112
+ `camara_s`, `turma_s`, `secao_s`, `nome_relator_s`, `dt_sessao_tdt`,
113
+ `dt_publicacao_tdt`, `dt_registro_atualizacao_tdt`, `ementa_s`,
114
+ `decisao_txt` (list of strings - the ruling body), `conteudo_txt` (OCR/Tika
115
+ full-text dump of the source PDF, including Tika metadata preamble),
116
+ `nome_arquivo_s`, `nome_arquivo_pdf_s`, `arquivo_indexado_s`.
117
+ - `conteudo_txt` free-text query (`q=conteudo_txt:"imposto de renda"`)
118
+ returned **zero hits** on a live probe, despite that exact phrase appearing
119
+ in `ementa_s`/`conteudo_txt` of documents known to exist in the index -
120
+ i.e. the full-text index is not reliably populated/analyzed for common
121
+ terms. This matches a caveat already in the manifest's own upstream notes
122
+ for `BR/CARF` ("0 in Neon is VPS pipeline issue"). Rather than guess a
123
+ working full-text query syntax, `carf_client.py` only exposes exact
124
+ `numero_processo_s` / `numero_decisao_s` lookup - no free-text search tool
125
+ for CARF.
126
+ - No confirmed live document/PDF URL: probing
127
+ `https://acordaos.economia.gov.br/solr/acordaos2/<nome_arquivo_pdf_s>`
128
+ returned HTTP 404, and the CARF public frontend
129
+ (`https://carf.fazenda.gov.br/` -> redirects to `https://idg.carf.fazenda.gov.br/`)
130
+ did not respond to a plain HTTP client within a generous timeout. `source_url`
131
+ therefore points at the confirmed-live Solr endpoint, never a guessed PDF path.
132
+
133
+ Wired in as `br_get_case_carf` only (no search tool, per above).
134
+
135
+ ### Planalto (REFLEGIS) - RE-CHECKED, STILL SKIPPED
136
+
137
+ The manifest's `BR/Planalto` entry names
138
+ `https://legislacao.presidencia.gov.br/` (REFLEGIS) and reports its own
139
+ scraper as "complete" - but that describes a different project's browser-
140
+ automation pipeline, not a documented API. Live re-check 2026-07-07:
141
+ `curl -v https://legislacao.presidencia.gov.br/` completes the TCP+TLS
142
+ handshake but then times out after 15s with **zero bytes received** on the
143
+ HTTP response - consistent with a bot-challenge/WAF gate in front of the
144
+ site, not a structured API a keyless HTTP client can use. This confirms and
145
+ extends this repo's pre-existing rejection of Planalto (see "Not covered" in
146
+ SOURCES.md and the README note): still no confirmed mechanical rule mapping
147
+ a URN Lex to a Planalto URL, and now also confirmed that plain HTTP clients
148
+ cannot even load the page to attempt one. Not implemented.
149
+
150
+ ### TST - real backend CONFIRMED LIVE, but NOT wired into a tool this release
151
+
152
+ `https://jurisprudencia.tst.jus.br/` is a React single-page app. Checked
153
+ live 2026-07-07:
154
+
155
+ - The root page returns HTTP 200 with the SPA shell HTML, referencing one
156
+ JS bundle: `/static/js/main.be6b1d66.js`. Every plausible API path tried
157
+ directly against the frontend host (`/api-jurisprudencia-nacional/api/`,
158
+ `/api-jurisprudencia-nacional/api/jurisprudencia/pesquisa`, a CKAN-style
159
+ `package_list` path by analogy with STJ, a POST `/pesquisa`) returned
160
+ either HTTP 200 with `content-type: text/html` (the SPA's own
161
+ client-side-routing fallback) or HTTP 405. The minified JS bundle itself
162
+ had no plain-text API base URL or `REACT_APP_*`-style env var.
163
+ - **Follow-up that found the real backend**: the SPA loads its API base URL
164
+ at runtime from `https://jurisprudencia.tst.jus.br/config.json`, which
165
+ discloses `"base_url": "https://jurisprudencia-backend2.tst.jus.br"` (plus
166
+ three unrelated `consultadocumento.tst.jus.br`/`consultaprocessual.tst.jus.br`
167
+ URLs for other TST systems, not probed further). A direct
168
+ `POST https://jurisprudencia-backend2.tst.jus.br/rest/pesquisa-textual/1/2`
169
+ with body `{"tipos": ["ACORDAO"]}` returned real data: `totalRegistros`
170
+ and a `registros` array of real rulings with `numero`/`numFormatado`,
171
+ `nomRelator`, `orgaoJudicante`, `dtaJulgamento`, `dtaPublicacao`,
172
+ `ementa`/`ementaHtml`, `txtInteiroTeor` (full ruling prose).
173
+ - **Confirmed working**: the `tipos` filter measurably narrows the count
174
+ (3.75M of 8.48M total documents for `ACORDAO` alone), and pagination via
175
+ the `{start}/{size}` path segments returns stable, disjoint pages across
176
+ repeated calls.
177
+ - **NOT confirmed**: filter fields reverse-engineered from the minified
178
+ frontend bundle for free-text/exact search (`ementa`, `e`, `ou`,
179
+ `termoExato`, a `numeracaoUnica` object shaped `{numero, digito, ano,
180
+ orgao, tribunal, vara}` for process-number lookup) were tried live and did
181
+ **not** change the result count in any combination tested - including the
182
+ exact `numeracaoUnica` of a record the endpoint had just returned itself.
183
+ Either the real request shape differs from what the static bundle appears
184
+ to construct, or the endpoint silently ignores filters it does not
185
+ recognize from a bare client (e.g. a session/CSRF header this client does
186
+ not send).
187
+
188
+ **Decision**: this session built `tst_client.py` implementing only the
189
+ confirmed contract - a `tipos`-filtered, paginated browse, plus a
190
+ best-effort local scan for a record by its own `id` (not a general lookup,
191
+ since there is no dedicated get-by-id endpoint) - plus `CasoTST` /
192
+ `parse_caso_tst` / `build_caso_tst_citation` in `models.py`/`citations.py`,
193
+ and offline + live tests (`tests/test_tst_parse_offline.py`,
194
+ `tests/test_tst_smoke.py`). All of that is **kept in the codebase and
195
+ tested**, but deliberately **not wired into a `server.py` MCP tool** this
196
+ release: this fleet's citation contract is about trustworthy retrieval of a
197
+ specific case, and a browse-only client without a working "find this exact
198
+ case" filter does not meet that bar cleanly enough to expose as a tool
199
+ without a stronger disclaimer than seems wise for v0.5.0. Revisit once an
200
+ exact-match filter is confirmed (e.g. via a browser network trace of the
201
+ real frontend request, which may send headers or a request shape this
202
+ static-analysis pass could not recover) or TST publishes a documented dados
203
+ abertos API analogous to STJ's CKAN portal.
204
+
205
+ ### Querido Diario - checked live, blocked (Cloudflare bot challenge)
206
+
207
+ `BR/QueridoDiario` (manifest: `https://queridodiario.ok.org.br`) covers
208
+ municipal official gazettes, not federal legislation or case law - already
209
+ out of this connector's federal-focused scope, and checked live 2026-07-07
210
+ for completeness anyway: `GET https://queridodiario.ok.org.br/api/gazettes`
211
+ returns HTTP 403 with a Cloudflare "Just a moment..." bot-challenge page
212
+ (`cf-mitigated` JS challenge), not JSON - the same failure mode already
213
+ recorded elsewhere in this fleet for `pt-PT` sources (see MEMORY note on
214
+ dre.tretas.org). Not implemented, both because it is out of scope and
215
+ because it is currently blocked to a plain HTTP client.
216
+
217
+ ## v0.4.0 update - DataJud CNJ court dockets (confirmed live, scoped honestly)
218
+
219
+ Added `br_search_processos` / `br_get_processo` against
220
+ `api-publica.datajud.cnj.jus.br` (DataJud CNJ). This closes part of the
221
+ "zero case law" gap the fleet had at v0.3.0 - but only part, and the scope
222
+ below is deliberately narrow and honest about what it is NOT.
223
+
224
+ **What was confirmed live** (real HTTP request, not a guess):
225
+
226
+ ```
227
+ POST https://api-publica.datajud.cnj.jus.br/api_publica_stj/_search
228
+ Authorization: APIKey cDZHYzlZa0JadVREZDJCendQbXY6SkJlTzNjLV9TRENyQk1RdnFKZGRQdw==
229
+ Content-Type: application/json
230
+ {"query": {"match_all": {}}, "size": 1}
231
+
232
+ -> HTTP 200, real STJ docket JSON: numeroProcesso, classe.nome
233
+ ("Agravo em Recurso Especial"), orgaoJulgador, dataAjuizamento, a full
234
+ `movimentos` timeline (Distribuicao, Conclusao, Publicacao, Peticao,
235
+ Provimento em Parte, ...), and `assuntos`.
236
+ ```
237
+
238
+ The API key above is CNJ's own openly-published shared key (DataJud Wiki,
239
+ https://datajud-wiki.cnj.jus.br/api-publica/acesso/ - "Autenticacao ... por
240
+ meio de uma Chave Publica, gerada e disponibilizada pelo DPJ/CNJ"), not
241
+ something reverse-engineered or scraped from a leak. CNJ states the key can
242
+ be rotated at any time, so the client reads it from `BR_ELI_DATAJUD_KEY` if
243
+ set, falling back to the published value in `caselaw_client.py`.
244
+
245
+ **Endpoints/index aliases** (confirmed via
246
+ https://datajud-wiki.cnj.jus.br/api-publica/endpoints/): one index per
247
+ tribunal, `api_publica_<code>` - `stj`, `tst`, `tse`, `stm`, `trf1..trf6`,
248
+ `tj<uf>` (27 state courts), `trt1..trt24`, `tre-<uf>` (27 electoral courts),
249
+ plus 3 military courts. All wired into `TRIBUNAL_INDEX` in `caselaw_client.py`.
250
+
251
+ **What this genuinely is NOT** (confirmed by testing, not assumed):
252
+
253
+ - **Not full-text jurisprudencia search.** DataJud indexes procedural
254
+ *docket* metadata sourced from each court's case-management system via the
255
+ Modelo Nacional de Interoperabilidade - `numeroProcesso`, `classe`,
256
+ `orgaoJulgador`, `assuntos`, and `movimentos` (a timeline of procedural
257
+ events, e.g. "Distribuicao", "Publicacao", "Provimento em Parte"). It does
258
+ **not** carry the prose text of a ruling/acordao/ementa. A `movimento`
259
+ entry is an event label, not a holding - `br_get_processo`'s docstring and
260
+ the server `INSTRUCTIONS` say this explicitly so the calling LLM doesn't
261
+ present a movement as if it quoted a court's reasoning.
262
+ - **STF is not covered - confirmed by a live 404, not an assumption:**
263
+
264
+ ```
265
+ POST https://api-publica.datajud.cnj.jus.br/api_publica_stf/_search
266
+ -> HTTP 404 {"error":{"type":"index_not_found_exception",
267
+ "reason":"no such index [api_publica_stf]", ...}}
268
+ ```
269
+
270
+ This is structural, not an outage: the STF sits outside the CNJ's
271
+ regulatory reach in a way STJ/TST/TRFs/TJs/etc. do not, so it never feeds
272
+ DataJud via the interoperability model the other 91 courts use. There is
273
+ no `br_get_stf_decisao` tool in this release because there is no confirmed
274
+ live source for it (see "still open" below).
275
+
276
+ **Redistribution constraint carried forward from the v0.3.0 audit**
277
+ (Resolucao CNJ 446/2022: bulk redistribution is restricted; docket data can
278
+ carry LGPD-sensitive party information): `caselaw_client.py` only performs
279
+ live, on-demand queries against the shared key - it caches individual query
280
+ results with a short TTL like the other "search"/"act" categories, and never
281
+ bulk-downloads or persists a corpus. This is the "bring-your-own-shared-key,
282
+ live-query-only" model the v0.3.0 note already called for.
283
+
284
+ ### Still open: no full-text STF/STJ ruling-text source found
285
+
286
+ Two other leads from this session's research were tried and did not pan out
287
+ as a full-text jurisprudencia source:
288
+
289
+ - A guessed HuggingFace dataset name (`eduagarcia/BrazilianCourtDecisionsHF`)
290
+ returned 401 in earlier probing this session; no genuinely public,
291
+ ungated, confirmed-real HuggingFace Brazilian-court-decisions dataset was
292
+ found to replace it as a fallback in the time available. If one exists it
293
+ was not surfaced by search in this session - this is a documented gap, not
294
+ a claim that none exists.
295
+ - STF's own portal and LexML/BR were already flagged unreliable by the wider
296
+ `worldwidelaw/legal-sources` audit (AWS WAF block on STF, 404s on LexML/BR)
297
+ before this session started - not retried blindly here, consistent with
298
+ that audit's own findings.
299
+
300
+ **Conclusion**: full ruling-text jurisprudencia (STF sumulas/acordaos, STJ
301
+ acordao text) remains an open gap for a future session. What v0.4.0 adds is
302
+ real and useful on its own terms - docket status/timeline/classe/assuntos
303
+ lookup across 91 non-STF courts - but callers should not expect it to answer
304
+ "what did the court hold" questions; only "what happened procedurally, and
305
+ when."
306
+
307
+ **Operational note added in this session's test run (2026-07-06): DataJud's
308
+ public API is genuinely slow and occasionally flaky.** Five repeated,
309
+ byte-identical `curl` requests for `{"query":{"match":{"classe.nome":"Agravo"}},
310
+ "size":1}` against `api_publica_stj` returned: one `HTTP 429` (rate limit),
311
+ then four `HTTP 200`s with response times ranging 11.8s-38.8s. Separately, the
312
+ same query against a field with 10000+ matching documents returned `hits.hits`
313
+ as an empty list on one attempt and populated on the next (`hits.total` stayed
314
+ `{"value": 10000, "relation": "gte"}` throughout - it's the returned `hits`
315
+ array, not the total count, that is unreliable under load). This produced one
316
+ flaky failure in `tests/test_caselaw_smoke.py::test_get_processo_by_numero`
317
+ during this session's pytest run (`5 passed, 1 failed` on one run; a rerun of
318
+ just that file passed 2/3, failed a different one) - confirmed via direct
319
+ `curl` to be DataJud's own instability, not a bug in `caselaw_client.py`'s
320
+ retry/cache logic. The client's existing `_RETRY_STATUS` set already retries
321
+ 429/5xx; a future session could consider also retrying on HTTP 200 responses
322
+ with an unexpectedly empty `hits.hits` for a query whose `hits.total` is
323
+ nonzero, if this proves persistent.
324
+
325
+ ## Current status (legislation APIs, v0.2.0/v0.3.0)
326
+
327
+ Two APIs, both public, keyless, no registration:
328
+
329
+ - `legis.senado.leg.br/dadosabertos` resolves a URN Lex to identification,
330
+ Diario Oficial da Uniao publication provenance, and amendment history
331
+ (`br_get_norma`). Confirmed live with a real query for the Codigo Civil:
332
+
333
+ ```
334
+ GET https://legis.senado.leg.br/dadosabertos/legislacao/urn?urn=urn:lex:br:federal:lei:2002-01-10;10406
335
+ Accept: application/json
336
+ -> HTTP 200, real data: identificacao (apelido "Codigo Civil (2002) (CC)"),
337
+ publicacoes (DOU provenance), vides (amendment history per-article).
338
+ The `observacao` field carries the Senado's own editorial notes,
339
+ including references to STF rulings on specific articles (e.g. ADI
340
+ 2.794-8 on Art. 66 par.1) - that's the API's data, not our claim, and
341
+ it isn't a substitute for checking the ruling itself.
342
+ ```
343
+
344
+ Wired into `br_get_norma` (`norma_client.py` / `citations.py` /
345
+ `server.py`). The URN Lex the caller queries with is echoed back as
346
+ `lex_uri` - never invented, per Article IV (parse ELI, don't invent it).
347
+
348
+ - `normas.leg.br/api/public/normas` resolves the same URN Lex to a schema.org
349
+ `Legislation` JSON-LD tree - one node per Parte/Livro/Titulo/Capitulo/Secao/
350
+ Artigo/paragrafo, each with its own URN Lex suffix and, on leaf nodes, real
351
+ inline article text (`br_get_norma_index` + `br_get_norma_texto`). This is
352
+ a different path on the same domain as the human-readable citation page -
353
+ the v0.2.0 session only checked the frontend, not its backing API. Tested
354
+ against the Codigo Civil (`urn:lex:br:federal:lei:2002-01-10;10406`): 2511
355
+ addressable nodes, `art1`'s text matches Art. 1 of the Codigo Civil. Full
356
+ response saved as `tests/fixtures/codigo_civil_normas.json` (5.3MB -
357
+ public-domain Brazilian federal legislation, no licensing concern in
358
+ redistributing it as a test fixture). Wired into `text_client.py` /
359
+ `norma_text.py`, both written clean-room against the live response - no
360
+ code reused from any AGPL project (see the `worldwidelaw/legal-sources`
361
+ note below).
362
+
363
+ - This connector does not scrape Planalto (planalto.gov.br) HTML. No
364
+ confirmed mechanical rule maps a URN Lex to a Planalto URL for every act
365
+ type, and fabricating one would risk the same citation-hallucination
366
+ failure mode this fleet exists to prevent. A minority of act types (mostly
367
+ decrees, `DEC-n`/`MPV-ss`) have no inline text on `normas.leg.br` either -
368
+ a Planalto-scraping fallback for those is a candidate follow-up, to be
369
+ written clean-room if picked up.
370
+
371
+ ### Note on `worldwidelaw/legal-sources` (AGPL-3.0)
372
+
373
+ That project's `sources/BR/Planalto/bootstrap.py` solves a different problem:
374
+ a URL-derivation rule for scraping Planalto HTML, used as its own fallback
375
+ when `normas.leg.br` has no inline text. Reading it for method - per the
376
+ fleet's own recon step 0 in `PLAYBOOK.md` - is what pointed this session at
377
+ probing `normas.leg.br` directly. No code, regex, or URL template from that
378
+ AGPL codebase was copied into this Apache-2.0 one.
379
+
380
+ ## History
381
+
382
+ ### v0.2.0 - fixed identification, still missing full text
383
+
384
+ v0.1.0 (below) had tested the wrong host for identification. v0.2.0 fixed
385
+ that by finding `legis.senado.leg.br/dadosabertos` - see "Current status"
386
+ above - but at the time concluded there was no confirmed way to get full
387
+ article text, and specifically ruled out scraping Planalto without a
388
+ confirmed URL rule. That gap is closed above by `normas.leg.br`'s JSON-LD
389
+ tree, which needs no such rule.
390
+
391
+ ### v0.1.0 - LexML SRU/OAI-PMH not confirmed live (wrong host tested)
392
+
393
+ LexML documents an SRU (Search/Retrieval via URL) service and a `urn:lex:br:...`
394
+ identifier scheme (Brazil's ELI-equivalent, same lineage as the Italian URN-NIR).
395
+ Live probing on 2026-07-06 returned HTTP 404 on every candidate path tried
396
+ against `www.lexml.gov.br` (`/busca/SRU`, `/sru`, `/SRU`, `/busca/sru`,
397
+ `/api/sru`, `/busca/oaisearch`, `/oai/oai.php`). The official technical PDF
398
+ (`projeto.lexml.gov.br/documentacao/Parte-4-Coleta-de-Metadados.pdf`) documents
399
+ the metadata schema but not a live base URL. A third-party wrapper
400
+ (`netoferraz/py-lexml-acervo`, GPL-3.0, last active ~2019) references the same
401
+ SRU standard without a working example URL in its README at the time of
402
+ checking.
403
+
404
+ **Conclusion (SUPERSEDED)**: was "either the service moved... or discontinued".
405
+ Correction: it moved to `legis.senado.leg.br/dadosabertos` - see the v0.2.0
406
+ update above.
407
+
408
+ ## DataJud/CNJ - confirmed live, redistribution-restricted (WIRED IN v0.4.0)
409
+
410
+ `api-publica.datajud.cnj.jus.br` is a real, unified Elasticsearch-backed REST
411
+ API across 91 courts (state + federal + labor + electoral), ~80M+ active
412
+ cases (per Mcp-Brasil audit, CNJ Justica em Numeros). Access uses a single
413
+ publicly-documented shared API key (not per-developer registration in the
414
+ usual sense) - but **Resolucao CNJ 446/2022 forbids bulk redistribution**, and
415
+ case data involving family/juvenile/criminal matters is LGPD-sensitive
416
+ (comparable to RODO special categories). This session's follow-up (see
417
+ "v0.4.0 update" above) confirmed the exact key, endpoint list, and query DSL
418
+ with a real HTTP request, and wired it in as `br_search_processos` /
419
+ `br_get_processo` - live bring-your-own-shared-key queries only, never
420
+ bulk-hosted, per the constraint already identified here.
421
+
422
+ ## Camara dos Deputados - confirmed live, low risk
423
+
424
+ `dadosabertos.camara.leg.br/api/v2/proposicoes` - keyless JSON, works as
425
+ documented. This is what `br-eli-mcp` v0.1.0 actually implements.