MCP stdio server for Swiss public transport data using:
- OJP 2.0 (
/ojp20) for trip planning, station lookup, and departure boards - GTFS-RT (
/gtfs-rt) for real-time trip updates and disruptions - Train Formation API for coach composition, sectors, and accessibility metadata
Create API access in OpenTransportData API Manager:
- Create an account at
https://api-manager.opentransportdata.swiss/. - Subscribe to all required APIs: OJP2, GTFS-RT, and Train Formation.
- Create one app and attach all 3 API subscriptions to that app.
- Generate an app token and use it for this MCP server.
Export tokens in your shell:
export OJP2_TOKEN="YOUR_OPENTRANSPORTDATA_TOKEN"
export GTFS_RT_TOKEN="YOUR_GTFS_RT_TOKEN"
export FORMATION_TOKEN="YOUR_TRAIN_FORMATION_TOKEN"
export MCP_SERVER_NAME="sbb-transport"
export CACHE_TTL_SECONDS="300"If your API Manager app gives you a single token for all 3 APIs, set all three env vars to that same value.
You can also keep credentials in a .env file and load them into the current shell:
set -a
source .env
set +aOptional endpoint overrides:
export OJP2_ENDPOINT="https://api.opentransportdata.swiss/ojp20"
export GTFS_RT_ENDPOINT="https://api.opentransportdata.swiss/la/gtfs-rt"Token is read from OJP2_TOKEN by default:
swiss-transportation-mcpThe process speaks MCP JSON-RPC on stdio with Content-Length framing.
CLI is parsed with structopt; flags override env values:
swiss-transportation-mcp \
--api-token "$OJP2_TOKEN" \
--server-name "sbb-transport"Show all CLI options:
swiss-transportation-mcp --helpAdd this MCP server entry to your Codex config at ~/.codex/config.toml:
[mcp_servers.sbb_transport]
command = "swiss-transportation-mcp"
args = []
env = { OJP2_TOKEN_ENV = "OJP2_TOKEN", GTFS_RT_TOKEN_ENV = "GTFS_RT_TOKEN", FORMATION_TOKEN_ENV = "FORMATION_TOKEN", MCP_SERVER_NAME = "sbb-transport", CACHE_TTL_SECONDS = "300" }
env_vars = ["OJP2_TOKEN", "GTFS_RT_TOKEN", "FORMATION_TOKEN"]Yes, AI agents (including Codex) fetch the token from the MCP server process environment.
This server reads the token env var names from OJP2_TOKEN_ENV, GTFS_RT_TOKEN_ENV, and FORMATION_TOKEN_ENV.
Defaults are OJP2_TOKEN, GTFS_RT_TOKEN, and FORMATION_TOKEN.
Export the real token env vars in your shell before starting Codex so the MCP process inherits them:
export OJP2_TOKEN="YOUR_OPENTRANSPORTDATA_TOKEN"
export GTFS_RT_TOKEN="YOUR_GTFS_RT_TOKEN"
export FORMATION_TOKEN="YOUR_TRAIN_FORMATION_TOKEN"In Codex CLI, secret-like variables are excluded from inherited env by default.
env_vars = ["OJP2_TOKEN", "GTFS_RT_TOKEN", "FORMATION_TOKEN"] is required so this MCP stdio server receives those variables.
If Codex cannot find swiss-transportation-mcp, enable shell profile inheritance so ~/.cargo/bin is available:
[shell_environment_policy]
experimental_use_profile = trueIf OJP2_TOKEN is not present (and --api-token is not passed), startup fails with:
missing OJP token: set <value of OJP2_TOKEN_ENV> or pass --api-token.
If GTFS_RT_TOKEN is not present (and --gtfs-rt-token is not passed), startup fails.
If FORMATION_TOKEN is not present (and --formation-token is not passed), startup fails.
Restart Codex after saving the config so the MCP server is discovered and its tools are loaded.
search_stations(query, limit)plan_trip(from_station, to_station, datetime, modes)get_departures(station_id, limit, time_window)get_trip_details(trip_id)monitor_trip(trip_id, notify_on_delay_minutes)list_transport_modes()get_disruptions(line?, station?)
See examples/tool_calls.json for ready-to-send tools/call payloads.
Prompt:
Give me the next train from Luzern to Zürich HB and coach sectors.
Example AI response:
Best option: IR70
- Departure: 20:10 from Luzern, platform 3
- Arrival: 20:50 at Zürich HB, platform 12
- Duration: 40 min
Boarding hint:
- Go to sector D for coach 2:9 (wheelchair spaces, stroller area)
Coach classes:
- 2nd class: 2:1, 2:2, 2:3, 2:4, 2:5, 2:6, 2:7, 2:8, 2:9
- 1st class: W1:10, 1:11, 1:12, 1:14
Sector mapping:
- B: coach 2:3
- C: coach 2:7
- D: coach 2:9
Prompt:
Plan rail options from stop 8505000 to 8503000.
Example AI response:
Resolved route: Luzern (8505000) -> Zürich HB (8503000)
Top 3 rail options:
1) IR70, dep 20:10, arr 20:50, ~40 min
2) IR75, dep 20:35, arr 21:24, ~49 min
3) IR70, dep 21:10, arr 21:50, ~40 min
Use this mode when you need deterministic planning and want to avoid name resolution ambiguity.
Prompt:
Show me a detailed bus itinerary from Bern, Bahnhof to Bern, Bärengraben.
Example AI response:
Connection: Bus 12 (direct)
- Departure: 19:55 from Bern, Bahnhof (platform A)
- Arrival: 20:01 at Bern, Bärenpark
- Duration: 6 min
Stop-by-stop:
1) Bern, Bahnhof (dep 19:55)
2) Bern, Bärenplatz (arr 19:56)
3) Bern, Zytglogge (arr 19:58)
4) Bern, Rathaus (arr 19:59)
5) Bern, Nydegg (arr 20:00)
6) Bern, Bärenpark (arr 20:01)
Prompt:
Give me a bus-only route from Zürich, Central to Zürich, Bucheggplatz with all legs.
Example AI response:
Trip summary:
- Total duration: ~16 min
- Transfers: 1
Leg 1: Bus 46
- Zürich, Central -> Zürich, Bahnhofquai/HB
Leg 2: Bus 46
- Zürich, Bahnhofquai/HB -> Zürich, Rosengartenstrasse
- Intermediate stops include Stampfenbachplatz, Nordstrasse, Lettenstrasse, Zürich Wipkingen Bahnhof
Leg 3: Bus 83
- Zürich, Rosengartenstrasse -> Zürich, Bucheggplatz (platform G)
Each leg includes realtime expected/scheduled times and platform updates where provided by the feed.