OLDP MCP Server
The Open Legal Data Platform exposes a Model Context Protocol (MCP) server, enabling AI agents (Claude, Cursor, etc.) to search, retrieve, and navigate German legal data including court decisions and legislation with cross-references.
Quick Start
Claude Desktop / Claude Code
Add to your claude_desktop_config.json:
{
"mcpServers": {
"oldp": {
"url": "https://de.openlegaldata.io/mcp"
}
}
}
Claude.ai (Custom Connector)
Go to Settings → Connectors → Add Custom Connector
Enter URL:
https://de.openlegaldata.io/mcpClick Add — works immediately without login
Claude Code CLI
claude mcp add --transport http oldp https://de.openlegaldata.io/mcp
Authentication
The MCP server supports two modes:
Mode |
How it works |
Rate limit |
|---|---|---|
Anonymous |
No login required |
500 requests/hour (shared for Anthropic IPs) |
Authenticated |
OAuth 2.0 login with OLDP account (or API token) |
Same per-user budget as the REST API (5,000 requests/hour by default), shared across REST API and MCP |
Authenticated MCP requests and REST API requests draw from one per-user quota: every HTTP request to either surface counts against it, custom per-token limits and the profile-completion bonus apply to both, and the account dashboard shows the combined usage.
OAuth Flow
The server implements OAuth 2.0 with PKCE and Dynamic Client Registration (RFC 7591). Discovery endpoints:
/.well-known/oauth-protected-resource— Resource metadata/.well-known/oauth-authorization-server— Authorization server metadata/oauth/register/— Dynamic Client Registration/oauth/authorize/— Authorization endpoint/oauth/token/— Token endpoint
The consent screen shown at /oauth/authorize/ overrides django-oauth-toolkit’s default template with oldp/assets/templates/oauth2_provider/authorize.html (OLDP micro layout, styles in scss/oldp/_oauth.scss). The template lives in the project template dir rather than the mcp app because oauth2_provider precedes oldp.apps.mcp in INSTALLED_APPS, so an app-level override would be shadowed.
Available Tools
Discovery
Tool |
Description |
|---|---|
|
Platform coverage summary: total cases, laws, courts, date ranges |
|
Browse/filter courts by type, state, jurisdiction, level of appeal |
|
List available law books (BGB, StGB, GG, etc.) with section counts |
Search
Tool |
Description |
|---|---|
|
Unified search across BOTH laws and cases in one call, grouped by type ( |
|
Full-text search via Elasticsearch. Returns snippets, not full text. Accepts citation-graph filters ( |
|
Full-text search across law sections. Returns snippets only |
|
Cases textually similar to a given case (Elasticsearch |
|
Structured ORM filtering by court, date, file number, ECLI, etc. |
See Search for the full filter matrix and combined-search examples across all three surfaces.
Retrieval
Tool |
Description |
|---|---|
|
Full case by ID/slug: metadata plus complete (untruncated) plain-text |
|
Law text by book code + section (e.g. “BGB” + “823”), complete plain-text |
|
Detailed court info: name, address, contact, case count |
Text format
get_case and get_law_section return the document text as plain text,
not HTML (the REST API keeps serving the stored HTML). It is derived the
same way as the text in the search index — tags stripped, HTML entities
decoded (ä → ä), reference markers removed — and then normalized
so it is compact and readable:
line breaks and indentation in the HTML source count as spaces;
block elements (paragraphs, headings, list items, …) end a line and empty lines are dropped, so every paragraph is one line;
lists start on a new line and a list number or Randnummer stays on the line of its item (
1. Einkünfte aus …,12 Die Revision ist …);sentence numbers (
<sup>) are separated from the following word ((1) 1 Der Einkommensteuer unterliegen).
Markup-heavy decisions are therefore much shorter than their HTML (e.g. BGH VII ZR 105/18: 48,596 characters of HTML, 27,081 of text).
Reading long texts in snippets (offset / length)
The tools never truncate: with default arguments content holds the whole
text. Long decisions can exceed an agent’s context budget, so both tools
accept two optional integer parameters to read the text piece by piece:
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Start position, in characters of the plain text described above |
|
|
Number of characters to return; |
Positions always refer to the plain text (identical to the full content),
never to the raw HTML.
As soon as offset or length is non-zero, the response omits content
and contains a snippet object next to the usual metadata:
{
"id": 318054,
"file_number": "VII ZR 105/18",
"...": "...",
"snippet": {
"text": "Tenor\nAuf die Revision des Beklagten ...",
"offset": 0,
"length": 10000,
"total_length": 27081,
"has_more": true,
"next_offset": 10000
}
}
Field |
Meaning |
|---|---|
|
The requested plain-text slice |
|
Start position of |
|
Actual length of |
|
Length of the whole plain text — use it to plan how many calls are needed |
|
|
|
Pass as |
To read a whole decision in chunks:
get_case(case_id=318054, length=10000)get_case(case_id=318054, offset=10000, length=10000)(the returnednext_offset)… repeat until
has_moreisfalse.
Negative values, or an offset greater than total_length, return an
{"error": ..., ...} payload (the latter includes total_length).
Deprecated: full_text and content_truncated
Earlier versions of get_case truncated content at 30,000 characters
unless full_text=True was passed (then at 100,000). Truncation has been
removed. For backwards compatibility:
full_textis still accepted (trueorfalse) but ignored; the content is always complete. Passing it adds adeprecation_warningslist to the response (and logs a server-side warning) telling the client to drop the argument and useoffset/lengthfor snippets.content_truncatedis still returned alongsidecontentand is alwaysfalse.
Note that in the same release content switched from HTML to plain text;
clients that need the HTML can use the REST API (/api/cases/<id>/,
/api/laws/<id>/).
Both will be removed in a future release; clients should stop sending
full_text and stop reading content_truncated.
Cross-References
Tool |
Description |
Backend |
|---|---|---|
|
Check if Aktenzeichen, ECLI, or § reference exists in the database |
SQL |
|
Forward refs: which laws and cases does a decision cite? |
SQL |
|
Reverse refs: which cases cite a given decision? |
Elasticsearch |
|
All cases interpreting a specific statute section; `sort=date |
most_cited` (landmark first) |
The same data is queryable via the
REST API’s citation surfaces:
nested actions like /api/cases/<id>/references/ for case-by-case
lookups, plus a flat /api/references/ resource with slug-based
filters (cited_by_law__book__slug=bgb&cited_by_law__slug=823) for
cross-cutting graph queries the MCP tools can’t express. The REST
nested actions and the MCP tools share a single service layer and
the same backends — references / forward-refs paths via the ORM,
citing_cases paths via Elasticsearch — so payload shapes match.
When Elasticsearch is unavailable, get_citing_cases and
get_cases_for_law return a structured error envelope rather than
silently degrading:
// Transient timeout — same query is sub-100ms after segments are
// paged in. Agent should wait + retry.
{"error": "Search timed out…", "retryable": true, "hint": "…"}
// Hard outage — retrying immediately won't help.
{"error": "Citation graph is temporarily unavailable. Try again in a few minutes.", "retryable": false}
Agents should branch on retryable rather than parsing the message.
See docs/elasticsearch.md
for the underlying CaseIndex.cited_laws / CaseIndex.cited_cases
fields.
Statistics
Tool |
Description |
|---|---|
|
Aggregated counts by court, year, jurisdiction |
Usage Examples
Legal Research
“Find BGH decisions from 2023 about § 823 BGB”
search_cases(query="§ 823 BGB", court_code="BGH", start_date="2023-01-01", end_date="2023-12-31")get_case(case_id=12345)for full text (orget_case(case_id=12345, length=10000)to read it in plain-text chunks)get_case_references(case_id=12345)for cited laws/cases
Citation Verification
“Is ‘VI ZR 123/22’ a valid BGH file number?”
validate_citation(citation="VI ZR 123/22")
Legislative Impact Analysis
“How is § 1004 BGB interpreted in case law?”
get_cases_for_law(book_code="BGB", section="1004")get_case(case_id=...)for relevant decisions
Design Principles
Verbatim text only: No AI-generated summaries. The agent summarizes; we provide the source.
Search → Retrieve pattern: Search returns snippets;
get_case/get_law_sectionreturns full text.Citation validation: Built-in hallucination guard for legal citations.
No truncation: Retrieval tools return the complete text. Agents that need to protect their context window request plain-text snippets via
offset/lengthinstead of relying on a server-side cut-off.
Disclaimer
This data is provided for informational purposes only and is not a substitute for professional legal advice. References are automatically extracted and may be incomplete. Verify critical citations against the full text.
Technical Details
Transport: Streamable HTTP at
/mcpPackage:
django-mcp-serverv0.5.7OAuth:
django-oauth-toolkitv3.xSearch: Elasticsearch 7.17 via django-haystack