uestra-mcp
# uestra-mcp
MCP-Server (stdio) für Fahrplandaten von ÜSTRA und GVH in der Region Hannover. Er stellt drei Tools bereit: Haltestellensuche, Live-Abfahrten mit Echtzeit und Verbindungssuche.
Repository: <https://github.com/Schneggox/uestra-mcp>
## Datenquelle
Die Daten kommen von der EFA-Schnittstelle der Region Hannover (`https://www.efa.de/efa`, Betreiber HannIT/Region Hannover, Software MENTZ EFA). Abgefragt wird `outputFormat=rapidJSON`, ohne API-Key und ohne Anmeldung. Es wird nichts gescrapt.
Die Schnittstelle ist offen erreichbar, aber nicht offiziell dokumentiert. Parameter, Fehlercodes und Antwortformate können sich ohne Ankündigung ändern. Das Verhalten in diesem Server ist gegen die Live-Schnittstelle gemessen, nicht gegen eine Spezifikation.
## Voraussetzungen
- Node.js 20 oder neuer
- Internetzugang zu `www.efa.de`
## Setup
```sh
git clone https://github.com/Schneggox/uestra-mcp.git
cd uestra-mcp
npm install
npm run build
```
`npm run build` schreibt nach `dist/` (nicht eingecheckt). Der Server startet mit `npm start` bzw. `node dist/index.js`; im Normalbetrieb startet ihn der MCP-Client selbst.
### Smoke-Test
```sh
npm run smoke
```
Der Smoke-Test ruft ohne MCP-Client die Tool-Logik gegen die Live-EFA auf: Suche nach "Kröpcke", Abfahrten an `de:03241:11` (Kröpcke) und eine Verbindung von `de:03241:11` nach `de:03241:31` (Hauptbahnhof). Er gibt die Ergebnisse als Text aus und endet mit Exit-Code 1, wenn eine Antwort leer ist oder ein Aufruf fehlschlägt.
## Einbindung in Claude Code
```sh
claude mcp add -s user uestra -- node <pfad-zum-repo>/dist/index.js
```
Beispiel unter Windows (Vorwärts-Slashes):
```sh
claude mcp add -s user uestra -- node C:/Users/sk8er/ClaudeWorkspace/uestra-mcp/dist/index.js
```
Ohne `-s user` gilt der Standard-Scope `local`: der Server ist dann nur in dem Projektverzeichnis verfügbar, in dem der Befehl ausgeführt wurde. Kontrolle:
```sh
claude mcp list
```
Die Zeile zum Server muss so aussehen:
```
uestra: node C:/Users/sk8er/ClaudeWorkspace/uestra-mcp/dist/index.js - ✔ Connected
```
Entfernen: `claude mcp remove uestra -s user`.
### Andere MCP-Clients
Für Clients mit `mcpServers`-Konfiguration (z. B. Claude Desktop, `claude_desktop_config.json`):
```json
{
"mcpServers": {
"uestra": {
"command": "node",
"args": ["<pfad-zum-repo>/dist/index.js"]
}
}
}
```
Unter Windows den Pfad mit Vorwärts-Slashes oder doppelten Backslashes schreiben.
## Tools
Jedes Tool liefert einen Text-Block (lesbar, Zeiten in Berliner Zeit) und zusätzlich `structuredContent` (maschinenlesbar). Zeitstempel im `structuredContent` sind ISO 8601 in UTC (`...Z`), nicht Berliner Zeit. Bei Fehlern kommt `isError: true` mit dem Text `Fehler: <Meldung>`.
Eine Haltestelle kann überall als Name oder als globale ID im Format `de:03241:11` angegeben werden. Namen werden über die Haltestellensuche automatisch zur ID des besten Treffers aufgelöst (Treffer aus der Region Hannover vor allen anderen) und führen ohne Treffer zu `Fehler: Keine Haltestelle zu "…" gefunden`; eine unbekannte `de:`-ID bei `departures` zu `Fehler: Haltestelle <ID> unbekannt`; wo es auf Genauigkeit ankommt, vorher mit `stop_search` die ID bestimmen.
### stop_search
Sucht Haltestellen nach Name. Liefert Name, Stop-ID, Ort, Typ und Koordinaten. Zurückgegeben werden nur Treffer vom Typ `stop`, sortiert nach Region (IDs `de:03241:…` zuerst), dann EFA-Bestwert. Ohne Treffer antwortet das Tool mit `Keine Haltestellen gefunden.`
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `query` | string, min. 1 Zeichen | (Pflicht) | Haltestellenname oder Teil davon |
| `limit` | Ganzzahl, 1 bis 25 | 8 | Maximale Trefferzahl |
Aufruf: `{ "query": "Kröpcke", "limit": 2 }`
```
1. Hannover, Kröpcke [de:03241:11] Ort: Hannover Typ: stop Koord (lat, lon): 52.37448, 9.73858
2. Hannover, Kröpcke/Theaterstraße [de:03241:1] Ort: Hannover Typ: stop Koord (lat, lon): 52.37381, 9.74221
```
`structuredContent`: `{ "stops": [{ "id", "name", "locality", "type", "lat", "lon" }] }`
### departures
Live-Abfahrten an einer Haltestelle: Sollzeit, Echtzeit (bei Abweichung mit Verspätung in Minuten), Linie, Produkt, Ziel und Steig.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `stop` | string, min. 1 Zeichen | (Pflicht) | Haltestellenname oder ID wie `de:03241:11` |
| `limit` | Ganzzahl, 1 bis 40 | 10 | Maximale Zahl Abfahrten |
Aufruf: `{ "stop": "Hauptbahnhof", "limit": 4 }`
```
Abfahrten Hannover, Hauptbahnhof (de:03241:31)
12:31 -> 12:44 (+13 min) ICE 857 InterCityExpress (Zug) -> S+U Berlin Hauptbahnhof [Gleis 1-8] Steig 9
12:31 -> 12:44 (+13 min) ICE 847 InterCityExpress (Zug) -> S+U Berlin Hauptbahnhof [Gleis 1-8] Steig 9
12:42 Stadtbahn 13 -> Hannover/Fasanenkrug Steig UB2
12:44 Stadtbahn 7 -> Hannover/Wettbergen Steig UB1
```
Lesart: `Soll -> Ist (Differenz)` bei Abweichung, sonst nur die Sollzeit. Hinter der Linie steht das Produkt in Klammern, wenn es nicht schon im Linien-Namen vorkommt. `ENTFAELLT` markiert ausfallende Fahrten, `(nur Fahrplan)` Fahrten ohne Echtzeitdaten.
`structuredContent`: `{ "stop": { "id", "name" }, "departures": [{ "planned", "estimated", "delayMinutes", "line", "product", "destination", "platform", "cancelled", "realtime" }] }`
### journey_planner
Verbindungssuche zwischen zwei Haltestellen, mit Echtzeitdaten (GVH, ÜSTRA, DB). Standard ist Abfahrt jetzt.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `from` | string, min. 1 Zeichen | (Pflicht) | Start: Haltestellenname oder `de:`-ID |
| `to` | string, min. 1 Zeichen | (Pflicht) | Ziel: Haltestellenname oder `de:`-ID |
| `departure` | string, optional | `"now"` | Abfahrtszeit, ISO 8601 (z. B. `2026-10-19T08:15`) oder `"now"` |
| `arrival` | string, optional | nicht gesetzt | Gewünschte Ankunftszeit, ISO 8601. Schließt `departure` aus |
| `results` | Ganzzahl, 1 bis 6 | 3 | Anzahl Verbindungen |
Zeitangaben:
- Ohne Zeitzone (`2026-10-19T08:15`) gilt die Eingabe als Berliner Ortszeit.
- Mit Zone (`Z`, `+02:00`, `+0200` oder `+02`) wird nach Berliner Zeit umgerechnet.
- Erlaubt ist nur `YYYY-MM-DD[THH:MM[:SS]][Zone]` (Trenner `T`, `t` oder Leerzeichen, Stunde und Minute zweistellig). Alles andere wird mit `Fehler: Zeitangabe nicht lesbar, erwartet YYYY-MM-DD[THH:MM][Zone]` abgelehnt.
- Nur ein Datum (`2026-10-19`) bedeutet 00:00 Uhr.
- Leere oder nur aus Leerzeichen bestehende Werte zählen als nicht angegeben.
- `departure` und `arrival` nie zugleich angeben; der Server antwortet sonst mit `Fehler: Entweder "departure" oder "arrival" angeben, nicht beides.`
Aufruf: `{ "from": "Kröpcke", "to": "Flughafen Hannover", "departure": "2026-10-19T08:15", "results": 2 }`
```
Verbindungen Hannover, Kröpcke -> Langenhagen (Han), Hannover Flughafen
1) Mo., 19.10. Abfahrt 07:57, Ankunft 08:36, Dauer 39 min, Umstiege 1
Stadtbahn 1 -> Langenhagen: Hannover/Kröpcke 07:57 bis Langenhagen/Zentrum 08:16
Bus 470 -> Flughafen: Langenhagen Zentrum 08:21 bis Flughafen 08:36
2) Mo., 19.10. Abfahrt 08:24, Ankunft 08:53, Dauer 29 min, Umstiege 1
Stadtbahn 13 -> Hannover/Fasanenkrug: Hannover/Kröpcke 08:24 bis Hannover Hauptbahnhof 08:26
S-Bahn S5 -> Langenhagen Flughafen: Hannover Hauptbahnhof 08:35 bis Langenhagen Flughafen 08:53
```
Das Datum steht nur, wenn die Abfahrt nicht heute ist. Fußwege erscheinen als `Fussweg A -> B (Dauer)`. Die Verbindungen werden nach Abfahrtszeit sortiert ausgegeben.
`structuredContent`: `{ "from": { "id", "name" }, "to": { "id", "name" }, "journeys": [{ "departure", "arrival", "durationSeconds", "interchanges", "legs": [{ "mode": "walk" | "transit", "line", "from", "to", "departure", "arrival", "destination", "durationSeconds" }] }] }`. `departure` und `arrival` sind hier die Sollzeiten in UTC.
## Bekannte Eigenheiten der EFA
- Der Stopfinder meldet bei mehrdeutigen Namen den Fehler `-8011` mit leerem Text und liefert trotzdem Treffer. Der Server behandelt das nicht als Fehler, solange Treffer vorhanden sind. Die Verbindungssuche löst Namen deshalb immer zuerst über den Stopfinder auf, weil Freitext direkt als Ziel mit `-8011` scheitert.
- Zeiten kommen mit Sekunden (z. B. Ankunft `10:39:50Z`). Der Server kappt auf volle Minuten, damit keine scheinbaren Verspätungen von einer Minute entstehen, wo Soll und Ist in derselben Minute liegen.
- Verbindungen sind von der EFA nach Rating sortiert, nicht nach Zeit, und es kommen teils mehr oder früher abfahrende Verbindungen zurück als angefragt (im Beispiel oben: Wunschzeit 08:15, erste Verbindung 07:57). Der Server sortiert nach Abfahrt und kappt dann auf `results`; der Zeitpunkt ist aber nur ein Richtwert.
- Reine Fußweg-Verbindungen zählen als Verbindung und belegen einen der `results`-Plätze.
- Die EFA gewichtet nicht nach Region (`Messe/Ost` liefert dort zuerst Friedrichshafen). Der Server bevorzugt deshalb Treffer mit ID `de:03241:…` (Kreisschlüssel der Region Hannover), dann den EFA-Bestwert, dann die Trefferqualität; Treffer außerhalb bleiben als Fallback erhalten (`Hildesheim Hauptbahnhof` findet Hildesheim). Im Zweifel mit `stop_search` die ID prüfen.
- Ein Stopfinder-Aufruf ohne Treffer meldet `-8020` mit leerem Text und leerer Trefferliste; der Server wertet das als leeres Ergebnis. Eine unbekannte `de:`-ID bei den Abfahrten liefert keine Fehlermeldung, sondern Fuzzy-Treffer fremder Haltestellen ohne Abfahrten; der Server meldet sie als unbekannt.
## Lizenz
MIT, siehe [LICENSE](LICENSE).
TDQS
Scored across 3 tools
Each tool targets a distinct task: stop_search resolves stop IDs, departures returns live departure boards, and journey_planner handles routing between stops. There is no meaningful overlap—finding a stop, checking departures, and planning a trip are clearly separate actions.
All names are snake_case, but the patterns differ: stop_search (noun_verb), departures (bare plural noun), and journey_planner (noun_noun). The lack of a single verb_noun convention makes the set less predictable, though each name is still readable and descriptive.
Three tools map cleanly onto the core transit workflow (find stop, view departures, plan journey) with no redundancy. The surface is slightly thin—no coordinate-based or nearby-stop lookup—but each tool earns its place.
The set covers the essential lifecycle: resolving stops, real-time departures, and journey planning with time parameters. Minor gaps exist (no nearby-stop-by-coordinates search or service-disruption/line-info lookup), but agents can work around them using stop_search.