The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Swiss Democracy MCP listing page.
🇨🇭 Part of the Swiss Public Data MCP Portfolio — connecting AI models to Swiss institutional data sources.
🌐 English | 🇩🇪 Deutsche Version
An MCP server providing access to Swiss direct democracy data, covering all federal popular votes since 1848 and elections since 1900.
→ democracy_search_votes(keyword="AHV 21", year_from=2022)
→ democracy_get_cantonal_results(vote_number="551")
→ democracy_get_party_positions(vote_number="551")
→ More use cases by audience →
| Source | Coverage | Auth |
|---|---|---|
| Swissvotes (Uni Bern) | All federal votes since 1848 · 874 columns · party positions · cantonal results | None ✓ |
| BFS / opendata.swiss | Real-time & archive (since 1981) · municipality level | None ✓ |
| SRGSSR Polis | Votes & elections since 1900 · municipality detail | OAuth2 key |
| Tool | Description |
|---|---|
democracy_search_votes | Search all federal popular votes since 1848 by keyword, date range, legal form, outcome, policy domain |
democracy_get_vote_detail | Full details for a specific vote: official title, parliamentary positions, national result, signatures |
democracy_get_party_positions | Party recommendations (FDP, SP, SVP, Die Mitte, GPS, GLP, …) with campaign finance data |
democracy_get_cantonal_results | Results for all 26 cantons: yes%, turnout, accepted flag |
democracy_list_vote_dates | List all voting dates with number of proposals per date |
| Tool | Description |
|---|---|
democracy_bfs_list_vote_dates | List all BFS voting dates (archive + current) |
democracy_bfs_get_vote_results | Real-time or archived results at national, cantonal, or municipality level |
| Tool | Description |
|---|---|
democracy_polis_list_votations | Historical votations since 1900 with municipality-level data |
democracy_polis_get_votation_detail | Full Polis detail, optionally with all municipality results |
democracy_polis_list_elections | National Council, Council of States, and cantonal elections since 1900 |
Add to claude_desktop_config.json:
The
SRGSSR_*variables are optional. Without them, all Swissvotes and BFS tools remain fully functional. Only the Polis tools require credentials.
MCP_HOST defaults to 127.0.0.1 (loopback). Set MCP_HOST=0.0.0.0 only
inside a sandboxed container/cloud deployment — never on a local machine, where
it would expose the server to your whole network.
Transport: stdio for Claude Desktop · Streamable HTTP for cloud/Render.com
Auth pattern: No-Auth-First — Swissvotes & BFS work without any credentials
Cache: Swissvotes CSV is loaded once at startup and cached for 24 hours
All configuration is via environment variables (see .env.example):
| Variable | Default | Purpose |
|---|---|---|
MCP_TRANSPORT | stdio | stdio (local) or streamable_http (cloud) |
MCP_HOST | 127.0.0.1 | HTTP bind address — set 0.0.0.0 only in a container |
MCP_PORT | 8000 | HTTP port |
LOG_LEVEL | INFO | structured JSON logs go to stderr |
MCP_CORS_ORIGINS | — | comma-separated CORS allow-origins for the HTTP transport. Empty means no browser client is permitted; * allows any origin and is logged as a warning. stdio clients are unaffected |
MCP_ALLOWED_HOSTS | — | comma-separated inbound Host allow-list. Only needed for a non-loopback bind |
SRGSSR_CONSUMER_KEY / SRGSSR_CONSUMER_SECRET | — | optional, only for democracy_polis_* tools |
Secrets are held as SecretStr and never logged. See
docs/secret-management.md and
docs/security.md.
This server intentionally exposes Tools only (no Resources or Prompts). It is
a Phase-1 read-only data wrapper (see docs/roadmap.md); the
tool surface is small (10 use-case-oriented tools) and every response is
self-contained with source provenance. Resources (e.g. vote://{anr}) are a
candidate for a later phase once the URI scheme stabilises.
This server speaks two protocol eras over the same endpoint. The client's first request on a connection decides which one applies; a later claim from the other era is refused.
| Era | Revision | Who reaches it |
|---|---|---|
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. |
| Per-request envelope | 2026-07-28 | A request carrying the 2026-07-28 _meta envelope opens a modern connection. |
Both revisions are pinned in
tests/test_protocol_version.py and asserted
against the installed SDK, so a Dependabot bump of mcp cannot move either one
silently. This server builds no ASGI app to send an initialize through, so
the gate asserts the SDK constants rather than a measured response — the
weaker form, named rather than left unsaid.
Note that the SDK's LATEST_PROTOCOL_VERSION is an alias for the modern
era, not for the handshake era — pinning against it alone would leave the era
that current clients actually negotiate free to drift.
Update policy. When the gate fails, do not edit the constant blindly: read
the spec changelog between the two revisions, verify the server still behaves,
then move the constant, this section, README.de.md and
CHANGELOG.md together.
Combine with other servers in the Swiss Public Data MCP portfolio:
Example multi-server query:
«Vergleiche die Abstimmungsresultate zur AHV-Reform mit der Altersstruktur der Kantone»
→ swiss-democracy-mcp + swiss-statistics-mcp
swissvotes.ch, opendata.swiss, *.bfs.admin.ch, api.srgssr.ch), HTTPS only. Caller-supplied URLs (the BFS result_url) are additionally resolved and rejected if they point at private, loopback or cloud-metadata IP ranges.limit and date ranges conservative. The server enforces a 30s timeout per request.The fixtures under tests/fixtures/ are recorded from the live sources and
dated. Source, retrieval date, selection rule and SHA-256 for every file:
tests/fixtures/PROVENANCE.md.
The selection rule is the point here. The Swissvotes dataset has 714 rows
and 874 columns, and the recorded rows are chosen by property, not by
position. "The first N rows" would cut away exactly the cells the fixture
exists for: the placeholders 9999 ("no information") and . ("not
applicable"), which appear in 667 of the 714 votes. A fixture without them
would look clean and prove nothing — which is precisely why nobody noticed the
tools were passing them through as if they were values.
The byte-order mark stays in the file too, because the source sets one and the server strips it explicitly. Without it the fixture could not show that it has to.
SRGSSR Polis needs OAuth credentials and is listed under NOT RECORDED in
PROVENANCE.md rather than given a date it never had.
See CONTRIBUTING.md (🇩🇪 Deutsch).
See SECURITY.md (🇩🇪 Deutsch) for the security posture and vulnerability reporting.
MIT — see LICENSE.
Data licenses:
Hayal Oezkan · github.com/malkreide
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):