#
tokens: 24622/50000 27/27 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .gitignore
├── biome.json
├── Dockerfile
├── LICENSE.md
├── package.json
├── README.md
├── src
│   ├── api
│   │   ├── timetableApi.ts
│   │   └── types.ts
│   ├── config.ts
│   ├── index.ts
│   ├── resources
│   │   ├── index.ts
│   │   └── timetableResources.ts
│   ├── tests
│   │   ├── api.test.ts
│   │   ├── config.test.ts
│   │   ├── index.test.ts
│   │   ├── integration.test.ts
│   │   ├── tools.test.ts
│   │   └── utils
│   │       ├── errorHandling.test.ts
│   │       └── logger.test.ts
│   ├── tools
│   │   ├── index.ts
│   │   └── timetableTools.ts
│   └── utils
│       ├── errorHandling.ts
│       └── logger.ts
├── TESTING.md
├── tsconfig.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
 1 | # API-Zugangsdaten für die Deutsche Bahn API
 2 | DB_TIMETABLE_CLIENT_ID=your-client-id
 3 | DB_TIMETABLE_CLIENT_SECRET=your-client-secret
 4 | 
 5 | # Server-Konfiguration
 6 | # Mögliche Werte: 'stdio' oder 'sse'
 7 | TRANSPORT_TYPE=stdio
 8 | PORT=8080
 9 | SSE_ENDPOINT=/sse
10 | 
11 | # Logging-Konfiguration
12 | # Mögliche Werte: 'debug', 'info', 'warn', 'error'
13 | LOG_LEVEL=info 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Abhängigkeiten
 2 | node_modules/
 3 | npm-debug.log
 4 | yarn-error.log
 5 | yarn-debug.log
 6 | package-lock.json
 7 | .pnpm-lock.yaml
 8 | 
 9 | # Kompilierte Ausgabe
10 | dist/
11 | build/
12 | *.tsbuildinfo
13 | 
14 | # Umgebungsvariablen
15 | .env
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | 
21 | # Logs
22 | logs/
23 | *.log
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | 
28 | # IDE und Editoren
29 | .idea/
30 | .vscode/
31 | *.swp
32 | *.swo
33 | .DS_Store
34 | .directory
35 | *.sublime-project
36 | *.sublime-workspace
37 | 
38 | # Cache
39 | .npm
40 | .eslintcache
41 | .cache/
42 | .parcel-cache/
43 | 
44 | # Test-Abdeckung
45 | coverage/
46 | .nyc_output/
47 | 
48 | # Temporäre Dateien
49 | tmp/
50 | temp/
51 | 
52 | # Betriebssystemdateien
53 | .DS_Store
54 | Thumbs.db 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![smithery badge](https://smithery.ai/badge/@jorekai/db-timetable-mcp)](https://smithery.ai/server/@jorekai/db-timetable-mcp)
  2 | # DB Timetable MCP Server
  3 | 
  4 | Ein Model Context Protocol (MCP) Server für die Deutsche Bahn Timetable API. Der Server bietet MCP-Tools und -Ressourcen, um auf Fahrplandaten, Stationsinformationen und Zugänderungen zuzugreifen.
  5 | 
  6 | **Pflicht zur Namensnennung:**  
  7 | 
  8 | Dieses Projekt stellt die Fahrplandaten der Deutschen Bahn bereit, die unter der [Creative Commons Attribution 4.0 International Lizenz (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) öffentlich einsehbar sind.
  9 | 
 10 | Weitere Infos zur API und Lizenzbedingungen findest du unter [developers.deutschebahn.com](https://developers.deutschebahn.com/). API Requests unterliegen den Bedingungen der Lizenz.
 11 | 
 12 | 
 13 | ## Funktionen
 14 | 
 15 | - **Aktuelle Fahrplände**: Abrufen aktueller Fahrplandaten für eine Station
 16 | - **Fahrplanänderungen**: Tracking der neuesten Änderungen
 17 | - **Geplante Fahrpläne**: Zugriff auf geplante Fahrplandaten für einen bestimmten Zeitpunkt
 18 | - **Stationssuche**: Suche nach Bahnhofsstationen anhand von Namen oder Codes
 19 | 
 20 | ## Voraussetzungen
 21 | 
 22 | - Node.js 18 oder höher
 23 | - API-Zugangsdaten für die DB Timetable API (Client-ID und Client-Secret)
 24 | 
 25 | ## Installation
 26 | 
 27 | 1. Repository klonen:
 28 |    ```
 29 |    git clone <repository-url>
 30 |    cd db-mcp
 31 |    ```
 32 | 
 33 | 2. Abhängigkeiten installieren:
 34 |    ```
 35 |    npm install
 36 |    ```
 37 | 
 38 | 3. TypeScript-Code kompilieren:
 39 |    ```
 40 |    npm run build
 41 |    ```
 42 | 
 43 | ## Konfiguration
 44 | 
 45 | Erstelle eine `.env`-Datei im Root-Verzeichnis des Projekts mit folgenden Umgebungsvariablen:
 46 | 
 47 | ```
 48 | DB_TIMETABLE_CLIENT_ID=deine-client-id
 49 | DB_TIMETABLE_CLIENT_SECRET=dein-client-secret
 50 | TRANSPORT_TYPE=stdio
 51 | PORT=8080
 52 | SSE_ENDPOINT=/sse
 53 | LOG_LEVEL=info
 54 | ```
 55 | 
 56 | ### Konfigurationsoptionen
 57 | 
 58 | - `DB_TIMETABLE_CLIENT_ID`: Client-ID für die DB API (erforderlich)
 59 | - `DB_TIMETABLE_CLIENT_SECRET`: Client-Secret für die DB API (erforderlich)
 60 | - `TRANSPORT_TYPE`: Transporttyp für den MCP-Server (`stdio` oder `sse`, Standard: `stdio`)
 61 | - `PORT`: Port für den SSE-Server (Standard: `8080`)
 62 | - `SSE_ENDPOINT`: Endpunkt für SSE-Verbindungen (Standard: `/sse`)
 63 | - `LOG_LEVEL`: Logging-Level (`debug`, `info`, `warn`, `error`, Standard: `info`)
 64 | 
 65 | ## Verwendung
 66 | 
 67 | ### Server starten
 68 | 
 69 | Im stdio-Modus (für CLI-Tests und Debugging):
 70 | 
 71 | ```bash
 72 | npm start
 73 | ```
 74 | 
 75 | Im SSE-Modus (für Webclients):
 76 | 
 77 | ```bash
 78 | TRANSPORT_TYPE=sse npm start
 79 | ```
 80 | 
 81 | ### Mit Inspect-Modus testen
 82 | 
 83 | Der Server kann mit dem FastMCP Inspector getestet werden:
 84 | 
 85 | ```bash
 86 | npx fastmcp inspect path/to/index.js
 87 | ```
 88 | 
 89 | ### MCP-Tools
 90 | 
 91 | Der Server stellt folgende Tools bereit:
 92 | 
 93 | 1. **getCurrentTimetable**: Ruft aktuelle Fahrplandaten für eine Station ab
 94 |    - Parameter: `evaNo` - EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)
 95 | 
 96 | 2. **getRecentChanges**: Ruft aktuelle Änderungen für eine Station ab
 97 |    - Parameter: `evaNo` - EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)
 98 | 
 99 | 3. **getPlannedTimetable**: Ruft geplante Fahrplandaten für eine Station ab
100 |    - Parameter: 
101 |      - `evaNo` - EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)
102 |      - `date` - Datum im Format YYMMDD (z.B. 230401 für 01.04.2023)
103 |      - `hour` - Stunde im Format HH (z.B. 14 für 14 Uhr)
104 | 
105 | 4. **findStations**: Sucht nach Stationen anhand eines Suchmusters
106 |    - Parameter: `pattern` - Suchmuster (z.B. "Frankfurt" oder "BLS")
107 | 
108 | ### MCP-Ressourcen
109 | 
110 | Der Server stellt folgende Ressourcen bereit:
111 | 
112 | 1. **Aktuelle Fahrplandaten**: `db-api:timetable/current/{evaNo}`
113 | 2. **Aktuelle Fahrplanänderungen**: `db-api:timetable/changes/{evaNo}`
114 | 3. **Geplante Fahrplandaten**: `db-api:timetable/planned/{evaNo}/{date}/{hour}`
115 | 4. **Stationssuche**: `db-api:station/{pattern}`
116 | 
117 | ## Entwicklung
118 | 
119 | ### Projekt-Struktur
120 | 
121 | ```
122 | db-mcp/
123 | ├── src/
124 | │   ├── api/             # API-Client und Typen
125 | │   ├── tools/           # MCP-Tools
126 | │   ├── resources/       # MCP-Ressourcen
127 | │   ├── utils/           # Hilfsfunktionen
128 | │   ├── config.ts        # Konfiguration
129 | │   └── index.ts         # Haupteinstiegspunkt
130 | ├── dist/                # Kompilierte Dateien
131 | ├── .env                 # Umgebungsvariablen
132 | ├── package.json
133 | ├── tsconfig.json
134 | └── README.md
135 | ```
136 | 
137 | ### NPM-Skripte
138 | 
139 | - `npm run build`: Kompiliert den TypeScript-Code
140 | - `npm start`: Startet den Server
141 | - `npm run dev`: Startet den Server im Entwicklungsmodus mit automatischem Neuladen
142 | - `npm test`: Führt Tests aus
143 | 
144 | ## Erweiterbarkeit
145 | 
146 | Potenzielle Erweiterungen
147 | 1. Datenverarbeitung und -anreicherung
148 |    - Semantische Fahrplandatenverarbeitung: XML zu strukturiertem JSON mit semantischer Anreicherung
149 |    - Historische Datenanalyse für Verspätungen und Betriebsstörungen
150 |    - Integration multimodaler Verkehrsverbindungen
151 | 2. Erweiterte MCP-Tools
152 |    - Routenplanung zwischen Stationen
153 |    - KI-basierte Verspätungs- und Auslastungsprognosen
154 |    - Reisestörungsanalyse
155 |    - Barrierefreiheitscheck für Stationen und Verbindungen
156 | 
157 | ## Lizenz
158 | 
159 | MCP Server: [MIT Lizenz](LICENSE)
160 | 
161 | DB Timetable API: [Creative Commons Namensnennung 4.0 International Lizenz](https://developers.deutschebahn.com/db-api-marketplace/apis/product/timetables)
162 | 
```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
 1 | MIT License
 2 | 
 3 | Copyright (c) [2025] [Nils Jorek]
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 | 	findStationsTool,
 3 | 	getCurrentTimetableTool,
 4 | 	getPlannedTimetableTool,
 5 | 	getRecentChangesTool,
 6 | } from "../tools/timetableTools.js";
 7 | 
 8 | export const tools = [
 9 | 	getCurrentTimetableTool,
10 | 	getRecentChangesTool,
11 | 	getPlannedTimetableTool,
12 | 	findStationsTool,
13 | ];
14 | 
15 | export {
16 | 	getCurrentTimetableTool,
17 | 	getRecentChangesTool,
18 | 	getPlannedTimetableTool,
19 | 	findStationsTool,
20 | };
21 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:22.12-alpine
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | # Copy package files
 6 | COPY package*.json ./
 7 | COPY package-lock.json ./
 8 | COPY tsconfig.json ./
 9 | 
10 | # Install dependencies
11 | RUN npm ci --only=production
12 | 
13 | # Copy source files
14 | COPY . .
15 | 
16 | # Build TypeScript
17 | RUN npm run build
18 | 
19 | # Set environment variables
20 | ENV NODE_ENV=production
21 | ENV TRANSPORT_TYPE=stdio
22 | 
23 | # Run in production mode
24 | CMD ["node", "dist/index.js"]
25 | 
26 | 
27 | 
```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from "vitest/config";
 2 | 
 3 | export default defineConfig({
 4 | 	test: {
 5 | 		globals: true,
 6 | 		environment: "node",
 7 | 		include: ["src/tests/**/*.test.ts"],
 8 | 		exclude: ["node_modules/", "dist/**", "vitest.config.ts", "**/types.ts"],
 9 | 		coverage: {
10 | 			provider: "v8",
11 | 			reporter: ["text", "json", "html"],
12 | 			exclude: [
13 | 				"node_modules/",
14 | 				"src/tests/**",
15 | 				"dist/**",
16 | 				"vitest.config.ts",
17 | 				"**/types.ts",
18 | 			],
19 | 		},
20 | 	},
21 | });
22 | 
```

--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 | 	PlannedTimetableResourceArgs,
 3 | 	StationResourceArgs,
 4 | 	TimetableResourceArgs,
 5 | 	currentTimetableResource,
 6 | 	plannedTimetableResource,
 7 | 	recentChangesResource,
 8 | 	stationResource,
 9 | } from "./timetableResources.js";
10 | 
11 | export const resources = [
12 | 	currentTimetableResource,
13 | 	recentChangesResource,
14 | 	plannedTimetableResource,
15 | 	stationResource,
16 | ];
17 | 
18 | export {
19 | 	currentTimetableResource,
20 | 	recentChangesResource,
21 | 	plannedTimetableResource,
22 | 	stationResource,
23 | 	TimetableResourceArgs,
24 | 	PlannedTimetableResourceArgs,
25 | 	StationResourceArgs,
26 | };
27 | 
```

--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
 3 | 	"vcs": {
 4 | 		"enabled": false,
 5 | 		"clientKind": "git",
 6 | 		"useIgnoreFile": false
 7 | 	},
 8 | 	"files": {
 9 | 		"ignoreUnknown": false,
10 | 		"ignore": [
11 | 			"dist/**",
12 | 			"node_modules/**",
13 | 			"**/*.test.ts",
14 | 			"**/*.spec.ts",
15 | 			"coverage/**"
16 | 		]
17 | 	},
18 | 	"formatter": {
19 | 		"enabled": true,
20 | 		"indentStyle": "tab"
21 | 	},
22 | 	"organizeImports": {
23 | 		"enabled": true
24 | 	},
25 | 	"linter": {
26 | 		"enabled": true,
27 | 		"rules": {
28 | 			"recommended": true
29 | 		}
30 | 	},
31 | 	"javascript": {
32 | 		"formatter": {
33 | 			"quoteStyle": "double"
34 | 		}
35 | 	}
36 | }
37 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"compilerOptions": {
 3 | 		"target": "ES2022",
 4 | 		"module": "NodeNext",
 5 | 		"moduleResolution": "NodeNext",
 6 | 		"esModuleInterop": true,
 7 | 		"allowSyntheticDefaultImports": true,
 8 | 		"strict": true,
 9 | 		"outDir": "dist",
10 | 		"sourceMap": true,
11 | 		"declaration": true,
12 | 		"resolveJsonModule": true,
13 | 		"skipLibCheck": true,
14 | 		"forceConsistentCasingInFileNames": true,
15 | 		"noImplicitAny": true,
16 | 		"noImplicitThis": true,
17 | 		"strictNullChecks": true,
18 | 		"strictFunctionTypes": true,
19 | 		"strictPropertyInitialization": true,
20 | 		"types": ["node", "vitest/globals"],
21 | 		"baseUrl": "."
22 | 	},
23 | 	"include": ["src/**/*"],
24 | 	"exclude": ["node_modules", "dist"]
25 | }
26 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "db-mcp",
 3 | 	"version": "1.0.0",
 4 | 	"type": "module",
 5 | 	"main": "dist/index.js",
 6 | 	"bin": {
 7 | 		"db-mcp": "dist/index.js"
 8 | 	},
 9 | 	"scripts": {
10 | 		"build": "tsc",
11 | 		"start": "node dist/index.js",
12 | 		"dev": "node --loader ts-node/esm src/index.ts",
13 | 		"dev:sse": "cross-env TRANSPORT_TYPE=sse node --loader ts-node/esm src/index.ts",
14 | 		"test": "vitest run",
15 | 		"test-ci": "vitest run --coverage.enabled --coverage.reporter='text-summary'",
16 | 		"lint": "npx biome check src",
17 | 		"coverage": "npm run test -- --coverage"
18 | 	},
19 | 	"keywords": ["mcp", "deutsche-bahn", "timetable", "api-client"],
20 | 	"author": "Nils Jorek <[email protected]>",
21 | 	"license": "MIT",
22 | 	"description": "MCP Server für die Deutsche Bahn Timetable API",
23 | 	"dependencies": {
24 | 		"dotenv": "^16.4.7",
25 | 		"fastmcp": "^1.20.5",
26 | 		"node-fetch": "^2.7.0",
27 | 		"zod": "^3.24.2"
28 | 	},
29 | 	"devDependencies": {
30 | 		"@biomejs/biome": "1.9.4",
31 | 		"@types/node": "^22.13.11",
32 | 		"@types/node-fetch": "^2.6.12",
33 | 		"@vitest/coverage-v8": "^3.0.9",
34 | 		"eslint": "^8.57.0",
35 | 		"ts-node": "^10.9.2",
36 | 		"typescript": "^5.8.2",
37 | 		"vitest": "^3.0.9"
38 | 	}
39 | }
40 | 
```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import path from "node:path";
 2 | import { fileURLToPath } from "node:url";
 3 | import dotenv from "dotenv";
 4 | 
 5 | // Lade Umgebungsvariablen
 6 | const __filename = fileURLToPath(import.meta.url);
 7 | const __dirname = path.dirname(__filename);
 8 | dotenv.config({ path: path.resolve(__dirname, "../../.env") });
 9 | 
10 | // Konfigurationswerte
11 | export const config = {
12 | 	server: {
13 | 		name: "DB Timetables MCP Server",
14 | 		version: "1.0.0",
15 | 		transportType: process.env.TRANSPORT_TYPE || "stdio", // 'stdio' oder 'sse'
16 | 		port: Number.parseInt(process.env.PORT || "8080", 10),
17 | 		endpoint: process.env.SSE_ENDPOINT || "/sse",
18 | 	},
19 | 	api: {
20 | 		baseUrl:
21 | 			"https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1",
22 | 		clientId: process.env.DB_TIMETABLE_CLIENT_ID || "",
23 | 		clientSecret: process.env.DB_TIMETABLE_CLIENT_SECRET || "",
24 | 	},
25 | 	logging: {
26 | 		level: process.env.LOG_LEVEL || "info",
27 | 	},
28 | };
29 | 
30 | if (!config.api.clientId || !config.api.clientSecret) {
31 | 	console.error(
32 | 		"API-Zugangsdaten fehlen! Bitte setze DB_TIMETABLE_CLIENT_ID und DB_TIMETABLE_CLIENT_SECRET in .env",
33 | 	);
34 | 	process.exit(1);
35 | }
36 | 
37 | export default config;
38 | 
```

