{
  "markdown": "> \\U0001f1e8\\U0001f1ed **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**\n\n# \\U0001f48a bag-epl-mcp\n\n![Version](https://img.shields.io/badge/version-1.0.3-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/bag-epl-mcp)\n![CI](https://github.com/malkreide/bag-epl-mcp/actions/workflows/ci.yml/badge.svg)\n\n> MCP Server for the Swiss BAG electronic benefits platform (ePL) — Spezialitaetenliste, GGSL, MiGeL\n\n[\\U0001f1e9\\U0001f1ea Deutsche Version](README.de.md)\n\n### Demo\n\n![Demo: Claude using epl_sl_suche and epl_rechtskontext](docs/assets/demo.svg)\n\n---\n\n## Overview\n\n`bag-epl-mcp` enables AI models to answer questions about mandatory health insurance coverage in Switzerland — in natural language, grounded in real data.\n\n| List | Purpose | Legal basis |\n|------|---------|-------------|\n| **Spezialitaetenliste (SL)** | Compulsory-insurance medications | KVG Art. 52 |\n| **GGSL** | Medications for congenital disorders (IV) | IVG Anhang |\n| **MiGeL** | Medical devices & aids | KLV Art. 20 |\n\n**Anchor query:** *\"Is this medication covered by mandatory health insurance?\"*\n→ `epl_sl_suche`: Live lookup in the Spezialitaetenliste (SL)\n→ [More use cases by audience →](EXAMPLES.md)\n\n---\n\n## Features\n\n- \\U0001f48a **6 tools, 2 resources, 2 prompts** for Swiss health insurance data\n- \\U0001f50d **`epl_sl_suche`** — search the Spezialitaetenliste for medications\n- ⚖️ **`epl_rechtskontext`** — legal context with Fedlex links\n- \\U0001f513 **No API key required** — all data publicly accessible\n- ☁️ **Dual transport** — stdio (Claude Desktop) + Streamable HTTP (cloud)\n- \\U0001f4da **Prompt templates** for insurance coverage checks and school health queries\n\n---\n\n## Prerequisites\n\n- Python 3.11+\n- [uv](https://github.com/astral-sh/uv) (recommended) or pip\n\n---\n\n## Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/malkreide/bag-epl-mcp.git\ncd bag-epl-mcp\n\n# Install\npip install -e .\n# or with uv:\nuv pip install -e .\n```\n\nOr with `uvx` (no permanent installation):\n\n```bash\nuvx bag-epl-mcp\n```\n\n---\n\n## Quickstart\n\n```bash\n# stdio (for Claude Desktop) — default, opens no network ports\npython -m bag_epl_mcp.server\n\n# Streamable HTTP (cloud) — transport selected via env var\nMCP_TRANSPORT=streamable-http MCP_HOST=0.0.0.0 MCP_PORT=8000 \\\n  pip install -e \".[http]\" && python -m bag_epl_mcp.server\n```\n\n> **Transport & host are configured exclusively via environment variables**\n> (`MCP_TRANSPORT`, `MCP_HOST`, `MCP_PORT`). The default is `stdio` bound to\n> nothing; `MCP_HOST` defaults to `127.0.0.1` and should only be set to\n> `0.0.0.0` inside a container/cloud environment.\n\nTry it immediately in Claude Desktop:\n\n> *\"Is Methylphenidate (Ritalin) covered by mandatory health insurance?\"*\n> *\"Which laws regulate admission to the Spezialitaetenliste?\"*\n> *\"Is a wheelchair covered by mandatory insurance?\"*\n\n---\n\n## Configuration\n\n### Claude Desktop\n\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"bag-epl\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"bag_epl_mcp.server\"]\n    }\n  }\n}\n```\n\nOr with `uvx`:\n\n```json\n{\n  \"mcpServers\": {\n    \"bag-epl\": {\n      \"command\": \"uvx\",\n      \"args\": [\"bag-epl-mcp\"]\n    }\n  }\n}\n```\n\n### Cloud Deployment (Streamable HTTP for browser access)\n\n**Render.com (recommended):**\n1. Push/fork the repository to GitHub\n2. On [render.com](https://render.com): New Web Service → connect GitHub repo\n3. Build command: `pip install -e \".[http]\"`\n4. Set the following environment variables:\n   - `MCP_TRANSPORT=streamable-http`\n   - `MCP_HOST=0.0.0.0` (required so the container accepts external traffic)\n   - `MCP_PORT=8000` (or Render's `$PORT`)\n   - *(optional)* `MCP_CORS_ORIGINS='[\"https://claude.ai\"]'` to extend the\n     browser CORS allow-list\n   - *(optional)* OpenTelemetry tracing is **on by default** but a no-op unless\n     the tracing deps are installed — build with `pip install -e \".[http,otel]\"`\n     and point `OTEL_EXPORTER_OTLP_ENDPOINT` at your collector. Set\n     `MCP_OTEL_ENABLED=0` to disable.\n5. Start command: `python -m bag_epl_mcp.server`\n6. In claude.ai under Settings → MCP Servers, add: `https://your-app.onrender.com/mcp`\n\n> **Security note:** the server exposes only public, read-only data and uses no\n> authentication. See [`docs/SECURITY.md`](docs/SECURITY.md) for the threat\n> model (egress allow-list, host binding, Lethal-Trifecta assessment).\n\n---\n\n## Available Tools\n\n| Tool | Description |\n|------|-------------|\n| `epl_sl_suche` | Search the Spezialitaetenliste for compulsory-insurance medications |\n| `epl_ggsl_abfrage` | Check GGSL coverage for congenital disorders |\n| `epl_migel_suche` | Search the MiGeL for medical devices & aids |\n| `epl_gesuchseingaenge` | List pending SL admission requests (transparency) |\n| `epl_rechtskontext` | Legal context for coverage questions (WZW criteria) |\n| `epl_server_info` | Server status and API phase information |\n\n### Example Use Cases\n\n| Query | Tool |\n|-------|------|\n| *\"Is Ritalin covered by insurance?\"* | `epl_sl_suche` |\n| *\"Which medications for congenital disorder GG-313?\"* | `epl_ggsl_abfrage` |\n| *\"Is a wheelchair covered?\"* | `epl_migel_suche` |\n| *\"Which laws regulate the SL?\"* | `epl_rechtskontext` |\n\n---\n\n## Architecture\n\n**Data flow (Phase 1):**\n\n```\n                         bag-epl-mcp (FastMCP)\n ┌────────────┐   MCP   ┌───────────────────────────────┐   HTTPS GET  ┌──────────────────┐\n │ MCP Client │◀───────▶│  tools (read-only)            │─────────────▶│ sl.bag.admin.ch  │\n │ (Claude    │ stdio / │   ├─ epl_sl_suche             │  egress      │ www.bag.admin.ch │\n │  Desktop,  │ Stream- │   ├─ epl_ggsl_abfrage         │  allow-list  │ www.fedlex...    │\n │  claude.ai)│ able    │   ├─ epl_migel_suche          │◀─────────────│ (public OGD)     │\n │            │ HTTP    │   ├─ epl_gesuchseingaenge     │  (no auth)   └──────────────────┘\n │            │         │   ├─ epl_rechtskontext        │\n │            │         │   └─ epl_server_info          │   structured JSON logs → stderr\n └────────────┘         │  resources: epl://uebersicht …│\n                        │  prompts:   epl_kassenpflicht…│\n                        └───────────────────────────────┘\n```\n\n**Phase roadmap** (details in [`docs/ROADMAP.md`](docs/ROADMAP.md)):\n\n```\nPhase 1 (current)  → legal context + entry points, no data retrieval\nPhase 2 (planned)  → FHIR/IDMP API, once publicly accessible\nPhase 3 (vision)   → MiGeL + AL via ePL-FHIR\n```\n\n**What Phase 1 does, and what it does not.** Five of the six tools make no\nnetwork request at all — there is exactly one outgoing HTTP call in the whole\nmodule. They return the legal basis and an entry point, and they now say so.\nThe previous wording, \"XML/XLSX downloads + SL website access\", advertised a\ncapability with no code path behind it; on 2026-08-08 it was removed rather\nthan implemented, because the underlying source is not machine-readable.\n\nThat one HTTP call goes to `sl.bag.admin.ch/api/search` and receives **HTTP 200\nwith `text/html`** — the 51 KB Angular shell. A freely invented path under the\nsame prefix returns the identical response, byte for byte: there is no API at\nthat address. Previously the resulting JSON parse error was caught by a bare\n`except Exception` and turned into the claim \"the SL database API is not\npublicly documented\" — a statement about the BAG's publishing practice,\nderived from a parser error. The tool now reports what was measured.\n\nThe SL front end calls `https://epl.bag.admin.ch/api/sl/` instead, on a\ndifferent host. That host answers 401 without authentication — but it answers\n401 for invented paths too, so this does **not** establish that any particular\nroute exists. It is deliberately **not** on the egress allow-list: without\nverifiable access, adding it would be a grant on suspicion.\n\n**MCP protocol version:** `2025-11-25` (surfaced via `epl_server_info`) — the\n`initialize` handshake ceiling, derived from the SDK rather than written down\nhere a second time. See [MCP Protocol Version](#mcp-protocol-version) for both\neras. SDK\nupdates are proposed monthly via Dependabot; the protocol version is reviewed on\nevery `mcp` SDK bump — see the versioning policy in [`docs/ROADMAP.md`](docs/ROADMAP.md).\n\n---\n\n## Safety & Limits\n\n- **Read-only:** All tools perform HTTP GET requests only — no data is written, modified, or deleted.\n- **No personal data:** The server accesses public regulatory lists (SL, GGSL, MiGeL). No personally identifiable information (PII) is processed or stored.\n- **No medical advice:** This server provides informational access to regulatory data only. For medical or legal decisions, always consult the official BAG sources and qualified professionals.\n- **Rate limits:** The SL website (sl.bag.admin.ch) is a public Angular SPA; the server enforces a 30s timeout per request. Use `limit` parameters conservatively.\n- **Data freshness:** Phase 1 tools link to live BAG sources. No caching is performed by this server.\n- **Links are measured, not assumed:** the addresses handed out as \"official source\" are re-checked by `scripts/record_fixtures.py` on every run, together with a control request to an invented path. Two BAG pages previously handed out (`.../Arzneimittel/geburtsgebrechen-spezialitaetenliste.html` and `.../Arzneimittel/gesuchseingaenge.html`) answered HTTP 404 on 2026-08-08 and were replaced by the entry point that verifiably resolves — not by a guessed replacement URL.\n- **Legal references are checked against the register:** every SR number the server prints is resolved to its ELI via the Fedlex SPARQL endpoint. This detour is necessary: Fedlex's web front end is a single-page app that answers HTTP 200 with the same byte count for *any* ELI, including an invented one. That is how a wrong GgV link (`eli/cc/1986/40_40_40`, no register entry) went unnoticed; the correct ELI is `eli/cc/1986/46_46_46`.\n- **Data licence (OGD-CH):** The underlying BAG/Fedlex data is Swiss Open Government Data, licensed **CC BY 4.0**. Tool outputs carry a `source` / `provenance` block (JSON) or a source-and-licence footer (Markdown) so attribution is preserved.\n- **Structured output:** every tool returns both a human-readable Markdown/JSON block (`content`) and a typed `structuredContent` validated against a per-tool output schema, so MCP clients can consume results programmatically without parsing prose.\n- **Terms of service:** Data is subject to the ToS of [sl.bag.admin.ch](https://sl.bag.admin.ch), [bag.admin.ch](https://www.bag.admin.ch), and [fedlex.admin.ch](https://www.fedlex.admin.ch).\n- **No guarantees:** This is a community project, not affiliated with the BAG or any government entity. Availability depends on upstream sources.\n\n---\n\n## MCP Protocol Version\n\nThis server speaks **two protocol eras** over the same endpoint. The client's\nfirst request on a connection decides which one applies; a later claim from the\nother era is refused.\n\n| Era | Revision | Who reaches it |\n|---|---|---|\n| `initialize` handshake | `2024-11-05` … **`2025-11-25`** | What today's clients speak. The server answers with the revision asked for, or with the `2025-11-25` ceiling when the request asks for something newer. |\n| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |\n\nBoth revisions are pinned in\n[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted\nagainst the installed SDK, so a Dependabot bump of `mcp` cannot move either one\nsilently. This server builds no ASGI app to send an `initialize` through, so\nthe gate asserts the SDK constants rather than a measured response — the\nweaker form, named rather than left unsaid.\n\nNote that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**\nera, not for the handshake era — pinning against it alone would leave the era\nthat current clients actually negotiate free to drift.\n\n**Update policy.** When the gate fails, do not edit the constant blindly: read\nthe spec changelog between the two revisions, verify the server still behaves,\nthen move the constant, this section, `README.de.md` and\n[`CHANGELOG.md`](CHANGELOG.md) together.\n\n---\n\n## Testing\n\n```bash\n# Unit + contract tests (no network) — this is what CI runs\nPYTHONPATH=src pytest tests/ -m \"not live\"\n\n# Live tests against the real BAG/Fedlex sources\nPYTHONPATH=src pytest tests/ -m \"live\"\n\n# Re-record the measurements (writes tests/fixtures/ + PROVENANCE.md)\nPYTHONPATH=src python scripts/record_fixtures.py\n```\n\n**100 tests** — 88 offline, 12 against the live sources.\n\n### Why there is a contract test file as well as live tests\n\nUntil 2026-08-08 six of the eight live tests **could not pass**. They compared\na string against a tool's return value:\n\n```python\nassert \"BAG ePL MCP Server\" in result   # result is a CallToolResult\n```\n\n`CallToolResult` is a Pydantic model; `in` iterates over `(field, value)`\npairs, so the comparison is always false. Nobody noticed, because CI excludes\n`-m live` — a test that only runs outside CI and is always red there reports to\nno one.\n\nAnd even fixed, four of them would have proved nothing: `assert \"313\" in\nresult` against a tool that writes its own input into a template, `assert\n\"Rollstuhl\" in result` likewise. They asserted that a tool echoes its input —\nwhich is precisely what those tools do.\n\nWhat must hold permanently therefore lives in `tests/test_quellen_vertrag.py`,\nwhich runs **inside** CI against the recorded measurements under\n`tests/fixtures/`. `PROVENANCE.md` records source, date, selection rule and\nSHA-256 for each one.\n\nFour of the recorded measurements are **controls** — an invented path under\n`sl.bag.admin.ch/api/`, an invented path in the BAG portal, an invented ELI,\nand an invented SR number. Without them each measurement would only show what\n*we* received, not what the source actually holds. The recorder aborts if a\ncontrol stops discriminating, if a live entry point dies, if one of the dead\npages returns, or if a legal reference drifts from the register.\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) ([Deutsch](SECURITY.de.md)) for the security\nposture and how to report a vulnerability.\n\n---\n\n## License\n\nMIT License — see [LICENSE](LICENSE)\n\n---\n\n## Author\n\nHayal Oezkan · [malkreide](https://github.com/malkreide)\n\n---\n\n## Credits & Related Projects\n\n- **BAG Spezialitaetenliste:** [sl.bag.admin.ch](https://sl.bag.admin.ch) — Federal Office of Public Health\n- **KVG:** [SR 832.10](https://www.fedlex.admin.ch/eli/cc/1995/1328_1328_1328/de) — Health Insurance Act\n- **KLV:** [SR 832.112.31](https://www.fedlex.admin.ch/eli/cc/1995/4964_4964_4964/de) — Healthcare Benefits Ordinance\n- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) — Anthropic / Linux Foundation\n- **Related:** [fedlex-mcp](https://github.com/malkreide/fedlex-mcp) — Swiss federal law\n- **Related:** [swiss-cultural-heritage-mcp](https://github.com/malkreide/swiss-cultural-heritage-mcp) — Cultural heritage data\n- **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide)\n\n<!-- mcp-name: io.github.malkreide/bag-epl-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    \"bag-epl-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"bag-epl-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 16189,
  "sha": "f78ece138a185294b563fd37ad85b72309956ca600e499c411598df5bf165a88",
  "repo_slug": "malkreide/bag-epl-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_bag_epl_mcp_e68bc85a/readme"
}