Swiss Courts Mcp

io.github.malkreide由 GitHub 账号 malkreide 发布v0.5.0更新于 Oct 24, 2026

Swiss court decisions via entscheidsuche.ch, including federal and cantonal courts

发布方自己的服务:Nyc Tennis Courts

概览

AI 生成的概览

让助手检索瑞士联邦与各州法院判决,并在实时来源不可用时回退到离线数据。

可以做什么
通过 entscheidsuche.ch 提供八个只读工具:按州、法院层级和日期筛选的判决全文检索、按签名获取单份判决、带法庭筛选的联邦最高法院检索,以及按所引法律条文检索。还可列出法院、返回最新判决、给出判决统计,并查看离线数据缓存的覆盖情况。另提供一个法律研究提示词,每个响应都会标明来源是实时还是离线数据。
适用场景
适合瑞士判例法研究,例如查找某项宪法权利的实践或联邦最高法院在某一主题上的裁判。与瑞士联邦法律服务器配合使用时,可同时获得法条文本与判例。
运行要求
作为本地 Python 包运行(需 Python 3.11 或更高版本),可通过 uvx 或 pip 安装,并在兼容 MCP 的客户端中使用。无需 API 密钥。实时结果需要访问 entscheidsuche.ch,可选的离线数据下载需要访问 zenodo.org。可选的 HTTP 模式使用 MCP_AUTH_SECRET、MCP_OAUTH_AUDIENCE、MCP_OAUTH_ISSUER、MCP_RESOURCE_URL 等环境变量。
安装前请注意
只读:所有工具均标记 readOnlyHint 为 true,没有写入或破坏性操作。查询与结果会发送给第三方来源 entscheidsuche.ch 和 zenodo.org。离线回退仅覆盖 2007 年至 2024 年 12 月的联邦最高法院,且只返回元数据而无全文,属于部分覆盖而非等价镜像。HTTP 传输默认关闭;无认证运行会触发警告,应置于带认证的反向代理之后。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Swiss Courts Mcp,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

Part of the Swiss Public Data MCP Portfolio

🏛️ swiss-courts-mcp

[Version] [License: MIT] [Python 3.11+] [MCP] [No Auth Required] [CI]

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

Deutsche Version

[Demo: Claude searches Swiss court decisions via MCP tool call]


Overview

Access 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.

🎯 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."

SourceCoverageData
entscheidsuche.ch (live, default)Federal + 26 cantonsCourt decisions since ~2000
SCD dump (offline fallback)Federal Supreme Court only, 2007–2024Metadata/regesten, no full text

Synergy with fedlex-mcp: Legislation (SR) + case law = complete legal research.

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). Every response declares its origin (source: "live" | "dump"), and dump answers carry a coverage_note — the fallback is partial, not equivalent.


Features

  • Full-text search across all Swiss court decisions
  • Multi-stage law reference search with regex parser and Elasticsearch boost scoring
  • Dedicated Federal Supreme Court search with chamber filter
  • Canton and court level filtering
  • Recent decisions feed
  • Court taxonomy listing
  • Decision statistics with aggregations
  • Trilingual support (German, French, Italian)
  • Offline fallback to a cached public dump when entscheidsuche.ch is unreachable — with explicit provenance on every response
  • No API key required

Prerequisites

  • Python 3.11 or higher
  • An MCP-compatible client (Claude Desktop, Cursor, Windsurf, etc.)

Installation

bash
pip install swiss-courts-mcp

Or install from source:

bash
git clone https://github.com/malkreide/swiss-courts-mcp.gitcd swiss-courts-mcppip install -e ".[dev]"

Quickstart

bash
# Run directlyswiss-courts-mcp
# Or via Python modulepython -m swiss_courts_mcp

Configuration

Claude Desktop

Add to your claude_desktop_config.json:

json
{  "mcpServers": {    "swiss-courts": {      "command": "python",      "args": ["-m", "swiss_courts_mcp"]    }  }}

Cloud Deployment (HTTP transport)

The HTTP transport is off by default. The default bind host is 127.0.0.1 (loopback only) — 0.0.0.0 must be opted into explicitly (the Dockerfile does this). Running HTTP without authentication logs a warning; only do so behind an authenticating reverse proxy.