--------------------------------------------------------------------------------
/src/tests/api.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, test, expect, vi, beforeEach } from 'vitest';
 2 | import { timetableApi } from '../api/timetableApi.js';
 3 | 
 4 | vi.mock('node-fetch', async () => {
 5 |     const actual = await vi.importActual('node-fetch');
 6 |     return {
 7 |         ...actual,
 8 |         default: vi.fn(() =>
 9 |             Promise.resolve({
10 |                 ok: true,
11 |                 text: () => Promise.resolve('<timetable>Test XML</timetable>'),
12 |             })
13 |         )
14 |     };
15 | });
16 | 
17 | describe('TimetableApiClient', () => {
18 |     beforeEach(() => {
19 |         vi.clearAllMocks();
20 |     });
21 | 
22 |     test('getCurrentTimetable ruft die richtige API-Endpoint auf', async () => {
23 |         const result = await timetableApi.getCurrentTimetable({ evaNo: '8000105' });
24 |         expect(result).toBe('<timetable>Test XML</timetable>');
25 |     });
26 | 
27 |     test('getRecentChanges ruft die richtige API-Endpoint auf', async () => {
28 |         const result = await timetableApi.getRecentChanges({ evaNo: '8000105' });
29 |         expect(result).toBe('<timetable>Test XML</timetable>');
30 |     });
31 | 
32 |     test('getPlannedTimetable ruft die richtige API-Endpoint auf', async () => {
33 |         const result = await timetableApi.getPlannedTimetable({
34 |             evaNo: '8000105',
35 |             date: '230401',
36 |             hour: '14'
37 |         });
38 |         expect(result).toBe('<timetable>Test XML</timetable>');
39 |     });
40 | 
41 |     test('findStations ruft die richtige API-Endpoint auf', async () => {
42 |         const result = await timetableApi.findStations({ pattern: 'Frankfurt' });
43 |         expect(result).toBe('<timetable>Test XML</timetable>');
44 |     });
45 | }); 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { config } from "../config.js";
 2 | 
 3 | export enum LogLevel {
 4 | 	DEBUG = "debug",
 5 | 	INFO = "info",
 6 | 	WARN = "warn",
 7 | 	ERROR = "error",
 8 | }
 9 | 
10 | const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
11 | 	[LogLevel.DEBUG]: 0,
12 | 	[LogLevel.INFO]: 1,
13 | 	[LogLevel.WARN]: 2,
14 | 	[LogLevel.ERROR]: 3,
15 | };
16 | 
17 | export type LogMetadata = Record<string, unknown>;
18 | 
19 | class Logger {
20 | 	private level: LogLevel;
21 | 
22 | 	constructor() {
23 | 		this.level = (config.logging.level as LogLevel) || LogLevel.INFO;
24 | 	}
25 | 
26 | 	private shouldLog(level: LogLevel): boolean {
27 | 		return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level];
28 | 	}
29 | 
30 | 	private formatMessage(
31 | 		level: LogLevel,
32 | 		message: string,
33 | 		meta?: LogMetadata,
34 | 	): string {
35 | 		const timestamp = new Date().toISOString();
36 | 		const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
37 | 		return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr}`;
38 | 	}
39 | 
40 | 	private log(level: LogLevel, message: string, meta?: LogMetadata): void {
41 | 		if (!this.shouldLog(level)) return;
42 | 
43 | 		const formattedMessage = this.formatMessage(level, message, meta);
44 | 
45 | 		switch (level) {
46 | 			case LogLevel.ERROR:
47 | 				console.error(formattedMessage);
48 | 				break;
49 | 			case LogLevel.WARN:
50 | 				console.warn(formattedMessage);
51 | 				break;
52 | 			case LogLevel.INFO:
53 | 				console.info(formattedMessage);
54 | 				break;
55 | 			case LogLevel.DEBUG:
56 | 				console.debug(formattedMessage);
57 | 				break;
58 | 			default:
59 | 				console.log(formattedMessage);
60 | 		}
61 | 	}
62 | 
63 | 	debug(message: string, meta?: LogMetadata): void {
64 | 		this.log(LogLevel.DEBUG, message, meta);
65 | 	}
66 | 
67 | 	info(message: string, meta?: LogMetadata): void {
68 | 		this.log(LogLevel.INFO, message, meta);
69 | 	}
70 | 
71 | 	warn(message: string, meta?: LogMetadata): void {
72 | 		this.log(LogLevel.WARN, message, meta);
73 | 	}
74 | 
75 | 	error(message: string, meta?: LogMetadata): void {
76 | 		this.log(LogLevel.ERROR, message, meta);
77 | 	}
78 | 
79 | 	setLevel(level: LogLevel): void {
80 | 		this.level = level;
81 | 	}
82 | }
83 | 
84 | export const logger = new Logger();
85 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { FastMCP } from "fastmcp";
 2 | import { config } from "./config.js";
 3 | import { resources } from "./resources/index.js";
 4 | import { tools } from "./tools/index.js";
 5 | import { logger } from "./utils/logger.js";
 6 | 
 7 | const server = new FastMCP({
 8 | 	name: config.server.name,
 9 | 	version: config.server.version as `${number}.${number}.${number}`,
10 | });
11 | 
12 | for (const tool of tools) {
13 | 	logger.info(`Tool hinzugefügt: ${tool.name}`);
14 | 	// @ts-ignore
15 | 	server.addTool(tool);
16 | }
17 | 
18 | for (const resource of resources) {
19 | 	logger.info(`Ressource hinzugefügt: ${resource.name}`);
20 | 	// @ts-ignore
21 | 	server.addResourceTemplate(resource);
22 | }
23 | 
24 | logger.info(`Starte ${config.server.name} v${config.server.version}`);
25 | 
26 | if (config.server.transportType === "sse") {
27 | 	logger.info(`Server startet im SSE-Modus auf Port ${config.server.port}`);
28 | 
29 | 	const endpoint = config.server.endpoint.startsWith("/")
30 | 		? config.server.endpoint
31 | 		: `/${config.server.endpoint}`;
32 | 
33 | 	server.start({
34 | 		transportType: "sse",
35 | 		sse: {
36 | 			endpoint: endpoint as `/${string}`,
37 | 			port: config.server.port,
38 | 		},
39 | 	});
40 | } else {
41 | 	logger.info("Server startet im stdio-Modus");
42 | 	server.start({
43 | 		transportType: "stdio",
44 | 	});
45 | }
46 | 
47 | server.on("connect", (event) => {
48 | 	// @ts-ignore - FastMCP Session-Typ hat möglicherweise keine id-Eigenschaft
49 | 	const sessionId = event.session?.id || "unbekannt";
50 | 	logger.info("Client verbunden", { sessionId });
51 | });
52 | 
53 | server.on("disconnect", (event) => {
54 | 	// @ts-ignore - FastMCP Session-Typ hat möglicherweise keine id-Eigenschaft
55 | 	const sessionId = event.session?.id || "unbekannt";
56 | 	logger.info("Client getrennt", { sessionId });
57 | });
58 | 
59 | process.on("SIGINT", () => {
60 | 	logger.info("Server wird beendet (SIGINT)");
61 | 	process.exit(0);
62 | });
63 | 
64 | process.on("SIGTERM", () => {
65 | 	logger.info("Server wird beendet (SIGTERM)");
66 | 	process.exit(0);
67 | });
68 | 
69 | process.on("uncaughtException", (error) => {
70 | 	logger.error("Unbehandelte Ausnahme", { error });
71 | 	process.exit(1);
72 | });
73 | 
74 | process.on("unhandledRejection", (reason) => {
75 | 	logger.error("Unbehandelte Promise-Ablehnung", { reason });
76 | });
77 | 
78 | export default server;
79 | 
```

--------------------------------------------------------------------------------
/src/utils/errorHandling.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { logger } from "../utils/logger.js";
 2 | 
 3 | export class AppError extends Error {
 4 | 	constructor(
 5 | 		public message: string,
 6 | 		public code = "INTERNAL_ERROR",
 7 | 		public statusCode = 500,
 8 | 		public details?: Record<string, unknown>,
 9 | 	) {
10 | 		super(message);
11 | 		this.name = this.constructor.name;
12 | 		Error.captureStackTrace(this, this.constructor);
13 | 	}
14 | }
15 | 
16 | export class ApiError extends AppError {
17 | 	constructor(
18 | 		message: string,
19 | 		code = "API_ERROR",
20 | 		statusCode = 500,
21 | 		details?: Record<string, unknown>,
22 | 	) {
23 | 		super(message, code, statusCode, details);
24 | 	}
25 | }
26 | 
27 | export class ValidationError extends AppError {
28 | 	constructor(message: string, details?: Record<string, unknown>) {
29 | 		super(message, "VALIDATION_ERROR", 400, details);
30 | 	}
31 | }
32 | 
33 | export class AuthenticationError extends AppError {
34 | 	constructor(message: string) {
35 | 		super(message, "AUTHENTICATION_ERROR", 401);
36 | 	}
37 | }
38 | export class ResourceNotFoundError extends AppError {
39 | 	constructor(message: string) {
40 | 		super(message, "RESOURCE_NOT_FOUND", 404);
41 | 	}
42 | }
43 | 
44 | export function asyncErrorHandler<T>(
45 | 	fn: (...args: unknown[]) => Promise<T>,
46 | ): (...args: unknown[]) => Promise<T> {
47 | 	return async (...args: unknown[]): Promise<T> => {
48 | 		try {
49 | 			return await fn(...args);
50 | 		} catch (error) {
51 | 			if (error instanceof AppError) {
52 | 				logger.error(`${error.name}: ${error.message}`, {
53 | 					code: error.code,
54 | 					statusCode: error.statusCode,
55 | 					details: error.details,
56 | 				});
57 | 				throw error;
58 | 			}
59 | 			const appError = new AppError(
60 | 				error instanceof Error ? error.message : "Unbekannter Fehler",
61 | 				"INTERNAL_ERROR",
62 | 				500,
63 | 				{ originalError: error },
64 | 			);
65 | 			logger.error(`${appError.name}: ${appError.message}`, {
66 | 				code: appError.code,
67 | 				statusCode: appError.statusCode,
68 | 				details: appError.details,
69 | 			});
70 | 			throw appError;
71 | 		}
72 | 	};
73 | }
74 | 
75 | export function withErrorHandling<T, R>(
76 | 	fn: (input: T) => Promise<R> | R,
77 | 	errorTransformer?: (error: unknown) => string,
78 | ): (input: T) => Promise<R> {
79 | 	return async (input: T): Promise<R> => {
80 | 		try {
81 | 			return await fn(input);
82 | 		} catch (error) {
83 | 			const errorMessage = errorTransformer
84 | 				? errorTransformer(error)
85 | 				: error instanceof Error
86 | 					? error.message
87 | 					: "Ein unbekannter Fehler ist aufgetreten";
88 | 
89 | 			logger.error("Fehler bei der Ausführung:", { error, input });
90 | 
91 | 			throw new Error(errorMessage);
92 | 		}
93 | 	};
94 | }
95 | 
```

--------------------------------------------------------------------------------
/src/api/timetableApi.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import fetch from "node-fetch";
 2 | import config from "../config.js";
 3 | import type { PlanParams, StationParams, TimetableParams } from "./types.js";
 4 | 
 5 | /**
 6 |  * API-Client für die DB Timetable API
 7 |  */
 8 | export class TimetableApiClient {
 9 | 	private baseUrl: string;
10 | 	private clientId: string;
11 | 	private clientSecret: string;
12 | 
13 | 	constructor() {
14 | 		this.baseUrl = config.api.baseUrl;
15 | 		this.clientId = config.api.clientId;
16 | 		this.clientSecret = config.api.clientSecret;
17 | 	}
18 | 
19 | 	/**
20 | 	 * Sendet eine Anfrage an die API mit entsprechenden Authentifizierungsheadern
21 | 	 */
22 | 	private async request<T>(endpoint: string): Promise<T> {
23 | 		try {
24 | 			const response = await fetch(`${this.baseUrl}${endpoint}`, {
25 | 				method: "GET",
26 | 				headers: {
27 | 					"DB-Client-Id": this.clientId,
28 | 					"DB-Api-Key": this.clientSecret,
29 | 					Accept: "application/xml",
30 | 				},
31 | 			});
32 | 
33 | 			if (!response.ok) {
34 | 				throw new Error(
35 | 					`API-Fehler: ${response.status} ${response.statusText}`,
36 | 				);
37 | 			}
38 | 
39 | 			// Die API gibt XML zurück, aber wir behandeln es für MCP als Text
40 | 			// In einer erweiterten Implementierung könnte man hier einen XML-Parser verwenden
41 | 			const data = await response.text();
42 | 
43 | 			// Für eine einfache Implementierung geben wir den XML-Text direkt zurück
44 | 			// In einer produktiven Implementierung würde man hier XML nach JSON konvertieren
45 | 			return data as unknown as T;
46 | 		} catch (error) {
47 | 			console.error("Fehler bei der API-Anfrage:", error);
48 | 			throw error;
49 | 		}
50 | 	}
51 | 
52 | 	/**
53 | 	 * Ruft den aktuellen Fahrplan für eine Station ab
54 | 	 */
55 | 	async getCurrentTimetable({ evaNo }: TimetableParams): Promise<string> {
56 | 		return this.request<string>(`/fchg/${evaNo}`);
57 | 	}
58 | 
59 | 	/**
60 | 	 * Ruft die letzten Änderungen für eine Station ab
61 | 	 */
62 | 	async getRecentChanges({ evaNo }: TimetableParams): Promise<string> {
63 | 		return this.request<string>(`/rchg/${evaNo}`);
64 | 	}
65 | 
66 | 	/**
67 | 	 * Ruft geplante Fahrplandaten für eine bestimmte Station und Zeitspanne ab
68 | 	 */
69 | 	async getPlannedTimetable({
70 | 		evaNo,
71 | 		date,
72 | 		hour,
73 | 	}: PlanParams): Promise<string> {
74 | 		return this.request<string>(`/plan/${evaNo}/${date}/${hour}`);
75 | 	}
76 | 
77 | 	/**
78 | 	 * Sucht nach Stationen, die dem angegebenen Muster entsprechen
79 | 	 */
80 | 	async findStations({ pattern }: StationParams): Promise<string> {
81 | 		return this.request<string>(`/station/${pattern}`);
82 | 	}
83 | }
84 | 
85 | // Export Singleton
86 | export const timetableApi = new TimetableApiClient();
87 | 
```

--------------------------------------------------------------------------------
/src/tests/config.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
 2 | 
 3 | // Mock von process und dotenv
 4 | vi.mock('node:path', () => ({
 5 |     default: {
 6 |         dirname: vi.fn(() => '/mock/dir'),
 7 |         resolve: vi.fn(() => '/mock/path/.env')
 8 |     }
 9 | }));
