legal-mcp-croatia
by AvoccadoTech
README.md
# legal-mcp-croatia
An [MCP](https://modelcontextprotocol.io) server that lets an AI assistant read Croatian public legal registers:
- **e-Oglasna ploča sudova** — the court noticeboard
- **Sudski registar** — the company register, including a change monitor
It is read-only, it stores nothing, and it logs nothing. There is no database, no account, and no service in the middle: the server runs on your machine, talks to public government APIs, and returns the answer. Nothing about what you searched for leaves the process.
That is deliberate. Which companies or people a law firm looks up *is* the firm's client list, and it is not ours to hold.
---
## Install
```bash
git clone https://github.com/AvoccadoTech/legal-mcp-croatia.git
cd legal-mcp-croatia
pip install -e .
```
Requires Python 3.11 or newer.
## Run
The server speaks MCP over stdio. Register it with any MCP-capable client:
```json
{
"mcpServers": {
"croatia-legal": {
"command": "python",
"args": ["-m", "croatia_legal_mcp"],
"env": {
"SUDREG_CLIENT_ID": "...",
"SUDREG_CLIENT_SECRET": "..."
}
}
}
}
```
e-Oglasna needs no credentials. Sudski registar does — see below. Without them the e-Oglasna tools work normally and the Sudreg tools return an error saying where to register.
Optional: set `EOGLASNA_DOWNLOAD_ROOT` to choose where downloaded PDFs go. It defaults to `./eoglasna-downloads`, and the server refuses to write outside it.
---
# e-Oglasna ploča sudova
The electronic noticeboard where Croatian courts publish notices they are legally required to make public — enforcement (*ovrha*), corporate and personal bankruptcy, land-registry proceedings, substitute service, and other court publications. Notices from FINA and from notaries appear on the same board.
Each notice carries the issuing court, the case number, publication and expiry dates, the parties named, and the scanned decision as PDF.
The API is genuinely open: no registration, no API key, no rate-limit documentation. The underlying data is published under the Croatian [Otvorena dozvola](https://data.gov.hr/otvorena-dozvola) (Open Licence).
**It is a noticeboard, not a case-law database.** There is no legislation and no judgments here — for those, see Narodne novine and the Supreme Court's case-law portal.
## Tools
| Tool | What it does |
|---|---|
| `eoglasna_search_notices` | Search by company name, personal name, case number or OIB, optionally within a publication date range |
| `eoglasna_get_notice` | One notice in full — every party, every attached document |
| `eoglasna_download_notice_document` | Save one attached PDF locally and return its path |
| `eoglasna_list_courts` | Every court and body publishing to the board, with codes |
`eoglasna_search_notices` takes a `source`:
| `source` | Endpoint | Use it for |
|---|---|---|
| `courts` *(default)* | `/court-notice` | Everything from courts, with court, case number and case type |
| `all` | `/notice` | Also FINA and notary notices, but without those three fields |
| `company_bankruptcies` | `/court-notice/legal-person-bankruptcy` | Corporate insolvency only — has a client's counterparty gone under? |
| `personal_bankruptcies` | `/court-notice/natural-person-bankruptcy` | Consumer insolvency only |
The bankruptcy views are separate endpoints rather than a filter, because `caseType` is another parameter the API accepts and ignores.
In `company_bankruptcies` every *debtor* is a company — the view is filtered on `LEGAL_PERSON_BANKRUPTCY` — which makes it the least risky view to put on a screen in front of an audience. It is not a guarantee that no individual appears: the other participants are not filtered, and the bankrupt company's *zakonski zastupnik* shows up as a named natural person with an OIB. Check the page before you project it.
## One thing worth knowing before you build your own
The published OpenAPI document describes the search filter as an object, and the API **accepts a JSON-encoded `filter=` parameter and then ignores it**. You get HTTP 200, a well-formed page of results, and the entire ~900,000-notice corpus sorted newest-first — for every search term, including nonsense ones.
It looks like it is working. It is not. Spring binds the filter from flat query parameters:
```
GET /api/v1/court-notice?text=HEP&datePublishedFrom=2026-01-01&page=0
```
A monitoring tool built on the documented form would report a match for every term it was ever given. There is a regression test in `tests/test_smoke.py` guarding this.
## Personal data
Notices name natural persons and include their OIB and, in the full record, their home address. This is a public register and republication is lawful, but it is still personal data under GDPR.
Two consequences the server enforces or flags:
- Addresses appear only when you request one specific notice, never in bulk search output.
- Every notice carries an **expiry date**. That is the date it comes off the board — the state has decided it should stop being public. If you keep a copy past that date, you are republishing something that was deliberately withdrawn. Build a retention rule.
**The expiry date is not a procedural deadline** and must never be presented as one. It says when the notice disappears, not when anyone has to do anything.
---
# Sudski registar
Croatia's company register, kept by the commercial courts: every company, branch and institution entered in the court register, with its registered name, legal form, seat, share capital, registry court, and whether a bankruptcy or liquidation is running.
Sole traders (*obrt*) are not here — they are in the Obrtni registar. Associations are in the Registar udruga. Both are separate registers with separate services.
## Credentials
Free, but not open. Register at [sudreg-data.gov.hr](https://sudreg-data.gov.hr/), confirm the email, and you are issued a Client ID and Client Secret. Set them in the environment of the server:
```bash
export SUDREG_CLIENT_ID=...
export SUDREG_CLIENT_SECRET=...
```
**Both values end in two literal dots.** They are part of the credential and are easily lost when copying. If the server reports rejected credentials, check the ends of the strings first.
Authentication is OAuth2 client credentials; the token lasts six hours and is cached in the process.
The credentials are issued to a registered user. Before serving several firms through one set, read the terms — a shared key across clients is a question for the register, not an implementation detail.
## Tools
| Tool | What it does |
|---|---|
| `sudreg_get_subject` | One company by OIB or MBS — name, legal form, seat, capital, status, proceedings, accounts filed |
| `sudreg_find_subject` | Find companies by registered name, returning MBS and OIB |
| `sudreg_check_watchlist` | **The monitor.** Which of the companies you watch have changed since you last looked |
| `sudreg_list_snapshots` | Which daily snapshots the open-data service is serving, and how fresh they are |
## The monitor, and why it holds no watchlist
Every subject in the register carries `scn_zadnje_izmjene` — an Oracle system change number, monotonically increasing, returned on every read. That one field is enough to make monitoring a pure function:
> you pass the identifiers and the SCNs you last saw, we say which ones moved, you store the new SCNs.
So there is no watchlist here, no seen-ledger, and no state of any kind. Which companies a firm watches is its client list; keeping it on the firm's side is not a limitation of this design, it *is* the design.
A run starts from the firm's own file, wherever the firm keeps it:
```json
[
{ "identifier": "75399377119", "identifier_type": "oib", "last_seen_scn": 6641203, "label": "Counterparty A" },
{ "identifier": "080000604", "identifier_type": "mbs", "last_seen_scn": 6640911, "label": "Client B" }
]
```
Pass those to `sudreg_check_watchlist`. The report leads with what moved, says what each changed subject looks like now — struck off, in bankruptcy, in liquidation — and ends with a table of SCNs to write back. Omit `last_seen_scn` on the first run and the report gives you the baseline instead.
Two honest limits, both stated in the output of the tool itself:
- **The register says *that* a subject changed, not which field.** Compare `sudreg_get_subject` against your own record to see what moved.
- **Open data is copied from the live register at 19:00 on working days.** Something filed this morning may not appear until tomorrow, and on a Monday the freshest snapshot is Friday — three days old. Every watchlist report prints the snapshot age on the first line rather than burying it, and pins every lookup in the run to that one snapshot so the whole report describes a single state.
One word of caution about `register status`. It means *entered in the register*, not trading and not solvent: a company keeps status 1 for years while in bankruptcy. The output says "registered" rather than "active" for exactly that reason, and prints any running proceeding in bold next to it.
## What is *not* in this register
**No officers, no board members, no beneficial owners.** Personal data was withdrawn from the open service, so nothing here answers "who can sign for this company". That comes from a court extract (*izvadak*), or for ownership from the Registar stvarnih vlasnika. The tool descriptions say so, because an assistant that guesses at representation from a company record is worse than no tool at all.
**No financial figures.** The register records *that* annual accounts were filed and when — useful on its own — but the numbers are in the Fina RGFI.
## One thing worth knowing before you build your own
This is a replication API, not a lookup API. Of its 39 methods, exactly one — `/detalji_subjekta` — takes an identifier. Everything else (`/tvrtke`, `/sjedista`, `/temeljni_kapitali`, `/promjene`) is a full table dump with `offset` and `limit` and no way to filter by company. It is built for someone mirroring the register nightly.
`/promjene` looks like the obvious basis for a monitor and is the wrong one for this job: it is the change list for the *whole* register, meant to be diffed against the copy you already hold. For a firm watching a few dozen counterparties, one `/detalji_subjekta` call each is smaller, exact, and needs no stored copy.
`/subjekti` accepts `tvrtka_naziv` and is the only server-side text filter the public API offers — and it carries identifiers but not the name, which lives in a separate table. So a name search resolves each candidate with a second call.
**`tvrtka_naziv` is a SQL LIKE pattern with no implicit wildcards.** Verified live on 2026-08-31:
```
tvrtka_naziv=KOESTLIN → 0 rows
tvrtka_naziv=%KOESTLIN% → the company
tvrtka_naziv=%koestlin% → the company (matching is case-insensitive)
```
This is the same class of trap as the e-Oglasna filter, failing the other way. e-Oglasna returns everything and looks like it worked; here you get an empty result that reads as a fact about the world — "no such company" about a company that exists, which is the worse of the two answers to hand a lawyer. The server wraps a bare term in `%…%` and passes through anything containing `%`, and there is a live test pinning all three cases.
Two smaller things that produce wrong output rather than no output:
- **`naznaka_imena` is not a suffix.** It is the distinguishing designation and it repeats part of the name — for MBS 010000162 the pair is `("KOESTLIN dioničko društvo,tvornica keksa i vafla", "KOESTLIN")`. Concatenating them invents a company name that no register ever issued. The full name is `ime` alone.
- **`postupak` is always present**, and code 1 reads "Bez postupka" — no proceeding. Rendering it verbatim puts a bankruptcy-shaped alarm on every healthy company. The live code list is 1 Bez postupka, 2 Stečaj, 3 Likvidacija, 4–7 the predstečaj stages.
---
## Not a substitute for a lawyer
These are search tools over public registers. They do not check whether a notice was validly served, whether a deadline runs, whether a filing was correct, or what any of it means. A qualified lawyer decides that.
## Tests
```bash
pip install -e ".[dev]"
pytest -m "not live" # offline: query building, identifiers, change detection
pytest -m live # hits the real APIs; Sudreg tests skip without credentials
```
## Roadmap
- **e-Savjetovanja** — draft legislation out for public consultation. Open licence, no key. Warns that a statute is being amended before it is.
- **Narodne novine** (official gazette) — ELI metadata is open and keyless; full text is HTML/PDF and the reuse licence is unclear.
Sister servers for other jurisdictions will follow the same shape: one repository per jurisdiction, tools named for the register they query.
## Licence
[Apache-2.0](LICENSE). Built by [Avoccado Tech](https://avoccado.io).
The code is free and always will be. If you would rather someone else kept it working when the state moves an endpoint — as the company register did this year, breaking every tutorial written about it — that is what we do for a living.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues