{
  "markdown": "> **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**\n\n# 🏛️ swiss-courts-mcp\n\n![Version](https://img.shields.io/badge/version-0.4.0-blue)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-purple)](https://modelcontextprotocol.io/)\n[![No Auth Required](https://img.shields.io/badge/auth-none%20required-brightgreen)](https://github.com/malkreide/swiss-courts-mcp)\n![CI](https://github.com/malkreide/swiss-courts-mcp/actions/workflows/ci.yml/badge.svg)\n\n> MCP Server for Swiss court decisions — Federal Supreme Court (BGer), Federal Administrative Court (BVGer), Federal Criminal Court (BStGer), and all 26 cantonal courts via entscheidsuche.ch\n\n[Deutsche Version](README.de.md)\n\n<p align=\"center\">\n  <img src=\"assets/demo.svg\" alt=\"Demo: Claude searches Swiss court decisions via MCP tool call\" width=\"720\">\n</p>\n\n---\n\n## Overview\n\nAccess Swiss court decisions from all judicial levels through a single MCP interface. Combines full-text search with structured filters for canton, court level, date range, and law references.\n\n**🎯 Anchor demo query:** *\"Find Federal Supreme Court case law on data protection (Art. 25 DSG) since 2020 — and if entscheidsuche.ch is down, still answer from the offline dump, clearly flagged.\"*\n\n| Source | Coverage | Data |\n|--------|----------|------|\n| [entscheidsuche.ch](https://entscheidsuche.ch) (live, default) | Federal + 26 cantons | Court decisions since ~2000 |\n| [SCD dump](https://doi.org/10.5281/zenodo.14867950) (offline fallback) | **Federal Supreme Court only, 2007–2024** | Metadata/regesten, **no full text** |\n\n**Synergy with [fedlex-mcp](https://github.com/malkreide/fedlex-mcp):** Legislation (SR) + case law = complete legal research.\n\n**Availability:** entscheidsuche.ch is non-profit infrastructure without an SLA. When it is unreachable, the server transparently falls back to a cached public dump (see [Offline fallback](#offline-fallback)). Every response declares its origin (`source: \"live\" | \"dump\"`), and dump answers carry a `coverage_note` — the fallback is **partial, not equivalent**.\n\n---\n\n## Features\n\n- Full-text search across all Swiss court decisions\n- Multi-stage law reference search with regex parser and Elasticsearch boost scoring\n- Dedicated Federal Supreme Court search with chamber filter\n- Canton and court level filtering\n- Recent decisions feed\n- Court taxonomy listing\n- Decision statistics with aggregations\n- Trilingual support (German, French, Italian)\n- **Offline fallback** to a cached public dump when entscheidsuche.ch is unreachable — with explicit provenance on every response\n- No API key required\n\n---\n\n## Prerequisites\n\n- Python 3.11 or higher\n- An MCP-compatible client (Claude Desktop, Cursor, Windsurf, etc.)\n\n---\n\n## Installation\n\n```bash\npip install swiss-courts-mcp\n```\n\nOr install from source:\n\n```bash\ngit clone https://github.com/malkreide/swiss-courts-mcp.git\ncd swiss-courts-mcp\npip install -e \".[dev]\"\n```\n\n---\n\n## Quickstart\n\n```bash\n# Run directly\nswiss-courts-mcp\n\n# Or via Python module\npython -m swiss_courts_mcp\n```\n\n---\n\n## Configuration\n\n### Claude Desktop\n\nAdd to your `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"swiss-courts\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"swiss_courts_mcp\"]\n    }\n  }\n}\n```\n\n### Cloud Deployment (HTTP transport)\n\nThe HTTP transport is **off by default**. The default bind host is `127.0.0.1`\n(loopback only) — `0.0.0.0` must be opted into explicitly (the Dockerfile does\nthis). Running HTTP without authentication logs a warning; only do so behind an\nauthenticating reverse proxy.\n\n```bash\n# Local HTTP (loopback), no auth — development only\nswiss-courts-mcp --http --port 8000\n\n# Container (binds 0.0.0.0, auth enabled) — see Dockerfile\ndocker build -t swiss-courts-mcp .\ndocker run -p 8000:8000 -e MCP_AUTH_SECRET=\"$(openssl rand -hex 32)\" swiss-courts-mcp\n```\n\nRelevant environment variables (see [`.env.example`](.env.example)):\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `MCP_HOST` | `127.0.0.1` | Bind host. Set to `0.0.0.0` only in containers. |\n| `MCP_PORT` | `8000` | Bind port. |\n| `MCP_ALLOW_PUBLIC_BIND` | `false` | Suppress the `0.0.0.0` warning (containers). |\n| `MCP_STATELESS_HTTP` | `true` | Stateless HTTP → horizontal scaling without sticky sessions. |\n| `MCP_AUTH_ENABLED` | `false` | Enable bearer-token auth for HTTP. |\n| `MCP_AUTH_SECRET` | — | HS256 signing key (dev). |\n| `MCP_OAUTH_JWKS_URL` | — | JWKS URL for RS256 validation (production). |\n| `MCP_REQUIRED_SCOPES` | — | Comma-separated required scopes. |\n| `MCP_CORS_ORIGINS` | — | Comma-separated allowed origins (no wildcard in prod). |\n\nAuthentication validates the user identity from the JWT `sub` claim only; see\n[ADR 0001](docs/adr/0001-http-auth.md).\n\n### Offline fallback (env)\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `SWISS_COURTS_FALLBACK_ENABLED` | `true` | Master switch. `0` disables the dump fallback (live-only). |\n| `SWISS_COURTS_FORCE_DUMP` | `false` | Force the dump path (skip live) — for pre-warming the cache or offline testing. |\n| `SWISS_COURTS_CACHE_DIR` | `platformdirs` cache | Override the cache directory for the downloaded dump. |\n| `SWISS_COURTS_DUMP_RECORD` | `14867950` | Zenodo record id of the SCD dump to use. |\n\nPre-warm the cache (downloads the ~120 MB SCD CSV once, so the first real\noutage does not pay the download cost):\n\n```bash\nSWISS_COURTS_FORCE_DUMP=1 python -m swiss_courts_mcp  # then issue one search\n```\n\n---\n\n## MCP Protocol Version\n\nThis server pins MCP protocol version **`2025-11-25`** (constant\n`PROTOCOL_VERSION` in `server.py`). A regression test detects drift against the\ninstalled SDK so a protocol bump is a conscious change (version + CHANGELOG +\nthis section). SDK updates land monthly via Dependabot.\n\n## Project Phase\n\n**Phase 1 — read-only** (see [ROADMAP.md](ROADMAP.md)). All tools are\n`readOnlyHint: true`; there are no writing or destructive operations. A move to\nPhase 2 (write) requires a clean re-audit and the gates listed in the roadmap.\n\n---\n\n## Available Tools\n\n### Court Decision Search\n\n| Tool | Description |\n|------|-------------|\n| `search_court_decisions` | Full-text search across all court decisions with canton, court level, and date filters |\n| `get_court_decision` | Retrieve a single decision by its unique signature |\n| `search_bger_decisions` | Search Federal Supreme Court decisions with optional chamber filter |\n| `search_by_law_reference` | Find decisions citing a specific law article (e.g., \"Art. 8 BV\") |\n\n### Court Information\n\n| Tool | Description |\n|------|-------------|\n| `list_courts` | List all indexed courts, optionally filtered by canton |\n| `get_recent_decisions` | Latest decisions, filterable by canton and court level |\n| `get_decision_statistics` | Statistics on indexed decisions by canton and year |\n| `get_fallback_status` | Offline-dump cache state, coverage, version, pre-warming (read-only) |\n\n### Tool Annotations\n\nAll eight tools share the same hints — they are read-only, idempotent,\nnon-destructive, and reach an external system:\n\n| Annotation | Value |\n|---|---|\n| `readOnlyHint` | `true` |\n| `destructiveHint` | `false` |\n| `idempotentHint` | `true` |\n| `openWorldHint` | `true` |\n\nA `rechtsrecherche` **prompt** is also provided (a second MCP primitive\nalongside tools).\n\n### Example Use Cases\n\n| Use Case | Tool Chain |\n|----------|------------|\n| Research case law on data protection | `search_court_decisions(\"Datenschutz\")` |\n| Find practice on a constitutional right | `search_by_law_reference(\"Art. 8 BV\")` |\n| Latest Federal Supreme Court rulings | `search_bger_decisions(\"Arbeitsrecht\", date_from=\"2024-01-01\")` |\n| Combined: Law text + case law | `fedlex_search_laws(\"DSG\")` then `search_by_law_reference(\"Art. 25 DSG\")` |\n\n[→ More use cases by audience →](EXAMPLES.md)\n\n---\n\n## Architecture\n\n```\n┌─────────────────────────────────────┐\n│         MCP Client (LLM)            │\n│   Claude / Cursor / Windsurf        │\n└──────────────┬──────────────────────┘\n               │ MCP Protocol\n┌──────────────▼──────────────────────────────┐\n│              swiss-courts-mcp               │\n│  8 tools · Pydantic validation              │\n│  Elasticsearch query builder                │\n│  Provenance envelope: source = live | dump  │\n└───────┬──────────────────────────────┬──────┘\n        │ ① live (default)             │ ② fallback\n        │ HTTPS POST/GET               │ on bot-block / 5xx / 429 /\n        │                              │ timeout, or SWISS_COURTS_FORCE_DUMP=1\n┌───────▼──────────────────┐   ┌───────▼───────────────────────────────┐\n│     entscheidsuche.ch    │   │   SCD dump — Zenodo 14867950 (CC BY)  │\n│  Elasticsearch backend   │   │   lazy download → platformdirs cache  │\n│  Federal + 26 cantons    │   │   → local SQLite search               │\n│  no auth · no SLA        │   │   BGer only · 2007–2024 · no full text │\n└──────────────────────────┘   └───────────────────────────────────────┘\n```\n\nLive-first, always: the offline dump only engages on an availability failure\n(bot-block, HTTP 5xx/429, timeout) or when forced. It is a behaviour of the\nexisting tools, not a separate search tool — why this source and not the\nfull-text one is in [ADR 0002](docs/adr/0002-offline-fallback.md); what it does\nand does not cover is under [Known Limitations](#known-limitations). Inspect\nthe cache at any time with `get_fallback_status`.\n\n---\n\n## Safety & Limits\n\n| Aspect | Details |\n|--------|---------|\n| **Access** | Read-only (`readOnlyHint: true`) — the server cannot modify or delete any data |\n| **Personal data** | No personal data — all decisions are public court rulings |\n| **Rate limits** | Built-in per-query caps (max 50 results per search, 50 aggregation buckets) |\n| **Timeout** | 30 seconds per API call |\n| **Data source auth** | No API keys required — entscheidsuche.ch is publicly accessible |\n| **HTTP transport auth** | Optional bearer-token auth (JWT, `sub`-claim identity); see [ADR 0001](docs/adr/0001-http-auth.md) |\n| **Egress** | Code-layer allow-lists (`entscheidsuche.ch` for live; `zenodo.org` for the offline dump), HTTPS-enforced; see [egress policy](docs/network-egress.md) |\n| **Error masking** | Internal exceptions are logged server-side only; clients receive friendly messages |\n| **Secrets** | No secrets in code/logs; `.env` git-ignored, Gitleaks on PRs; see [secret management](docs/secret-management.md) |\n| **Licenses** | Court decisions are public domain under Swiss law ([BGG Art. 27](https://www.fedlex.admin.ch/eli/cc/2006/218/de#art_27)) |\n| **Terms of Service** | Subject to [entscheidsuche.ch](https://entscheidsuche.ch) usage terms — please be kind to the server |\n\n---\n\n## Known Limitations\n\n- Search is limited to decisions indexed by entscheidsuche.ch (not all decisions are publicly available)\n- Full-text document content is not returned — only metadata, title, and abstract\n- Statistics depend on Elasticsearch aggregation support of the backend\n- The court taxonomy structure from `Facetten_alle.json` may vary\n\n**Offline fallback (partial coverage — read this):** the fallback is a safety\nnet for availability, **not an equivalent mirror** of the live source:\n\n- **Court scope:** Federal Supreme Court only (BGer/BGE). Bundesverwaltungsgericht,\n  Bundesstrafgericht and **all 26 cantonal courts are not covered.**\n- **Time span:** 2007 – December 2024 (the SCD dump's range). Decisions outside\n  this window are not in the dump.\n- **Content:** metadata/regesten only — **no full text** offline.\n- **Update latency:** the SCD dump is refreshed roughly quarterly on Zenodo, so\n  the offline data lags the live index. `get_fallback_status` reports the cached\n  version and can check Zenodo for a newer one.\n- **Law-reference search** offline only matches references named in the decision's\n  subject/regest (`topic`/`issue`) — there is no offline cited-law index.\n- **`get_court_decision` is best-effort offline:** SCD case ids (`docref`, e.g.\n  `1C_517/2016`) differ from entscheidsuche signatures, so some lookups are\n  honestly reported as non-resolvable.\n- Responses always disclose their origin via `source` (`live`/`dump`) and a\n  `coverage_note`; the server never silently narrows coverage — an uncovered\n  query gets an explicit \"not covered\" answer, never a silent empty result.\n\n---\n\n## Testing\n\nUnit tests mock all HTTP with `respx`. Run from the project root. The five\ngates CI runs — `check_gate_docs.py` holds this list against `ci.yml`, so it\ncannot quietly fall behind:\n\n<!-- gates:start -->\n```bash\nPYTHONPATH=src pytest tests/ -m \"not live\"\npython scripts/check_ruff_pin.py\nruff check src/ tests/ scripts/\nruff format --check src/ tests/ scripts/\npython scripts/check_version_sync.py\npython scripts/check_gate_docs.py\n```\n<!-- gates:end -->\n\nThe live tests are not a gate — they hit the real source and run on a schedule\n([`live.yml`](.github/workflows/live.yml)), not on pull requests:\n\n<!-- live:start -->\n```bash\nPYTHONPATH=src pytest tests/ -v -m live\n```\n<!-- live:end -->\n\nEditing `live.yml` is a special case: GitHub only honours `schedule` on the\ndefault branch, so changes take effect after the merge — trigger it by hand\n(`workflow_dispatch`) to test them before that.\n\nThe offline-fallback tests mock the Zenodo download with `respx` and use a\nsmall committed fixture — the ~120 MB dump is never downloaded in CI.\n\n---\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md).\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n---\n\n## Security\n\nSee [SECURITY.md](SECURITY.md) for the security posture and how to report a vulnerability.\n\n---\n\n## License\n\n[MIT](LICENSE)\n\n---\n\n## Author\n\nHayal Oezkan · [malkreide](https://github.com/malkreide)\n\n---\n\n## Credits & Related Projects\n\n- [entscheidsuche.ch](https://entscheidsuche.ch) — Swiss court decision search engine (live source)\n- **Swiss Federal Supreme Court Dataset (SCD)** — offline fallback source, **CC BY 4.0**:\n  Geering, F. & Merane, J. (2025). *Swiss Federal Supreme Court Dataset (SCD)*, Version 2024-3. Zenodo. https://doi.org/10.5281/zenodo.14867950\n- [fedlex-mcp](https://github.com/malkreide/fedlex-mcp) — MCP Server for Swiss federal law (legislation synergy)\n- [zurich-opendata-mcp](https://github.com/malkreide/zurich-opendata-mcp) — MCP Server for Zurich open data\n- [Model Context Protocol](https://modelcontextprotocol.io/) — Open protocol for AI tool integration\n\n<!-- mcp-name: io.github.malkreide/swiss-courts-mcp -->\n\n<!-- BEGIN GENERATED: install -->\n## Installation\n\nRun via [`uv`](https://docs.astral.sh/uv/)'s `uvx` — no clone or manual install needed. Add to your MCP client config (`mcpServers` for Claude Desktop, Cursor and Windsurf; use a top-level `servers` key for VS Code in `.vscode/mcp.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"swiss-courts-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"swiss-courts-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 15133,
  "sha": "e0821a4b699b6b00c23a1bdcebeeb7b954b7edc7654e7723f6508599cb51a574",
  "repo_slug": "malkreide/swiss-courts-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_swiss_courts_mcp_a8bfba2e/readme"
}