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.
- br_eli_mcp-0.6.0/.github/workflows/release.yml +63 -0
- br_eli_mcp-0.6.0/.gitignore +20 -0
- br_eli_mcp-0.6.0/CONSTITUTION.md +75 -0
- br_eli_mcp-0.6.0/DISCOVERY.md +425 -0
- br_eli_mcp-0.6.0/LICENSE +202 -0
- br_eli_mcp-0.6.0/PKG-INFO +152 -0
- br_eli_mcp-0.6.0/README.md +121 -0
- br_eli_mcp-0.6.0/SOURCES.md +187 -0
- br_eli_mcp-0.6.0/glama.json +4 -0
- br_eli_mcp-0.6.0/pyproject.toml +65 -0
- br_eli_mcp-0.6.0/server.json +22 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/__init__.py +3 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/audit.py +94 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/cache.py +56 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/carf_client.py +138 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/caselaw_client.py +231 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/citations.py +397 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/client.py +92 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/models.py +177 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/norma_client.py +93 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/norma_text.py +112 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/server.py +1151 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/stj_client.py +174 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/tcu_client.py +166 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/text_client.py +89 -0
- br_eli_mcp-0.6.0/src/br_eli_mcp/tst_client.py +241 -0
- br_eli_mcp-0.6.0/tests/fixtures/carf_acordao_sample.json +35 -0
- br_eli_mcp-0.6.0/tests/fixtures/codigo_civil_normas.json +1 -0
- br_eli_mcp-0.6.0/tests/fixtures/stj_espelho_sample.json +26 -0
- br_eli_mcp-0.6.0/tests/fixtures/tcu_documento_sample.json +41 -0
- br_eli_mcp-0.6.0/tests/fixtures/tcu_resumidos_sample.json +355 -0
- br_eli_mcp-0.6.0/tests/fixtures/tst_acordao_sample.json +74 -0
- br_eli_mcp-0.6.0/tests/fixtures/tst_pesquisa_exact_sample.json +140 -0
- br_eli_mcp-0.6.0/tests/test_carf_parse_offline.py +62 -0
- br_eli_mcp-0.6.0/tests/test_carf_smoke.py +40 -0
- br_eli_mcp-0.6.0/tests/test_caselaw_smoke.py +52 -0
- br_eli_mcp-0.6.0/tests/test_norma_smoke.py +27 -0
- br_eli_mcp-0.6.0/tests/test_smoke.py +26 -0
- br_eli_mcp-0.6.0/tests/test_stj_parse_offline.py +46 -0
- br_eli_mcp-0.6.0/tests/test_stj_smoke.py +37 -0
- br_eli_mcp-0.6.0/tests/test_tcu_parse_offline.py +74 -0
- br_eli_mcp-0.6.0/tests/test_tcu_smoke.py +50 -0
- br_eli_mcp-0.6.0/tests/test_text_smoke.py +30 -0
- br_eli_mcp-0.6.0/tests/test_tst_parse_offline.py +105 -0
- 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.
|