{
  "markdown": "# 🚆 sbb-opendata-mcp\n\n🇨🇭 **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide/swiss-public-data-mcp)**\n\n[![PyPI](https://img.shields.io/pypi/v/sbb-opendata-mcp)](https://pypi.org/project/sbb-opendata-mcp/)\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[![Data Source](https://img.shields.io/badge/Data-data.sbb.ch-red)](https://data.sbb.ch/)\n![CI](https://github.com/malkreide/sbb-opendata-mcp/actions/workflows/ci.yml/badge.svg)\n\n> MCP server connecting AI models to Swiss Federal Railways (SBB) open data – passenger frequency, live rail disruptions, infrastructure & real-estate projects, train counts, platform data, rolling stock and station search from [data.sbb.ch](https://data.sbb.ch/). **No API key required.**\n\n[🇩🇪 Deutsche Version](README.de.md)\n\n### Demo\n\n![Demo: Claude queries SBB passenger frequency](docs/assets/demo.svg)\n\n---\n\n## Overview\n\n**sbb-opendata-mcp** gives AI assistants like Claude direct access to public SBB\ndata – no copy-pasting or manual API calls. A question like *\"How many passengers\npassed through Zürich HB every day in 2024?\"* is answered with real measured data.\n\nThe SBB Open Data portal speaks the OpenDataSoft REST API (v2.1). This server\ntranslates it into clean Markdown and JSON for the AI model, and adds MCP\n`structuredContent` alongside the human-readable text so programmatic clients can\nconsume the underlying records without re-parsing. The server is model-agnostic\nand works with any MCP-compatible client.\n\n**Anchor demo query:** *\"Compare Zürich HB, Bern and Basel SBB by passenger frequency and platform capacity.\"*\n→ [More use cases by audience](EXAMPLES.md) →\n\n---\n\n## Features\n\n- 📊 **Passenger frequency** – boardings/alightings by station and year (daily averages)\n- 🚨 **Live rail disruptions** – traffic messages, updated every 5 minutes\n- 🏗️ **Infrastructure projects** – station and line construction\n- 🏢 **Real-estate projects** – SBB property development (daily updates)\n- 🚆 **Trains per segment** – train counts per route (SBB, BLS, SOB …)\n- 🛤️ **Platform data** – length, type, area, step-free access\n- 🚃 **Rolling stock** – capacity and year built\n- 🔁 **Station comparison** – up to 10 stations across multiple datasets\n- 🔍 **Stop search** – Swiss DiDok register (all of Switzerland)\n- 📦 **Dataset catalogue** – list all ~89 SBB open datasets\n- 🔑 **No API key** – all data is public and free to use\n- ☁️ **Dual transport** – stdio for Claude Desktop, Streamable HTTP for cloud deployment\n\n---\n\n## Prerequisites\n\n- Python 3.11+\n- No API key — all data comes from the public [data.sbb.ch](https://data.sbb.ch) portal\n\nInstall [uv](https://github.com/astral-sh/uv) (recommended):\n\n```bash\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n```\n\n---\n\n## Installation\n\nFrom [PyPI](https://pypi.org/project/sbb-opendata-mcp/):\n\n```bash\npip install sbb-opendata-mcp\n```\n\nOr with `uvx` (no permanent installation):\n\n```bash\nuvx sbb-opendata-mcp\n```\n\nFor local development, install from a clone in editable mode:\n\n```bash\ngit clone https://github.com/malkreide/sbb-opendata-mcp.git\ncd sbb-opendata-mcp\npip install -e \".[dev]\"\n```\n\n---\n\n## Quickstart\n\n```bash\n# Start the server (stdio mode for Claude Desktop)\nsbb-opendata-mcp\n```\n\nTry it immediately in Claude Desktop:\n\n> *\"How many people boarded at Zürich HB daily in 2024?\"*\n> *\"Are there any current disruptions on the Swiss rail network?\"*\n\n---\n\n## Configuration\n\n### Environment Variables\n\nThe server needs no configuration to run over stdio. The variables below tune the\noptional Streamable HTTP transport, logging and observability.\n\n| Variable | Effect | Default |\n|---|---|---|\n| `MCP_HOST` | Bind host for the HTTP transport. Keep `127.0.0.1` locally; only bind `0.0.0.0` inside a controlled container/cloud environment. | `127.0.0.1` |\n| `MCP_PORT` | Port for the HTTP transport. | `8000` |\n| `MCP_ALLOWED_HOSTS` | Comma-separated host allow-list for DNS-rebinding protection (e.g. `your-app.onrender.com,your-app.onrender.com:*`). | localhost only |\n| `MCP_ALLOWED_ORIGINS` | Comma-separated browser-origin allow-list (e.g. `https://your-app.onrender.com`). | _(none)_ |\n| `LOG_LEVEL` | Log verbosity (`DEBUG`/`INFO`/`WARNING`/…). | `INFO` |\n| `LOG_FORMAT` | `json` for structured logs; anything else for human-readable text. Always written to stderr. | `text` |\n\n> 🔒 DNS-rebinding / Origin protection is **always on**; localhost is allow-listed\n> so local HTTP development works out of the box. Logs go to **stderr** — stdout is\n> reserved for the stdio JSON-RPC channel.\n\n### Claude Desktop Configuration\n\n```json\n{\n  \"mcpServers\": {\n    \"sbb-opendata\": {\n      \"command\": \"uvx\",\n      \"args\": [\"sbb-opendata-mcp\"]\n    }\n  }\n}\n```\n\n**Config file locations:**\n- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`\n- Windows: `%APPDATA%\\Claude\\claude_desktop_config.json`\n\nRestart Claude Desktop — the server is downloaded automatically on first use.\n\n### Other MCP Clients\n\nWorks with Cursor, Windsurf, VS Code + Continue, LibreChat, Cline and self-hosted\nmodels via `mcp-proxy` — same configuration as above.\n\n### Cloud Deployment (Streamable HTTP)\n\nFor use via **claude.ai in the browser** or remote servers (e.g. [Render.com](https://render.com)). The cloud transport is **Streamable HTTP** (endpoint `/mcp`).\n\n**Docker (recommended):**\n\n```bash\n# Build + run with explicit resource limits (see docker-compose.yml)\ndocker compose up --build\n# → http://127.0.0.1:8000/mcp\n```\n\nThe image is a multi-stage build running as a **non-root** user; `docker-compose.yml`\nadds `read_only`, `no-new-privileges` and memory/CPU/PID limits.\n\n**Manual / Render.com:**\n\n```bash\npip install -e .\n\n# Bind publicly (behind a rate-limiting reverse proxy) and configure\n# DNS-rebinding / Origin protection for your hostname:\nexport MCP_HOST=0.0.0.0\nexport MCP_ALLOWED_HOSTS=\"your-app.onrender.com,your-app.onrender.com:*\"\nexport MCP_ALLOWED_ORIGINS=\"https://your-app.onrender.com\"\npython -m sbb_opendata_mcp.server --http --port 8000\n```\n\n> ⚠️ **Binding:** In a network transport the server binds to `127.0.0.1` by\n> default so a locally started server is **not** exposed to your whole network.\n> Set `MCP_HOST=0.0.0.0` **only** in a container/cloud environment where binding\n> to all interfaces is intended (the Docker image does this for you), and place\n> the server behind a reverse proxy that enforces rate limiting (and\n> authentication, if the endpoint should not be public). See [`SECURITY.md`](SECURITY.md).\n\n---\n\n## Available Tools\n\n| Tool | Description | Data Update |\n|------|-------------|-------------|\n| `sbb_get_passenger_frequency` | Boardings/alightings by station and year (daily avg.) | Annual |\n| `sbb_get_rail_disruptions` | Live rail traffic messages | Every 5 min. |\n| `sbb_get_real_estate_projects` | SBB real estate development projects | Daily |\n| `sbb_get_trains_per_segment` | Train counts per route segment (SBB, BLS, SOB …) | Annual |\n| `sbb_get_platform_data` | Platform data (length, type, area) | Ongoing |\n| `sbb_get_rolling_stock` | Rolling stock (capacity, year built) | Ongoing |\n| `sbb_compare_stations` | Compare up to 10 stations (multi-dataset) | – |\n| `sbb_search_stations` | Search stops (Swiss DiDok register, all CH) | Ongoing |\n| `sbb_list_datasets` | List all ~89 SBB open datasets | – |\n\nAll tools support `response_format: \"markdown\"` (human-readable) and `\"json\"`\n(machine-readable), plus pagination. Every tool also returns MCP `structuredContent`\n(the underlying records/metadata) alongside the rendered text.\n\n### Example Use Cases\n\n| Query | Tool |\n|---|---|\n| *\"How many people boarded at Zürich HB daily in 2024?\"* | `sbb_get_passenger_frequency` |\n| *\"Are there any current disruptions on the Swiss rail network?\"* | `sbb_get_rail_disruptions` |\n| *\"Compare Zürich HB, Bern and Basel SBB\"* | `sbb_compare_stations` |\n| *\"Which SBB real-estate construction projects are running?\"* | `sbb_get_real_estate_projects` |\n| *\"How many trains run yearly on the Zürich–Winterthur route?\"* | `sbb_get_trains_per_segment` |\n| *\"Which stops exist in Wädenswil?\"* | `sbb_search_stations` |\n\n→ [More use cases by audience](EXAMPLES.md)\n\n---\n\n## Architecture\n\n```\n┌─────────────────┐     ┌───────────────────────────┐     ┌──────────────────────────┐\n│   Claude / AI   │────▶│   SBB Open Data MCP       │────▶│       data.sbb.ch        │\n│   (MCP Host)    │◀────│   (MCP Server)            │◀────│                          │\n└─────────────────┘     │                           │     │  OpenDataSoft REST v2.1  │\n                        │  9 Tools                  │     │  (public, no API key)    │\n                        │  Stdio | Streamable HTTP  │     │                          │\n                        │                           │     │  passagierfrequenz       │\n                        │  Shared httpx client      │     │  rail-traffic-information │\n                        │  (pooled, lifespan-managed)│    │  construction-projects   │\n                        │  ODSQL escaping + Pydantic │     │  perron · rollmaterial   │\n                        │  validation               │     │  zugzahlen · dienststellen│\n                        └───────────────────────────┘     └──────────────────────────┘\n```\n\n---\n\n## Project Structure\n\n```\nsbb-opendata-mcp/\n├── src/sbb_opendata_mcp/\n│   ├── __init__.py\n│   └── server.py                   # FastMCP server, all 10 tool definitions\n├── tests/\n│   └── test_server.py              # Unit + live API smoke tests\n├── audits/                         # MCP best-practice audit evidence\n├── docs/assets/demo.svg            # README demo asset\n├── .github/workflows/ci.yml        # GitHub Actions (Python 3.11/3.12/3.13)\n├── Dockerfile                      # Multi-stage, non-root runtime image\n├── docker-compose.yml              # Local run with resource limits\n├── claude_desktop_config.json      # Example Claude Desktop config\n├── pyproject.toml\n├── CHANGELOG.md\n├── CONTRIBUTING.md\n├── SECURITY.md\n├── EXAMPLES.md\n├── LICENSE\n├── README.md                       # This file (English)\n└── README.de.md                    # German version\n```\n\n---\n\n## Safety & Limits\n\n- **Read-only:** All 9 tools perform read-only HTTP GET requests — no data is written, modified, or deleted upstream.\n- **No personal data:** Queries are transient and not stored. The portal returns aggregated statistics, infrastructure and operational metadata. No PII is processed or retained.\n- **No API key:** Data is public and free. There is no authentication and no secret to manage.\n- **Injection-hardened:** `year`/`canton` are regex-validated and every value interpolated into an ODSQL `where` clause is escaped via a central helper.\n- **Data freshness:** Real-time tools (disruptions) reflect the upstream source at query time; statistical datasets update annually/daily (see the tool table).\n- **Terms of service:** Data is published under the [data.sbb.ch licence](https://data.sbb.ch/page/licence) (NonCommercialAllowed-CommercialAllowed-ReferenceRequired).\n- **No guarantees:** This server is a community project, not affiliated with SBB. Availability depends on the upstream API.\n\nSee [`SECURITY.md`](SECURITY.md) for the full security posture.\n\n---\n\n## Known Limitations\n\n- **Passenger frequency:** Updated annually; the latest full year may lag by some months.\n- **Rail disruptions:** Returns all current Swiss rail messages → use `limit` and pagination.\n- **Trains per segment:** Counts are yearly aggregates, not real-time.\n- **Station search:** Covers the full Swiss DiDok register (all operators), not just SBB.\n- **No rate limiting of its own:** Place a public HTTP deployment behind a rate-limiting reverse proxy.\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\nNo API key is required.\n\n```bash\n# Unit tests (no network required)\nPYTHONPATH=src pytest tests/ -m \"not live\"\n\n# Live API smoke tests (require network access to data.sbb.ch)\nPYTHONPATH=src pytest tests/ -m live\n\n# Re-record the fixtures from data.sbb.ch (writes tests/fixtures/PROVENANCE.md)\npython scripts/record_fixtures.py\n```\n\nThe unit-test payloads are **recorded, not invented**. Source, retrieval date,\nselection rule and SHA-256 per file are in\n[`tests/fixtures/PROVENANCE.md`](tests/fixtures/PROVENANCE.md).\n\n`tests/fixtures/dataset_fields.json` is not a data excerpt but **the contract**:\nthe Explore v2.1 API declares each dataset's field names, and a `select` or\n`order_by` on a field it does not have is answered with **HTTP 400** — not with\nfewer columns. `TestFieldContract` holds every field name the server uses\nagainst that declaration, so the next rename fails a test instead of a user's\nrequest. Until 2026-08-08 three of ten tools were permanently broken for\nexactly this reason.\n\n> **Live tests are not run by CI** (`-m \"not live\"`). Two of those three broken\n> tools *had* live tests covering them — `test_live_search_waedenswil` and\n> `test_live_list_datasets`. The coverage existed; the run did not.\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 · [github.com/malkreide](https://github.com/malkreide)\n\n---\n\n## Credits & Related Projects\n\n- **Data:** [data.sbb.ch](https://data.sbb.ch/) – Swiss Federal Railways (SBB) Open Data, OpenDataSoft REST API v2.1\n- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) – Anthropic / Linux Foundation\n- **Related:**\n  - [swiss-transport-mcp](https://github.com/malkreide/swiss-transport-mcp) – real-time timetables, journeys & disruptions (opentransportdata.swiss)\n  - [swiss-road-mobility-mcp](https://github.com/malkreide/swiss-road-mobility-mcp) – micromobility & EV charging\n  - [zurich-opendata-mcp](https://github.com/malkreide/zurich-opendata-mcp) – MCP server for Zurich city open data\n- **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide/swiss-public-data-mcp)\n\n<!-- mcp-name: io.github.malkreide/sbb-opendata-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    \"sbb-opendata-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"sbb-opendata-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 16320,
  "sha": "3f95c31bb362b001165f0fa91c4acad5b38b6244b3c667437ebe1ea492c6c26b",
  "repo_slug": "malkreide/sbb-opendata-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_sbb_opendata_mcp_a8ed7f6c/readme"
}