Deutsche Bahn MCP Server
Provides tools that wrap public Deutsche Bahn APIs for station master data, timetables, elevator/escalator facility status, and parking information, enabling live queries such as station searches, timetable changes, facility outages, and parking forecasts.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Deutsche Bahn MCP ServerWhich elevators at Frankfurt Hbf are out of order right now?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Deutsche Bahn MCP Server (local training fork)
A Model Context Protocol server that wraps four public Deutsche Bahn APIs (station master data, timetables, elevator/escalator status, parking) as 13 MCP tools, 7 prompts and 5 resources. An MCP client such as Kiro or Claude Desktop launches it, and the LLM can then answer questions like "Which elevators at Frankfurt Hbf are out of order right now?" with live data.
This is a fork of PaulvonBerg/db-mcp-server, adapted for local use in the "Agentic AI" training class: it runs on the current MCP Python SDK (2.x), starts over stdio directly from your MCP client with a single uv run command, and has pinned dependencies so it works the same on every laptop. The upstream Cloud Run deployment is still possible but not needed here (see What changed compared to upstream).
Table of contents
Related MCP server: db-mcp
Prerequisites
Python 3.11 or newer. You do not have to install it yourself:
uvdownloads a matching interpreter on first run if none is found.uv, the Python package and project manager. On macOS:
brew install uv, or on any platform the official installer:curl -LsSf https://astral.sh/uv/install.sh | shCheck with
uv --version(0.11 was used here; any recent release works).A DB API Marketplace account at https://developers.deutschebahn.com (free). See Step 1.
An MCP client, e.g. Kiro or Claude Desktop, to actually talk to the server.
curlandpython3are enough for the smoke tests.gitto clone this repository.
Clone the repository and note its absolute path; you will need it in the client configuration:
git clone <this-repo-url> db-mcp-server
cd db-mcp-server
pwd # -> /ABSOLUTE/PATH/TO/db-mcp-server, used belowStep 1: Get DB API credentials
The server talks to the DB API Marketplace with two values: a Client ID and an API Key (secret). Both belong to an application that you create in the portal, and every API you want to call has to be subscribed for that application. Missing subscriptions are the most common reason the tools fail, so do not skip the second half.
Register / log in at https://developers.deutschebahn.com.
Create an application ("Anwendung") in your account area. The portal shows you two values for it:
Client ID -> goes into
DB_API_KEYAPI Key / Client Secret -> goes into
DB_API_SECRET
(Yes, the names are confusing: the variable called
DB_API_KEYholds the Client ID, andDB_API_SECRETholds the thing the portal calls API Key.)Subscribe the application to these four products (catalogue -> product -> "Abonnieren" / subscribe -> pick your application and the plan):
Product in the portal
Plan
Used by
StaDa - Station Data
Free4All
get_station_by_name,get_stations_by_position,search_szentralen,get_szentralen_by_location, resourcesTimetables
Free
get_planned_timetable,get_recent_timetable_changes,get_full_timetable_changesFaSta - Station Facilities Status
Free4All
find_facilities,get_facilities_by_stationParking Information // DB Bahnpark (marked deprecated)
Testzugang
get_parking_by_station,search_parking_facilities,get_parking_prognosesThe Parking subscription stays in state Anstehende Genehmigung (pending approval) but works anyway.
How a missing subscription shows up: the tool result reads Error executing tool get_station_by_name: External API error (HTTP 403) and the raw API answer behind it is {"httpCode":"403","httpMessage":"Forbidden","moreInformation":"Not registered to plan"}. The credentials were accepted (otherwise you would get HTTP 401), the application just has no plan for that API. Go back to the portal and subscribe.
Step 2: Configure the server
cd /ABSOLUTE/PATH/TO/db-mcp-server
cp .env.example .envOpen .env in an editor and fill in the two required values:
Key in | Value from the portal |
| the application's Client ID |
| the application's API Key / Secret |
| leave empty for local use (Cloud Run only) |
| leave empty for local use (Cloud Run only) |
.env is listed in .gitignore, so your credentials cannot end up in a commit. Verify:
git check-ignore .env # prints ".env" when the file is ignoredconfig.py loads .env from the repository directory itself, so the server finds it no matter which working directory the MCP client starts it from.
Step 3a: Run it from an MCP client (recommended, stdio)
MCP clients can start a server themselves and talk to it over stdin/stdout ("stdio transport"). The entry point for that is stdio_main.py. With uv run, the first launch creates .venv/, installs the pinned dependencies from pyproject.toml / uv.lock, and starts the server; later launches start immediately.
Kiro
Add this to .kiro/settings/mcp.json in your workspace (or to ~/.kiro/settings/mcp.json for all workspaces). Replace the path with the output of pwd from the clone:
{
"mcpServers": {
"deutschebahn": {
"command": "uv",
"args": ["run", "--directory", "/ABSOLUTE/PATH/TO/db-mcp-server", "stdio_main.py"],
"disabled": false
}
}
}Notes:
Use the absolute path of your clone.
--directorymakesuvchange into the repo before running, so the relativestdio_main.pyand the.envfile are found.If the client reports
uv: command not foundorspawn uv ENOENT, it does not see your shell'sPATH. Put the full path into"command": runwhich uvin a terminal (typically/Users/<you>/.local/bin/uvor/opt/homebrew/bin/uv) and use that string.The first launch takes a while (uv resolves and installs ~10 packages, and may download Python). Kiro shows the server as connecting; give it a minute before you suspect an error.
The server logs at DEBUG level to stderr; the client shows that in its MCP log view. Lots of log lines are normal.
The same
command/argssnippet works for Claude Desktop (claude_desktop_config.json, under"mcpServers") and any other stdio MCP client.
Without uv (plain venv)
If you prefer a classic virtual environment, create it once with the pinned requirements.txt and point the client at the venv's interpreter:
cd /ABSOLUTE/PATH/TO/db-mcp-server
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt"deutschebahn": {
"command": "/ABSOLUTE/PATH/TO/db-mcp-server/.venv/bin/python",
"args": ["/ABSOLUTE/PATH/TO/db-mcp-server/stdio_main.py"],
"disabled": false
}Note the pinned versions in requirements.txt: an unpinned pip install mcp would give you SDK 2.x, which is what this fork needs, but the upstream code was written for 1.x and breaks on it (see Troubleshooting).
Step 3b: Run it as an HTTP server (optional)
main.py is the upstream entry point: a FastAPI app with the MCP endpoint at /mcp (Streamable HTTP transport) and a health check at /health. It is useful if you want to watch the server in its own terminal, connect several clients to one process, or test with curl.
uv run --directory /ABSOLUTE/PATH/TO/db-mcp-server main.py
# or on another port:
PORT=8081 uv run --directory /ABSOLUTE/PATH/TO/db-mcp-server main.pyThe server binds 0.0.0.0 on port 8080 by default (PORT env var overrides it). Kiro entry for a running HTTP server:
"deutschebahn-http": {
"url": "http://127.0.0.1:8081/mcp",
"type": "http",
"disabled": false
}Two things to know:
Use
127.0.0.1, notlocalhost. The server listens on IPv4 only, and on many machineslocalhostresolves to the IPv6::1first, which makes the client's first connection attempt fail.Port 8080 is popular. Docker Desktop, Jenkins and many dev servers sit on it; if you see
[Errno 48] address already in use, pick another port withPORT=8081.
The rate limiter (60 requests/minute, 1000/hour per client IP) and the security headers from upstream are only active in this HTTP mode. In stdio mode there is no network listener at all.
Step 4: Verify
Stdio smoke test (no client needed)
This pipes a minimal MCP handshake into the server and lists its tools. sleep 3 keeps stdin open long enough for the answers to arrive. Expected output: a line starting with 13 tools:.
(printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 3) \
| uv run --directory /ABSOLUTE/PATH/TO/db-mcp-server stdio_main.py 2>/dev/null \
| python3 -c 'import sys,json; [print(len(m["result"]["tools"]), "tools:", *sorted(t["name"] for t in m["result"]["tools"])) for m in map(json.loads, sys.stdin) if m.get("id")==2]'13 tools: find_facilities get_facilities_by_station get_full_timetable_changes get_parking_by_station get_parking_prognoses get_planned_timetable get_recent_timetable_changes get_station_by_name get_stations_by_position get_szentralen_by_location ping search_parking_facilities search_szentralenDrop the 2>/dev/null to see the server log if nothing comes back. (For the plain-venv variant, replace the uv run ... line with /ABSOLUTE/PATH/TO/db-mcp-server/.venv/bin/python /ABSOLUTE/PATH/TO/db-mcp-server/stdio_main.py.)
HTTP mode
With PORT=8081 uv run --directory /ABSOLUTE/PATH/TO/db-mcp-server main.py running in another terminal:
curl -s http://127.0.0.1:8081/health
# {"status":"healthy","service":"Deutsche Bahn MCP Server"}Opening /mcp in a browser gives 406 Not Acceptable; that is expected, the MCP endpoint wants JSON-RPC POSTs with Accept: application/json, text/event-stream.
First prompts in your client
Once the server shows up as connected in Kiro (or Claude Desktop), try:
"Find the station Frankfurt Hauptbahnhof" ->
get_station_by_name. ExpectFrankfurt (Main) Hbf, station number 1866, EVA numbers 8000105 and 8098105. If the agent searches for the literal stringFrankfurt Hauptbahnhofit gets zero results and should retry withFrankfurt*orFrankfurt (Main) Hbf; see the quirks section."Which elevators at Frankfurt Hbf are out of order right now?" ->
find_facilities(station_number=1866, type="ELEVATOR", state="INACTIVE"). An empty list is a legitimate answer (all working)."Show me the planned departures from Frankfurt Hbf (EVA 8000105) today at 14:00" ->
get_planned_timetable("8000105", "<today as YYMMDD>", "14"). Expect several dozen stops."Are there delays at Frankfurt Hbf right now?" ->
get_recent_timetable_changes("8000105")/get_full_timetable_changes("8000105")."What parking is there at Frankfurt (Main) Hbf?" ->
search_parking_facilities.
Then watch the tool calls the agent makes and compare with the reference below.
Tool, prompt and resource reference
Tools (13)
Tool | Purpose | Key parameters | DB API |
| Liveness check, returns | none | none |
| Search station master data by (official) name or wildcard pattern. Returns name, station |
| StaDa |
| Stations around a coordinate, with distance (km). Pre-filters by federal state. |
| StaDa |
| Scheduled arrivals/departures for one hour slice. Returns |
| Timetables |
| Changes (delays, cancellations, platform changes) from the last 2 minutes. Same result shape. | EVA number as string | Timetables |
| All currently known changes for the station. Same result shape. | EVA number as string | Timetables |
| Parking facilities at a station by ID (accepts EVA number, StaDa number, RIL100 code or DHID). |
| Parking |
| Parking facilities by station name (API-side | official name, e.g. | Parking |
| Occupancy forecast up to 2 h ahead for one facility. |
| Parking |
| Elevators / escalators at a station with optional filters. |
| FaSta |
| Station overview incl. all facilities with description, state and coordinates. | StaDa station number (e.g. 1866) | FaSta |
| DB mobility service centres (S-Zentralen) near a coordinate. |
| StaDa |
| Paginated list of all S-Zentralen. |
| StaDa |
Identifier cheat sheet: StaDa number (e.g. 1866) is what FaSta wants as station_number; the evaNumbers[].number (e.g. 8000105) is what the Timetables tools want as eva_number. get_station_by_name returns both.
Errors from the DB API are raised as MCP tool errors (isError: true) with a short message such as External API error (HTTP 403), Invalid parameter: ... or External service temporarily unavailable, so the LLM sees what went wrong.
Prompts (7)
Prompt | Arguments |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Each prompt returns a user message that instructs the model which tools to chain (defined in prompts/travel_prompts.py).
Resources (5)
URI | Content |
| What StaDa station categories 1-7 mean |
| ICE, IC, RE, RB, S, ... explained |
| Category 1/2 hubs, fetched live from StaDa |
| Accessibility services at DB stations |
| Live summary of known timetable changes at six major hubs (Frankfurt, Berlin, München, Hamburg, Köln, Hannover) via Timetables |
Example questions
Station search: "Find all train stations in Munich", "What stations are within 5 km of 52.52, 13.405?"
Real-time: "Are there any delays at Frankfurt Hauptbahnhof right now?", "Show me the departures at Frankfurt Hbf today at 3 PM"
Accessibility: "Is barrier-free travel from Frankfurt to Köln possible right now?", "Which escalators at Köln Hbf are out of order?"
Parking: "What parking is available at Frankfurt (Main) Hbf tomorrow at 10 AM?"
Planning: "What are the major railway hubs in Bavaria?" (uses the
major-hubsresource plus StaDa)
Known quirks and gotchas for the exercises
These are properties of the DB APIs, not bugs in the server. They are good material for observing how an agent copes with imperfect tools.
StaDa name search is literal. The
searchstringparameter matches the official station name and supports*wildcards; there is no fuzzy matching.Berlin Hbf-> no result.Berlin HauptbahnhoforBerlin*-> works.Frankfurt HbfandFrankfurt Hauptbahnhof-> no result;Frankfurt (Main) Hbf,Frankfurt*or*Frankfurt*Hbf*-> works. Watch whether your agent retries with a wildcard on its own.The Timetables
/planendpoint only serves the current day. For any other date the DB API answers HTTP 404 with an empty body.get_planned_timetableturns that into{"stops": [], "note": "...returned 404 for this date/hour..."}instead of an opaque error (upstream behaviour), so the agent learns why and can pick today's date. For hours with no trains (or stations without plan data) you getstops: []plus a note as well.Date and hour format:
dateisYYMMDD(date +%y%m%din a shell),houris two digitsHH. LLMs like to pass2026-10-06or14:00; the DB API answers those with the same HTTP 404 as a wrong date, so you getstops: []plus the 404 note. If an agent keeps getting that note for "today", check the format it used.Best demo station: Frankfurt (Main) Hbf, StaDa number
1866, EVA8000105. It has plan data, live changes, facilities and parking. Berlin Hauptbahnhof (StaDa1071, EVA8011160) frequently returns an empty<timetable/>for plan and changes, which looks like a broken tool if you do not know this.Tool results are prompt text. The timetable tools always return the same shape
{station, eva, stops, note?};noteis only present whenstopsis empty and explains the likely reason. Compare this with the upstream behaviour (bareNone, which the SDK reported as "Error executing tool ...") to see why well-formed empty results matter for agents.Parking identifiers:
get_parking_prognosesneeds the facilityidfromget_parking_by_station/search_parking_facilities; only facilities withhasPrognosis: truehave forecasts.FaSta
statefilter:INACTIVEmeans out of order. An empty list forstate="INACTIVE"is good news, not a failure.Rate limiting (HTTP mode only): 60 requests/minute and 1000/hour per client IP on
/mcp. Agents that loop over many stations will hit it. Stdio mode has no limiter; the DB API itself has quotas on the free plans.
Troubleshooting
Symptom | Cause | Fix |
| You are running the upstream 1.x code against MCP SDK 2.x, or this fork against an SDK 1.x you installed manually. | Use this fork via |
Tool result | Credentials are valid but the application is not subscribed to that API's plan. | Subscribe to StaDa, Timetables, FaSta and Parking in the DB portal (Step 1). Each API needs its own subscription. |
Tool result | Wrong or empty | Re-check |
| Something else (often Docker Desktop) listens on 8080. |
|
MCP client says | The client process does not have your shell | Put the absolute path from |
Client shows the server as "connecting" for a long time on first start | uv is creating | Wait, or run the stdio smoke test once in a terminal so the venv exists before the client starts it. |
Timetable tool returns | No data for that station/date/hour: wrong date format, not today's date, or a station without plan data. | Read the note. Use |
| Literal search, no fuzzy matching. | Use the official name ( |
|
| Connect via |
HTTP client cannot connect to |
| Use |
Lots of DEBUG log lines in the client's MCP log |
| Normal. Lower the level in |
What changed compared to upstream
This clone diverges from PaulvonBerg/db-mcp-server@74852f7 in the following ways (git status --short / git diff --stat in the repo show the exact files):
# | Change | Why | Files |
1 | Ported to MCP Python SDK 2.x ( | Upstream targets SDK 1.x; the renamed modules raise |
|
2 |
| The README already documented |
|
3 | New | Lets MCP clients start the server themselves ( |
|
4 | New |
|
|
5 | Well-formed empty timetable results. New | Upstream returned |
|
6 | Cloud Run deployment files removed. | This fork is for local use from an MCP client; the container deployment is documented upstream (see below). |
|
Configuration reference
All settings come from environment variables; locally they are read from .env next to config.py.
Variable | Required | Meaning |
| yes | DB API Marketplace Client ID of your application |
| yes | DB API Marketplace API Key / Secret of your application |
| no | Listen port for |
| no | Google Cloud project; only used when running on Cloud Run ( |
| no | Extra allowed host / CORS origin for a deployed HTTP instance. |
| no | Backend URL of a Cloud Run service, added to the allowed hosts. |
Logging is configured in mcp_server.py (DEBUG to stderr); there is no LOG_LEVEL switch in the code.
Deploying as a remote server (optional, upstream)
The upstream project was built to run on Google Cloud Run behind HTTPS, with credentials in Secret Manager and an (unfinished) OAuth 2.1 layer. This fork removed the Dockerfile and .gcloudignore, but the HTTP server in main.py still has everything a remote deployment needs:
config.py: whenK_SERVICEis set (Cloud Run),DB_API_KEY/DB_API_SECRETare read from Secret Manager secrets of the same name inGCP_PROJECT_ID.main.py:TrustedHostMiddleware, CORS (incl.https://claude.ai), security headers and the in-memory rate limiter fromrate_limiter.py;CUSTOM_DOMAIN/CLOUD_RUN_URLextend the allowed hosts.auth_server.py: placeholder router; OAuth is not enforced.Clients connect to
https://<your-host>/mcpwith an HTTP MCP entry, or vianpx -y mcp-remote <url> --transport http-onlyfor clients without native HTTP support.
If you want to containerize it, the upstream Dockerfile is three lines (python:3.11-slim, pip install -r requirements.txt, uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}) and the step-by-step gcloud run deploy / gcloud secrets create instructions are in the upstream README. Use this fork's pinned requirements.txt when building, otherwise the image gets whatever mcp version is current.
Project layout
db-mcp-server/
├── stdio_main.py # stdio entry point for MCP clients (new in this fork)
├── main.py # FastAPI + Streamable HTTP entry point (/mcp, /health), honours PORT
├── mcp_server.py # logging setup, imports/registers tools, resources, prompts
├── server_instance.py # the shared MCPServer instance ("deutschebahn_mcp_server")
├── config.py # loads .env, API base URLs, Secret Manager branch for Cloud Run
├── utils.py # fetch_from_db_api (httpx + xmltodict), validation, tool_error_handler
├── models.py # Pydantic models (StaDaStation, Facility, ParkingFacility, ...)
├── rate_limiter.py # in-memory sliding-window limiter (HTTP mode only)
├── auth_server.py # OAuth placeholder (not active)
├── tools/
│ ├── station_tools.py # ping, get_station_by_name, get_stations_by_position
│ ├── timetable_tools.py # get_planned_timetable, get_recent_/get_full_timetable_changes
│ ├── parking_tools.py # get_parking_by_station, search_parking_facilities, get_parking_prognoses
│ └── facility_tools.py # find_facilities, get_facilities_by_station, S-Zentralen tools
├── resources/travel_resources.py # the 5 file:// reference resources
├── prompts/travel_prompts.py # the 7 prompts
├── OpenAPI definitions/ # DB API specs (StaDa, Timetables, FaSta, Parking, CallaBike)
├── pyproject.toml, uv.lock # pinned dependencies for `uv run` (new in this fork)
├── requirements.txt # same pins for plain pip installs
└── .env.example # template for .env (copy, then fill in DB_API_KEY / DB_API_SECRET)License and credits
This project is licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0), see LICENSE. It is based on the Deutsche Bahn MCP Server by Paul von Berg, https://github.com/PaulvonBerg/db-mcp-server; if you reuse this code or its ideas, please keep that attribution:
Based on Deutsche Bahn MCP Server by Paul von Berg
https://github.com/PaulvonBerg/db-mcp-serverData attribution: the underlying APIs and data are provided by Deutsche Bahn AG via the DB API Marketplace. StaDa, Timetables and FaSta data are CC BY 4.0; Parking Information data is licensed under "Datenlizenz Deutschland – Namensnennung – Version 2.0" (dl-de/by-2-0), may not be modified in content, and requires the attribution "Parking Information Daten der DB BahnPark – API über den DB API Marketplace". Users must comply with the DB API terms of service. This software is not affiliated with Deutsche Bahn AG.
Thanks to Deutsche Bahn for the public APIs, to Anthropic for the Model Context Protocol, and to the MCP Python SDK team. See CONTRIBUTING.md for upstream's contribution guidelines.
This server cannot be deployed
Maintenance
Related MCP Connectors
Gateway between LLM agents and world data through eight tools and a bundled endpoint catalog.
TravelMind: 8 MCP tools for travel (12306 trains, flights, hotels, geocode, planning, policy).
Machine-readable utilities and datasets for AI agents.
- geoOAuthco.thinair
Geocoding, routing, isochrones, traffic, weather, and place search for AI agents. 19 MCP tools.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceProvides unified access to Deutsche Bahn APIs for real-time railway data, station information, timetables, disruptions, parking, and accessibility services.21-
- AlicenseAqualityDmaintenanceAn MCP server that exposes the Deutsche Bahn public transport API to any MCP-compatible client (Claude Desktop, Cursor, Cline, Continue, etc.). Five tools cover station search, departures, journey planning, trip details, and nearby stations.6MIT
- AlicenseAqualityCmaintenanceAn (unofficial) MCP server for the Deutsche Bahn Timetables API — station search, planned departures, and real-time changes (delays, platform changes, cancellations) as tools for Claude and other MCP clients.42MIT
- AlicenseNot gradedqualityBmaintenanceProvides real-time Belgian rail (SNCB/NMBS) data via the iRail API, enabling AI agents to query train schedules and live information.206 npmMIT