10 | 
11 | vi.mock('dotenv', () => ({
12 |     default: {
13 |         config: vi.fn()
14 |     }
15 | }));
16 | 
17 | describe('Konfiguration', () => {
18 |     // Speichere originale Umgebungsvariablen
19 |     const originalEnv = { ...process.env };
20 |     const originalExit = process.exit;
21 | 
22 |     beforeEach(() => {
23 |         // Mock der process.exit
24 |         process.exit = vi.fn() as unknown as (code?: number) => never;
25 | 
26 |         // Zuvor geladene Module löschen
27 |         vi.resetModules();
28 | 
29 |         // Umgebungsvariablen zurücksetzen
30 |         process.env = { ...originalEnv };
31 | 
32 |         // Grundlegende API-Credentials setzen, die für das Laden der Konfiguration benötigt werden
33 |         process.env.DB_TIMETABLE_CLIENT_ID = 'test-client-id';
34 |         process.env.DB_TIMETABLE_CLIENT_SECRET = 'test-client-secret';
35 |     });
36 | 
37 |     afterEach(() => {
38 |         // Originale Umgebungsvariablen und Exit-Funktion wiederherstellen
39 |         process.env = originalEnv;
40 |         process.exit = originalExit;
41 | 
42 |         vi.clearAllMocks();
43 |     });
44 | 
45 |     test('lädt die Standardkonfiguration, wenn keine Umgebungsvariablen gesetzt sind', async () => {
46 |         const { config } = await import('../config.js');
47 | 
48 |         expect(config.server.transportType).toBe('stdio');
49 |         expect(config.server.port).toBe(8080);
50 |         expect(config.server.endpoint).toBe('/sse');
51 |         expect(config.logging.level).toBe('info');
52 |         expect(config.api.baseUrl).toBe('https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1');
53 |     });
54 | 
55 |     test('nutzt Umgebungsvariablen, wenn gesetzt', async () => {
56 |         process.env.TRANSPORT_TYPE = 'sse';
57 |         process.env.PORT = '9000';
58 |         process.env.SSE_ENDPOINT = '/custom-endpoint';
59 |         process.env.LOG_LEVEL = 'debug';
60 | 
61 |         const { config } = await import('../config.js');
62 | 
63 |         expect(config.server.transportType).toBe('sse');
64 |         expect(config.server.port).toBe(9000);
65 |         expect(config.server.endpoint).toBe('/custom-endpoint');
66 |         expect(config.logging.level).toBe('debug');
67 |     });
68 | 
69 |     test('beendet den Prozess, wenn API-Credentials fehlen', async () => {
70 |         // Entferne API-Credentials
71 |         process.env.DB_TIMETABLE_CLIENT_ID = '';
72 |         process.env.DB_TIMETABLE_CLIENT_SECRET = '';
73 | 
74 |         // Ausgabe umlenken, um Fehlermeldungen zu unterdrücken
75 |         const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
76 | 
77 |         try {
78 |             await import('../config.js');
79 |         } catch (e) {
80 |             // Ignorieren, da der Prozess beendet werden würde
81 |         }
82 | 
83 |         expect(process.exit).toHaveBeenCalledWith(1);
84 |         expect(consoleSpy).toHaveBeenCalled();
85 | 
86 |         consoleSpy.mockRestore();
87 |     });
88 | }); 
```

--------------------------------------------------------------------------------
/src/tests/tools.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, test, expect, vi, beforeEach } from 'vitest';
 2 | import { getCurrentTimetableTool, getRecentChangesTool, getPlannedTimetableTool, findStationsTool } from '../tools/timetableTools.js';
 3 | import { timetableApi } from '../api/timetableApi.js';
 4 | 
 5 | vi.mock('../api/timetableApi.js', () => ({
 6 |     timetableApi: {
 7 |         getCurrentTimetable: vi.fn().mockResolvedValue('<timetable>Current Data</timetable>'),
 8 |         getRecentChanges: vi.fn().mockResolvedValue('<timetable>Recent Changes</timetable>'),
 9 |         getPlannedTimetable: vi.fn().mockResolvedValue('<timetable>Planned Data</timetable>'),
10 |         findStations: vi.fn().mockResolvedValue('<stations>Station Data</stations>')
11 |     }
12 | }));
13 | 
14 | describe('Timetable Tools', () => {
15 |     beforeEach(() => {
16 |         vi.clearAllMocks();
17 |     });
18 | 
19 |     describe('getCurrentTimetableTool', () => {
20 |         test('ruft timetableApi.getCurrentTimetable auf', async () => {
21 |             const args = { evaNo: '8000105' };
22 |             const result = await getCurrentTimetableTool.execute(args);
23 | 
24 |             expect(timetableApi.getCurrentTimetable).toHaveBeenCalledWith(args);
25 |             expect(result).toBe('<timetable>Current Data</timetable>');
26 |         });
27 | 
28 |         test('validiert Eingabeparameter', async () => {
29 |             const invalidArgs = { evaNo: '' };
30 |             await expect(getCurrentTimetableTool.execute(invalidArgs)).rejects.toThrow();
31 |         });
32 |     });
33 | 
34 |     describe('getRecentChangesTool', () => {
35 |         test('ruft timetableApi.getRecentChanges auf', async () => {
36 |             const args = { evaNo: '8000105' };
37 |             const result = await getRecentChangesTool.execute(args);
38 | 
39 |             expect(timetableApi.getRecentChanges).toHaveBeenCalledWith(args);
40 |             expect(result).toBe('<timetable>Recent Changes</timetable>');
41 |         });
42 |     });
43 | 
44 |     describe('getPlannedTimetableTool', () => {
45 |         test('ruft timetableApi.getPlannedTimetable auf', async () => {
46 |             const args = { evaNo: '8000105', date: '230401', hour: '14' };
47 |             const result = await getPlannedTimetableTool.execute(args);
48 | 
49 |             expect(timetableApi.getPlannedTimetable).toHaveBeenCalledWith(args);
50 |             expect(result).toBe('<timetable>Planned Data</timetable>');
51 |         });
52 | 
53 |         test('validiert Datumsformat', async () => {
54 |             const invalidArgs = { evaNo: '8000105', date: '2304', hour: '14' };
55 |             await expect(getPlannedTimetableTool.execute(invalidArgs)).rejects.toThrow();
56 |         });
57 | 
58 |         test('validiert Stundenformat', async () => {
59 |             const invalidArgs = { evaNo: '8000105', date: '230401', hour: '24' };
60 |             await expect(getPlannedTimetableTool.execute(invalidArgs)).rejects.toThrow();
61 |         });
62 |     });
63 | 
64 |     describe('findStationsTool', () => {
65 |         test('ruft timetableApi.findStations auf', async () => {
66 |             const args = { pattern: 'Frankfurt' };
67 |             const result = await findStationsTool.execute(args);
68 | 
69 |             expect(timetableApi.findStations).toHaveBeenCalledWith(args);
70 |             expect(result).toBe('<stations>Station Data</stations>');
71 |         });
72 |     });
73 | }); 
```

--------------------------------------------------------------------------------
/src/tests/integration.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, test, expect, vi, beforeEach } from 'vitest';
 2 | import { tools } from '../tools/index.js';
 3 | import { resources } from '../resources/index.js';
 4 | 
 5 | vi.mock('../api/timetableApi.js', () => ({
 6 |     timetableApi: {
 7 |         getCurrentTimetable: vi.fn().mockResolvedValue('<timetable>Current Data</timetable>'),
 8 |         getRecentChanges: vi.fn().mockResolvedValue('<timetable>Recent Changes</timetable>'),
 9 |         getPlannedTimetable: vi.fn().mockResolvedValue('<timetable>Planned Data</timetable>'),
10 |         findStations: vi.fn().mockResolvedValue('<stations>Station Data</stations>')
11 |     }
12 | }));
13 | 
14 | const mockMcpServer = {
15 |     addTool: vi.fn(),
16 |     addResource: vi.fn(),
17 |     executeTool: vi.fn(),
18 |     loadResource: vi.fn()
19 | };
20 | 
21 | mockMcpServer.executeTool.mockImplementation((toolId, _args) => {
22 |     if (toolId === 'db-timetable:getCurrentTimetable') {
23 |         return Promise.resolve('<timetable>Current Data</timetable>');
24 |     }
25 |     if (toolId === 'db-timetable:findStations') {
26 |         return Promise.resolve('<stations>Station Data</stations>');
27 |     }
28 |     return Promise.resolve(null);
29 | });
30 | 
31 | mockMcpServer.loadResource.mockImplementation((uri) => {
32 |     if (uri === 'db-api:timetable/current/8000105') {
33 |         return Promise.resolve('<timetable>Current Data</timetable>');
34 |     }
35 |     if (uri === 'db-api:station/Frankfurt') {
36 |         return Promise.resolve('<stations>Station Data</stations>');
37 |     }
38 |     return Promise.reject(new Error(`Resource nicht gefunden: ${uri}`));
39 | });
40 | 
41 | describe('MCP Server Integration', () => {
42 |     // biome-ignore lint/suspicious/noExplicitAny: <explanation>
43 |     let server: any;
44 | 
45 |     beforeEach(() => {
46 |         server = mockMcpServer;
47 | 
48 |         vi.clearAllMocks();
49 |     });
50 | 
51 |     describe('Tool Ausführung', () => {
52 |         test('kann getCurrentTimetable ausführen', async () => {
53 |             for (const tool of tools) {
54 |                 server.addTool(tool);
55 |             }
56 | 
57 |             const result = await server.executeTool('db-timetable:getCurrentTimetable', { evaNo: '8000105' });
58 |             expect(result).toBe('<timetable>Current Data</timetable>');
59 |             expect(server.addTool).toHaveBeenCalled();
60 |         });
61 | 
62 |         test('kann findStations ausführen', async () => {
63 |             const result = await server.executeTool('db-timetable:findStations', { pattern: 'Frankfurt' });
64 |             expect(result).toBe('<stations>Station Data</stations>');
65 |         });
66 |     });
67 | 
68 |     describe('Resource Anfragen', () => {
69 |         test('kann die aktuelle Fahrplantafel abrufen', async () => {
70 |             for (const resource of resources) {
71 |                 server.addResource(resource);
72 |             }
73 | 
74 |             const result = await server.loadResource('db-api:timetable/current/8000105');
75 |             expect(result).toBe('<timetable>Current Data</timetable>');
76 |             expect(server.addResource).toHaveBeenCalled();
77 |         });
78 | 
79 |         test('kann Stationen suchen', async () => {
80 |             const result = await server.loadResource('db-api:station/Frankfurt');
81 |             expect(result).toBe('<stations>Station Data</stations>');
82 |         });
83 | 
84 |         test('liefert einen Fehler bei ungültigem Ressourcen-URI', async () => {
85 |             await expect(server.loadResource('db-api:invalid')).rejects.toThrow();
86 |         });
87 |     });
88 | }); 
```

--------------------------------------------------------------------------------
/src/tests/utils/logger.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
  2 | import { logger, LogLevel } from '../../utils/logger.js';
  3 | 
  4 | describe('Logger', () => {
  5 |     const originalConsole = {
  6 |         error: console.error,
  7 |         warn: console.warn,
  8 |         info: console.info,
  9 |         debug: console.debug,
 10 |         log: console.log
 11 |     };
 12 | 
 13 |     beforeEach(() => {
 14 |         console.error = vi.fn();
 15 |         console.warn = vi.fn();
 16 |         console.info = vi.fn();
 17 |         console.debug = vi.fn();
 18 |         console.log = vi.fn();
 19 |     });
 20 | 
 21 |     afterEach(() => {
 22 |         console.error = originalConsole.error;
 23 |         console.warn = originalConsole.warn;
 24 |         console.info = originalConsole.info;
 25 |         console.debug = originalConsole.debug;
 26 |         console.log = originalConsole.log;
 27 |     });
 28 | 
 29 |     describe('Log Methoden', () => {
 30 |         test('debug() ruft console.debug auf', () => {
 31 |             logger.setLevel(LogLevel.DEBUG);
 32 |             logger.debug('Debug Nachricht');
 33 | 
 34 |             expect(console.debug).toHaveBeenCalled();
 35 |             const callArg = (console.debug as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
 36 |             expect(callArg).toContain('DEBUG');
 37 |             expect(callArg).toContain('Debug Nachricht');
 38 |         });
 39 | 
 40 |         test('info() ruft console.info auf', () => {
 41 |             logger.info('Info Nachricht');
 42 | 
 43 |             expect(console.info).toHaveBeenCalled();
 44 |             const callArg = (console.info as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
 45 |             expect(callArg).toContain('INFO');
 46 |             expect(callArg).toContain('Info Nachricht');
 47 |         });
 48 | 
 49 |         test('warn() ruft console.warn auf', () => {
 50 |             logger.warn('Warn Nachricht');
 51 | 
 52 |             expect(console.warn).toHaveBeenCalled();
 53 |             const callArg = (console.warn as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
 54 |             expect(callArg).toContain('WARN');
 55 |             expect(callArg).toContain('Warn Nachricht');
 56 |         });
 57 | 
 58 |         test('error() ruft console.error auf', () => {
 59 |             logger.error('Error Nachricht');
 60 | 
 61 |             expect(console.error).toHaveBeenCalled();
 62 |             const callArg = (console.error as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
 63 |             expect(callArg).toContain('ERROR');
 64 |             expect(callArg).toContain('Error Nachricht');
 65 |         });
 66 |     });
 67 | 
 68 |     describe('Log Level', () => {
 69 |         test('zeigt keine DEBUG-Nachrichten bei INFO Level', () => {
 70 |             logger.setLevel(LogLevel.INFO);
 71 |             logger.debug('Debug sollte nicht angezeigt werden');
 72 | 
 73 |             expect(console.debug).not.toHaveBeenCalled();
 74 |         });
 75 | 
 76 |         test('zeigt keine INFO-Nachrichten bei WARN Level', () => {
 77 |             logger.setLevel(LogLevel.WARN);
 78 |             logger.info('Info sollte nicht angezeigt werden');
 79 | 
 80 |             expect(console.info).not.toHaveBeenCalled();
 81 |         });
 82 | 
 83 |         test('zeigt keine WARN-Nachrichten bei ERROR Level', () => {
 84 |             logger.setLevel(LogLevel.ERROR);
 85 |             logger.warn('Warn sollte nicht angezeigt werden');
 86 | 
 87 |             expect(console.warn).not.toHaveBeenCalled();
 88 |         });
 89 | 
 90 |         test('zeigt ERROR-Nachrichten bei jedem Level', () => {
 91 |             logger.setLevel(LogLevel.DEBUG);
 92 |             logger.error('Error sollte angezeigt werden');
 93 | 
 94 |             expect(console.error).toHaveBeenCalled();
 95 |         });
 96 |     });
 97 | 
 98 |     describe('Metadaten', () => {
 99 |         test('formatiert Metadaten korrekt', () => {
100 |             const metadata = { user: 'test', action: 'login' };
101 |             logger.info('Info mit Metadaten', metadata);
102 | 
103 |             const callArg = (console.info as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
104 |             expect(callArg).toContain('INFO');
105 |             expect(callArg).toContain('Info mit Metadaten');
106 |             expect(callArg).toContain('"user":"test"');
107 |             expect(callArg).toContain('"action":"login"');
108 |         });
109 |     });
110 | }); 
```

--------------------------------------------------------------------------------
/TESTING.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Testing des DB Timetable MCP Servers
  2 | 
  3 | Dieses Dokument beschreibt verschiedene Methoden zum Testen und Debuggen des DB Timetable MCP Servers.
  4 | 
  5 | ## Automatische Tests
  6 | 
  7 | Der Server enthält automatische Tests, die mit Vitest ausgeführt werden können:
  8 | 
  9 | ```bash
 10 | npm test
 11 | ```
 12 | 
 13 | Die Tests überprüfen:
 14 | - API-Client-Funktionalität
 15 | - Tool-Implementierung
 16 | - Server-Integration
 17 | 
 18 | ## Manuelles Testen
 19 | 
 20 | ### Mit FastMCP Inspector
 21 | 
 22 | Der FastMCP Inspector ist ein Befehlszeilenwerkzeug, mit dem MCP-Server interaktiv getestet werden können.
 23 | 
 24 | 1. Installiere den FastMCP Inspector global:
 25 |    ```bash
 26 |    npm install -g fastmcp
 27 |    ```
 28 | 
 29 | 2. Starte den Server im stdio-Modus:
 30 |    ```bash
 31 |    npm start
 32 |    ```
 33 | 
 34 | 3. In einem anderen Terminal-Fenster führe den Inspector aus:
 35 |    ```bash
 36 |    fastmcp inspect
 37 |    ```
 38 | 
 39 | 4. Verwende den Inspector, um mit den Tools und Ressourcen zu interagieren.
 40 | 
 41 | ### Testen im stdio-Modus
 42 | 
 43 | Im stdio-Modus arbeitet der Server über die Standardein- und -ausgabe. Dies ist nützlich für die Entwicklung und das Debugging.
 44 | 
 45 | 1. Starte den Server:
 46 |    ```bash
 47 |    npm start
 48 |    ```
 49 | 
 50 | 2. Sende MCP-formatierte Nachrichten, z.B.:
 51 |    ```json
 52 |    {"type":"request","id":"test-1","method":"tool","params":{"tool":"getCurrentTimetable","input":{"evaNo":"8000105"}}}
 53 |    ```
 54 | 
 55 | ### Testen im SSE-Modus
 56 | 
 57 | Im SSE-Modus (Server-Sent Events) erstellt der Server einen HTTP-Endpunkt, der für Webanwendungen zugänglich ist.
 58 | 
 59 | 1. Starte den Server im SSE-Modus:
 60 |    ```bash
 61 |    TRANSPORT_TYPE=sse npm start
 62 |    ```
 63 | 
 64 | 2. Verwende einen SSE-Client, um zu testen (z.B. mit einer Browserkonsole oder einem Tool wie curl).
 65 | 
 66 | ## Test-Beispiele
 67 | 
 68 | ### Aktuelle Fahrplandaten abrufen
 69 | 
 70 | ```json
 71 | {
 72 |   "type": "request",
 73 |   "id": "test-1",
 74 |   "method": "tool",
 75 |   "params": {
 76 |     "tool": "getCurrentTimetable",
 77 |     "input": {
 78 |       "evaNo": "8000105"
 79 |     }
 80 |   }
 81 | }
 82 | ```
 83 | 
 84 | ### Stationssuche
 85 | 
 86 | ```json
 87 | {
 88 |   "type": "request",
 89 |   "id": "test-2",
 90 |   "method": "tool",
 91 |   "params": {
 92 |     "tool": "findStations",
 93 |     "input": {
 94 |       "pattern": "Frankfurt"
 95 |     }
 96 |   }
 97 | }
 98 | ```
 99 | 
100 | ### Geplante Fahrplandaten abrufen
101 | 
102 | ```json
103 | {
104 |   "type": "request",
105 |   "id": "test-3",
106 |   "method": "tool",
107 |   "params": {
108 |     "tool": "getPlannedTimetable",
109 |     "input": {
110 |       "evaNo": "8000105",
111 |       "date": "230401",
112 |       "hour": "14"
113 |     }
114 |   }
115 | }
116 | ```
117 | 
118 | ### Ressource abrufen
119 | 
120 | ```json
121 | {
122 |   "type": "request",
123 |   "id": "test-4",
124 |   "method": "resource",
125 |   "params": {
126 |     "uri": "db-api:timetable/current/8000105"
127 |   }
128 | }
129 | ```
130 | 
131 | ## Debugging
132 | 
133 | ### Logging
134 | 
135 | Der Server verwendet einen strukturierten Logger mit verschiedenen Log-Levels:
136 | 
137 | - DEBUG: Detaillierte Debugging-Informationen
138 | - INFO: Allgemeine Informationen (Standard)
139 | - WARN: Warnungen
140 | - ERROR: Fehlermeldungen
141 | 
142 | Das Log-Level kann in der .env-Datei eingestellt werden:
143 | 
144 | ```
145 | LOG_LEVEL=debug
146 | ```
147 | 
148 | ### Fehlerbehandlung testen
149 | 
150 | Um die Fehlerbehandlung zu testen, können Sie ungültige Parameter an die Tools übergeben:
151 | 
152 | ```json
153 | {
154 |   "type": "request",
155 |   "id": "test-error",
156 |   "method": "tool",
157 |   "params": {
158 |     "tool": "getCurrentTimetable",
159 |     "input": {
160 |       "evaNo": ""
161 |     }
162 |   }
163 | }
164 | ```
165 | 
166 | ### Bekannte Fehlercodes
167 | 
168 | - `VALIDATION_ERROR`: Ungültige Eingabeparameter
169 | - `API_ERROR`: Fehler bei der API-Anfrage
170 | - `INTERNAL_ERROR`: Interner Serverfehler
171 | - `AUTHENTICATION_ERROR`: Authentifizierungsfehler
172 | - `RESOURCE_NOT_FOUND`: Ressource nicht gefunden
173 | 
174 | ## Typische Teststationen
175 | 
176 | Hier sind einige gültige EVA-Nummern für Tests:
177 | 
178 | - 8000105: Frankfurt (Main) Hbf
179 | - 8000096: Berlin Hbf
180 | - 8000152: Hamburg Hbf
181 | - 8000244: München Hbf
182 | - 8000098: Köln Hbf
183 | 
184 | ## Fehlerbehebung
185 | 
186 | ### API-Zugangsdaten
187 | 
188 | Stellen Sie sicher, dass in der .env-Datei gültige API-Zugangsdaten für die DB Timetable API konfiguriert sind:
189 | 
190 | ```
191 | DB_TIMETABLE_CLIENT_ID=your-client-id
192 | DB_TIMETABLE_CLIENT_SECRET=your-client-secret
193 | ```
194 | 
195 | ### Netzwerkfehler
196 | 
197 | Bei Netzwerkfehlern überprüfen Sie:
198 | 
199 | 1. Internetverbindung
200 | 2. API-Erreichbarkeit
201 | 3. Gültigkeit der API-Zugangsdaten
202 | 
203 | ### Server startet nicht
204 | 
205 | 1. Prüfen Sie, ob die erforderlichen Abhängigkeiten installiert sind: `npm install`
206 | 2. Prüfen Sie, ob der TypeScript-Code kompiliert wurde: `npm run build`
207 | 3. Überprüfen Sie die Log-Ausgabe auf Fehlermeldungen 
```

--------------------------------------------------------------------------------
/src/tools/timetableTools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { timetableApi } from "../api/timetableApi.js";
  3 | import type {
  4 | 	PlanParams,
  5 | 	StationParams,
  6 | 	TimetableParams,
  7 | } from "../api/types.js";
  8 | import { ValidationError, asyncErrorHandler } from "../utils/errorHandling.js";
  9 | import { type LogMetadata, logger } from "../utils/logger.js";
 10 | 
 11 | const TimetableParamsSchema = z.object({
 12 | 	evaNo: z
 13 | 		.string()
 14 | 		.min(1)
 15 | 		.describe("EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)"),
 16 | });
 17 | 
 18 | const PlanParamsSchema = z.object({
 19 | 	evaNo: z
 20 | 		.string()
 21 | 		.min(1)
 22 | 		.describe("EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)"),
 23 | 	date: z
 24 | 		.string()
 25 | 		.regex(/^\d{6}$/)
 26 | 		.describe("Datum im Format YYMMDD (z.B. 230401 für 01.04.2023)"),
 27 | 	hour: z
 28 | 		.string()
 29 | 		.regex(/^([0-1][0-9]|2[0-3])$/)
 30 | 		.describe("Stunde im Format HH (z.B. 14 für 14 Uhr)"),
 31 | });
 32 | 
 33 | const StationParamsSchema = z.object({
 34 | 	pattern: z
 35 | 		.string()
 36 | 		.min(1)
 37 | 		.describe("Suchmuster für Stationen (z.B. Frankfurt oder 8000105)"),
 38 | });
 39 | 
 40 | export const getCurrentTimetableTool = {
 41 | 	name: "getCurrentTimetable",
 42 | 	description:
 43 | 		"Ruft die aktuellen Fahrplandaten einer bestimmten Bahnhofsstation ab. Dies beinhaltet Informationen zu Ankunfts- und Abfahrtszeiten, Gleisbelegungen, Verspätungen und weitere Echtzeitinformationen für den aktuellen Betriebstag.",
 44 | 	parameters: TimetableParamsSchema,
 45 | 	execute: asyncErrorHandler(async (args) => {
 46 | 		logger.info("Rufe aktuelle Fahrplandaten ab", args as LogMetadata);
 47 | 
 48 | 		const validateResult = TimetableParamsSchema.safeParse(args);
 49 | 		if (!validateResult.success) {
 50 | 			throw new ValidationError(
 51 | 				"Ungültige Parameter für getCurrentTimetable",
 52 | 				validateResult.error.format(),
 53 | 			);
 54 | 		}
 55 | 
 56 | 		const result = await timetableApi.getCurrentTimetable(
 57 | 			args as TimetableParams,
 58 | 		);
 59 | 		return result;
 60 | 	}),
 61 | };
 62 | 
 63 | export const getRecentChangesTool = {
 64 | 	name: "getRecentChanges",
 65 | 	description:
 66 | 		"Ermittelt die neuesten Fahrplanänderungen für eine spezifische Bahnhofsstation. Dazu gehören Verspätungen, Gleisänderungen, Ausfälle und andere kurzfristige Anpassungen im Betriebsablauf, die in Echtzeit aktualisiert werden.",
 67 | 	parameters: TimetableParamsSchema,
 68 | 	execute: asyncErrorHandler(async (args) => {
 69 | 		logger.info("Rufe aktuelle Änderungen ab", args as LogMetadata);
 70 | 
 71 | 		const validateResult = TimetableParamsSchema.safeParse(args);
 72 | 		if (!validateResult.success) {
 73 | 			throw new ValidationError(
 74 | 				"Ungültige Parameter für getRecentChanges",
 75 | 				validateResult.error.format(),
 76 | 			);
 77 | 		}
 78 | 
 79 | 		const result = await timetableApi.getRecentChanges(args as TimetableParams);
 80 | 		return result;
 81 | 	}),
 82 | };
 83 | 
 84 | export const getPlannedTimetableTool = {
 85 | 	name: "getPlannedTimetable",
 86 | 	description:
 87 | 		"Holt die geplanten Fahrplandaten für eine angegebene Bahnhofsstation zu einem bestimmten Datum und einer bestimmten Stunde ein. Diese Funktion ist nützlich, um Fahrpläne im Voraus zu planen und Informationen über zukünftige Zugverbindungen zu erhalten.",
 88 | 	parameters: PlanParamsSchema,
 89 | 	execute: asyncErrorHandler(async (args) => {
 90 | 		logger.info("Rufe geplante Fahrplandaten ab", args as LogMetadata);
 91 | 
 92 | 		const validateResult = PlanParamsSchema.safeParse(args);
 93 | 		if (!validateResult.success) {
 94 | 			throw new ValidationError(
 95 | 				"Ungültige Parameter für getPlannedTimetable",
 96 | 				validateResult.error.format(),
 97 | 			);
 98 | 		}
 99 | 
100 | 		const result = await timetableApi.getPlannedTimetable(args as PlanParams);
101 | 		return result;
102 | 	}),
103 | };
104 | 
105 | export const findStationsTool = {
106 | 	name: "findStations",
107 | 	description:
108 | 		"Durchsucht das Verzeichnis der Bahnhofsstationen anhand eines gegebenen Suchmusters. Dies kann der Name der Station oder die EVA-Nummer sein. Das Tool liefert eine Liste von Stationen, die dem Suchmuster entsprechen.",
109 | 	parameters: StationParamsSchema,
110 | 	execute: asyncErrorHandler(async (args) => {
111 | 		logger.info("Suche nach Stationen", args as LogMetadata);
112 | 
113 | 		const validateResult = StationParamsSchema.safeParse(args);
114 | 		if (!validateResult.success) {
115 | 			throw new ValidationError(
116 | 				"Ungültige Parameter für findStations",
117 | 				validateResult.error.format(),
118 | 			);
119 | 		}
120 | 
121 | 		const result = await timetableApi.findStations(args as StationParams);
122 | 		return result;
123 | 	}),
124 | };
125 | 
```

