rhein-ruhr-fahrplan-mcp

MCP-Server für ÖPNV-Fahrplandaten im Rhein-Ruhr-Gebiet (Datenbasis: Open Data des Verkehrsverbunds Rhein-Ruhr).

Documentation

rhein-ruhr-fahrplan-mcp

PyPI Python wheel Downloads MCP GTFS + EFA uv

MCP-Server für ÖPNV-Fahrplandaten im Rhein-Ruhr-Gebiet (Datenbasis: Open Data des Verkehrsverbunds Rhein-Ruhr). Auf PyPI: https://pypi.org/project/rhein-ruhr-fahrplan-mcp/

Zwei Datenpfeiler, beide keyless:

PfeilerQuelleInhalt
Sollfahrplan (optional)VRR-GTFS (Open Data ÖPNV, CKAN-API) → SQLiteNur die konfigurierten Verkehrsunternehmen (FAHRPLAN_AGENCY_IDS); z. B. NIAG: 123 Routen, 6.975 Fahrten, 3.869 Haltestellen
Echtzeit (immer aktiv)VRR-EFA efa.vrr.deLive-Abfahrten/-Ankünfte mit Prognose, Verspätungen, Steige, Störungshinweise – im gesamten Verbundgebiet

Default = Live-Only: Ohne Konfiguration startet der Server sofort und liefert alle Live-Tools über die EFA-Schnittstelle. GTFS-Tools (Sollplan, Haltestellen-Index) melden dann einen klaren Hinweis mit Einrichtungsanleitung – bewusst so, weil ein ungefilterter Import die komplette Datenbank (~500 MB, ~7 Mio. Stop-Times) anlegen würde.

Tools (9)

ToolQuelleZweck
next_departuresEFA (live)Live-Abfahrten mit Echtzeitprognose
next_arrivalsEFA (live)Live-Ankünfte mit Echtzeitprognose
plan_connectionEFA (live)Verbindungen von A nach B im Gesamtnetz
line_disruptionsEFA (live)Aktuelle Störungen/Baustellen/Umleitungen
find_stopGTFS → EFA-FallbackHaltestelle finden (DHID auflösen)
stop_scheduleGTFSSollfahrplan (auch morgen/übermorgen)
sync_statusGTFSFeed-Status: Version, Gültigkeit, Zeilenzahlen
update_feedGTFSFeed aktualisieren (nur bei neuer Ressource – idempotent)
get_default_startGTFS → EFAKonfigurierte Standard-Start-Haltestelle (FAHRPLAN_DEFAULT_START) auflösen

Schnellstart

Variante A: uvx (stdio, ohne Docker)

uvx rhein-ruhr-fahrplan-mcp

Oder dauerhaft installiert:

uv tool install rhein-ruhr-fahrplan-mcp
rhein-ruhr-fahrplan-mcp
  • Startet sofort (Live-Only). Für den GTFS-Sollplan einmalig die Umgebungsvariablen setzen (siehe unten) und update_feed aufrufen bzw. Server neu starten – der Import läuft dann automatisch im Hintergrund (~1–2 min, 39 MB Download).
  • Datenbank-Pfad fest: ~/.local/share/rhein-ruhr-fahrplan-mcp/fahrplan.db (XDG, cwd-unabhängig; via FAHRPLAN_DB_PATH überschreibbar).

mcphub/Client-Registrierung (stdio-Typ):

{
  "fahrplan-mcp": {
    "type": "stdio",
    "command": "uvx",
    "args": ["rhein-ruhr-fahrplan-mcp"],
    "env": {"FAHRPLAN_AGENCY_IDS": "nia-25"}
  }
}

(env optional – ohne Agencies reiner Live-Only-Betrieb.)

Variante B: Docker (Streamable HTTP)

docker compose up -d --build

Im Hub als Streamable-HTTP-Server registrieren (Netz-Anbindung via compose.override.yaml, siehe Deployment):

http://fahrplan-mcp:8080/mcp

Konfiguration: Verkehrsunternehmen (FAHRPLAN_AGENCY_IDS)

Der GTFS-Import filtert auf die agency_ids des VRR-Feeds. Setze eine oder mehrere:

# z. B. nur NIAG (Kreise Kleve & Wesel):
export FAHRPLAN_AGENCY_IDS=nia-25

# oder mehrere Unternehmen:
export FAHRPLAN_AGENCY_IDS=nia-25,swk-02,dvg-20

in compose.yaml:

    environment:
      - FAHRPLAN_AGENCY_IDS=nia-25
      # - FAHRPLAN_OPERATOR=NIAG   # optional: Live-Abfahrten auch filtern