bash
# Local HTTP (loopback), no auth — development onlyswiss-courts-mcp --http --port 8000
# Container (binds 0.0.0.0, auth enabled) — see Dockerfiledocker build -t swiss-courts-mcp .docker run -p 8000:8000 -e MCP_AUTH_SECRET="$(openssl rand -hex 32)" swiss-courts-mcp

Relevant environment variables (see .env.example):

VariableDefaultPurpose
MCP_HOST127.0.0.1Bind host. Set to 0.0.0.0 only in containers.
MCP_PORT8000Bind port.
MCP_ALLOW_PUBLIC_BINDfalseSuppress the 0.0.0.0 warning (containers).
MCP_STATELESS_HTTPtrueStateless HTTP → horizontal scaling without sticky sessions.
MCP_AUTH_ENABLEDfalseEnable bearer-token auth for HTTP.
MCP_AUTH_SECRET—HS256 signing key (dev).
MCP_OAUTH_JWKS_URL—JWKS URL for RS256 validation (production).
MCP_OAUTH_AUDIENCE—Required with auth. Resource identifier the IdP binds tokens to (aud).
MCP_OAUTH_ISSUER—Required with auth. Issuer of the IdP that mints the tokens (iss), compared character for character.
MCP_RESOURCE_URLbind addressPublic URL of this server — the RFC 9728 resource identifier. Set it on any non-loopback bind.
MCP_REQUIRED_SCOPES—Comma-separated required scopes.
MCP_CORS_ORIGINS—Comma-separated allowed origins (no wildcard in prod).

Authentication validates the user identity from the JWT sub claim only; see ADR 0001.

MCP_OAUTH_AUDIENCE is mandatory once auth is on. The aud claim is what binds a token to this server. Without it the verifier did not check the audience at all and accepted any correctly signed token from the same issuer — including one minted for a different service (confused deputy). The server now refuses to start in auth mode without it.

MCP_OAUTH_ISSUER is mandatory too, for the same reason on the other side of the token. The iss claim binds a token to the IdP this server trusts. This matters as soon as several tenants share one JWKS URL — the normal case with a hosted IdP: the signature is then valid for all of them, and only iss separates them. Measured with the variable unset, audience correct: a token with iss: https://another-tenant.example was accepted, and so was a token with no iss at all. The value is compared literally per RFC 8414/9207, trailing slash included, and is therefore not normalised — unlike MCP_RESOURCE_URL, where a trailing slash is trimmed.

It is also what makes discovery work. authorization_servers[0] in the RFC 9728 document is the value an SDK client adopts as its authorization server. Without the variable the server named itself there — and under that address /.well-known/oauth-authorization-server, /.well-known/openid-configuration, /authorize, /token and /register all answer 404, because this is a resource server, not an authorization server.

MCP_RESOURCE_URL is what an OAuth client needs. Per RFC 9728 the server publishes a resource identifier at /.well-known/oauth-protected-resource and names it in the WWW-Authenticate header of every 401 — that is where a client looks to find out where to get a token. Without the variable the server publishes its bind address; measured with a container bind, that was {"resource": "http://0.0.0.0:8000", …}, an address no client can dial. Set it on any non-loopback bind; the server warns at startup when it is missing.

The SDK's own validate_token_resource stays off for a measured reason: it compares the token's resource indicator literally against resource_server_url, which here is the bind address. A token whose audience is not that exact URL is rejected with 401 — so switching it on would lock out every real client while the audience check above is what actually protects the server.

Offline fallback (env)

VariableDefaultPurpose
SWISS_COURTS_FALLBACK_ENABLEDtrueMaster switch. 0 disables the dump fallback (live-only).
SWISS_COURTS_FORCE_DUMPfalseForce the dump path (skip live) — for pre-warming the cache or offline testing.
SWISS_COURTS_CACHE_DIRplatformdirs cacheOverride the cache directory for the downloaded dump.
SWISS_COURTS_DUMP_RECORD14867950Zenodo record id of the SCD dump to use.

Pre-warm the cache (downloads the ~120 MB SCD CSV once, so the first real outage does not pay the download cost):

bash
SWISS_COURTS_FORCE_DUMP=1 python -m swiss_courts_mcp  # then issue one search

MCP Protocol Versions

The server serves two protocol eras over the same endpoint; the client's first request decides which one it gets:

EraRevisionConstant in server.pyHandshake
Legacy2025-11-25HANDSHAKE_PROTOCOL_VERSIONinitialize, session ID
Modern2026-07-28MODERN_PROTOCOL_VERSIONnone — per-request _meta envelope

Both revisions are pinned and both have a drift guard against the installed SDK, so a protocol bump stays a conscious change (constant + CHANGELOG + this section). SDK updates land monthly via Dependabot.

Spec 2026-07-28 is the era the SDK's own client picks. mcp.Client probes server/discover and only falls back to the handshake if that is refused; against this server it does not fall back. Measured through the assembled ASGI stack in tests/test_modern_era.py, not inferred from constant names.

What differs in the modern era:

  • No initialize, no session ID. Every POST is self-contained and carries params._meta with the protocol version and the client capabilities, plus the routing headers MCP-Protocol-Version, Mcp-Method and (for tools/call, prompts/get, resources/read) Mcp-Name.
  • server/discover replaces the handshake for capability discovery, and subscriptions/listen replaces the change notifications.
  • ping, logging/setLevel and the resources/subscribe pair are gone. In this era the server answers them with -32601; in the legacy era they remain.
  • ttlMs / cacheScope ride on the listing methods (SEP-2549) — see CACHE_HINTS.
  • serverInfo is stamped into the _meta of every response, because there is no handshake to carry it once. It names the build version, the display title and the project URL; the purpose text stays in instructions, which travels once with server/discover.

An initialize asking for 2026-07-28 still gets 2025-11-25 back: the handshake is not the way into the modern era, and that answer says nothing about whether the server speaks it.

Project Phase

Phase 1 — read-only (see ROADMAP.md). All tools are readOnlyHint: true; there are no writing or destructive operations. A move to Phase 2 (write) requires a clean re-audit and the gates listed in the roadmap.


Available Tools

Court Decision Search

ToolDescription
search_court_decisionsFull-text search across all court decisions with canton, court level, and date filters
get_court_decisionRetrieve a single decision by its unique signature
search_bger_decisionsSearch Federal Supreme Court decisions with optional chamber filter
search_by_law_referenceFind decisions citing a specific law article (e.g., "Art. 8 BV")

Court Information

ToolDescription
list_courtsList all indexed courts, optionally filtered by canton
get_recent_decisionsLatest decisions, filterable by canton and court level
get_decision_statisticsStatistics on indexed decisions by canton and year
get_fallback_statusOffline-dump cache state, coverage, version, pre-warming (read-only)

Tool Annotations

All eight tools share the same hints — they are read-only, idempotent, non-destructive, and reach an external system:

AnnotationValue
readOnlyHinttrue
destructiveHintfalse
idempotentHinttrue
openWorldHinttrue

A rechtsrecherche prompt is also provided (a second MCP primitive alongside tools).

Example Use Cases

Use CaseTool Chain
Research case law on data protectionsearch_court_decisions("Datenschutz")
Find practice on a constitutional rightsearch_by_law_reference("Art. 8 BV")
Latest Federal Supreme Court rulingssearch_bger_decisions("Arbeitsrecht", date_from="2024-01-01")
Combined: Law text + case lawfedlex_search_laws("DSG") then search_by_law_reference("Art. 25 DSG")

→ More use cases by audience →


Architecture

┌─────────────────────────────────────┐│         MCP Client (LLM)            ││   Claude / Cursor / Windsurf        │└──────────────┬──────────────────────┘               │ MCP Protocol┌──────────────▼──────────────────────────────┐│              swiss-courts-mcp               ││  8 tools · Pydantic validation              ││  Elasticsearch query builder                ││  Provenance envelope: source = live | dump  │└───────┬──────────────────────────────┬──────┘        │ ① live (default)             │ ② fallback        │ HTTPS POST/GET               │ on bot-block / 5xx / 429 /        │                              │ timeout, or SWISS_COURTS_FORCE_DUMP=1┌───────▼──────────────────┐   ┌───────▼───────────────────────────────┐│     entscheidsuche.ch    │   │   SCD dump — Zenodo 14867950 (CC BY)  ││  Elasticsearch backend   │   │   lazy download → platformdirs cache  ││  Federal + 26 cantons    │   │   → local SQLite search               ││  no auth · no SLA        │   │   BGer only · 2007–2024 · no full text │└──────────────────────────┘   └───────────────────────────────────────┘

