
Connects Claude to Switzerland's public broadcaster APIs covering five domains: SRF Meteo weather (location search, current conditions, 24h/7d forecasts), video and audio metadata (TV/radio shows, episodes, livestreams across SRF/RTS/RSI/RTR), electronic program guides, and Polis historical democracy data (Swiss votations and elections since 1900 with cantonal breakdowns). OAuth2 token management is built in. Exposes 15 tools for parametrized queries, stable resources for EPG schedules and closed votation results, and two workflow prompts for voting analysis and daily briefings. Supports stdio for Claude Desktop and SSE for browser deployment. Requires free API credentials from developer.srgssr.ch. Particularly relevant for questions about Swiss media, politics, or localized weather that need grounding in real institutional data rather than LLM priors.
🇨🇭 Part of the Swiss Public Data MCP Portfolio
MCP server connecting AI models to SRG SSR public APIs – weather, TV/radio metadata, program guide and Swiss votations/elections since 1900 (SRF, RTS, RSI, RTR, SWI).
srgssr-mcp gives AI assistants like Claude direct access to the public APIs of SRG SSR – Switzerland's national public broadcaster. Weather forecasts, TV and radio metadata, electronic program guides, and historical democratic data (votations and elections since 1900) are all accessible through a single standardised MCP interface.
The server covers five thematic clusters: SRF Weather, Video, Audio, EPG and Polis (Swiss Democracy). Each cluster maps to a group of purpose-built tools that translate raw SRG SSR API data into clean JSON responses.
Anchor demo query: "What were the cantonal results of the popular vote on initiative X in Zurich?" – answered with historical real-time data from the Polis system, not a hallucination.
⚠️ Terms of use: SRG SSR APIs are available for non-commercial use. For commercial use, contact api@srgssr.ch directly.
# Clone the repository
git clone https://github.com/malkreide/srgssr-mcp.git
cd srgssr-mcp
# Install
pip install -e .
Or with uvx (no permanent installation):
uvx srgssr-mcp
Or via pip:
pip install srgssr-mcp
# Set credentials
export SRGSSR_CONSUMER_KEY="your-consumer-key"
export SRGSSR_CONSUMER_SECRET="your-consumer-secret"
# Start the server (stdio mode for Claude Desktop)
srgssr-mcp
Try it immediately in Claude Desktop:
"What will the weather be like in Zurich tomorrow?" "What's on SRF 1 tonight?" "Which popular votes took place in the canton of Bern between 2010 and 2020?"
Minimal (recommended):
{
"mcpServers": {
"srgssr": {
"command": "uvx",
"args": ["srgssr-mcp"],
"env": {
"SRGSSR_CONSUMER_KEY": "your-consumer-key",
"SRGSSR_CONSUMER_SECRET": "your-consumer-secret"
}
}
}
}
Config file locations:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonAfter saving, restart Claude Desktop completely.
Compatible with Cursor, Windsurf, VS Code + Continue, LibreChat, Cline, and self-hosted models via mcp-proxy. Set the same environment variables.
For use via claude.ai in the browser (e.g. on managed workstations without local software):
SRGSSR_CONSUMER_KEY=... \
SRGSSR_CONSUMER_SECRET=... \
SRGSSR_MCP_TRANSPORT=streamable-http \
SRGSSR_MCP_HOST=0.0.0.0 \
SRGSSR_MCP_PORT=8000 \
python -m srgssr_mcp.server
Transport, host, port and mount path are all driven by environment variables
(see srgssr_mcp.server.Settings). Valid values for SRGSSR_MCP_TRANSPORT
are stdio (default), sse, and streamable-http.
💡 "stdio for the developer laptop, SSE for the browser."
This server exposes all three orthogonal MCP primitives:
| Primitive | Mental model | What's here |
|---|---|---|
| Tools (verbs) | Executable functions / parametrized queries | 15 tools — search, list, fetch, aggregate |
| Resources (nouns) | Cache-friendly passive data behind URIs | EPG entries and immutable votation results |
| Prompts (recipes) | Reusable workflow templates | Voting analysis & daily briefing |
Tools cover parametrized searches (year ranges, free-text, paginated listings) where every call may yield different results. Resources expose stable data points that are safe to cache: a published EPG for a given channel/date, or the final result of a closed Swiss votation. Prompts standardise recurring multi-step analyses so users don't have to phrase them from scratch.
| URI template | Description |
|---|---|
epg://{bu}/{channel_id}/{date} | Daily TV/radio program guide for SRF, RTS, RSI (e.g. epg://srf/srf-1/2026-04-30) |
votation://{votation_id} | Detailed result of a closed Swiss popular vote (e.g. votation://v1) |
EPG station ids — hyphenated, and not the same as the livestream ids:
| Business unit | TV | Radio |
|---|---|---|
| SRF | srf-1, srf-2, srf-info | srf-1, srf-2, srf-2-kultur, srf-3, srf-4, srf-musikwelle, srf-virus |
| RTS | rts-1, rts-2, rts-info | LA1ERE, ESPACE2, COULEUR3, OPTION_MUSIQUE |
| RSI | la-1, la-2 | rete-uno, rete-due, rete-tre |
| Name | Arguments | Purpose |
|---|---|---|
analyse_abstimmungsverhalten | votation_id, focus (stadt_land / sprachregionen / kantone) | Structured analysis of a Swiss popular vote |
tagesbriefing_kanton | location, channel_id, business_unit, date | Daily briefing combining weather and EPG |
This server uses snake_case for tool names, following Python ecosystem idioms. While MCP best practice favors camelCase for optimal LLM tokenization, snake_case remains acceptable and keeps tool names aligned with the underlying Python function identifiers.
All tools follow the pattern srgssr_<domain>_<action> with the namespace prefix srgssr_ and a semantically meaningful <domain>_<action> suffix (e.g. srgssr_weather_current, srgssr_polis_get_votations).
| Tool | Description | Data Source |
|---|---|---|
srgssr_weather_search_location | Search for a location by name or postal code to obtain a geolocationId | SRF Meteo |
srgssr_weather_current | Current weather conditions for a Swiss location | SRF Meteo |
srgssr_weather_forecast_24h | Hourly 24-hour forecast | SRF Meteo |
srgssr_weather_forecast_7day | Daily 7-day forecast | SRF Meteo |
| Tool | Description | Data Source |
|---|---|---|
srgssr_video_get_shows | List TV shows for a business unit (character_filter a–z/# selects one initial; omit it to fan out over all 27) | SRG SSR IL |
srgssr_video_get_episodes | Retrieve latest episodes of a show | SRG SSR IL |
srgssr_video_get_livestreams | List live TV channels | SRG SSR IL |
| Tool | Description | Data Source |
|---|---|---|
srgssr_audio_get_shows | List radio shows for one radio channel (channel_id required — the v2 API has no per-unit listing) | SRG SSR IL |
srgssr_audio_get_episodes | Retrieve audio episodes of a show | SRG SSR IL |
srgssr_audio_get_livestreams | List live radio stations | SRG SSR IL |
| Tool | Description | Data Source |
|---|---|---|
srgssr_epg_get_programs | Daily program schedule for a TV or radio channel | SRG SSR IL |
| Tool | Description | Data Source |
|---|---|---|
srgssr_polis_get_votations | Popular votes since 1900 (national or cantonal) | Polis API |
srgssr_polis_get_votation_results | Detailed results of a specific vote | Polis API |
srgssr_polis_get_elections | Election results since 1900 | Polis API |
| Code | Unit | Language |
|---|---|---|
srf | SRF (Schweizer Radio und Fernsehen) | German |
rts | RTS (Radio Télévision Suisse) | French |
rsi | RSI (Radiotelevisione svizzera) | Italian |
rtr | RTR (Radiotelevisiun Svizra Rumantscha) | Romansh |
swi | SWI swissinfo.ch | Multilingual |
| Query | Tool |
|---|---|
| "Weather in Zurich tomorrow?" | srgssr_weather_forecast_24h |
| "What's on SRF 1 tonight?" | srgssr_epg_get_programs |
| "Latest Tagesschau episodes?" | srgssr_video_get_episodes |
| "Popular votes in Canton Bern 2010–2020?" | srgssr_polis_get_votations |
| "Cantonal results of the mask initiative vote?" | srgssr_polis_get_votation_results |
| "All current RTS radio shows?" | srgssr_audio_get_livestreams → srgssr_audio_get_shows |
→ More use cases by audience →
┌─────────────┐
│ Claude / LLM│
└──────┬──────┘
│ MCP (stdio)
┌──────▼───────────────────┐
│ srgssr-mcp Server │
│ ├─ Weather Tools (4) │
│ ├─ EPG Tools (1) │
│ ├─ Polis Tools (3) │
│ ├─ Video Tools (3) │
│ └─ Audio Tools (3) │
└──────┬───────────────────┘
│ HTTPS (OAuth2)
┌──────▼──────────────┐
│ SRG SSR Public APIs │
│ developer.srgssr.ch│
└─────────────────────┘
| Source | Data | Access |
|---|---|---|
| developer.srgssr.ch | SRG SSR PUBLIC API V2 (weather, A/V, EPG, Polis) | OAuth2 (free registration) |
Attribution: SRG SSR APIs are subject to the SRG SSR Terms of Use.
This server is in Phase 1: Read-only Wrapper.
The server exposes only GET-style operations against public SRG SSR APIs. There are no write, mutate or delete capabilities by design — see Safety & Limits for the threat-model implications.
swiss-statistics-mcp or swiss-transport-mcp).mcp 2.x serves two protocol eras over the same server, and the client's
first request on a connection decides which one applies:
| 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. |
PROTOCOL_VERSION in src/srgssr_mcp/_app.py names
the modern era. It is validated at import time against the installed SDK's
SUPPORTED_PROTOCOL_VERSIONS — but that list is backwards-compatible and still
contains 2024-11-05, so the membership check catches a revision being dropped,
never a drift. tests/test_protocol_version.py
holds both eras against the SDK and is the check that catches drift. Bumps are
tracked in CHANGELOG.md under the matching release.
The table above used to be an assertion held against SDK constants.
tests/test_spec_2026_07_28.py now sends real
requests through MCPServer.streamable_http_app() over httpx.ASGITransport
and asserts the answers, in both eras. What that measurement pins:
| Property | On the wire |
|---|---|
| Modern entry | server/discover, no initialize, no Mcp-Session-Id; supportedVersions: ["2026-07-28"] |
| Handshake ceiling | A client asking for 2026-07-28 via initialize gets 2025-11-25 back — measured, not inferred |
| Server identity | io.modelcontextprotocol/serverInfo rides the _meta of every modern result and carries the installed package version |
| Freshness (SEP-2549) | ttlMs: 300000, cacheScope: public on server/discover and the four listing methods |
| Tool display names | title per BaseMetadata, mirrored into annotations.title for handshake-era clients, which read only there |
Withdrawn at 2026-07-28 | ping, logging/setLevel, resources/subscribe, resources/unsubscribe and tasks/* answer -32601 |
| Logging capability | Not declared, and not used: SEP-2577 deprecates it as of 2026-07-28, and delivery there is a per-request opt-in the server must not presume. Tool activity goes to structlog on stderr — see src/srgssr_mcp/tools/__init__.py |
The measurement paid for itself on its first run, against 425 green tests and
96% coverage: serverInfo.version was the empty string, every tools/call
came back isError (a FastMCP 1.x ctx.info(msg, **kwargs) call shape that
mcp 2.x rejects), and eight tools refused the enum string their own published
inputSchema advertises (ConfigDict(strict=True) demands an enum instance,
which no JSON payload can carry). None of that is visible from inside the
process — which is the whole argument for testing the wire.
.github/dependabot.yml, monthly cadence, grouped under the mcp-sdk label) and run the full test suite before merge.CHANGELOG.md and, if it changes the externally observable wire contract, triggers a minor or major release per Semantic Versioning.srgssr-mcp/
├── src/srgssr_mcp/
│ ├── __init__.py # Package
│ └── server.py # MCPServer: 15 tools, OAuth2 client
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions CI (Python 3.11–3.13)
├── pyproject.toml # Build configuration (hatchling)
├── CHANGELOG.md
├── CONTRIBUTING.md # English
├── CONTRIBUTING.de.md # German
├── SECURITY.md # Security policy (English)
├── SECURITY.de.md # Security policy (German)
├── LICENSE # MIT
├── README.md # This file (English)
└── README.de.md # German version
| Aspect | Details |
|---|---|
| Access | Read-only — the server only reads from SRG SSR APIs and cannot post, modify or delete any content |
| Personal data | No personal data — all endpoints serve public broadcast metadata, weather observations and historical votation/election results |
| Rate limits | Subject to the tier of your OAuth2 application on developer.srgssr.ch; the server adds sensible per-query caps (e.g. max 100 episodes, 50 shows per list call) |
| Timeout | 30 seconds per upstream API call |
| Authentication | OAuth2 Client Credentials (free registration); secrets stay local, never logged |
| Licensing & use | SRG SSR APIs are for non-commercial use; commercial use requires written permission from api@srgssr.ch |
| Terms of Service | Subject to the SRG SSR Developer Terms of Use — users remain responsible for attribution and compliance |
See CONTRIBUTING.md (English) · CONTRIBUTING.de.md (German)
For the full security posture, vulnerability reporting process and accepted-risk register, see SECURITY.md (English) · SECURITY.de.md (German). The key egress control is summarised below.
The server implements a code-layer egress allowlist (SEC-021, combined with SEC-004 SSRF defense) to prevent unintended external requests. Every outbound HTTP request is validated by _validate_url_safe() in src/srgssr_mcp/_http.py before it is issued.
Three controls per request:
http://, file://, ftp:// and other non-HTTPS schemes are rejected.ALLOWED_HOSTS = {"api.srgssr.ch"} (exact match — subdomain tricks like api.srgssr.ch.attacker.example are blocked). One host covers everything: the OAuth2 token endpoint and every data endpoint.169.254.169.254 cloud-metadata), CGNAT, multicast and reserved ranges (IPv4 + IPv6). Any single match aborts the request — defense-in-depth against DNS rebinding.Violations surface as ValueError and are mapped to a localized Konfigurationsfehler: … message by _handle_error, so internal network details never leak to the MCP client.
Adding a new SRG SSR domain:
ALLOWED_HOSTS in src/srgssr_mcp/_http.py.CHANGELOG.md.tests/test_unit.py (mirror test_validate_url_safe_accepts_public_srgssr_host).Network-Layer Egress (for future SSE/HTTP deployments): see docs/network-egress.md. For the current stdio transport, network-layer controls do not apply — the process runs in the MCP client's user context.
The server uses structured logging (OBS-003) via structlog with JSON output to stderr — keeping stdout clean for the stdio transport's JSON-RPC traffic.
Format:
timestamp on every recorddebug, info, notice, warning, error, critical, alert, emergencytool, business_unit, channel_id, query, etc.Example output:
{"event": "tool_invoked", "tool": "srgssr_weather_search_location", "query": "Bern", "level": "info", "logger": "mcp.srgssr.weather", "timestamp": "2026-04-30T14:23:45.123Z"}
{"event": "tool_succeeded", "tool": "srgssr_weather_search_location", "query": "Bern", "result_count": 3, "matched_variant": "Bern", "level": "info", "logger": "mcp.srgssr.weather", "timestamp": "2026-04-30T14:23:45.456Z"}
Log levels (RFC 5424):
| Level | Used for |
|---|---|
debug | OAuth token cache hits, internal state |
info | Tool invocations, successful responses, server lifecycle |
warning | Recoverable conditions (rate-limit approaching, unsupported business unit) |
error | API failures, timeouts (recoverable) |
critical | Credential issues, service degradation |
Configuration:
The default level is info. Override via the SRGSSR_LOG_LEVEL environment variable (debug, info, warning, error, critical):
SRGSSR_LOG_LEVEL=debug srgssr-mcp
JSON output is aggregator-friendly — pipe stderr to Datadog, Splunk, Loki, etc., and filter by structured fields (tool, business_unit, level) without regex parsing.
# Unit tests (no network required)
PYTHONPATH=src pytest tests/ -m "not live"
# Integration tests (requires SRG SSR API keys)
PYTHONPATH=src pytest tests/ -m "live"
# Linting
ruff check src/
See CHANGELOG.md
All data exposed by this server is fetched live from a single upstream
provider, SRG SSR Public API V2 (https://api.srgssr.ch). Every tool
return is a typed Pydantic BaseModel that
embeds source / license / provenance_url / fetched_at at the top
level — so downstream consumers can record the data origin without
round-tripping through this README. The SDK exposes the corresponding
outputSchema in the tools/list manifest so MCP clients can plan
follow-up calls precisely.
| Cluster | Provider | License | Notes |
|---|---|---|---|
| Weather | SRF Meteo (api.srgssr.ch) | SRG SSR Terms of Use | Geo-restricted to Switzerland |
| Video / Audio / EPG | SRF · RTS · RSI · RTR · SWI | SRG SSR Terms of Use | Metadata only — stream URLs are not redistributed |
| Polis (Votations / Elections) | SRG SSR Polis | SRG SSR Terms of Use | Historical data since 1900 |
Use of the SRG SSR APIs
This server's MIT license covers the source code only; it does not relicense the upstream data.
MIT License — see LICENSE
The SRG SSR APIs used in this project are subject to the SRG SSR Terms of Use.
Hayal Oezkan · github.com/malkreide
| Server | Description |
|---|---|
| zurich-opendata-mcp | City of Zurich open data (OSTLUFT air quality, weather, parking, geodata) |
| swiss-transport-mcp | Swiss public transport – OJP 2.0 journey planning, SIRI-SX disruptions |
| swiss-environment-mcp | BAFU environmental data – air quality, hydrology, natural hazards |
| swiss-statistics-mcp | BFS STAT-TAB – 682 statistical datasets |
| fedlex-mcp | Swiss federal law via Fedlex SPARQL |
Synergy example: "What were the results of the 2020 popular votes in Canton Zurich – and how did turnout compare to the national average?"
→ srgssr-mcp (Polis, cantonal results) + swiss-statistics-mcp (BFS, turnout data)
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):
{
"mcpServers": {
"srgssr-mcp": {
"command": "uvx",
"args": [
"srgssr-mcp"
]
}
}
}