Skip to main content
Glama
typex1
by typex1

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

  1. Prerequisites

  2. Step 1: Get DB API credentials

  3. Step 2: Configure the server

  4. Step 3a: Run it from an MCP client (recommended, stdio)

  5. Step 3b: Run it as an HTTP server (optional)

  6. Step 4: Verify

  7. Tool, prompt and resource reference

  8. Known quirks and gotchas for the exercises

  9. Troubleshooting

  10. What changed compared to upstream

  11. Configuration reference

  12. Deploying as a remote server (optional, upstream)

  13. Project layout

  14. License and credits

Related MCP server: db-mcp

Prerequisites

  • Python 3.11 or newer. You do not have to install it yourself: uv downloads 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 | sh

    Check 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. curl and python3 are enough for the smoke tests.

  • git to 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 below

Step 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.

  1. Register / log in at https://developers.deutschebahn.com.

  2. Create an application ("Anwendung") in your account area. The portal shows you two values for it:

    • Client ID -> goes into DB_API_KEY

    • API Key / Client Secret -> goes into DB_API_SECRET

    (Yes, the names are confusing: the variable called DB_API_KEY holds the Client ID, and DB_API_SECRET holds the thing the portal calls API Key.)

  3. 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, resources

    Timetables

    Free

    get_planned_timetable, get_recent_timetable_changes, get_full_timetable_changes

    FaSta - Station Facilities Status

    Free4All

    find_facilities, get_facilities_by_station

    Parking Information // DB Bahnpark (marked deprecated)

    Testzugang

    get_parking_by_station, search_parking_facilities, get_parking_prognoses

    The 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 .env

Open .env in an editor and fill in the two required values:

Key in .env

Value from the portal

DB_API_KEY

the application's Client ID

DB_API_SECRET

the application's API Key / Secret

GCP_PROJECT_ID

leave empty for local use (Cloud Run only)

CUSTOM_DOMAIN

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 ignored

config.py loads .env from the repository directory itself, so the server finds it no matter which working directory the MCP client starts it from.

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. --directory makes uv change into the repo before running, so the relative stdio_main.py and the .env file are found.

  • If the client reports uv: command not found or spawn uv ENOENT, it does not see your shell's PATH. Put the full path into "command": run which uv in a terminal (typically /Users/<you>/.local/bin/uv or /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 / args snippet 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.py

The 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, not localhost. The server listens on IPv4 only, and on many machines localhost resolves to the IPv6 ::1 first, 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 with PORT=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_szentralen

