Model Context Protocol server for German federal law — 5,942 statutes indexed, anti-hallucination citation verification, daily drift detection against the official source. Built for legal agents that need to ground every § they cite.
10 tools, one resource, one trust contract:
| You ask Claude / Cursor… | …with GitLaw MCP it answers |
|---|---|
| "Verify § 573 BGB" | Returns the real paragraph text. Or verified: false with a structured reason. Never invents. |
| "Mein Vermieter kündigt wegen Eigenbedarf — was kann ich tun?" | Semantic search finds § 574 BGB, returns the text, the LLM grounds its answer in real statute |
| "How do you know your BGB is current?" | check_upstream_currency("BGB") — returns the days_behind vs. gesetze-im-internet.de live |
| "What's the integrity hash of your corpus right now?" | get_corpus_status() — single SHA-256 every consumer can verify |
LLMs hallucinate German law all the time. They confidently cite § 999 StGB (doesn't exist), invent paragraph titles, swap statutes. We measured gpt-4o-mini on 25 real Lebenslagen questions: 5.9% of its cited paragraphs were fake. That's catastrophic for a lawyer, harmful for a citizen, dishonest for AI.
With GitLaw MCP available as a tool, hallucination rate drops to 0% — the model has no reason to invent when verify_citation is one call away. See eval/eval_summary.md for the reproducible report.
This server gives any MCP-compatible client (Claude Desktop, Cursor, Continue, custom agents) the legal-tools surface they need:
- Semantic search across all 5,942 federal statutes → grounded retrieval
- Citation verification → real paragraph text or structured rejection — no hallucinated §
- Exact lookup by abbreviation + paragraph
- Citation-graph traversal (94k nodes, 200k edges) — who cites whom
- Corpus provenance — every served paragraph has a public source URL and SHA-256
- Live drift detection — daily HEAD-check against gesetze-im-internet.de, surfaces stale law data
This is the most important section of the README. Trust isn't a vibe — it's evidence.
| Question | Where to look |
|---|---|
| Is every cited § actually in the corpus? | verify_citation() returns verified: false if not. 0% hallucination measured. |
| Where does each law come from? | verify_law_provenance(abbr) → official source URL + SHA-256 + git timestamp |
| Is the corpus the same one another agent is seeing? | get_corpus_status() → single aggregate SHA-256, deterministic, public |
| Has anything changed upstream since we synced? | check_upstream_currency(abbr) → days behind upstream + last-modified timestamps |
| What's your full promise / disclosure of gaps? | Read freshness/TRUST.md — it's the most honest legal-tech trust document you'll read this year. |
Live drift status (the integrity check is automated; this section reflects the latest sync):
6 of 36 monitored laws are stale vs. upstream gesetze-im-internet.de
BGB: 50 days behind ZPO: 49 days behind SGG: 49 days behind
GG: 29 days behind HGB: 29 days behind AO: 21 days behind
We tell you this on purpose. A citizen looking up tenant rights should know if our § 573 BGB is older than the official version. Daily cron (upstream-sync.yml) refreshes it automatically.
git clone https://github.com/mikelninh/gitlaw
cd gitlaw
pip install -e gitlaw_mcp
python rag/build_vectorstore.py # one-off, ~15 min, costs ~$0.50 in OpenAI embeddingsThen add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):
{
"mcpServers": {
"gitlaw": {
"command": "gitlaw-mcp",
"env": {
"OPENAI_API_KEY": "sk-..."
}
}
}
}Restart Claude Desktop. Ask: "Verifiziere § 573 BGB." — you should get the real paragraph text back.
For Cursor / Continue / custom agents: same command + env, the config schema is identical.
118 passed in 10s
- 81 citation-parsing & hallucination tests —
§ 999 StGBcorrectly rejected,Art. 5 Abs. 1 S. 1 GGcorrectly parsed, mixed-case / whitespace / unicode handled - 8 lookup-content tests — returned paragraph text actually contains the domain-specific terms a lawyer expects under that number (catches silent index drift)
- 2 citation-graph tests — known cross-references between paragraphs are preserved
- 10 semantic-search Lebenslagen tests — plain-language queries ("Mieterhöhung Zustimmung", "Cyberstalking Nachstellung") surface the canonical paragraph in top-N
- 14 adversarial tests — prompt-injection, 10k-char inputs, unicode soup, SQL-ish payloads degrade gracefully
- 2 latency-budget tests —
verify_citation< 50 ms,lookup_paragraph< 100 ms after warmup
Known limitations (honest):
- Citation parser doesn't yet handle the nested
Abs. X Nr. Yform (e.g.§ 573 Abs. 2 Nr. 2 BGB) — falls back to flat parsing. On the roadmap. - Some specialised statutes (EFZG, certain Landesgesetze) aren't in the indexed corpus. Federal Bundesrecht is complete.
| Tool | Purpose | Example |
|---|---|---|
search_laws(query, limit=5) |
Semantic search across all paragraphs (FAISS + OpenAI embeddings) | "Beleidigung im Internet" |
verify_citation(citation) |
Parse § 185 StGB style strings → real text or structured rejection. The anti-hallucination tool. |
"§ 185 Abs. 1 StGB" |
lookup_paragraph(abbr, paragraph) |
Exact lookup when you have structured input | ("StGB", "263a") |
list_laws(filter=None, limit=50) |
Enumerate available laws (5,942 indexed) | filter="bgb" |
find_related_paragraphs(citation) |
Walk the citation graph (94k nodes, 200k edges) — who cites X, what X cites | "§ 185 StGB" |
hybrid_search(query, limit, expand) |
Semantic + 1-hop graph expansion in one call | "Eigenbedarf", expand=2 |
| Tool | Purpose | Example output |
|---|---|---|
get_corpus_status() |
Single integrity hash + law count + when manifest was last built | aggregate_sha256: b93152a9… |
verify_law_provenance(abbr) |
Source URL + SHA-256 + git timestamp for one law | source_url, corpus_sha256, corpus_bytes |
check_upstream_currency(abbr) |
Compares our git timestamp vs. gesetze-im-internet.de Last-Modified | drift_status: "stale", days_behind: 50 |
list_drifted_laws() |
Every monitored law where upstream is newer than our corpus, sorted by staleness | sorted list of drifted laws |
Plus the resource gitlaw://law/{abbreviation} returning the full markdown content of a law.
verify_citation("§ 185 StGB")
→ {
"verified": true,
"law": { "name": "Strafgesetzbuch", "abbreviation": "StGB" },
"paragraph": {
"number": "§ 185",
"title": "Beleidigung",
"text": "Die Beleidigung wird mit Freiheitsstrafe bis zu einem Jahr ..."
},
"source": "laws/stgb.md"
}
verify_citation("§ 999 StGB")
→ {
"verified": false,
"reason": "paragraph_not_found",
"law": { "name": "Strafgesetzbuch", "abbreviation": "StGB" },
"hint": "StGB exists, but '§ 999' was not found in the corpus."
}
verify_citation("§ 185 XYZ")
→ {
"verified": false,
"reason": "law_not_found",
"law_abbreviation_searched": "XYZ"
}
Requires Python 3.10+.
From the GitLaw repo root:
pip install -e gitlaw_mcp
# or with uv:
uv pip install -e gitlaw_mcpThen verify:
gitlaw-mcp --help # see CLI options (FastMCP defaults)
python -m gitlaw_mcp.server # start the server (stdio)search_laws requires OPENAI_API_KEY (used for query embeddings). The other three tools work entirely offline against the local Markdown corpus.
The rag/vectorstore/ directory must exist. If not:
python rag/build_vectorstore.py # builds FAISS index, ~$0.50, ~10 minSee the Quickstart above — the
canonical config uses the gitlaw-mcp entry-point installed by pip install -e gitlaw_mcp.
If you'd rather invoke without pip install (e.g. running straight from the
cloned source), the equivalent config is:
{
"mcpServers": {
"gitlaw": {
"command": "python",
"args": ["-m", "gitlaw_mcp.server"],
"cwd": "/absolute/path/to/gitlaw",
"env": { "OPENAI_API_KEY": "sk-..." }
}
}
}Cursor / Continue / custom agents accept the same command + args + env shape.
In Claude Desktop, after wiring up the server:
"Was sagt § 185 StGB zu Beleidigung im Internet? Verifiziere bitte die Zitate."
Claude will call search_laws for context, then verify_citation on each citation it produces, and clearly mark which ones are verified vs. unverified.
"Welche Gesetze haben die Abkürzung 'BGB'?"
Calls list_laws(filter='bgb') and returns the three matches.
"Existiert eigentlich § 999 StGB?"
Calls verify_citation('§ 999 StGB') and reports it does not exist.
gitlaw_mcp/
├── server.py — FastMCP server, 5 @tool decorators + 1 @resource
├── citations.py — German legal-citation regex parser + paragraph extractor
├── graph_builder.py — extracts cross-references from all 5,936 laws → JSON
├── graph_viewer.html — D3 force-directed network of 290 laws + 242 cross-edges
├── data/
│ ├── citation_graph.json — 94K paragraphs, 200K refs (gitignored, regen via builder)
│ ├── citation_graph_top.json — top-30 laws subset
│ └── citation_graph_laws.json — law-level aggregation (used by viewer)
├── demo.py — runnable smoke test (no API key needed)
├── Dockerfile — multi-stage prod build, non-root user, healthcheck
├── ARCHITECTURE.md — cloud migration path (AWS Fargate / Azure Container Apps)
├── pyproject.toml
└── README.md
reuses:
laws/ — 5,936 markdown files (one per law, paragraphs as ### headings)
rag/vectorstore/ — FAISS index (paragraph-level chunks, OpenAI embeddings)
The 5,936 laws form a citation network — every paragraph that mentions another (§ 11 Absatz 3 inside the body of § 185) becomes a graph edge. After running:
python -m gitlaw_mcp.graph_builder # ~6s, writes data/citation_graph.json… you get 94,178 paragraphs (nodes) and 200,464 references (edges) — 199,301 intra-law plus 1,163 cross-law. The MCP tool find_related_paragraphs(citation) walks this graph in both directions: paragraphs that cite the input, and paragraphs cited by the input.
For the visual story, open graph_viewer.html (D3 v7, no build step) to see all 290 actively-cross-referencing laws as a force-directed network, with hover, click-to-pin, and search:
python -m http.server 8000
# then open http://localhost:8000/gitlaw_mcp/graph_viewer.html# Build (from repo root):
docker build -t gitlaw-mcp:0.1.0 -f gitlaw_mcp/Dockerfile .
# Smoke-test inside the container:
docker run --rm gitlaw-mcp:0.1.0 python -m gitlaw_mcp.demo
# Run as MCP server (with bind-mounted vectorstore):
docker run --rm -i \
-e OPENAI_API_KEY=$OPENAI_API_KEY \
-v $(pwd)/rag/vectorstore:/app/rag/vectorstore:ro \
gitlaw-mcp:0.1.0.github/workflows/mcp-ci.yml runs three jobs in parallel on every push touching gitlaw_mcp/ or laws/:
- smoke — install + run
demo.py(validates 4,852+ laws indexed, anti-hallucination cases pass) - lint — ruff check + format + mypy
- docker — multi-stage build with BuildKit cache, then run the demo inside the resulting image
Total wall-time ~2-3 min. See ARCHITECTURE.md for the full cloud-deployment path (AWS Fargate / Azure Container Apps).
The server is stateless across requests but loads:
- the abbreviation→file index lazily on first call (~50ms scan of
laws/*.mdheaders) - the FAISS vectorstore lazily on first
search_lawscall (~2-3s, ~150MB RAM)
The other tools (verify_citation, lookup_paragraph, list_laws) are pure file-reads against the markdown corpus — no API key needed, sub-millisecond per call.
citations.py handles common German legal citation formats:
| Input | Parsed |
|---|---|
§ 185 StGB |
StGB / § 185 |
§185 StGB |
StGB / § 185 |
§ 185 Abs. 1 StGB |
StGB / § 185 (subsection: "Abs. 1") |
§ 185 I StGB |
StGB / § 185 (subsection: "I", Roman numeral) |
§ 263a StGB |
StGB / § 263a (paragraph with letter suffix) |
Art. 5 GG |
GG / Art 5 |
Art 5 Abs. 1 S. 1 GG |
GG / Art 5 (subsection: "Abs. 1 S. 1") |
The Roman-numeral sub-pattern uses a lookahead so X in XYZ is not mistaken for a Roman 10. Letter-suffixed paragraphs (263a, 129b) are supported.
The MCP server is also packaged for HTTP+SSE transport so any hosted MCP client (or your own agent on AWS/Render/Cloudflare Workers) can connect over TLS without running it locally.
# Deploys to https://gitlaw-mcp.fly.dev/sse in Frankfurt (eu-central):
flyctl auth login
flyctl launch --no-deploy # accepts fly.toml as-is
flyctl secrets set OPENAI_API_KEY=sk-...
flyctl volumes create gitlaw_data --region fra --size 1
flyctl deployAfter that, push-to-deploy is wired via .github/workflows/fly-deploy.yml —
add FLY_API_TOKEN to your repo secrets and every commit on main that
touches gitlaw_mcp/, laws/, or fly.toml ships within ~60s.
The Dockerfile.fly variant binds to 0.0.0.0:8000 and serves SSE; the
default gitlaw_mcp/Dockerfile stays in stdio mode for Claude Desktop.
-
HTTP/SSE transport— done (Dockerfile.fly + fly.toml + SSE in server.py) -
Citation graph +— done (94k nodes, 200k edges)find_related_paragraphstool -
Eval harness with reproducible hallucination measurement— done (eval/, 25 questions) -
Corpus provenance manifest— done (freshness/manifest.json, per-law SHA-256) -
Live drift detection vs. gesetze-im-internet.de— done (freshness/sync.py, daily cron) - Phase 1b — auto-resync stale markdown when drift detected (needs XML→markdown parser, ~2 weekends)
- Nested
§ X Abs. Y Nr. Zcitation parsing - Schweizer / Österreichischer Rechtskorpus (already partially in
laws_*.py) - Landesrecht (state-level law)
- Per-tenant rate limiting (relevant once multi-tenant SSE clients exist)
GitLaw MCP is one of three Model Context Protocol servers built as a thin agent-readable layer over real-world workflows. The pattern is deliberately reproducible — same architecture, different domains:
- gitlaw-mcp — German federal law (you're here)
- safevoice-mcp — victim-of-digital-harassment tooling: classification, applicable §, Strafantrag-Fristen, jurisdiction, anonymisation (DE/AT/CH/UK)
- grailsense — NFT collector intelligence over Blockscout: archetype classification + shareable soul cards
Together they're an early sketch of what public-good civic infrastructure looks like in the LLM era: open source, MIT, verifiable, composable.
- Issues / bug reports — GitHub Issues
- Strategic discussion — GitHub Discussions
- Direct — open an issue tagged
questionif it's broader than a bug - Built by @mikelninh — Berlin
MIT. Part of the GitLaw project — open infrastructure for digital legal services in Germany. The underlying corpus of German federal law is public domain per § 5 UrhG.