swiss-rail-mcp
# swiss-rail-mcp
A zero-auth MCP server for Swiss public transport. Wraps
[`transport.opendata.ch`](https://transport.opendata.ch) and
[`data.sbb.ch`](https://data.sbb.ch) into a single MCP server with
**12 tools, 3 resources, and 4 prompts**.
[](https://pypi.org/project/swiss-rail-mcp/)
[](https://pypi.org/project/swiss-rail-mcp/)
[](LICENSE)
## What it does
Lets an MCP client (Claude Desktop, Claude Code, …) answer real Swiss
transport questions without an API key:
- *"When's the next train from Regensdorf to Zürich HB?"*
- *"What's the last realistic train home from Bern tonight?"*
- *"Any disruptions on the Gotthard line?"*
- *"Does Lugano have step-free access and WiFi?"*
Communication is stdio only. No keys, no accounts, no rate-limited
proxy — just two public, unauthenticated upstream APIs.
## Install
Run on demand without installing globally:
```sh
uvx swiss-rail-mcp
```
Or install it as a persistent `uv` tool:
```sh
uv tool install swiss-rail-mcp
```
Both options pull from PyPI and expose the `swiss-rail-mcp` console
script. Requires Python 3.11+.
## Use with Claude Desktop
Add the server to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"swiss-rail": {
"command": "uvx",
"args": ["swiss-rail-mcp"]
}
}
}
```
Restart Claude Desktop. The tools, resources, and `/swiss-rail:*`
slash-commands should appear in the picker.
## Use with Claude Code
```sh
claude mcp add swiss-rail -- uvx swiss-rail-mcp
```
## Tools
### Core (timetable, via `transport.opendata.ch`)
| Tool | Purpose |
| --- | --- |
| `find_station(query)` | Resolve a place name to canonical stations. |
| `get_connections(from_station, to_station, limit=4, time=None, date=None)` | Connections between two stations. |
| `get_stationboard(station, limit=10)` | Next departures from a station. |
### SBB open data (via `data.sbb.ch`)
| Tool | Purpose |
| --- | --- |
| `get_disruptions(scope=None, limit=10)` | Active rail-traffic disruptions, optionally filtered. |
| `get_station_facilities(station)` | Elevator, WiFi, step-free access, waiting room. |
| `get_passenger_frequency(station)` | Most recent average daily passenger count. |
### Conversational (compose the above)
| Tool | Purpose |
| --- | --- |
| `last_train_home(home_station, current_station, after_time=None)` | Latest realistic train tonight. |
| `plan_evening_out(from_station, to_station, latest_return_time)` | Outbound options plus latest safe return. |
| `commuter_summary(home_station, work_station)` | Next connections + disruption verdict for a daily commute. |
| `journey_with_layover(from_station, to_station, layover_station, layover_minutes)` | Two-leg journey with a deliberate stop. |
| `accessible_route(from_station, to_station, limit=3)` | Connections annotated with endpoint accessibility. |
| `bike_friendly_route(from_station, to_station, limit=3)` | Connections annotated with a per-leg bike-allowed heuristic. |
All datetimes returned by the tools are ISO 8601 with a Europe/Zurich
offset.
## Resources
Read-only URIs the client can fetch. Useful for letting the LLM pull
formatted markdown views of live data without a tool call.
| URI | Returns |
| --- | --- |
| `transport://disruptions/active` | Markdown list of all active disruptions. |
| `transport://station/{name}/board` | Markdown table of the next departures. |
| `transport://station/{name}/info` | Markdown summary of facilities + frequency. |
## Prompts
Slash-commands in Claude Desktop (`/swiss-rail:<name>`) and
callable prompts in other MCP clients.
| Prompt | Use case |
| --- | --- |
| `plan_weekend_trip(from_station, max_hours=8)` | Day-trip planner with disruption check. |
| `morning_commute_check(home_station, work_station)` | Quick sanity-check before leaving the house. |
| `last_train_check(home_station, current_station=None)` | "What's the last train home?" from where you are. |
| `evening_out_planner(home_station, destination_city)` | Plan a night out with a safe return. |
## Example sessions
Real responses from Claude with this MCP server wired up, captured via
`claude -p --mcp-config …` against the live upstream APIs. Reproduce with
the snippet at the bottom of this section.
> **Q:** *When's the next train from Bern to Zürich HB?*
>
> **A:** Next IR from Bern at 03:00, arriving Zürich HB at 04:11
> (direct, 71 min, platform 5).
> **Q:** *When's the last realistic train home from Bern to Regensdorf
> tonight?*
>
> **A:** Your last realistic option is the IC at **23:02 from Bern**
> (platform 6) → Zürich HB 00:16 → SN to Regensdorf-Watt, arriving
> **00:49**. After that you're into Nachtnetz SN trains (last one
> departs Bern 01:06, arrives 02:49) which usually carry a night
> surcharge.
> **Q:** *Are there any active disruptions on the Swiss rail network
> right now? Give me just the headline count and the most severe one.*
>
> **A:** 10 active events on the feed. The most severe is a critical
> interruption on the Basel Bad Bf–Freiburg line between Haltingen and
> Efringen-Kirchen (accident involving a person), affecting ICE, ECE,
> EC, NJ, RE 7 and RB 27 — service has since resumed but residual
> delays/cancellations are possible through 05:00.
> **Q:** *Does Zürich HB have step-free access and WiFi?*
>
> **A:** Yes — Zürich HB has both step-free access and WiFi (elevators
> are also available).
> **Q:** *How busy is Lugano station compared to Bern?*
>
> **A:** Bern is much busier, averaging ~177,000 daily passengers in
> 2025 versus ~32,300 at Lugano — roughly 5.5× the foot traffic.
> **Q:** *I'm in Bern and want to grab a coffee in Olten for 30 minutes
> on my way to Basel SBB. Plan the journey.*
>
> **A:** Take the IR from Bern at 03:00 (platform 5), arriving Olten
> 03:27. Note: the next onward train to Basel SBB isn't until 04:48
> (platform 10AB, arriving 05:26), so your "30 min" coffee stretches to
> about 81 minutes at this hour — if you're traveling later in the day,
> ask again with a specific time and I'll find a tighter 30-min
> layover.
### Reproduce
```sh
cat > /tmp/swiss-mcp.json <<'EOF'
{ "mcpServers": { "swiss-rail": {
"command": "uvx", "args": ["swiss-rail-mcp"]
}}}
EOF
claude -p \
--mcp-config /tmp/swiss-mcp.json \
"When's the next train from Bern to Zürich HB?"
```
Times and disruption counts will differ on each run — these were
captured live.
## Data sources
Two zero-auth sources. Both are unaffiliated with this project.
- [`transport.opendata.ch`](https://transport.opendata.ch) — proxies
SBB's HAFAS timetable endpoints. Community-funded; be considerate
with request volume.
- [`data.sbb.ch`](https://data.sbb.ch) — SBB's public Opendatasoft
Explore API. Disruptions, station facilities, passenger frequency.
For SBB data that requires an API key (real-time delays, occupancy,
fares, train compositions, OJP, SIRI), see
[malkreide/swiss-transport-mcp](https://github.com/malkreide/swiss-transport-mcp).
This server stays zero-auth on purpose; see
[LIMITATIONS.md](LIMITATIONS.md) for the full list of what that excludes
and the heuristics used in its place.
## Development
```sh
uv sync
uv run pytest # all tests (hits live APIs)
uv run pytest -m "not integration" # offline only
uv run ruff check
uv run ruff format --check
```
Enable verbose HTTP logging with `SWISS_TRANSPORT_DEBUG=1`.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 12 tools
Several tools are specialized variants of get_connections (plan_evening_out, last_train_home, commuter_summary, journey_with_layover, accessible_route, bike_friendly_route), creating overlap in purpose. However, descriptions specify distinct user intents, so agents can generally choose correctly.
The set mixes get_* verbs (get_connections, get_stationboard, etc.) with descriptive phrases (last_train_home, commuter_summary, journey_with_layover) and adjective-noun compounds (accessible_route, bike_friendly_route). This is not a consistent verb_noun pattern.
12 tools is within the typical range for a domain-specific server, but about half are variants of the same journey-planning function, making the set feel somewhat inflated. Still, it is not excessive.
Covers core Swiss rail tasks: station search, connections, departures, disruptions, station facilities, and passenger frequency. It also adds accessibility and bike heuristics. Minor gaps exist (e.g., no generic 'last connection' tool, ticket info), but the domain is well covered.