@iflow-mcp/faaak2-db-mcp 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/24918_process.log +2 -1
- package/iflow-mcp-faaak2-db-mcp-1.0.1.tgz +0 -0
- package/package.json +1 -1
- package/server_config.json +1 -0
- package/build/api-client.js +0 -3
- package/build/index.js +0 -108
- package/build/instructions.js +0 -38
- package/build/slim.js +0 -139
- package/build/tools/find-journeys.js +0 -38
- package/build/tools/find-station.js +0 -24
- package/build/tools/find-trip.js +0 -105
- package/build/tools/get-departures.js +0 -39
package/24918_process.log
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "@iflow-mcp/faaak2-db-mcp", "version": "1.0.
|
|
1
|
+
{"name": "@iflow-mcp/faaak2-db-mcp", "version": "1.0.1", "type": "module", "scripts": {"build": "tsc", "start": "node build/index.js", "serve": "PORT=3000 node build/index.js"}, "dependencies": {"@modelcontextprotocol/sdk": "^1.11.0", "db-vendo-client": "^6.10.8", "zod": "^3.24.0"}, "devDependencies": {"@types/node": "^22.0.0", "typescript": "^5.7.0"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"serverConfig":{"mcpServers":{"db-mcp":{"command":"npx","args":["-y","-p","@iflow-mcp/faaak2-db-mcp","node","-e","import process from 'process';process.chdir(process.env.HOME||process.cwd());import exec from 'child_process';const p=exec.exec('node node_modules/@iflow-mcp/faaak2-db-mcp/build/index.js');p.stdout.pipe(process.stdout);p.stderr.pipe(process.stderr);p.on('close',c=>process.exit(c))"],"values":{}}}}}
|
package/build/api-client.js
DELETED
package/build/index.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { instructions } from "./instructions.js";
|
|
3
|
-
import { registerFindStation } from "./tools/find-station.js";
|
|
4
|
-
import { registerGetDepartures } from "./tools/get-departures.js";
|
|
5
|
-
import { registerFindTrip } from "./tools/find-trip.js";
|
|
6
|
-
import { registerFindJourneys } from "./tools/find-journeys.js";
|
|
7
|
-
function createServer() {
|
|
8
|
-
const server = new McpServer({ name: "db-mcp", version: "1.0.0" }, { instructions });
|
|
9
|
-
registerFindStation(server);
|
|
10
|
-
registerGetDepartures(server);
|
|
11
|
-
registerFindTrip(server);
|
|
12
|
-
registerFindJourneys(server);
|
|
13
|
-
return server;
|
|
14
|
-
}
|
|
15
|
-
if (process.env.PORT) {
|
|
16
|
-
// Remote: Streamable HTTP transport
|
|
17
|
-
const { createServer: createHttpServer } = await import("node:http");
|
|
18
|
-
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
19
|
-
const PORT = parseInt(process.env.PORT, 10);
|
|
20
|
-
const sessions = new Map();
|
|
21
|
-
// Session timeout: clean up sessions after 30 min
|
|
22
|
-
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
23
|
-
setInterval(() => {
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
for (const [id, session] of sessions) {
|
|
26
|
-
if (now - session.createdAt > SESSION_TIMEOUT_MS) {
|
|
27
|
-
session.transport.close?.();
|
|
28
|
-
sessions.delete(id);
|
|
29
|
-
console.log(`Session expired: ${id} (${sessions.size} active)`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}, 60_000);
|
|
33
|
-
const httpServer = createHttpServer(async (req, res) => {
|
|
34
|
-
const url = new URL(req.url || "/", `http://localhost:${PORT}`);
|
|
35
|
-
// Health check for Railway
|
|
36
|
-
if (url.pathname === "/" && req.method === "GET") {
|
|
37
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38
|
-
res.end(JSON.stringify({ status: "ok", name: "db-mcp" }));
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (url.pathname !== "/mcp") {
|
|
42
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
43
|
-
res.end(JSON.stringify({ error: "Not found. Use POST /mcp" }));
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
// CORS
|
|
47
|
-
res.setHeader("Access-Control-Allow-Origin", "https://mcp-builder.de");
|
|
48
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
49
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
50
|
-
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
51
|
-
if (req.method === "OPTIONS") {
|
|
52
|
-
res.writeHead(204);
|
|
53
|
-
res.end();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
// Parse body for POST
|
|
57
|
-
let parsedBody = undefined;
|
|
58
|
-
if (req.method === "POST") {
|
|
59
|
-
const chunks = [];
|
|
60
|
-
for await (const chunk of req) {
|
|
61
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
62
|
-
}
|
|
63
|
-
parsedBody = JSON.parse(Buffer.concat(chunks).toString());
|
|
64
|
-
}
|
|
65
|
-
// Route by session
|
|
66
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
67
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
68
|
-
const session = sessions.get(sessionId);
|
|
69
|
-
await session.transport.handleRequest(req, res, parsedBody);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (sessionId && !sessions.has(sessionId)) {
|
|
73
|
-
// Session expired or lost (e.g. after redeploy) — tell client to re-initialize
|
|
74
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
75
|
-
res.end(JSON.stringify({ error: "Session not found. Please start a new session." }));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// New session
|
|
79
|
-
const transport = new StreamableHTTPServerTransport({
|
|
80
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
81
|
-
onsessioninitialized: (id) => {
|
|
82
|
-
sessions.set(id, { server, transport, createdAt: Date.now() });
|
|
83
|
-
console.log(`Session created: ${id} (${sessions.size} active)`);
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
transport.onclose = () => {
|
|
87
|
-
const id = transport.sessionId;
|
|
88
|
-
if (id) {
|
|
89
|
-
sessions.delete(id);
|
|
90
|
-
console.log(`Session closed: ${id} (${sessions.size} active)`);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
const server = createServer();
|
|
94
|
-
await server.connect(transport);
|
|
95
|
-
await transport.handleRequest(req, res, parsedBody);
|
|
96
|
-
});
|
|
97
|
-
httpServer.listen(PORT, "127.0.0.1", () => {
|
|
98
|
-
console.log(`DB-MCP Streamable HTTP server listening on port ${PORT}`);
|
|
99
|
-
console.log(`Endpoint: http://localhost:${PORT}/mcp`);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
// Local: stdio transport
|
|
104
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
105
|
-
const server = createServer();
|
|
106
|
-
const transport = new StdioServerTransport();
|
|
107
|
-
await server.connect(transport);
|
|
108
|
-
}
|
package/build/instructions.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export const instructions = `Du bist ein Reiseassistent für Bahnreisen in Deutschland.
|
|
2
|
-
Du hast Zugriff auf Echtzeit-Daten der Deutschen Bahn über einen MCP Server.
|
|
3
|
-
|
|
4
|
-
ZEITBERECHNUNG
|
|
5
|
-
- Verwende für alle Berechnungen ("Liegt dieser Halt noch vor mir?") ausschließlich
|
|
6
|
-
die tatsächlichen Zeiten (departure/arrival), niemals die geplanten.
|
|
7
|
-
- Wenn departure/arrival = null: Schätzung aus plannedDeparture + departureDelay verwenden
|
|
8
|
-
und als Schätzung kennzeichnen.
|
|
9
|
-
|
|
10
|
-
PFLICHTANGABEN BEI JEDER VERBINDUNGSAUSKUNFT
|
|
11
|
-
- Abfahrtszeit (tatsächlich)
|
|
12
|
-
- Gleis (aktuell) — immer angeben, nie weglassen
|
|
13
|
-
- Linie und Richtung
|
|
14
|
-
- Umstiegsbahnhöfe
|
|
15
|
-
- Ankunftszeit am Ziel
|
|
16
|
-
- Aktive Störungshinweise (remarks vom Typ warning/status)
|
|
17
|
-
|
|
18
|
-
GLEISWECHSEL
|
|
19
|
-
- Wenn departurePlatform != plannedDeparturePlatform: explizit warnen.
|
|
20
|
-
- Beispiel: "⚠️ Gleiswechsel! Abfahrt jetzt Gleis 4 (geplant: Gleis 7)"
|
|
21
|
-
|
|
22
|
-
SCHIENENERSATZVERKEHR
|
|
23
|
-
- Wenn remarks einen Eintrag mit summary "replacement service" enthält:
|
|
24
|
-
Nutzer darauf hinweisen dass es sich um einen Ersatzbus handelt und
|
|
25
|
-
Bauarbeiten-Zeitraum aus dem status-Remark nennen.
|
|
26
|
-
|
|
27
|
-
FAHRGASTRECHTE — 60-MINUTEN-REGEL
|
|
28
|
-
- Wenn arrivalDelay oder departureDelay >= 3600 Sekunden (60 Minuten):
|
|
29
|
-
Folgenden Hinweis ausgeben:
|
|
30
|
-
"⚖️ Fahrgastrechte: Bei dieser Verspätung hast du Anspruch auf 25% Erstattung
|
|
31
|
-
des Ticketpreises. Ab 120 Minuten sind es 50%. Erstattung über bahn.de/fahrgastrechte
|
|
32
|
-
oder direkt am Serviceschalter."
|
|
33
|
-
|
|
34
|
-
ALTERNATIVE VERBINDUNGEN
|
|
35
|
-
- Immer alle noch erreichbaren Zwischenhalte des aktuellen Zuges prüfen,
|
|
36
|
-
nicht nur den Endbahnhof.
|
|
37
|
-
- departure-Parameter für find_journeys = tatsächliche Ankunft am jeweiligen
|
|
38
|
-
Halt (mit Verspätung), nicht die geplante Zeit.`;
|
package/build/slim.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Strips DB API responses down to the fields the LLM actually needs.
|
|
3
|
-
* Removes coordinates, operator details, ril100 IDs, excessive product info, etc.
|
|
4
|
-
* Also removes undefined/null values to minimize JSON size.
|
|
5
|
-
*/
|
|
6
|
-
/** JSON.stringify replacer that drops undefined/null values */
|
|
7
|
-
function dropNulls(_key, value) {
|
|
8
|
-
return value === null || value === undefined ? undefined : value;
|
|
9
|
-
}
|
|
10
|
-
/** Compact JSON without pretty-printing, dropping nulls */
|
|
11
|
-
export function compact(obj) {
|
|
12
|
-
return JSON.stringify(obj, dropNulls);
|
|
13
|
-
}
|
|
14
|
-
function pick(obj, keys) {
|
|
15
|
-
if (!obj)
|
|
16
|
-
return {};
|
|
17
|
-
const out = {};
|
|
18
|
-
for (const k of keys) {
|
|
19
|
-
if (obj[k] !== undefined && obj[k] !== null)
|
|
20
|
-
out[k] = obj[k];
|
|
21
|
-
}
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
24
|
-
function slimStop(stop) {
|
|
25
|
-
if (!stop)
|
|
26
|
-
return null;
|
|
27
|
-
return { id: stop.id, name: stop.name };
|
|
28
|
-
}
|
|
29
|
-
function slimRemarks(remarks) {
|
|
30
|
-
if (!Array.isArray(remarks))
|
|
31
|
-
return undefined;
|
|
32
|
-
const kept = remarks
|
|
33
|
-
.filter((r) => r.type === "warning" || r.type === "status")
|
|
34
|
-
.map((r) => pick(r, ["type", "summary", "text", "validFrom", "validUntil"]));
|
|
35
|
-
return kept.length > 0 ? kept : undefined;
|
|
36
|
-
}
|
|
37
|
-
function slimStopover(s) {
|
|
38
|
-
const out = { stop: s.stop?.name ?? null };
|
|
39
|
-
// Only include times that exist
|
|
40
|
-
const arr = s.arrival ?? s.plannedArrival;
|
|
41
|
-
if (arr)
|
|
42
|
-
out.arrival = arr;
|
|
43
|
-
if (s.arrivalDelay)
|
|
44
|
-
out.arrivalDelay = s.arrivalDelay;
|
|
45
|
-
const dep = s.departure ?? s.plannedDeparture;
|
|
46
|
-
if (dep)
|
|
47
|
-
out.departure = dep;
|
|
48
|
-
if (s.departureDelay)
|
|
49
|
-
out.departureDelay = s.departureDelay;
|
|
50
|
-
// Platform only if present
|
|
51
|
-
const plat = s.departurePlatform ?? s.plannedDeparturePlatform;
|
|
52
|
-
if (plat)
|
|
53
|
-
out.platform = plat;
|
|
54
|
-
if (s.cancelled)
|
|
55
|
-
out.cancelled = true;
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
// ── public API ───────────────────────────────────────────────────────
|
|
59
|
-
function slimLeg(leg) {
|
|
60
|
-
const out = {
|
|
61
|
-
origin: slimStop(leg.origin),
|
|
62
|
-
destination: slimStop(leg.destination),
|
|
63
|
-
departure: leg.departure ?? leg.plannedDeparture,
|
|
64
|
-
plannedDeparture: leg.plannedDeparture,
|
|
65
|
-
departureDelay: leg.departureDelay ?? undefined,
|
|
66
|
-
arrival: leg.arrival ?? leg.plannedArrival,
|
|
67
|
-
plannedArrival: leg.plannedArrival,
|
|
68
|
-
arrivalDelay: leg.arrivalDelay ?? undefined,
|
|
69
|
-
};
|
|
70
|
-
if (leg.walking) {
|
|
71
|
-
out.walking = true;
|
|
72
|
-
if (leg.distance)
|
|
73
|
-
out.distance = leg.distance;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
out.line = leg.line ? pick(leg.line, ["name", "productName", "mode"]) : undefined;
|
|
77
|
-
out.direction = leg.direction;
|
|
78
|
-
out.departurePlatform = leg.departurePlatform ?? leg.plannedDeparturePlatform;
|
|
79
|
-
out.plannedDeparturePlatform = leg.plannedDeparturePlatform;
|
|
80
|
-
out.arrivalPlatform = leg.arrivalPlatform ?? leg.plannedArrivalPlatform;
|
|
81
|
-
out.plannedArrivalPlatform = leg.plannedArrivalPlatform;
|
|
82
|
-
if (leg.cancelled)
|
|
83
|
-
out.cancelled = true;
|
|
84
|
-
const remarks = slimRemarks(leg.remarks);
|
|
85
|
-
if (remarks)
|
|
86
|
-
out.remarks = remarks;
|
|
87
|
-
if (Array.isArray(leg.stopovers)) {
|
|
88
|
-
out.stopovers = leg.stopovers.map(slimStopover);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
93
|
-
export function slimJourneys(data) {
|
|
94
|
-
const journeys = data.journeys ?? [];
|
|
95
|
-
return {
|
|
96
|
-
journeys: journeys.map((j) => ({
|
|
97
|
-
legs: (j.legs ?? []).map(slimLeg),
|
|
98
|
-
...(j.price ? { price: j.price } : {}),
|
|
99
|
-
})),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
export function slimTrip(data) {
|
|
103
|
-
const trip = data.trip ?? data;
|
|
104
|
-
return {
|
|
105
|
-
trip: {
|
|
106
|
-
...pick(trip, ["id", "direction", "cancelled"]),
|
|
107
|
-
line: trip.line ? pick(trip.line, ["name", "productName", "mode"]) : undefined,
|
|
108
|
-
origin: slimStop(trip.origin),
|
|
109
|
-
destination: slimStop(trip.destination),
|
|
110
|
-
departure: trip.departure ?? trip.plannedDeparture,
|
|
111
|
-
plannedDeparture: trip.plannedDeparture,
|
|
112
|
-
departureDelay: trip.departureDelay ?? undefined,
|
|
113
|
-
arrival: trip.arrival ?? trip.plannedArrival,
|
|
114
|
-
plannedArrival: trip.plannedArrival,
|
|
115
|
-
arrivalDelay: trip.arrivalDelay ?? undefined,
|
|
116
|
-
remarks: slimRemarks(trip.remarks),
|
|
117
|
-
stopovers: Array.isArray(trip.stopovers)
|
|
118
|
-
? trip.stopovers.map(slimStopover)
|
|
119
|
-
: undefined,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
export function slimDeparture(d) {
|
|
124
|
-
return {
|
|
125
|
-
tripId: d.tripId,
|
|
126
|
-
line: d.line ? pick(d.line, ["name", "productName", "mode"]) : undefined,
|
|
127
|
-
direction: d.direction,
|
|
128
|
-
when: d.when ?? d.plannedWhen,
|
|
129
|
-
plannedWhen: d.plannedWhen,
|
|
130
|
-
delay: d.delay ?? undefined,
|
|
131
|
-
platform: d.platform ?? d.plannedPlatform,
|
|
132
|
-
plannedPlatform: d.plannedPlatform,
|
|
133
|
-
...(d.cancelled ? { cancelled: true } : {}),
|
|
134
|
-
remarks: slimRemarks(d.remarks),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
export function slimDepartures(departures) {
|
|
138
|
-
return departures.map((d) => slimDeparture(d));
|
|
139
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { client } from "../api-client.js";
|
|
3
|
-
import { slimJourneys, compact } from "../slim.js";
|
|
4
|
-
export function registerFindJourneys(server) {
|
|
5
|
-
server.tool("find_journeys", "Find journey connections between two stations. Returns journeys with legs, lines, platforms, stopovers, and remarks.", {
|
|
6
|
-
from_id: z.string().describe("Departure station ID (e.g. '8000261' for München Hbf)"),
|
|
7
|
-
to_id: z.string().describe("Arrival station ID (e.g. '8000105' for Frankfurt Hbf)"),
|
|
8
|
-
departure: z.string().describe("ISO departure date/time (e.g. '2026-03-08T14:00')"),
|
|
9
|
-
results: z.number().default(4).describe("Number of journeys to return (default 4)"),
|
|
10
|
-
}, async ({ from_id, to_id, departure, results }) => {
|
|
11
|
-
try {
|
|
12
|
-
for (const [label, id] of [["from_id", from_id], ["to_id", to_id]]) {
|
|
13
|
-
if (!/^\d{6,9}$/.test(id)) {
|
|
14
|
-
return {
|
|
15
|
-
isError: true,
|
|
16
|
-
content: [{ type: "text", text: `find_journeys failed: Invalid ${label} '${id}'. Expected a numeric HAFAS ID (e.g. '8000261' for München Hbf). Use find_station to look up the correct ID.` }],
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const data = await client.journeys(from_id, to_id, {
|
|
21
|
-
departure: new Date(departure),
|
|
22
|
-
results,
|
|
23
|
-
stopovers: true,
|
|
24
|
-
remarks: true,
|
|
25
|
-
});
|
|
26
|
-
const slim = slimJourneys(data);
|
|
27
|
-
return {
|
|
28
|
-
content: [{ type: "text", text: compact(slim) }],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
return {
|
|
33
|
-
isError: true,
|
|
34
|
-
content: [{ type: "text", text: `find_journeys failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { client } from "../api-client.js";
|
|
3
|
-
export function registerFindStation(server) {
|
|
4
|
-
server.tool("find_station", "Search for a Deutsche Bahn station by name. Returns raw JSON array of matching locations.", {
|
|
5
|
-
query: z.string().describe("Station name to search for"),
|
|
6
|
-
results: z
|
|
7
|
-
.number()
|
|
8
|
-
.default(1)
|
|
9
|
-
.describe("Number of results to return (default 1)"),
|
|
10
|
-
}, async ({ query, results }) => {
|
|
11
|
-
try {
|
|
12
|
-
const data = await client.locations(query, { results });
|
|
13
|
-
return {
|
|
14
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
return {
|
|
19
|
-
isError: true,
|
|
20
|
-
content: [{ type: "text", text: `find_station failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
package/build/tools/find-trip.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { client } from "../api-client.js";
|
|
3
|
-
import { slimTrip, compact } from "../slim.js";
|
|
4
|
-
const PRODUCT_PREFIXES = {
|
|
5
|
-
nationalExpress: ["ICE", "TGV", "RJ", "RJX", "ECE"],
|
|
6
|
-
national: ["IC", "EC", "EN", "NJ", "FLX"],
|
|
7
|
-
regionalExpress: ["RE", "IRE", "MEX", "FEX"],
|
|
8
|
-
regional: ["RB", "RS"],
|
|
9
|
-
suburban: ["S"],
|
|
10
|
-
};
|
|
11
|
-
function getProductFilters(trainName) {
|
|
12
|
-
const prefix = trainName.replace(/\s+/g, "").replace(/\d+$/, "").toUpperCase();
|
|
13
|
-
for (const [product, prefixes] of Object.entries(PRODUCT_PREFIXES)) {
|
|
14
|
-
if (prefixes.includes(prefix)) {
|
|
15
|
-
const products = {
|
|
16
|
-
nationalExpress: false,
|
|
17
|
-
national: false,
|
|
18
|
-
regionalExpress: false,
|
|
19
|
-
regional: false,
|
|
20
|
-
suburban: false,
|
|
21
|
-
bus: false,
|
|
22
|
-
ferry: false,
|
|
23
|
-
subway: false,
|
|
24
|
-
tram: false,
|
|
25
|
-
};
|
|
26
|
-
products[product] = true;
|
|
27
|
-
return products;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Unknown prefix — don't filter
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
export function registerFindTrip(server) {
|
|
34
|
-
server.tool("find_trip", "Find a specific train trip by name and station. Fetches all departures for the day, matches the train, then retrieves full trip details with stopovers and remarks.", {
|
|
35
|
-
train_name: z.string().describe("Train name to search for (e.g. 'ICE 599')"),
|
|
36
|
-
station_id: z.string().describe("Station ID (e.g. '8000261' for München Hbf)"),
|
|
37
|
-
date: z.string().describe("ISO date string (e.g. '2026-03-08')"),
|
|
38
|
-
}, async ({ train_name, station_id, date }) => {
|
|
39
|
-
try {
|
|
40
|
-
if (!/^\d{6,9}$/.test(station_id)) {
|
|
41
|
-
return {
|
|
42
|
-
isError: true,
|
|
43
|
-
content: [{ type: "text", text: `find_trip failed: Invalid station_id '${station_id}'. Expected a numeric HAFAS ID (e.g. '8000261' for München Hbf). Use find_station to look up the correct ID.` }],
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// Step 1: Get departures for the day (db profile max 720 min, so split into two 12h windows)
|
|
47
|
-
const products = getProductFilters(train_name);
|
|
48
|
-
const baseOpt = { duration: 720 };
|
|
49
|
-
if (products)
|
|
50
|
-
baseOpt.products = products;
|
|
51
|
-
const res1 = await client.departures(station_id, {
|
|
52
|
-
...baseOpt,
|
|
53
|
-
when: new Date(`${date}T00:00`),
|
|
54
|
-
});
|
|
55
|
-
let departures = (res1.departures ?? []);
|
|
56
|
-
// Search second half of day if needed
|
|
57
|
-
const normalize = (s) => s.toLowerCase().replace(/\s+/g, "");
|
|
58
|
-
let match = departures.find((d) => d.line?.name != null &&
|
|
59
|
-
normalize(d.line.name) === normalize(train_name));
|
|
60
|
-
if (!match) {
|
|
61
|
-
const res2 = await client.departures(station_id, {
|
|
62
|
-
...baseOpt,
|
|
63
|
-
when: new Date(`${date}T12:00`),
|
|
64
|
-
});
|
|
65
|
-
const moreDepartures = (res2.departures ?? []);
|
|
66
|
-
departures = [...departures, ...moreDepartures];
|
|
67
|
-
match = moreDepartures.find((d) => d.line?.name != null &&
|
|
68
|
-
normalize(d.line.name) === normalize(train_name));
|
|
69
|
-
}
|
|
70
|
-
if (!match || !match.tripId) {
|
|
71
|
-
const availableNames = [
|
|
72
|
-
...new Set(departures
|
|
73
|
-
.map((d) => d.line?.name)
|
|
74
|
-
.filter((n) => !!n)),
|
|
75
|
-
];
|
|
76
|
-
return {
|
|
77
|
-
content: [
|
|
78
|
-
{
|
|
79
|
-
type: "text",
|
|
80
|
-
text: JSON.stringify({
|
|
81
|
-
error: `No departure found matching '${train_name}' at station ${station_id} on ${date}`,
|
|
82
|
-
available_trains: availableNames,
|
|
83
|
-
}, null, 2),
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
// Step 2: Fetch full trip details
|
|
89
|
-
const trip = await client.trip(match.tripId, match.line?.name ?? "", {
|
|
90
|
-
stopovers: true,
|
|
91
|
-
remarks: true,
|
|
92
|
-
});
|
|
93
|
-
const slim = slimTrip(trip);
|
|
94
|
-
return {
|
|
95
|
-
content: [{ type: "text", text: compact(slim) }],
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
return {
|
|
100
|
-
isError: true,
|
|
101
|
-
content: [{ type: "text", text: `find_trip failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { client } from "../api-client.js";
|
|
3
|
-
import { slimDepartures, compact } from "../slim.js";
|
|
4
|
-
export function registerGetDepartures(server) {
|
|
5
|
-
server.tool("get_departures", "Get upcoming departures from a Deutsche Bahn station. Returns raw JSON array of departures with line.name, direction, when, delay, platform, remarks.", {
|
|
6
|
-
station_id: z.string().describe("Station ID (e.g. '8000261' for München Hbf)"),
|
|
7
|
-
when: z
|
|
8
|
-
.string()
|
|
9
|
-
.optional()
|
|
10
|
-
.describe("ISO 8601 datetime string (optional, defaults to now)"),
|
|
11
|
-
duration: z
|
|
12
|
-
.number()
|
|
13
|
-
.default(60)
|
|
14
|
-
.describe("Duration in minutes to query (default 60)"),
|
|
15
|
-
}, async ({ station_id, when, duration }) => {
|
|
16
|
-
try {
|
|
17
|
-
if (!/^\d{6,9}$/.test(station_id)) {
|
|
18
|
-
return {
|
|
19
|
-
isError: true,
|
|
20
|
-
content: [{ type: "text", text: `get_departures failed: Invalid station_id '${station_id}'. Expected a numeric HAFAS ID (e.g. '8000261' for München Hbf). Use find_station to look up the correct ID.` }],
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
const opt = { duration };
|
|
24
|
-
if (when)
|
|
25
|
-
opt.when = new Date(when);
|
|
26
|
-
const res = await client.departures(station_id, opt);
|
|
27
|
-
const slim = slimDepartures((res.departures ?? []));
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: compact(slim) }],
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
return {
|
|
34
|
-
isError: true,
|
|
35
|
-
content: [{ type: "text", text: `get_departures failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
}
|