--------------------------------------------------------------------------------
/src/resources/timetableResources.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { timetableApi } from "../api/timetableApi.js";
  2 | import { ValidationError } from "../utils/errorHandling.js";
  3 | import { logger } from "../utils/logger.js";
  4 | 
  5 | export interface TimetableResourceArgs {
  6 | 	evaNo: string;
  7 | }
  8 | 
  9 | export interface PlannedTimetableResourceArgs {
 10 | 	evaNo: string;
 11 | 	date: string;
 12 | 	hour: string;
 13 | }
 14 | 
 15 | export interface StationResourceArgs {
 16 | 	pattern: string;
 17 | }
 18 | 
 19 | export const currentTimetableResource = {
 20 | 	uriTemplate: "db-api:timetable/current/{evaNo}",
 21 | 	name: "Aktuelle Fahrplandaten",
 22 | 	description:
 23 | 		"Aktuelle Fahrplandaten für eine Bahnhofsstation mit Informationen zu Ankunfts- und Abfahrtszeiten, Gleisen, Verspätungen und anderen relevanten Betriebsinformationen. Die Daten werden in Echtzeit von der DB Timetable API abgerufen und im XML-Format bereitgestellt.",
 24 | 	mimeType: "application/xml",
 25 | 	arguments: [
 26 | 		{
 27 | 			name: "evaNo",
 28 | 			description: "EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)",
 29 | 			required: true,
 30 | 		},
 31 | 	],
 32 | 	async load({ evaNo }: TimetableResourceArgs) {
 33 | 		logger.info("Lade aktuelle Fahrplandaten", { evaNo });
 34 | 
 35 | 		if (!evaNo) {
 36 | 			throw new ValidationError("EVA-Nummer ist erforderlich");
 37 | 		}
 38 | 
 39 | 		try {
 40 | 			const data = await timetableApi.getCurrentTimetable({ evaNo });
 41 | 			return {
 42 | 				text: data,
 43 | 			};
 44 | 		} catch (error) {
 45 | 			logger.error("Fehler beim Laden der aktuellen Fahrplandaten", {
 46 | 				error,
 47 | 				evaNo,
 48 | 			});
 49 | 			throw error;
 50 | 		}
 51 | 	},
 52 | };
 53 | 
 54 | export const recentChangesResource = {
 55 | 	uriTemplate: "db-api:timetable/changes/{evaNo}",
 56 | 	name: "Aktuelle Fahrplanänderungen",
 57 | 	description:
 58 | 		"Enthält aktuelle Fahrplanänderungen in Echtzeit für eine bestimmte Bahnhofsstation. Dies umfasst Informationen zu Verspätungen, Gleisänderungen, Ausfällen und andere relevante betriebliche Anpassungen. Die Daten werden von der DB Timetable API im XML-Format abgerufen.",
 59 | 	mimeType: "application/xml",
 60 | 	arguments: [
 61 | 		{
 62 | 			name: "evaNo",
 63 | 			description: "EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)",
 64 | 			required: true,
 65 | 		},
 66 | 	],
 67 | 	async load({ evaNo }: TimetableResourceArgs) {
 68 | 		logger.info("Lade aktuelle Fahrplanänderungen", { evaNo });
 69 | 
 70 | 		if (!evaNo) {
 71 | 			throw new ValidationError("EVA-Nummer ist erforderlich");
 72 | 		}
 73 | 
 74 | 		try {
 75 | 			const data = await timetableApi.getRecentChanges({ evaNo });
 76 | 			return {
 77 | 				text: data,
 78 | 			};
 79 | 		} catch (error) {
 80 | 			logger.error("Fehler beim Laden der aktuellen Fahrplanänderungen", {
 81 | 				error,
 82 | 				evaNo,
 83 | 			});
 84 | 			throw error;
 85 | 		}
 86 | 	},
 87 | };
 88 | 
 89 | export const plannedTimetableResource = {
 90 | 	uriTemplate: "db-api:timetable/planned/{evaNo}/{date}/{hour}",
 91 | 	name: "Geplante Fahrplandaten",
 92 | 	description:
 93 | 		"Geplante Fahrplandaten für eine Bahnhofsstation zu einer bestimmten Zeit",
 94 | 	mimeType: "application/xml",
 95 | 	arguments: [
 96 | 		{
 97 | 			name: "evaNo",
 98 | 			description: "EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)",
 99 | 			required: true,
100 | 		},
101 | 		{
102 | 			name: "date",
103 | 			description: "Datum im Format YYMMDD (z.B. 230401 für 01.04.2023)",
104 | 			required: true,
105 | 		},
106 | 		{
107 | 			name: "hour",
108 | 			description: "Stunde im Format HH (z.B. 14 für 14 Uhr)",
109 | 			required: true,
110 | 		},
111 | 	],
112 | 	async load({ evaNo, date, hour }: PlannedTimetableResourceArgs) {
113 | 		logger.info("Lade geplante Fahrplandaten", { evaNo, date, hour });
114 | 
115 | 		if (!evaNo || !date || !hour) {
116 | 			throw new ValidationError(
117 | 				"Alle Parameter (evaNo, date, hour) sind erforderlich",
118 | 			);
119 | 		}
120 | 
121 | 		if (!/^\d{6}$/.test(date)) {
122 | 			throw new ValidationError("Datum muss im Format YYMMDD sein");
123 | 		}
124 | 
125 | 		if (!/^([0-1][0-9]|2[0-3])$/.test(hour)) {
126 | 			throw new ValidationError(
127 | 				"Stunde muss im Format HH sein und zwischen 00 und 23 liegen",
128 | 			);
129 | 		}
130 | 
131 | 		try {
132 | 			const data = await timetableApi.getPlannedTimetable({
133 | 				evaNo,
134 | 				date,
135 | 				hour,
136 | 			});
137 | 			return {
138 | 				text: data,
139 | 			};
140 | 		} catch (error) {
141 | 			logger.error("Fehler beim Laden der geplanten Fahrplandaten", {
142 | 				error,
143 | 				evaNo,
144 | 				date,
145 | 				hour,
146 | 			});
147 | 			throw error;
148 | 		}
149 | 	},
150 | };
151 | 
152 | export const stationResource = {
153 | 	uriTemplate: "db-api:station/{pattern}",
154 | 	name: "Stationssuche",
155 | 	description: "Suche nach Bahnhofsstationen anhand eines Musters",
156 | 	mimeType: "application/xml",
157 | 	arguments: [
158 | 		{
159 | 			name: "pattern",
160 | 			description: "Suchmuster für Stationen (z.B. Frankfurt oder 8000105)",
161 | 			required: true,
162 | 		},
163 | 	],
164 | 	async load({ pattern }: StationResourceArgs) {
165 | 		logger.info("Suche nach Stationen", { pattern });
166 | 
167 | 		if (!pattern) {
168 | 			throw new ValidationError("Suchmuster ist erforderlich");
169 | 		}
170 | 
171 | 		try {
172 | 			const data = await timetableApi.findStations({ pattern });
173 | 			return {
174 | 				text: data,
175 | 			};
176 | 		} catch (error) {
177 | 			logger.error("Fehler bei der Stationssuche", { error, pattern });
178 | 			throw error;
179 | 		}
180 | 	},
181 | };
182 | 
```

--------------------------------------------------------------------------------
/src/tests/utils/errorHandling.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, test, expect, vi, beforeEach } from 'vitest';
  2 | import {
  3 |     AppError,
  4 |     ApiError,
  5 |     ValidationError,
  6 |     AuthenticationError,
  7 |     ResourceNotFoundError,
  8 |     asyncErrorHandler,
  9 |     withErrorHandling
 10 | } from '../../utils/errorHandling.js';
 11 | import { logger } from '../../utils/logger.js';
 12 | 
 13 | vi.mock('../../utils/logger.js', () => ({
 14 |     logger: {
 15 |         error: vi.fn(),
 16 |         warn: vi.fn(),
 17 |         info: vi.fn(),
 18 |         debug: vi.fn()
 19 |     }
 20 | }));
 21 | 
 22 | describe('Error Handling Utilities', () => {
 23 |     beforeEach(() => {
 24 |         vi.clearAllMocks();
 25 |     });
 26 | 
 27 |     describe('Error Klassen', () => {
 28 |         test('AppError hat korrekte Eigenschaften', () => {
 29 |             const error = new AppError('Test Fehler', 'TEST_CODE', 400, { test: 'details' });
 30 | 
 31 |             expect(error.message).toBe('Test Fehler');
 32 |             expect(error.code).toBe('TEST_CODE');
 33 |             expect(error.statusCode).toBe(400);
 34 |             expect(error.details).toEqual({ test: 'details' });
 35 |             expect(error.name).toBe('AppError');
 36 |         });
 37 | 
 38 |         test('ApiError erbt von AppError mit korrekten Standardwerten', () => {
 39 |             const error = new ApiError('API Fehler');
 40 | 
 41 |             expect(error.message).toBe('API Fehler');
 42 |             expect(error.code).toBe('API_ERROR');
 43 |             expect(error.statusCode).toBe(500);
 44 |             expect(error instanceof AppError).toBe(true);
 45 |         });
 46 | 
 47 |         test('ValidationError hat korrekte Eigenschaften', () => {
 48 |             const error = new ValidationError('Validierungsfehler', { field: 'test' });
 49 | 
 50 |             expect(error.message).toBe('Validierungsfehler');
 51 |             expect(error.code).toBe('VALIDATION_ERROR');
 52 |             expect(error.statusCode).toBe(400);
 53 |             expect(error.details).toEqual({ field: 'test' });
 54 |             expect(error instanceof AppError).toBe(true);
 55 |         });
 56 | 
 57 |         test('AuthenticationError hat korrekte Eigenschaften', () => {
 58 |             const error = new AuthenticationError('Auth Fehler');
 59 | 
 60 |             expect(error.message).toBe('Auth Fehler');
 61 |             expect(error.code).toBe('AUTHENTICATION_ERROR');
 62 |             expect(error.statusCode).toBe(401);
 63 |             expect(error instanceof AppError).toBe(true);
 64 |         });
 65 | 
 66 |         test('ResourceNotFoundError hat korrekte Eigenschaften', () => {
 67 |             const error = new ResourceNotFoundError('Resource nicht gefunden');
 68 | 
 69 |             expect(error.message).toBe('Resource nicht gefunden');
 70 |             expect(error.code).toBe('RESOURCE_NOT_FOUND');
 71 |             expect(error.statusCode).toBe(404);
 72 |             expect(error instanceof AppError).toBe(true);
 73 |         });
 74 |     });
 75 | 
 76 |     describe('asyncErrorHandler', () => {
 77 |         test('gibt das Ergebnis zurück, wenn keine Fehler auftreten', async () => {
 78 |             const successFn = async () => 'Erfolg';
 79 |             const handledFn = asyncErrorHandler(successFn);
 80 | 
 81 |             const result = await handledFn();
 82 |             expect(result).toBe('Erfolg');
 83 |             expect(logger.error).not.toHaveBeenCalled();
 84 |         });
 85 | 
 86 |         test('loggt und wirft AppError weiter', async () => {
 87 |             const errorFn = async () => {
 88 |                 throw new ValidationError('Test Validierungsfehler');
 89 |             };
 90 |             const handledFn = asyncErrorHandler(errorFn);
 91 | 
 92 |             await expect(handledFn()).rejects.toThrow(ValidationError);
 93 |             expect(logger.error).toHaveBeenCalled();
 94 |         });
 95 | 
 96 |         test('wandelt Standard-Fehler in AppError um', async () => {
 97 |             const errorFn = async () => {
 98 |                 throw new Error('Standard Fehler');
 99 |             };
100 |             const handledFn = asyncErrorHandler(errorFn);
101 | 
102 |             await expect(handledFn()).rejects.toThrow(AppError);
103 |             expect(logger.error).toHaveBeenCalled();
104 |         });
105 |     });
106 | 
107 |     describe('withErrorHandling', () => {
108 |         test('gibt das Ergebnis zurück, wenn keine Fehler auftreten', async () => {
109 |             const successFn = () => 'Erfolg';
110 |             const handledFn = withErrorHandling(successFn);
111 | 
112 |             const result = await handledFn({});
113 |             expect(result).toBe('Erfolg');
114 |             expect(logger.error).not.toHaveBeenCalled();
115 |         });
116 | 
117 |         test('verarbeitet Fehler und wirft neue Error-Instanz', async () => {
118 |             const errorFn = () => {
119 |                 throw new Error('Originalfehler');
120 |             };
121 |             const handledFn = withErrorHandling(errorFn);
122 | 
123 |             await expect(handledFn({})).rejects.toThrow('Originalfehler');
124 |             expect(logger.error).toHaveBeenCalled();
125 |         });
126 | 
127 |         test('verwendet errorTransformer, wenn angegeben', async () => {
128 |             const errorFn = () => {
129 |                 throw new Error('Originalfehler');
130 |             };
131 |             const errorTransformer = () => 'Transformierter Fehler';
132 |             const handledFn = withErrorHandling(errorFn, errorTransformer);
133 | 
134 |             await expect(handledFn({})).rejects.toThrow('Transformierter Fehler');
135 |             expect(logger.error).toHaveBeenCalled();
136 |         });
137 |     });
138 | }); 
```