Live-first, always: the offline dump only engages on an availability failure (bot-block, HTTP 5xx/429, timeout) or when forced. It is a behaviour of the existing tools, not a separate search tool — why this source and not the full-text one is in ADR 0002; what it does and does not cover is under Known Limitations. Inspect the cache at any time with get_fallback_status.


Safety & Limits

AspectDetails
AccessRead-only (readOnlyHint: true) — the server cannot modify or delete any data
Personal dataNo personal data — all decisions are public court rulings
Rate limitsBuilt-in per-query caps (max 50 results per search, 50 aggregation buckets)
Timeout30 seconds per API call
Data source authNo API keys required — entscheidsuche.ch is publicly accessible
HTTP transport authOptional bearer-token auth (JWT, sub-claim identity); see ADR 0001
EgressCode-layer allow-lists (entscheidsuche.ch for live; zenodo.org for the offline dump), HTTPS-enforced; see egress policy
Error maskingInternal exceptions are logged server-side only; clients receive friendly messages
SecretsNo secrets in code/logs; .env git-ignored, Gitleaks on PRs; see secret management
LicensesCourt decisions are public domain under Swiss law (BGG Art. 27)
Terms of ServiceSubject to entscheidsuche.ch usage terms — please be kind to the server

Known Limitations

  • Search is limited to decisions indexed by entscheidsuche.ch (not all decisions are publicly available)
  • Full-text document content is not returned — only metadata, title, and abstract
  • Statistics depend on Elasticsearch aggregation support of the backend
  • The court taxonomy structure from Facetten_alle.json may vary

Offline fallback (partial coverage — read this): the fallback is a safety net for availability, not an equivalent mirror of the live source:

  • Court scope: Federal Supreme Court only (BGer/BGE). Bundesverwaltungsgericht, Bundesstrafgericht and all 26 cantonal courts are not covered.
  • Time span: 2007 – December 2024 (the SCD dump's range). Decisions outside this window are not in the dump.
  • Content: metadata/regesten only — no full text offline.
  • Update latency: the SCD dump is refreshed roughly quarterly on Zenodo, so the offline data lags the live index. get_fallback_status reports the cached version and can check Zenodo for a newer one.
  • Law-reference search offline only matches references named in the decision's subject/regest (topic/issue) — there is no offline cited-law index.
  • get_court_decision is best-effort offline: SCD case ids (docref, e.g. 1C_517/2016) differ from entscheidsuche signatures, so some lookups are honestly reported as non-resolvable.
  • Responses always disclose their origin via source (live/dump) and a coverage_note; the server never silently narrows coverage — an uncovered query gets an explicit "not covered" answer, never a silent empty result.

Testing

Unit tests mock all HTTP with respx. Run from the project root. The five gates CI runs — check_gate_docs.py holds this list against ci.yml, so it cannot quietly fall behind:

bash
PYTHONPATH=src pytest tests/ -m "not live"python scripts/check_ruff_pin.pyruff check src/ tests/ scripts/ruff format --check src/ tests/ scripts/python scripts/check_version_sync.pypython scripts/check_gate_docs.py

The live tests are not a gate — they hit the real source and run on a schedule (live.yml), not on pull requests:

bash
PYTHONPATH=src pytest tests/ -v -m live

Editing live.yml is a special case: GitHub only honours schedule on the default branch, so changes take effect after the merge — trigger it by hand (workflow_dispatch) to test them before that.

The offline-fallback tests mock the Zenodo download with respx and use a small committed fixture — the ~120 MB dump is never downloaded in CI.


Changelog

See CHANGELOG.md.


Contributing

See CONTRIBUTING.md.


Security

See SECURITY.md for the security posture and how to report a vulnerability.


License

MIT


Author

Hayal Oezkan · malkreide


Credits & Related Projects

Installation

Run via 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):

json
{  "mcpServers": {    "swiss-courts-mcp": {      "command": "uvx",      "args": [        "swiss-courts-mcp"      ]    }  }}

来源:README.md,提交 4b8fc7f

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

2
  1. v0.5.0最新Sep 19, 2026
  2. v0.4.0Sep 16, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分

其他 Courts 集成