{
  "markdown": "[🇩🇪 Deutsche Version](README.de.md)\n\n# 🏛️ parlament-mcp\n\n[![CI](https://github.com/malkreide/parlament-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/malkreide/parlament-mcp/actions/workflows/ci.yml)\n[![PyPI version](https://badge.fury.io/py/parlament-mcp.svg)](https://badge.fury.io/py/parlament-mcp)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Swiss Public Data MCP Portfolio](https://img.shields.io/badge/Portfolio-Swiss%20Public%20Data%20MCP-blue)](https://github.com/malkreide)\n\n> **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)** –\n> connecting AI models to Swiss public data sources.\n\n> **Note:** This server covers the **federal** level (Curia Vista). This repo\n> additionally hosts a self-contained subproject under\n> [`openparldata-mcp/`](openparldata-mcp/README.md) — the **subnational**\n> counterpart for the 26 cantons and ~70 municipal parliaments\n> ([OpenParlData.ch](https://openparldata.ch)). The two are independent servers.\n\nAn MCP server that connects AI models to the **Swiss Federal Parliament** via the\n[Curia Vista OData API](https://ws.parlament.ch/odata.svc/) (`ws.parlament.ch`).\nAccess motions, interpellations, votes, members, sessions, and the **verbatim\ndebate transcripts** of the Amtliches Bulletin – with **no API key required**\n(Phase 1 – No-Auth-First).\n\n---\n\n## 🎯 Anchor Demo Queries\n\n**Metadata layer:**\n\n> *\"Welche Vorstösse zu KI in der Schule sind hängig?\"*\n> → `parlament_search_business(keyword=\"KI\", keyword2=\"Schule\", status=\"Eingereicht\")`\n\n**Verbatim transcripts (Amtliches Bulletin):**\n\n> *\"What did National Councillor Munz say in the 2024 spring session about the\n> Volksschule? Give me the exact wording with a correct AB citation.\"*\n> → `parlament_search_transcripts(speaker_name=\"Munz\", session_id=5202, keyword=\"Volksschule\")`\n> → then `parlament_get_transcript(transcript_id=…)` for the full wording.\n>\n> Returns short, **citable** excerpts (`AB 2024 N, 2024-03-13, Munz Martina`) with\n> a stable source URL; the verbatim text is fetched on demand, never in bulk.\n>\n> [→ More use cases by audience →](EXAMPLES.md)\n\nPerfect for the **KI-Fachgruppe Stadtverwaltung Zürich**: find pending motions\non AI in education, or quote what was actually said in the chamber – instantly.\n\n<p align=\"center\">\n  <img src=\"assets/demo.svg\" alt=\"Demo: Claude queries pending AI motions via MCP tool call\" width=\"720\">\n</p>\n\n---\n\n## 🔧 Tools\n\n| Tool | Description |\n|---|---|\n| `parlament_search_business` | Search Vorstösse by keyword, type, status, council, date |\n| `parlament_get_business` | Full details of a single business (texts, FC response) |\n| `parlament_search_members` | Find councillors by canton (e.g. ZH), party, council |\n| `parlament_get_votes` | Parliamentary votes with Ja/Nein meaning |\n| `parlament_get_sessions` | List recent sessions with IDs for follow-up queries |\n| `parlament_search_transcripts` | Search debate transcripts → **citable excerpts** with AB citation + source URL (speaker / session / business / date filters) |\n| `parlament_get_transcript` | Fetch the **verbatim full text** of a single speech by ID (capped, paginated, `is_excerpt` flagged) |\n\n---\n\n## 🏗️ Architecture\n\n```\n┌──────────────────────────────────┐\n│     MCP Host (Claude Desktop /    │\n│     Claude API / IDE)             │\n└─────────────┬─────────────────────┘\n              │ MCP Protocol (JSON-RPC 2.0)\n              │ Transport: stdio (local) / SSE (cloud)\n┌─────────────▼─────────────────────┐\n│          parlament-mcp            │\n│   FastMCP · Python · Pydantic v2  │\n│                                   │\n│  ┌── metadata layer (server.py) ──┐\n│  │  search_business · get_business │\n│  │  search_members · get_votes     │\n│  │  get_sessions                   │\n│  └─────────────────────────────────┘\n│  ┌── transcript layer ────────────┐  ← separate module (transcripts.py)\n│  │  search_transcripts (excerpts) │    · Language='DE' dedups editions\n│  │  get_transcript (verbatim)     │    · Type=1 = real speeches only\n│  └─────────────────────────────────┘    · retry + 45s read timeout\n└─────────────┬─────────────────────┘\n              │ HTTPS / OData v3\n┌─────────────▼─────────────────────┐\n│  ws.parlament.ch / odata.svc      │\n│  Curia Vista – No Auth Required   │\n│                                   │\n│  Business · Vote · MemberCouncil  │   metadata path\n│  Session ─< Meeting ─< Subject ─< Transcript   transcript path\n└───────────────────────────────────┘\n```\n\n---\n\n## 🚀 Installation\n\n### Claude Desktop (stdio)\n\nAdd to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"parlament\": {\n      \"command\": \"uvx\",\n      \"args\": [\"parlament-mcp\"]\n    }\n  }\n}\n```\n\n### Local development\n\n```bash\ngit clone https://github.com/malkreide/parlament-mcp\ncd parlament-mcp\npip install -e .\npython -m parlament_mcp.server\n```\n\n### Cloud / Railway (SSE)\n\n```bash\nMCP_TRANSPORT=sse MCP_HOST=0.0.0.0 PORT=8080 python -m parlament_mcp.server\n# SSE endpoint: http://your-host:8080/sse\n```\n\n### Network binding\n\nBy default the server binds to `127.0.0.1` (localhost only). Set\n`MCP_HOST=0.0.0.0` **only** inside a container/cloud context (Docker, Railway,\nRender, Kubernetes). Never bind to `0.0.0.0` on a local dev machine – it exposes\nthe server to your local network (NeighborJack risk); the server logs a warning\nif you do so outside a detected container.\n\nTransport is selected via `MCP_TRANSPORT` (`stdio` default, or `sse` /\n`streamable-http`); `--http` is kept as an alias for `streamable-http`.\n\n### Authentication (optional)\n\nThe HTTP transport is open by default (public read-only data). To require a\nbearer token, serve via the CORS/auth app factory and set `MCP_BEARER_TOKENS`:\n\n```bash\nMCP_BEARER_TOKENS=\"alice:tok_abc,bob:tok_def\" MCP_ALLOWED_ORIGINS=\"https://claude.ai\" \\\n  uvicorn parlament_mcp.server:create_http_app --factory --host 0.0.0.0 --port 8080\n```\n\nEach request then needs `Authorization: Bearer <token>`; identity comes from the\nvalidated token, not a session header (see [`docs/security.md`](docs/security.md)).\n\n### Docker\n\n```bash\ndocker compose up --build          # binds 127.0.0.1:8080 only\n# or build the hardened image directly (non-root, read-only FS):\ndocker build -t parlament-mcp .\n```\n\nKubernetes manifests (hardened `securityContext`, resource limits, egress\n`NetworkPolicy`, `Mcp-Session-Id` sticky routing) live in [`deploy/k8s/`](deploy/k8s/);\nan HAProxy stick-table example is in [`deploy/haproxy.cfg`](deploy/haproxy.cfg).\n\n---\n\n## 🔗 Synergies\n\n| Partner Server | Combination |\n|---|---|\n| [`openparldata-mcp`](openparldata-mcp/README.md) | **Federal ↔ subnational** — same question across cantons & municipalities |\n| [`fedlex-mcp`](https://github.com/malkreide/fedlex-mcp) | Law text ↔ parliamentary debate that created it |\n| [`zurich-opendata-mcp`](https://github.com/malkreide/zurich-opendata-mcp) | City policy ↔ cantonal/federal motions |\n| [`swiss-statistics-mcp`](https://github.com/malkreide/swiss-statistics-mcp) | Data backing ↔ motions citing statistics |\n\n**Power query example:**\n```\n\"Zeige mir alle Zürcher Motionen zu KI in der Bildung\n und verlinke die relevanten Bundesgesetze aus fedlex-mcp.\"\n```\n\n---\n\n## 📊 Data Source\n\n- **API:** [ws.parlament.ch/odata.svc](https://ws.parlament.ch/odata.svc/)\n- **Authentication:** None (Phase 1 – No-Auth-First)\n- **Protocol:** OData v3 / JSON\n- **Coverage:** All parliamentary businesses since 1978; votes since ~2000.\n  **Structured verbatim transcripts (Amtliches Bulletin) from 1999-12-06 onward**\n  (earlier years 1891–1999 exist only as archive scans — see Known Limitations).\n- **Update cycle:** Real-time (official government data)\n\n### Copyright — verbatim quotation is allowed\n\nOfficial proceedings of Swiss authorities are **excluded from copyright** under\n**Art. 5 para. 1 lit. a URG** (Swiss Copyright Act). The verbatim wording of\nparliamentary debates in the Amtliches Bulletin may therefore be reproduced and\nquoted freely — which is exactly what the transcript tools return: the wording\nitself, never a summary standing in for it.\n\n---\n\n## 📜 Data sources & licenses\n\n| Source | License | Attribution |\n|---|---|---|\n| Curia Vista (ws.parlament.ch) | CC BY 4.0 | © Schweizer Parlament, CC BY 4.0 |\n\nEvery tool returns a typed structured response (FastMCP exposes the output\nschema) carrying `source`, `license`, `provenance`, `match_type` and `count`\nalongside typed `results`. Data is passed through unmodified.\n\n## 🧭 Phase\n\nThis server is in **Phase 1 — Read-only Wrapper** (all tools `readOnlyHint: true`,\nno writes). The full phase model and transition criteria are in\n[`docs/roadmap.md`](docs/roadmap.md).\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## 🧱 MCP primitives\n\nPhase 1 uses **Tools only** — rationale and the Phase-2 Resources plan are in\n[`docs/adr/ADR-003-mcp-primitives.md`](docs/adr/ADR-003-mcp-primitives.md).\n\n## 🏷️ Tool annotations\n\nAll tools declare explicit annotations consistent with their behaviour:\n\n| Tool | readOnly | destructive | idempotent | openWorld |\n|---|:--:|:--:|:--:|:--:|\n| `parlament_search_business`  | ✅ | — | ✅ | ✅ |\n| `parlament_get_business`     | ✅ | — | ✅ | ✅ |\n| `parlament_search_members`   | ✅ | — | ✅ | ✅ |\n| `parlament_get_votes`        | ✅ | — | ✅ | ✅ |\n| `parlament_get_sessions`       | ✅ | — | ✅ | ✅ |\n| `parlament_search_transcripts` | ✅ | — | ✅ | ✅ |\n| `parlament_get_transcript`     | ✅ | — | ✅ | ✅ |\n\n## 📈 Observability\n\nStructured JSON logs go to **stderr** (stdout stays reserved for the stdio\nprotocol). OpenTelemetry tracing wraps each tool call and auto-instruments\noutgoing HTTP; set `OTEL_EXPORTER_OTLP_ENDPOINT` (with the `otel-export` extra)\nto ship spans. See [`docs/security.md`](docs/security.md) for the full security\nposture (Lethal-Trifecta assessment, egress allow-list, gateway hardening).\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** | Parliamentary businesses are public record by law (BGÖ). No private data is accessed or stored. |\n| **Rate limits** | Built-in per-query caps: max. 100 results (businesses/members), 50 (votes), 30 (transcript search), 10 (sessions). Transcript full text is capped per call and paginated — you never pull a whole session by accident. |\n| **Timeout** | 20 seconds per metadata call; 45 seconds for transcript reads (verbatim search is heavier) |\n| **Authentication** | No API keys required — Curia Vista is publicly accessible |\n| **Data source** | Official Swiss federal government data (Schweizerische Parlamentsdienste) |\n| **Terms of Service** | Subject to ToS of [ws.parlament.ch](https://ws.parlament.ch/) — Schweizerische Parlamentsdienste |\n\n---\n\n## Known Limitations\n\n**General**\n- OData `substringof()` filter is case-sensitive for some fields.\n- Session names may be `null` in the API for very recent sessions – use session ID.\n\n**Transcripts (Amtliches Bulletin)** — verified live 2026-07-19:\n- **Temporal coverage: from 1999-12-06 only.** The structured `Transcript` entity\n  reaches back to Dec 1999. Debates from **1891–1999 exist only as scanned\n  archive documents** (Bundesarchiv / Amtsdruckschriften) and are **not\n  connected** here (no OCR in scope). A query whose whole date window predates\n  coverage returns an explanatory error, not an empty result.\n- **No page number in the source.** The API carries no page/column field, so the\n  classic `AB <year> N <page>` form cannot be built. We emit a stable, verifiable\n  substitute — `AB <year> <N|S>, <date>, <speaker>` — plus the authoritative\n  `source_url` (`SubjectId`) and the `transcript_id`. This is a documented,\n  honest trade-off, not an omission.\n- **Language behaviour (important).** `Language` is the *edition*, not the spoken\n  language. The tools filter `Language eq 'DE'` purely to **deduplicate** the three\n  byte-identical editions (DE/FR/IT) down to one copy. Every speech is returned in\n  its **original wording**; a French or Italian speech is **not** hidden — its real\n  language is reported in the `language` field (`de`/`fr`/`it`). The response\n  states this via `language_note`.\n- **Truncation is explicit, never silent.** Search returns short excerpts\n  (`snippet`, ~320 chars) with `is_excerpt`, `total_length_chars` and a hint to\n  fetch the full text. `parlament_get_transcript` caps output at `max_chars`; when\n  it truncates it sets `is_excerpt=True` and returns a `next_offset` to continue.\n- **Verbatim only, never summarised.** The wording *is* the product; the tools\n  never substitute a summary for the actual text.\n- **Latency:** a free-text `keyword` combined with a `speaker_name` is the slowest\n  path (~40 s). Add a `session_id`, `business_number` or date window to keep reads\n  around 1–2 s.\n\n---\n\n## 🧪 Testing\n\n```bash\npip install -e \".[dev]\"\n\n# Unit + mocked integration tests (no network), as run in CI:\nPYTHONPATH=src pytest tests/ -m \"not live\"\n\n# Include live tests against the real ws.parlament.ch API:\nPYTHONPATH=src pytest tests/ -m live\n```\n\nHTTP is mocked with `respx`; network-dependent tests are marked\n`@pytest.mark.live` and excluded from CI via `-m \"not live\"`. Tool definitions are\npinned in `tool-hashes.json` (`python -m parlament_mcp.tool_hashes --check`).\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 policy and posture (vulnerability\nreporting, Lethal-Trifecta assessment, accepted risks).\n\n---\n\n## License\n\nMIT © [Hayal Oezkan](https://github.com/malkreide) — see [LICENSE](LICENSE)\n\n---\n\n## Author\n\n**Hayal Oezkan** · [github.com/malkreide](https://github.com/malkreide)\n\n<!-- mcp-name: io.github.malkreide/parlament-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    \"parlament-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"parlament-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 15681,
  "sha": "4e3b9b4c7b3299e5d825c4242e60b640e8c311f4a5e97fc53559d50957f10f0b",
  "repo_slug": "malkreide/parlament-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_parlament_mcp_f4902ba6/readme"
}