Skip to main content
Glama
callmemitch27

QLD Legislation MCP Server

README.md
# Australian Legislation & Caselaw MCP

**Semantic legal research over primary Australian sources, with citations you can actually rely on.**

An end-to-end pipeline that extracts current, in-force legislation and court decisions from official sources, parses their *native structure* (no OCR, no PDF scraping, no layout guessing), embeds them into local vector stores, and serves them to any MCP client — Claude, or anything else that speaks the protocol.

Every search hit carries a `cite_as` string with the consolidation currency date or paragraph pinpoint built in:

> *Work Health and Safety Act 2011 (Qld) s 175 (current as at 2026-03-29)*
> *Cromwell Corporation Ltd v ARA Real Estate Investors XXI Pte Ltd [2020] FCA 1492 [12]–[16]*

## Coverage

| Corpus | Source | Format | Scale |
|---|---|---|---|
| Queensland legislation | [legislation.qld.gov.au](https://www.legislation.qld.gov.au) | Authorised QuILLS XML | ~570 Acts + ~430 regulations |
| Commonwealth legislation | [Federal Register of Legislation](https://www.legislation.gov.au) | Compilation Epub (OPC drafting styles) | ~1,250 in-force principal Acts |
| Federal Court caselaw | [Open Australian Legal Corpus](https://huggingface.co/datasets/isaacus/open-australian-legal-corpus) | Born-digital HTML → text | ~63,700 decisions |
| NSW caselaw | Open Australian Legal Corpus | Born-digital HTML → text | ~117,000 decisions |

Each corpus lives in its **own ChromaDB directory** — independently portable (copy a folder to move a corpus), independently updatable, and re-ingestable while the others stay served. Cross-corpus search embeds the query once and fans out across the stores' HNSW indexes (~ms each), so searching everything costs about the same as searching one.

## Why the retrieval is precise

- **The section is the chunk.** Legislation is parsed from its structural markup — chapter/part/division/section, schedules, dictionary definitions — so retrieval returns the exact provision a lawyer would cite, with its full structural path (`Part 9 > Division 3 > s 175 Power to seize evidence`).
- **The paragraph is the pinpoint.** Modern FCA judgments number their paragraphs in the text; chunking is sequence-checked (a numbered line only opens paragraph *n* at *last + 1*), so quoted statutes can't fake a paragraph break and pinpoints like `[12]–[18]` are trustworthy. Where numbering genuinely isn't recoverable (old decisions, OALC's NSW text), chunks cite at case level rather than inventing pinpoints.
- **Dates are part of the citation.** Every legislation hit carries the reprint's currency date; the server instructs clients to cite with it. A consolidated store is not a substitute for checking amendments after that date — the tooling makes that explicit instead of hiding it.
- **Definitions are first-class.** Dictionary definitions become their own retrievable chunks, so "how does this Act define *worker*?" is a direct lookup, and one tool compares a term's definition across every instrument in the corpus.

## MCP tools

**Legislation** (all searches take `jurisdiction='qld'|'cth'|'all'` and `doc_type='act'|'sl'|'all'`):

| Tool | Purpose |
|---|---|
| `search_legislation` | Semantic search; filter by Act, kind, exact substring; optional cross-encoder rerank |
| `keyword_search` | Exact-substring search, no embedding — near-instant |
| `find_acts` | "Which Act governs X?" — triage over one vector per instrument, then drill in |
| `get_provision` | A provision verbatim, optionally with neighbouring sections and the definitions it uses |
| `get_act_structure` | An instrument's table of provisions |
| `read_part` | Every provision in one chapter/part/division/schedule, in order, in one call |
| `get_definitions` | One instrument's dictionary (or one term) |
| `search_definitions` | A term's definition across the whole corpus, side by side |
| `find_references` | Where a provision or instrument is cited, internally and by other instruments |
| `list_acts` | Everything in the store, with type and currency date |

**Caselaw** (`corpus='fca'|'nsw'|'all'`):

| Tool | Purpose |
|---|---|
| `search_caselaw` | Semantic search over decisions; filter by court, year; hits carry paragraph pinpoints |
| `get_case` | A decision's catchwords/orders coverpage, or a paragraph range of its reasons |

**Ops:**

| Tool | Purpose |
|---|---|
| `corpus_status` | What's loaded, chunk counts, currency-date ranges per corpus |

## Quick start

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[mcp]'                    # extract + ingest + MCP server

# Queensland: enumerate -> download authorised XML -> embed
qld-legis enumerate --out data/manifest.json
qld-legis fetch --manifest data/manifest.json --out-dir data/xml
qld-legis-ingest data/xml --reset          # bge-m3 embeddings; --device mps on Apple silicon
qld-legis-ingest-acts data/xml             # small "which Act governs X?" triage index

qld-legis-mcp                              # stdio MCP server, ready for Claude Desktop
```

Claude Desktop config:

```json
{
  "mcpServers": {
    "au-legislation": {
      "command": "/path/to/.venv/bin/qld-legis-mcp",
      "env": { "QLD_SETTINGS": "/path/to/repo/config/settings.json" }
    }
  }
}
```

Everything is resumable and idempotent: fetches skip existing files, ingests skip unchanged documents by content hash, and chunk ids are deterministic so re-ingesting an amended Act updates it in place.

### Add the Commonwealth

```bash
qld-legis-cth enumerate      # in-force principal Acts via the FRL OData API
qld-legis-cth fetch          # compilation Epubs (resumable)
qld-legis-cth ingest         # embeds into its own collection/store
```

The Epub's XHTML preserves the Office of Parliamentary Counsel's drafting styles (`ActHead1..5`, `subsection`, `Definition`, `Penalty`), so Commonwealth structure parses natively too — stdlib only, no extra dependencies.

### Add caselaw

```bash
qld-legis-caselaw fetch --source federal_court_of_australia --source nsw_caselaw
qld-legis-caselaw ingest                          # FCA -> its own store
qld-legis-caselaw ingest --source nsw_caselaw     # NSW -> its own store
```

Decisions bootstrap from the [Open Australian Legal Corpus](https://huggingface.co/datasets/isaacus/open-australian-legal-corpus) (Isaacus) — text already extracted from the courts' born-digital HTML. The multi-GB corpus file is streamed and filtered on the fly, never held in memory or stored whole. For decisions newer than OALC's last update, Isaacus's [corpus creator](https://github.com/isaacus-dev/open-australian-legal-corpus-creator) emits the same JSONL shape, which `ingest` consumes unchanged.

## Keeping it current

```bash
qld-legis-update                 # differential: fetch + re-embed only what changed
qld-legis-update --include-sl    # regulations too
qld-legis-update --dry-run       # show the diff first
```

The updater diffs the official in-force list against saved state: new documents are ingested, changed ones re-ingested (old chunks deleted first, so renumbered provisions don't linger), repealed ones removed. Byte-identical XML skips the re-embed even when the version id churns. State saves after every document, so interrupted runs resume.

## Performance

- **Embeddings:** [bge-m3](https://huggingface.co/BAAI/bge-m3) (multilingual, 1024-dim, 8k-token window). Optional **ONNX int8** backend (`qld-legis-export-onnx`) runs queries ~4× faster on CPUs with VNNI — vectors stay compatible with a torch-built store (cosine ≥ 0.99 measured).
- **Triage-first search:** broad questions hit a tiny one-vector-per-instrument index before touching the full section corpus.
- **Optional reranking:** a cross-encoder (`bge-reranker-v2-m3`) re-scores top candidates when precision matters more than latency.
- Query-time embedding is one string at a time — **CPU is fine for serving**; no GPU needed.

## Deployment

The included `Dockerfile` bakes the models in (starts offline; no runtime downloads) and `docker-compose.yml` mounts the stores as volumes:

```bash
docker compose up -d --build
```

To share the server, put it behind an HTTPS tunnel (Tailscale Funnel, Cloudflare Tunnel) and serve the endpoint at an unguessable path — the URL itself becomes the shared key, which suits MCP clients whose connector UI only accepts a URL:

```bash
qld-legis-mcp --http --host 127.0.0.1 --port 8000 --path "/mcp-<random>/mcp"
```

Bind to localhost and let the tunnel terminate TLS. `QLD_MCP_TOKEN` adds optional bearer-token auth for programmatic clients; `QLD_MCP_ALLOWED_HOSTS` re-enables Host pinning if you want it. All configuration works via env vars (`QLD_DB_PATH`, `QLD_CTH_DB_PATH`, `QLD_FCA_DB_PATH`, ...) or `config/settings.json`.

## Sensible caveats

This is a research tool, not legal advice. The stores hold **consolidated reprints as at their currency date** — always check for amendments after the `as_at` date in the citation before relying on a provision. Caselaw coverage runs to the corpus's last update; verify anything decided since.

## Data licensing

| Material | Licence |
|---|---|
| Queensland legislation | © State of Queensland, [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| Commonwealth legislation | © Commonwealth of Australia, CC BY 4.0 |
| FCA judgments | Reproducible unaltered (incl. commercially) with acknowledgment to the Court |
| NSW Caselaw / OALC | See the [OALC licence](https://huggingface.co/datasets/isaacus/open-australian-legal-corpus/blob/main/LICENCE.md); OALC itself is CC BY 4.0 |

Redistribution of downloaded material must retain the required attributions. High Court material (not enabled by default) is personal/non-commercial only. This repository's own code is [MIT](LICENSE).

## Development

```bash
pip install -e . pytest
pytest tests/          # pure-logic tests: parsers, chunkers, diffing - no network, no ML deps
```

Ingest CLIs all have `--dry-run` modes that parse and report chunk statistics without writing anything, and the repo's source-endpoint documentation (browse-data expressions, XML rendition URLs, the QuILLS DTD walk, FRL OData quirks) lives in the module docstrings next to the code that uses it.

## Roadmap

- Other state legislation (NSW/WA/SA/Tas are already in OALC with text extracted; Victoria needs its own extractor)
- NSW paragraph pinpoints via direct NSW Caselaw HTML (its pages carry proper paragraph anchors; OALC's text loses the numbering)
- High Court decisions (licence limits: personal/non-commercial)
- Point-in-time / amendment-history indexing