Drop 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. Expect Frankfurt (Main) Hbf, station number 1866, EVA numbers 8000105 and 8098105. If the agent searches for the literal string Frankfurt Hauptbahnhof it gets zero results and should retry with Frankfurt* or Frankfurt (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

ping()

Liveness check, returns {}

none

none

get_station_by_name(name, limit=10)

Search station master data by (official) name or wildcard pattern. Returns name, station number, evaNumbers, category, services, accessibility flags.

name (see quirks), limit <= 50

StaDa

get_stations_by_position(latitude, longitude, radius=2.0, limit=10)

Stations around a coordinate, with distance (km). Pre-filters by federal state.

radius in km

StaDa

get_planned_timetable(eva_number, date, hour)

Scheduled arrivals/departures for one hour slice. Returns {station, eva, stops, note?}.

date = YYMMDD, hour = HH (24h)

Timetables /plan

get_recent_timetable_changes(eva_number)

Changes (delays, cancellations, platform changes) from the last 2 minutes. Same result shape.

EVA number as string

Timetables /rchg

get_full_timetable_changes(eva_number)

All currently known changes for the station. Same result shape.

EVA number as string

Timetables /fchg

get_parking_by_station(stop_place_id)

Parking facilities at a station by ID (accepts EVA number, StaDa number, RIL100 code or DHID).

stop_place_id, e.g. "8000105", "1866", "FF"

Parking

search_parking_facilities(station_name, with_passenger_relevance=True)

Parking facilities by station name (API-side stationName filter).

official name, e.g. Frankfurt (Main) Hbf

Parking

get_parking_prognoses(facility_id, datetime)

Occupancy forecast up to 2 h ahead for one facility.

facility_id from the two tools above, datetime ISO 8601 (2025-07-24T14:30:00)

Parking

find_facilities(station_number, type=None, state=None, equipment_number=None)

Elevators / escalators at a station with optional filters.

type ELEVATOR or ESCALATOR; state ACTIVE, INACTIVE, UNKNOWN

FaSta

get_facilities_by_station(station_number)

Station overview incl. all facilities with description, state and coordinates.

StaDa station number (e.g. 1866)

FaSta

get_szentralen_by_location(latitude, longitude, radius=10000)

DB mobility service centres (S-Zentralen) near a coordinate.

radius in metres

StaDa

search_szentralen(limit=50, offset=0)

Paginated list of all S-Zentralen.

limit, offset

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

accessibility_check

start_station, end_station

parking_prognosis

station_name, datetime

recent_timetable_changes

station_name

planned_timetable_window

station_name, date, start_time, end_time

station_services

station_name

nearby_stations

latitude, longitude, radius=2.0

current_disruptions

station_name

Each prompt returns a user message that instructs the model which tools to chain (defined in prompts/travel_prompts.py).

Resources (5)

URI

Content

file://reference/station-categories

What StaDa station categories 1-7 mean

file://reference/train-types

ICE, IC, RE, RB, S, ... explained

file://stations/major-hubs

Category 1/2 hubs, fetched live from StaDa

file://services/accessibility-guide

Accessibility services at DB stations

file://status/current-disruptions

Live summary of known timetable changes at six major hubs (Frankfurt, Berlin, München, Hamburg, Köln, Hannover) via Timetables /fchg

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-hubs resource 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 searchstring parameter matches the official station name and supports * wildcards; there is no fuzzy matching. Berlin Hbf -> no result. Berlin Hauptbahnhof or Berlin* -> works. Frankfurt Hbf and Frankfurt Hauptbahnhof -> no result; Frankfurt (Main) Hbf, Frankfurt* or *Frankfurt*Hbf* -> works. Watch whether your agent retries with a wildcard on its own.

  • The Timetables /plan endpoint only serves the current day. For any other date the DB API answers HTTP 404 with an empty body. get_planned_timetable turns 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 get stops: [] plus a note as well.

  • Date and hour format: date is YYMMDD (date +%y%m%d in a shell), hour is two digits HH. LLMs like to pass 2026-10-06 or 14:00; the DB API answers those with the same HTTP 404 as a wrong date, so you get stops: [] 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, EVA 8000105. It has plan data, live changes, facilities and parking. Berlin Hauptbahnhof (StaDa 1071, EVA 8011160) 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?}; note is only present when stops is empty and explains the likely reason. Compare this with the upstream behaviour (bare None, which the SDK reported as "Error executing tool ...") to see why well-formed empty results matter for agents.

  • Parking identifiers: get_parking_prognoses needs the facility id from get_parking_by_station / search_parking_facilities; only facilities with hasPrognosis: true have forecasts.

  • FaSta state filter: INACTIVE means out of order. An empty list for state="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

ImportError: cannot import name 'McpError' from 'mcp' or ModuleNotFoundError: No module named 'mcp.server.fastmcp'

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 uv run --directory ... stdio_main.py (pinned mcp==2.3.0), or pip install -r requirements.txt into a fresh venv. Do not pip install mcp unpinned.

Tool result External API error (HTTP 403); server log shows "Not registered to plan"

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 External API error (HTTP 401) or API credentials are not configured

Wrong or empty DB_API_KEY / DB_API_SECRET.

Re-check .env (Client ID -> DB_API_KEY, API Key -> DB_API_SECRET). Restart the server after editing.

[Errno 48] address already in use (Errno 98 on Linux) when starting main.py

Something else (often Docker Desktop) listens on 8080.

PORT=8081 uv run --directory ... main.py and use http://127.0.0.1:8081/mcp in the client.

MCP client says uv: command not found / spawn uv ENOENT

The client process does not have your shell PATH.

Put the absolute path from which uv into "command".

Client shows the server as "connecting" for a long time on first start

uv is creating .venv and installing dependencies.

Wait, or run the stdio smoke test once in a terminal so the venv exists before the client starts it.

Timetable tool returns stops: [] with a note

No data for that station/date/hour: wrong date format, not today's date, or a station without plan data.

Read the note. Use YYMMDD and today's date; try Frankfurt Hbf 8000105.

get_station_by_name returns []

Literal search, no fuzzy matching.

Use the official name (Frankfurt (Main) Hbf) or a wildcard (Frankfurt*).

Invalid host header in HTTP mode

TrustedHostMiddleware only allows localhost and 127.0.0.1 (plus CUSTOM_DOMAIN).

Connect via 127.0.0.1, or set CUSTOM_DOMAIN.