Ohne FAHRPLAN_AGENCY_IDS:

  • Live-Tools (next_departures, next_arrivals, plan_connection, line_disruptions) funktionieren normal (ganzes Gebiet, alle Unternehmen)
  • GTFS-Tools (find_stop mit Index, stop_schedule, sync_status) und der Auto-Import antworten mit einem Konfigurationshinweis statt zu importieren

Verkehrsunternehmen (Auswahl)

Die agency_ids stammen aus dem VRR-GTFS-Feed (agency.txt, Stand August 2026). Häufige Unternehmen:

agency_idUnternehmenGebiet/Angebot
nia-25Niederrheinische VerkehrsbetriebeBus, Kreise Kleve & Wesel
dvg-20Duisburger VerkehrsgesellschaftBus/Tram Duisburg
swk-02SWK MOBILKrefeld
new-80NEW mobil und aktiv M'gladbachMönchengladbach
vvg-65NEW mobil und aktiv ViersenViersen
btm-70, rbg-70RheinbahnDüsseldorf
bgs-00, bgs-30–bgs-34BOGESTRABochum/Gelsenkirchen
btm-13, eva-10–eva-12RuhrbahnEssen/Mülheim
dsw-36–dsw-39DSW21Dortmund
sto-15, btm-15, eva-15STOAGOberhausen
wsw-16, wsw-66WSW mobilWuppertal
hst-00, hst-50Hagener StraßenbahnHagen
sws-18Stadtwerke SolingenSolingen
swr-16Stadtwerke RemscheidRemscheid
ver-45VEREnnepe-Ruhr-Kreis
ves-40VestischeRecklinghausen/Marl
bvr-88BVR Busverkehr RheinlandRheinland-Bus
bsm-76Bahnen der Stadt MonheimMonheim
hcr-35Straßenbahn Herne-CastropHerne/Castrop
swn-60Stadtwerke NeussNeuss
ddb-8003DB Regio NRWRegionalzug NRW
ddb-NXNational ExpressRegionalzug Ruhrgebiet
ddb-RRRheinRuhrBahnRegionalzug
ddb-R2eurobahnRegionalzug
ddb-W3WestfalenBahnRegionalzug
ddb-N2NordWestBahnRegionalzug
ddb-M2REGIOBAHNRegionalzug

Rail-Freight-/Charter-Einträge im Feed (Train Charter Services, Train Rental International, Ulmer Eisenbahnfreunde u. a.) sind für den Personenverkehr nicht relevant.

Komplette Liste: agency.txt aus dem aktuellen Feed (Download via CKAN-API, siehe unten) – alle 58 IDs mit Namen. Ein Blick per Python:

python3 -c "import csv; [print(r['agency_id'], '|', r['agency_name'])
             for r in csv.DictReader(open('agency.txt'))]"

Architektur

┌─────────────────────── Server (stdio | http) ─────────────────────────┐
│                                                                        │
│  FastMCP                                                               │
│    ├── Live: EFA-Client (urllib, keyless)  ← immer aktiv                │
│    │     efa.vrr.de/standard/XML_DM_REQUEST (rapidJSON)               │
│    │     XML_TRIP_REQUEST2, XML_STOPFINDER_REQUEST                    │
│    └── Soll: GTFS-SQLite (nur mit FAHRPLAN_AGENCY_IDS) ← Auto-Update  │
│          CKAN-API (alle 6 h) → bei Neuheit: atomarer Import            │
└────────────────────────────────────────────────────────────────────────┘

Eigenschaften

  • Live-Only-Default: Ohne Konfiguration voll nutzbar (Live-Tools); GTFS-Teil schaltet sich mit FAHRPLAN_AGENCY_IDS dazu.
  • Selbst-aktualisierend (mit Agencies): Daemon-Thread prüft alle FAHRPLAN_FEED_CHECK_INTERVAL Sekunden (Default 6 h) die CKAN-API; Import nur bei neuer Ressource – atomar per .tmp + os.replace(). Initial- und Auto-Import laufen über dasselbe Lock (kein Doppel-Import).
  • Operator-Filter für Live-Daten: FAHRPLAN_OPERATOR (z. B. NIAG) filtert next_departures/next_arrivals; plan_connection sortiert passend bevorzugt. Leer = alle Unternehmen.
  • Keine API-Keys, keine Accounts. VRR-Open-Data-Nutzungsbedingungen: Test-/Hobby-/Entwicklungsnutzung erlaubt; bei dauerhafter öffentlicher Nutzung Mail an openvrr@vrr.de.

Deployment (Docker)

Der HTTP-Modus lauscht nur im Container-Netz auf :8080/mcp (kein Host-Port). Für die Anbindung an einen MCP-Hub in einem bestehenden Docker-Netz eine lokale Datei compose.override.yaml neben compose.yaml anlegen:

services:
  fahrplan-mcp:
    networks:
      - mein-hub-netz   # Name des Netzes, in dem der Hub läuft

networks:
  mein-hub-netz:
    external: true

docker compose up -d merged das Override automatisch. Die Override-Datei ist umgebungsspezifisch und gehört nicht ins Repository (bereits in .gitignore).

Konfiguration (Env)

VariableDefaultBedeutung
FAHRPLAN_AGENCY_IDS(leer)Kommaseparierte GTFS-Agency-IDs für den Sollplan-Import. Leer = Live-Only; GTFS-Tools antworten mit Konfigurationshinweis
FAHRPLAN_DEFAULT_START(leer)Standard-Start-Haltestelle (DHID, EFA-ID oder eindeutiger Name – passende IDs liefert find_stop); abrufbar via get_default_start
FAHRPLAN_OPERATOR(leer)Live-Operator-Filter (z. B. NIAG); leer = alle Unternehmen
FAHRPLAN_DB_PATH~/.local/share/rhein-ruhr-fahrplan-mcp/fahrplan.dbSQLite-DB-Pfad (Dockerfile: /data/fahrplan.db)
FAHRPLAN_MCP_TRANSPORTstdiostdio oder http (Dockerfile setzt http)
FAHRPLAN_MCP_HOST / FAHRPLAN_MCP_PORT0.0.0.0:8080 (Dockerfile)nur http-Modus
FAHRPLAN_FEED_CHECK_INTERVAL21600 (6 h)CKAN-Prüfung; 0 deaktiviert Auto-Update

Verifizierte EFA-Eigenheiten (Pitfalls)

  • outputFormat=rapidJSON: Response-Keys heißen stopEvents/journeys (nicht departureList/trips).
  • Freie Haltestellennamen können mehrdeutig sein → Antwort mit locations[]-Vorschlägen und leeren Events. Deshalb: IMMER per DHID/ID auflösen.
  • …_Parent-Suffix aus GTFS parent_station wird von EFA nicht akzeptiert → DHID ohne Suffix.
  • Bei Ankünften (itdTripDateTimeDepArr=arr) heißen die Zeitfelder trotzdem departureTimePlanned/-Estimated.
  • Der EFA liefert alle Zeiten mit Z-Suffix (UTC) – der Client konvertiert sie nach Europe/Berlin, bevor er sie ausgibt (früher zeigten die Tools versehentlich UTC, was wie ein ignorierter departure_time-Parameter aussah; gefixt in v1.2.1).
  • Verspätungen werden als UTC-Differenz berechnet – zeitzonenunabhängig korrekt.
  • MS_REQUEST (Meldungs-Endpoint) liefert HTTP 400 → Störungen aus den eingebetteten infos[] der DM-Antworten extrahieren.
  • calcNumberRequests greift in rapidJSON nicht zuverlässig → plan_connection kann mehr Verbindungen liefern als max_results (Ergebnisse korrekt, nur Begrenzung weich).

GTFS-Feed-Details

  • CKAN-Dataset: 496eea5d-d6ef-4dc2-aeb0-d15c4fbf3178 („Soll-Fahrplandaten VRR“), API: https://opendata.ruhr/api/3/action/package_show?id=…
  • Ressourcen-UUID wechselt monatlich → Import ermittelt sie immer frisch per CKAN-API.
  • Filter: agency_id IN (FAHRPLAN_AGENCY_IDS); transitiv referenzierende Zeilen bleiben erhalten.
  • stop_times.txt des Gesamtfeeds (~7 Mio. Zeilen) wird gestreamt (erste Listen-Version wurde OOM-killed).
  • Ungefilterter Gesamtfeed wäre ~500 MB Datenbank – deshalb bewusst kein Default-Import.

Entwicklung

git clone … && cd rhein-ruhr-fahrplan-mcp
uv venv && source .venv/bin/activate
uv pip install -e .
rhein-ruhr-fahrplan-mcp                   # stdio starten (Live-Only)
FAHRPLAN_AGENCY_IDS=nia-25 rhein-ruhr-fahrplan-mcp   # mit GTFS-Sollplan
python3 tests/stdio_e2e.py $(command -v rhein-ruhr-fahrplan-mcp)      # stdio-E2E
FAHRPLAN_AGENCY_IDS=nia-25 python3 tests/stdio_fullcycle.py $(command -v rhein-ruhr-fahrplan-mcp)  # inkl. Autoimport

Releases: Version in pyproject.toml hochziehen → Commit → git tag vX.Y.Z && git push origin vX.Y.Z – die GitHub-Action published automatisch nach PyPI (Trusted Publishing).