swiss-transport-mcp is an MCP server that provides OJP 2.0 journey planning and SIRI-SX disruption information, along with occupancy and fares, including train formation details. The project is listed as part of the Swiss Public Data MCP portfolio and is oriented toward Swiss public transport data.
π οΈ Key Features
OJP 2.0 journey planning
SIRI-SX disruptions
Occupancy information
Fare data
Train formation details
π Use Cases
Build LLM or agent apps that need Swiss journey planning results
Surface real-time disruption context (SIRI-SX)
Provide capacity/occupancy and fare details in travel workflows
Generate responses that include train formation context
β‘ Developer Benefits
Implemented as an MCP server (Model Context Protocol)
Uses topics aligned with open Swiss transport data
Labeled Python 3.11+ support
β οΈ Limitations
Scope is described for Swiss public transport (e.g., SBB-related and Zurich context), not broader regions.
MCP server connecting AI models to the Swiss public transport system β journey planning, real-time departures, disruptions, occupancy, ticket prices, train formations and open data from opentransportdata.swiss.
swiss-transport-mcp gives AI assistants like Claude a complete Swiss travel information system β not just timetables, but also real-time disruption alerts, occupancy forecasts, ticket prices, and a full train formation view. All accessible through a single, standardised MCP interface.
The various APIs at opentransportdata.swiss speak different protocols β OJP 2.0 (XML/SOAP), SIRI-SX (XML), REST/JSON. This server translates everything into clean JSON for the AI model, acting as a multilingual protocol interpreter.
Anchor demo query:"Plan a school trip for 25 students from Zurich to the Technorama in Winterthur β check for disruptions and find the best departure."
β More use cases by audience β
Features
πΊοΈ Journey planning (A β B with transfers, duration, transport mode) via OJP 2.0
π Real-time departures with delays and platform information
π Stop search by name or coordinates
π¨ Live disruption alerts (cancellations, closures) via SIRI-SX
π Occupancy forecasts for trains (SBB, BLS, Thurbo, SOB)
Optional: additional keys for SIRI-SX, Occupancy, Formation, OJP Fare
Installation
bash
# Clone the repository
git clone https://github.com/malkreide/swiss-transport-mcp.git
cd swiss-transport-mcp
# Install
pip install -e .
Or with uvx (no permanent installation):
bash
uvx swiss-transport-mcp
Quickstart
bash
# Set the minimum required key (OJP core tools)export TRANSPORT_API_KEY=your_key_here
# Start the server (stdio mode for Claude Desktop)
swiss-transport-mcp
Try it immediately in Claude Desktop:
"What are the next departures from Zurich Stadelhofen?""How do I get from WΓ€denswil to Bern by train?"
Configuration
Environment Variables
Variable
API
Required
TRANSPORT_API_KEY
Unified key for OJP + CKAN
β (or individual keys)
TRANSPORT_OJP_API_KEY
OJP 2.0 Journey Planner
Optional (override)
TRANSPORT_CKAN_API_KEY
CKAN data catalogue
Optional (separate subscription)
SIRI_SX_API_KEY
Disruption alerts (SIRI-SX)
Optional
OCCUPANCY_API_KEY
Occupancy forecast
Optional
FORMATION_API_KEY
Train formation
Optional
OJP_FARE_API_KEY
Ticket prices (OJP Fare)
Optional
APIs without a key are silently disabled β the server starts fine with just the 6 core tools.
Operational / security variables:
Variable
Effect
Default
MCP_ENV / ENV
Process environment. Must be dev/development/local/test to allow disabling TLS verification.
(unset β production)
TRANSPORT_SSL_VERIFY
Set to false to disable TLS certificate verification. Honoured only when MCP_ENV marks a dev environment β otherwise the request is ignored and verification stays on.
true
TRANSPORT_CKAN_URL
Override the CKAN base URL. Must stay on the egress allow-list (*.opentransportdata.swiss); off-site overrides are refused.
https://api.opentransportdata.swiss/ckan-api
MCP_CORS_ORIGINS
Comma-separated list of browser origins allowed to call the HTTP transport. Use * to allow any origin (not recommended). The Mcp-Session-Id header is exposed to these origins.
https://claude.ai
LOG_FORMAT
json for structured logs (RFC 5424 severity); anything else for human-readable text. Always written to stderr.
text
OTEL_TRACES_ENABLED
1 to enable OpenTelemetry tracing (requires the otel extra: pip install 'swiss-transport-mcp[otel]'). No-op otherwise.
(off)
MCP_STATELESS
1 to run the Streamable HTTP transport statelessly β no server-side session state, so instances need no sticky load balancing. Recommended for horizontal scale-out.
(off β stateful)
MCP_ALLOWED_HOSTS
Comma-separated list of the names this server is reachable under, port included where it matters (e.g. fahrplan.example.ch:8080). Requests arriving under any other Host are rejected with 421; loopback stays allowed so container health checks keep working. Unset on a non-loopback bind, the check is off and a warning is logged.
(unset β off)
π Egress allow-list: all outbound requests are restricted to https:// on opentransportdata.swiss hosts. Any other host is refused before a request is sent (SSRF / egress hardening).
For use via claude.ai in the browser (e.g. on managed workstations without local software). The cloud transport is Streamable HTTP (MCP_TRANSPORT=streamable-http, endpoint /mcp). SSE (/sse) is still supported but deprecated.
MCP_TRANSPORT
Use
Endpoint
stdio (default)
Local Claude Desktop subprocess
β
streamable-http (or http)
Cloud / container (recommended)
/mcp
sse
Legacy browser transport (deprecated)
/sse
Docker (recommended):
bash
# Build + run with explicit resource limits (see docker-compose.yml)
TRANSPORT_API_KEY=xxx docker compose up --build
# β http://127.0.0.1:8000/mcp
The image is a multi-stage build running as a non-root user; docker-compose.yml adds read_only, no-new-privileges and memory/CPU/PID limits.
Render.com:
Push/fork the repository to GitHub
On render.com: New Web Service β connect GitHub repo (Docker runtime)
Set env MCP_TRANSPORT=streamable-httpand MCP_HOST=0.0.0.0
In claude.ai under Settings β MCP Servers, add: https://your-app.onrender.com/mcp
π‘ "stdio for the developer laptop, Streamable HTTP for the cloud."
Scaling horizontally: run with MCP_STATELESS=1. In stateless mode the
server keeps no per-session state, so any instance can serve any request and a
plain round-robin load balancer suffices β no sticky sessions / Mcp-Session-Id
affinity required. If you need stateful streaming instead, route by
Mcp-Session-Id at the edge LB (e.g. HAProxy stick-tables) so each session
stays pinned to one instance.
β οΈ Binding: In a network transport the server binds to 127.0.0.1 by
default so a locally started server is not exposed to your whole network
(e.g. public Wi-Fi). Set MCP_HOST=0.0.0.0only in a container/cloud
environment where binding to all interfaces is intended (the Docker image
does this for you).
Available Tools
Core Tools (OJP 2.0 / CKAN)
Tool
Description
Data Source
transport_search_stop
Search stops/stations by name
OJP 2.0
transport_nearby_stops
Find nearby stops by coordinates
OJP 2.0
transport_departures
Real-time departure board with delays & platforms
OJP 2.0
transport_trip_plan
Plan journey A β B with transfers, duration, mode
OJP 2.0
transport_search_datasets
Search open data catalogue (~90 datasets)
CKANΒΉ
transport_get_dataset
Get full details of a specific dataset
CKANΒΉ
ΒΉ CKAN tools require a separate subscription in the API Manager.
Extension Tools (optional API keys)
Tool
Description
Data Source
get_transport_disruptions
π¨ Live disruptions, cancellations, line closures
SIRI-SX
get_train_occupancy
π Occupancy forecast for specific trains
Occupancy JSON
get_ticket_price
π° Ticket prices for connections
OJP Fare
get_train_composition
π Train formation, classes, accessibility
Formation REST
check_transport_api_status
π Health check for all configured APIs
All
Example Use Cases
Query
Tool
"Next trains from Zurich Stadelhofen?"
transport_departures
"Plan a trip for 25 students from Zurich to Winterthur Technorama"
transport_trip_plan
"Any disruptions between Zurich and Bern?"
get_transport_disruptions
"How full is IC 1009 today?"
get_train_occupancy
"What does a ticket from WΓ€denswil to Bern cost?"
Read-only: All tools perform read-only requests (HTTP GET / OJP XML POST for queries only) β no data is written, modified, or deleted on any upstream system.
No personal data: Journey queries are transient and not stored by this server. The APIs return scheduled timetable and real-time operational data. No personally identifiable information (PII) is processed or retained.
Rate limits: opentransportdata.swiss enforces per-key rate limits (documented in the API Manager). The server's built-in RateLimiter (SIRI-SX: 2 req/min, Formation/OJP Fare: 5 req/min) stays within these bounds automatically. Use the limit parameters conservatively for bulk queries.
API key required: A free key from api-manager.opentransportdata.swiss is mandatory. Keys are bound to your account's subscription β only subscribe to APIs you intend to use.
Data freshness: Real-time tools (departures, disruptions, occupancy) reflect the upstream source at query time. The server caches responses for short TTLs (120sβ1800s) to reduce API load β see the Caching Strategy table above.
Terms of service: Data is subject to the ToS of opentransportdata.swiss. OJP, SIRI-SX, and the CKAN catalogue are published under open licences (ODbL / CC BY 4.0) for non-commercial and research use.
No guarantees: This server is a community project, not affiliated with the Federal Office of Transport (BAV/OFT) or SBB. Availability depends on upstream APIs.
Before you install (consent)
Adding this server to your MCP client lets the connected AI model issue Swiss
public-transport queries on your behalf, using your opentransportdata.swiss
API key, and make outbound HTTPS requests to opentransportdata.swiss. Nothing
is written upstream and no PII is stored, but you should review the tool list
above and confirm you are comfortable granting that access before configuring
the server.
Running the HTTP transport safely (no built-in auth)
The server has no authentication of its own. When you run the Streamable
HTTP transport (MCP_TRANSPORT=streamable-http), the MCP SDK issues a
cryptographically random Mcp-Session-Id per session, but there is no user
identity bound to it. Therefore:
Do not expose a no-auth instance directly to the public internet. Put it
behind an authenticating reverse proxy (OAuth2 proxy, mTLS, or your platform's
access control), or restrict it to a trusted network.
Keep the default MCP_HOST=127.0.0.1 for local use; only bind 0.0.0.0
inside a controlled container/cloud environment (see Deployment).
Scope MCP_CORS_ORIGINS to the origins you actually trust.
Set MCP_ALLOWED_HOSTS whenever you bind beyond loopback. It guards against
DNS rebinding: a page on your network resolves its own hostname to this
server's address and then talks to it from the browser. CORS does not stop
that β from the browser's point of view the request is same-origin β and
neither would a token, since the attacking page runs in a context that holds
one. Only the Host check does. Left unset the check stays off, which is the
right default only when something in front of the server validates Host.
See SECURITY.md for the full security posture and the
accepted-risk decisions (gateway-level controls).
Known Limitations
OJP Fare: Discounts (Halbtax, GA, regional passes) are not always reflected
Formation: Stop-based data is only available for TODAY (real-time dependency)
Occupancy: SBB, BLS, Thurbo and SOB only β no private railways
SIRI-SX: Returns ALL Swiss disruptions β use the filter_text parameter
CKAN: Requires a separate subscription in the API Manager
MCP Protocol Version
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. That pin says which revisions the SDK offers.
That the server actually serves them is measured in
tests/test_modern_wire.py, against the very app
main() hands to uvicorn: real 2026-07-28 single-exchange POSTs β no
initialize, no Mcp-Session-Id β for server/discover, tools/list and a
tools/call, the three rejection rungs (missing envelope, routing header
disagreeing with the body, unserved revision), and a legacy initialize on the
same endpoint to show both eras coexist.
An earlier version of this section claimed the repo built no ASGI app to send a
request through, and offered that as the reason the gate could only assert
constants. It did build one.
Server identity.2026-07-28 has no initialize, and so no single place
where a client reads serverInfo. The revision puts the Implementation
(name, title, version, website) into server/discoverand into the _meta
of every result instead. This server fills those from its own distribution
metadata, so the version on the wire is the version that was installed β it
cannot drift from pyproject.toml.
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.
Testing
bash
# Unit tests (no API key required)
PYTHONPATH=src pytest tests/ -m "not live"# Integration tests (API key required)
TRANSPORT_API_KEY=xxx pytest tests/ -m "live"
Where the test data comes from
All four upstream APIs need a Bearer token from the opentransportdata.swiss
API-Manager, so CI cannot record a real response β measured and kept in
tests/fixtures/upstream_auth_probe.json. The XML payloads in the test modules
are therefore hand-written, not recorded, and cannot refute the production
code: both come from the same reading of the docs, and where both are wrong
they are wrong together.
What can be recorded is the contract. OJP 2.0 is a CEN standard
(CEN/TS 17118) with a public XML schema, and tests/fixtures/ojp_2_0_contract.json
is a dated index derived from it β element names, the structures this server
builds on, the enumerations it sends as values, plus the SHA-256 of every
schema file read. tests/test_ojp_contract.py holds the requests and parsers
against it. The schema itself is deliberately not vendored: the source
repository carries no licence file.
bash
python scripts/record_fixtures.py # re-record
python scripts/record_fixtures.py --check # recompute against the pinned tag
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):