HTTP client cannot connect to http://localhost:8081/mcp

localhost resolved to IPv6 ::1; the server listens on IPv4.

Use http://127.0.0.1:8081/mcp.

Lots of DEBUG log lines in the client's MCP log

mcp_server.py configures logging.basicConfig(level=DEBUG) to stderr.

Normal. Lower the level in mcp_server.py if it bothers you.

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 (mcp==2.3.0): mcp.server.fastmcp.FastMCP -> mcp.server.mcpserver.MCPServer, McpError -> MCPError; utils.tool_error_handler now raises mcp.server.mcpserver.exceptions.ToolError, so a failing tool reaches the LLM as an isError result with a readable message instead of crashing the request.

Upstream targets SDK 1.x; the renamed modules raise ImportError on 2.x. McpError(str) was also wrong on 1.x (it expected an ErrorData).

server_instance.py, mcp_server.py, utils.py

2

main.py honours the PORT env var (default 8080).

The README already documented PORT, the code ignored it. Needed because 8080 is often taken.

main.py

3

New stdio_main.py: mcp.run(transport="stdio").

Lets MCP clients start the server themselves (command/args), no separate HTTP process. main.py (Streamable HTTP at /mcp, /health) is unchanged in behaviour.

stdio_main.py (new)

4

New pyproject.toml + uv.lock with pinned dependencies and [tool.uv] package = false; requirements.txt pinned to the same versions (mcp==2.3.0, httpx==0.28.1, xmltodict==1.0.4, pydantic==2.13.5, python-dotenv==1.2.4, fastapi==0.142.2, uvicorn[standard]==0.54.0, google-cloud-secret-manager==2.31.0, Authlib==1.8.0).

uv run --directory <repo> stdio_main.py creates the venv and installs everything on first run. Teaching point: upstream's unpinned mcp silently pulled in 2.x and broke the 1.x code the day the SDK released a new major version. Pin what you ship. package = false because the repo is a flat set of modules, not an installable package.

pyproject.toml, uv.lock (new), requirements.txt

5

Well-formed empty timetable results. New _normalize_timetable helper; all three timetable tools return {station, eva, stops, note?}, handle <timetable/>, empty bodies and missing stops gracefully, and a single stop is normalised to a one-element list. get_planned_timetable maps the upstream HTTP 404 for non-current dates to the same shape with an explanatory note. fetch_from_db_api returns {} for an empty XML body instead of logging a parse error.

Upstream returned None for empty responses, which the SDK's output validation turned into an opaque "Error executing tool ...". Tool results are prompt text for the LLM: an empty result without explanation makes agents conclude the tool is broken.

tools/timetable_tools.py, utils.py

6

Cloud Run deployment files removed. Dockerfile and .gcloudignore are gone. auth_server.py (a placeholder, OAuth is not active) and the Secret Manager branch in config.py are untouched because main.py imports them.

This fork is for local use from an MCP client; the container deployment is documented upstream (see below).

Dockerfile, .gcloudignore (deleted)

Configuration reference

All settings come from environment variables; locally they are read from .env next to config.py.

Variable

Required

Meaning

DB_API_KEY

yes

DB API Marketplace Client ID of your application

DB_API_SECRET

yes

DB API Marketplace API Key / Secret of your application

PORT

no

Listen port for main.py (HTTP mode). Default 8080.

GCP_PROJECT_ID

no

Google Cloud project; only used when running on Cloud Run (K_SERVICE set), where secrets come from Secret Manager instead of .env.

CUSTOM_DOMAIN

no

Extra allowed host / CORS origin for a deployed HTTP instance.

CLOUD_RUN_URL

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: when K_SERVICE is set (Cloud Run), DB_API_KEY / DB_API_SECRET are read from Secret Manager secrets of the same name in GCP_PROJECT_ID.

  • main.py: TrustedHostMiddleware, CORS (incl. https://claude.ai), security headers and the in-memory rate limiter from rate_limiter.py; CUSTOM_DOMAIN / CLOUD_RUN_URL extend the allowed hosts.

  • auth_server.py: placeholder router; OAuth is not enforced.

  • Clients connect to https://<your-host>/mcp with an HTTP MCP entry, or via npx -y mcp-remote <url> --transport http-only for 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-server

Data 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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    An 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.
    6
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    An (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.
    4
    2
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides real-time Belgian rail (SNCB/NMBS) data via the iRail API, enabling AI agents to query train schedules and live information.
    206 npm
    MIT