--------------------------------------------------------------------------------
/src/tests/index.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
  2 | 
  3 | vi.mock('fastmcp', () => ({
  4 |     FastMCP: vi.fn().mockImplementation(() => ({
  5 |         addTool: vi.fn(),
  6 |         addResourceTemplate: vi.fn(),
  7 |         start: vi.fn(),
  8 |         on: vi.fn()
  9 |     }))
 10 | }));
 11 | 
 12 | vi.mock('../config.js', () => ({
 13 |     config: {
 14 |         server: {
 15 |             name: 'Test MCP Server',
 16 |             version: '1.0.0',
 17 |             transportType: 'stdio',
 18 |             port: 8080,
 19 |             endpoint: '/test'
 20 |         },
 21 |         api: {
 22 |             baseUrl: 'https://test-api.example.com',
 23 |             clientId: 'test-id',
 24 |             clientSecret: 'test-secret'
 25 |         },
 26 |         logging: {
 27 |             level: 'info'
 28 |         }
 29 |     }
 30 | }));
 31 | 
 32 | vi.mock('../resources/index.js', () => ({
 33 |     resources: [
 34 |         { name: 'TestResource1', pattern: 'test:resource1' },
 35 |         { name: 'TestResource2', pattern: 'test:resource2' }
 36 |     ]
 37 | }));
 38 | 
 39 | vi.mock('../tools/index.js', () => ({
 40 |     tools: [
 41 |         { name: 'TestTool1', description: 'Test Tool 1' },
 42 |         { name: 'TestTool2', description: 'Test Tool 2' }
 43 |     ]
 44 | }));
 45 | 
 46 | vi.mock('../utils/logger.js', () => ({
 47 |     logger: {
 48 |         info: vi.fn(),
 49 |         error: vi.fn(),
 50 |         warn: vi.fn(),
 51 |         debug: vi.fn()
 52 |     }
 53 | }));
 54 | 
 55 | describe('MCP Server', () => {
 56 |     const originalListeners = {
 57 |         SIGINT: process.listeners('SIGINT'),
 58 |         SIGTERM: process.listeners('SIGTERM'),
 59 |         uncaughtException: process.listeners('uncaughtException'),
 60 |         unhandledRejection: process.listeners('unhandledRejection')
 61 |     };
 62 | 
 63 |     beforeEach(() => {
 64 |         process.removeAllListeners('SIGINT');
 65 |         process.removeAllListeners('SIGTERM');
 66 |         process.removeAllListeners('uncaughtException');
 67 |         process.removeAllListeners('unhandledRejection');
 68 | 
 69 |         process.exit = vi.fn() as unknown as (code?: number) => never;
 70 | 
 71 |         vi.resetModules();
 72 |     });
 73 | 
 74 |     afterEach(() => {
 75 |         process.removeAllListeners('SIGINT');
 76 |         process.removeAllListeners('SIGTERM');
 77 |         process.removeAllListeners('uncaughtException');
 78 |         process.removeAllListeners('unhandledRejection');
 79 | 
 80 |         for (const listener of originalListeners.SIGINT) {
 81 |             process.on('SIGINT', listener);
 82 |         }
 83 | 
 84 |         for (const listener of originalListeners.SIGTERM) {
 85 |             process.on('SIGTERM', listener);
 86 |         }
 87 | 
 88 |         for (const listener of originalListeners.uncaughtException) {
 89 |             process.on('uncaughtException', listener);
 90 |         }
 91 | 
 92 |         for (const listener of originalListeners.unhandledRejection) {
 93 |             process.on('unhandledRejection', listener);
 94 |         }
 95 | 
 96 |         vi.clearAllMocks();
 97 |     });
 98 | 
 99 |     test('Initialisiert FastMCP Server mit den korrekten Parametern', async () => {
100 |         const { FastMCP } = await import('fastmcp');
101 |         await import('../index.js');
102 | 
103 |         expect(FastMCP).toHaveBeenCalledWith({
104 |             name: 'Test MCP Server',
105 |             version: '1.0.0'
106 |         });
107 |     });
108 | 
109 |     test('Fügt alle Tools und Ressourcen hinzu', async () => {
110 |         const { logger } = await import('../utils/logger.js');
111 |         const serverModule = await import('../index.js');
112 |         const server = serverModule.default;
113 | 
114 |         expect(server.addTool).toHaveBeenCalledTimes(2);
115 |         expect(server.addResourceTemplate).toHaveBeenCalledTimes(2);
116 |         expect(logger.info).toHaveBeenCalledWith('Tool hinzugefügt: TestTool1');
117 |         expect(logger.info).toHaveBeenCalledWith('Tool hinzugefügt: TestTool2');
118 |         expect(logger.info).toHaveBeenCalledWith('Ressource hinzugefügt: TestResource1');
119 |         expect(logger.info).toHaveBeenCalledWith('Ressource hinzugefügt: TestResource2');
120 |     });
121 | 
122 |     test('Startet den Server im stdio-Modus', async () => {
123 |         const serverModule = await import('../index.js');
124 |         const server = serverModule.default;
125 | 
126 |         expect(server.start).toHaveBeenCalledWith({
127 |             transportType: 'stdio'
128 |         });
129 |     });
130 | 
131 |     test('Startet den Server im SSE-Modus, wenn konfiguriert', async () => {
132 |         vi.doMock('../config.js', () => ({
133 |             config: {
134 |                 server: {
135 |                     name: 'Test MCP Server',
136 |                     version: '1.0.0',
137 |                     transportType: 'sse',
138 |                     port: 9000,
139 |                     endpoint: 'test-endpoint'
140 |                 },
141 |                 api: {
142 |                     baseUrl: 'https://test-api.example.com',
143 |                     clientId: 'test-id',
144 |                     clientSecret: 'test-secret'
145 |                 },
146 |                 logging: {
147 |                     level: 'info'
148 |                 }
149 |             }
150 |         }));
151 | 
152 |         vi.resetModules();
153 | 
154 |         const serverModule = await import('../index.js');
155 |         const server = serverModule.default;
156 | 
157 |         expect(server.start).toHaveBeenCalledWith({
158 |             transportType: 'sse',
159 |             sse: {
160 |                 endpoint: '/test-endpoint',
161 |                 port: 9000
162 |             }
163 |         });
164 |     });
165 | 
166 |     test('Registriert Event-Handler für connect und disconnect', async () => {
167 |         const serverModule = await import('../index.js');
168 |         const server = serverModule.default;
169 | 
170 |         expect(server.on).toHaveBeenCalledTimes(2);
171 |         expect(server.on).toHaveBeenCalledWith('connect', expect.any(Function));
172 |         expect(server.on).toHaveBeenCalledWith('disconnect', expect.any(Function));
173 |     });
174 | 
175 |     test('Registriert Prozess-Event-Handler', async () => {
176 |         await import('../index.js');
177 | 
178 |         expect(process.listeners('SIGINT').length).toBeGreaterThan(0);
179 |         expect(process.listeners('SIGTERM').length).toBeGreaterThan(0);
180 |         expect(process.listeners('uncaughtException').length).toBeGreaterThan(0);
181 |         expect(process.listeners('unhandledRejection').length).toBeGreaterThan(0);
182 | 
183 |         process.emit('SIGINT');
184 |         const { logger } = await import('../utils/logger.js');
185 |         expect(logger.info).toHaveBeenCalledWith('Server wird beendet (SIGINT)');
186 |         expect(process.exit).toHaveBeenCalledWith(0);
187 |     });
188 | }); 
```

--------------------------------------------------------------------------------
/src/api/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Type-Definitionen für die DB Timetable API
  3 |  * Basierend auf der OpenAPI-Spezifikation
  4 |  */
  5 | 
  6 | export enum EventStatus {
  7 | 	PLANNED = "p", // Geplantes Ereignis
  8 | 	ADDED = "a", // Hinzugefügtes Ereignis
  9 | 	CANCELLED = "c", // Storniertes Ereignis
 10 | }
 11 | 
 12 | export enum MessageType {
 13 | 	HIM = "h", // Störungsmeldung (HIM - Hafas Information Manager)
 14 | 	QUALITY_CHANGE = "q", // Qualitätsänderung
 15 | 	FREE = "f", // Freie Meldung
 16 | 	CAUSE_OF_DELAY = "d", // Verspätungsursache
 17 | 	IBIS = "i", // IBIS-Meldung (Integriertes Bord-Informations-System)
 18 | 	UNASSIGNED_IBIS_MESSAGE = "u", // Nicht zugeordnete IBIS-Meldung
 19 | 	DISRUPTION = "r", // Betriebsstörung
 20 | 	CONNECTION = "c", // Anschlussinformation
 21 | }
 22 | 
 23 | export enum Priority {
 24 | 	HIGH = "1", // Hohe Priorität
 25 | 	MEDIUM = "2", // Mittlere Priorität
 26 | 	LOW = "3", // Niedrige Priorität
 27 | 	DONE = "4", // Erledigt
 28 | }
 29 | 
 30 | export enum ConnectionStatus {
 31 | 	WAITING = "w", // Wartend
 32 | 	TRANSITION = "n", // Übergang
 33 | 	ALTERNATIVE = "a", // Alternative
 34 | }
 35 | 
 36 | export enum DelaySource {
 37 | 	LEIBIT = "L", // LEIBIT (Leit- und Informationssystem für den Betriebsdienst)
 38 | 	RISNE_AUT = "NA", // RIS::NE automatisch (Reisendeninformationssystem Nahverkehr)
 39 | 	RISNE_MAN = "NM", // RIS::NE manuell
 40 | 	VDV = "V", // VDV (Verband Deutscher Verkehrsunternehmen)
 41 | 	ISTP_AUT = "IA", // ISTP automatisch (Integrierte Steuerung Transportprozesse)
 42 | 	ISTP_MAN = "IM", // ISTP manuell
 43 | 	AUTOMATIC_PROGNOSIS = "A", // Automatische Prognose
 44 | }
 45 | 
 46 | export enum DistributorType {
 47 | 	CITY = "s", // Stadt
 48 | 	REGION = "r", // Region
 49 | 	LONG_DISTANCE = "f", // Fernverkehr
 50 | 	OTHER = "x", // Sonstige
 51 | }
 52 | 
 53 | export enum TripType {
 54 | 	P = "p", // Personenzug (Personentransport)
 55 | 	E = "e", // Eilzug (Schnellerer Personenzug)
 56 | 	Z = "z", // Zusatzzug (Sonderzug)
 57 | 	S = "s", // S-Bahn (Stadtschnellbahn)
 58 | 	H = "h", // Hilfszug (Notfall- oder Wartungszug)
 59 | 	N = "n", // Nachtzug (Nachtverkehr)
 60 | }
 61 | 
 62 | export enum ReferenceTripRelationToStop {
 63 | 	BEFORE = "b", // Vor dem Halt
 64 | 	END = "e", // Ende des Halts
 65 | 	BETWEEN = "c", // Zwischen Halten
 66 | 	START = "s", // Beginn des Halts
 67 | 	AFTER = "a", // Nach dem Halt
 68 | }
 69 | 
 70 | export interface Event {
 71 | 	/** Geplante Ankunftszeit (changed) */
 72 | 	cde?: string;
 73 | 	/** Letzte Änderungszeit */
 74 | 	clt?: string;
 75 | 	/** Geänderter Bahnsteig */
 76 | 	cp?: string;
 77 | 	/** Geänderter Bahnsteigpfad */
 78 | 	cpth?: string;
 79 | 	/** Status des geänderten Ereignisses */
 80 | 	cs?: EventStatus;
 81 | 	/** Geänderte Zeit */
 82 | 	ct?: string;
 83 | 	/** Verzögerungscode */
 84 | 	dc?: number;
 85 | 	/** Haltinformations-ID */
 86 | 	hi?: number;
 87 | 	/** Linie */
 88 | 	l?: string;
 89 | 	/** Zugehörige Meldungen */
 90 | 	m?: Message[];
 91 | 	/** Geplante Abfahrtszeit */
 92 | 	pde?: string;
 93 | 	/** Geplanter Bahnsteig */
 94 | 	pp?: string;
 95 | 	/** Geplanter Bahnsteigpfad */
 96 | 	ppth?: string;
 97 | 	/** Status des geplanten Ereignisses */
 98 | 	ps?: EventStatus;
 99 | 	/** Geplante Zeit */
100 | 	pt?: string;
101 | 	/** Zugattribut */
102 | 	tra?: string;
103 | 	/** Flügelzüge (durch Komma getrennte IDs) */
104 | 	wings?: string;
105 | }
106 | 
107 | export interface Message {
108 | 	/** Code */
109 | 	c?: number;
110 | 	/** Kategorie */
111 | 	cat?: string;
112 | 	/** Verzögerung in Minuten */
113 | 	del?: number;
114 | 	/** Verteiler-Meldungen */
115 | 	dm?: DistributorMessage[];
116 | 	/** Ereigniscode */
117 | 	ec?: string;
118 | 	/** Externer Link */
119 | 	elnk?: string;
120 | 	/** Externer Text */
121 | 	ext?: string;
122 | 	/** Gültig von (Zeitstempel) */
123 | 	from?: string;
124 | 	/** Eindeutige Meldungs-ID */
125 | 	id: string;
126 | 	/** Interner Text */
127 | 	int?: string;
128 | 	/** Besitzer/Ersteller der Meldung */
129 | 	o?: string;
130 | 	/** Priorität der Meldung */
131 | 	pr?: Priority;
132 | 	/** Typ der Meldung */
133 | 	t: MessageType;
134 | 	/** Zugehörige Zugbezeichnungen */
135 | 	tl?: TripLabel[];
136 | 	/** Gültig bis (Zeitstempel) */
137 | 	to?: string;
138 | 	/** Zeitstempel der Meldungserstellung */
139 | 	ts: string;
140 | }
141 | 
142 | export interface DistributorMessage {
143 | 	/** Interner Text */
144 | 	int?: string;
145 | 	/** Name des Verteilers */
146 | 	n?: string;
147 | 	/** Typ des Verteilers */
148 | 	t?: DistributorType;
149 | 	/** Zeitstempel */
150 | 	ts?: string;
151 | }
152 | 
153 | export interface TripLabel {
154 | 	/** Kategorie des Zuges (z.B. ICE, RE, S) */
155 | 	c: string;
156 | 	/** Zusätzliche Flags */
157 | 	f?: string;
158 | 	/** Zugnummer */
159 | 	n: string;
160 | 	/** Betreiber-ID */
161 | 	o: string;
162 | 	/** Zugtyp */
163 | 	t?: TripType;
164 | }
165 | 
166 | export interface TripReference {
167 | 	/** Referenzierte Zugbezeichnungen */
168 | 	rt?: TripLabel[];
169 | 	/** Zugbezeichnung */
170 | 	tl: TripLabel;
171 | }
172 | 
173 | export interface Connection {
174 | 	/** Status der Verbindung */
175 | 	cs: ConnectionStatus;
176 | 	/** EVA-Nummer (eindeutige Stationskennung) */
177 | 	eva?: number;
178 | 	/** Verbindungs-ID */
179 | 	id: string;
180 | 	/** Referenzierter Halt */
181 | 	ref?: TimetableStop;
182 | 	/** Quell-Halt */
183 | 	s: TimetableStop;
184 | 	/** Zeitstempel */
185 | 	ts: string;
186 | }
187 | 
188 | export interface HistoricDelay {
189 | 	/** Ankunftsverspätung */
190 | 	ar?: string;
191 | 	/** Verspätungsursache */
192 | 	cod?: string;
193 | 	/** Abfahrtsverspätung */
194 | 	dp?: string;
195 | 	/** Quelle der Verspätungsinformation */
196 | 	src?: DelaySource;
197 | 	/** Zeitstempel */
198 | 	ts?: string;
199 | }
200 | 
201 | export interface HistoricPlatformChange {
202 | 	/** Ankunftsgleiswechsel */
203 | 	ar?: string;
204 | 	/** Ursache des Gleiswechsels */
205 | 	cot?: string;
206 | 	/** Abfahrtsgleiswechsel */
207 | 	dp?: string;
208 | 	/** Zeitstempel */
209 | 	ts?: string;
210 | }
211 | 
212 | export interface ReferenceTrip {
213 | 	/** Ist abgeschlossen */
214 | 	c: boolean;
215 | 	/** Endpunkt */
216 | 	ea: ReferenceTripStopLabel;
217 | 	/** Referenz-ID */
218 | 	id: string;
219 | 	/** Referenz-Zugbezeichnung */
220 | 	rtl: ReferenceTripLabel;
221 | 	/** Startpunkt */
222 | 	sd: ReferenceTripStopLabel;
223 | }
224 | 
225 | export interface ReferenceTripLabel {
226 | 	/** Kategorie des Zuges */
227 | 	c: string;
228 | 	/** Zugnummer */
229 | 	n: string;
230 | }
231 | 
232 | export interface ReferenceTripStopLabel {
233 | 	/** EVA-Nummer der Station */
234 | 	eva: number;
235 | 	/** Index im Fahrplan */
236 | 	i: number;
237 | 	/** Name der Station */
238 | 	n: string;
239 | 	/** Geplante Zeit */
240 | 	pt: string;
241 | }
242 | 
243 | export interface ReferenceTripRelation {
244 | 	/** Referenzfahrt */
245 | 	rt: ReferenceTrip;
246 | 	/** Beziehung zum Halt */
247 | 	rts: ReferenceTripRelationToStop;
248 | }
249 | 
250 | export interface TimetableStop {
251 | 	/** Ankunftsereignis */
252 | 	ar?: Event;
253 | 	/** Verbindungen */
254 | 	conn?: Connection[];
255 | 	/** Abfahrtsereignis */
256 | 	dp?: Event;
257 | 	/** EVA-Nummer der Station */
258 | 	eva: number;
259 | 	/** Historische Verspätungen */
260 | 	hd?: HistoricDelay[];
261 | 	/** Historische Gleiswechsel */
262 | 	hpc?: HistoricPlatformChange[];
263 | 	/** Eindeutige ID des Halts */
264 | 	id: string;
265 | 	/** Meldungen */
266 | 	m?: Message[];
267 | 	/** Referenz zu einem anderen Zug */
268 | 	ref?: TripReference;
269 | 	/** Referenzfahrtbeziehungen */
270 | 	rtr?: ReferenceTripRelation[];
271 | 	/** Zugbezeichnung */
272 | 	tl?: TripLabel;
273 | }
274 | 
275 | export interface Timetable {
276 | 	/** EVA-Nummer der Station */
277 | 	eva?: number;
278 | 	/** Meldungen */
279 | 	m?: Message[];
280 | 	/** Liste der Halte */
281 | 	s?: TimetableStop[];
282 | 	/** Name der Station */
283 | 	station?: string;
284 | }
285 | 
286 | export interface StationData {
287 | 	/** DS100-Code (betriebliche Abkürzung) */
288 | 	ds100: string;
289 | 	/** EVA-Nummer (eindeutige Stationskennung) */
290 | 	eva: number;
291 | 	/** Metadaten */
292 | 	meta?: string;
293 | 	/** Name der Station */
294 | 	name: string;
295 | 	/** Plattform-Information */
296 | 	p?: string;
297 | }
298 | 
299 | export interface MultipleStationData {
300 | 	/** Liste von Stationsdaten */
301 | 	station: StationData[];
302 | }
303 | 
304 | export interface TimetableParams {
305 | 	/** EVA-Nummer der Station */
306 | 	evaNo: string;
307 | }
308 | 
309 | export interface PlanParams extends TimetableParams {
310 | 	date: string; // Format YYMMDD
311 | 	hour: string; // Format HH
312 | }
313 | 
314 | export interface StationParams {
315 | 	pattern: string;
316 | }
317 | 
318 | export interface TimetableResponse {
319 | 	timetable: Timetable;
320 | }
321 | 
322 | export interface StationResponse {
323 | 	stations: MultipleStationData;
324 | }
325 | 
```