#
tokens: 17937/50000 27/27 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
# API-Zugangsdaten für die Deutsche Bahn API
DB_TIMETABLE_CLIENT_ID=your-client-id
DB_TIMETABLE_CLIENT_SECRET=your-client-secret

# Server-Konfiguration
# Mögliche Werte: 'stdio' oder 'sse'
TRANSPORT_TYPE=stdio
PORT=8080
SSE_ENDPOINT=/sse

# Logging-Konfiguration
# Mögliche Werte: 'debug', 'info', 'warn', 'error'
LOG_LEVEL=info 
```

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

```
# Abhängigkeiten
node_modules/
npm-debug.log
yarn-error.log
yarn-debug.log
package-lock.json
.pnpm-lock.yaml

# Kompilierte Ausgabe
dist/
build/
*.tsbuildinfo

# Umgebungsvariablen
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDE und Editoren
.idea/
.vscode/
*.swp
*.swo
.DS_Store
.directory
*.sublime-project
*.sublime-workspace

# Cache
.npm
.eslintcache
.cache/
.parcel-cache/

# Test-Abdeckung
coverage/
.nyc_output/

# Temporäre Dateien
tmp/
temp/

# Betriebssystemdateien
.DS_Store
Thumbs.db 
```

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

```markdown
[![smithery badge](https://smithery.ai/badge/@jorekai/db-timetable-mcp)](https://smithery.ai/server/@jorekai/db-timetable-mcp)
# DB Timetable MCP Server

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.

**Pflicht zur Namensnennung:**  

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.

Weitere Infos zur API und Lizenzbedingungen findest du unter [developers.deutschebahn.com](https://developers.deutschebahn.com/). API Requests unterliegen den Bedingungen der Lizenz.


## Funktionen

- **Aktuelle Fahrplände**: Abrufen aktueller Fahrplandaten für eine Station
- **Fahrplanänderungen**: Tracking der neuesten Änderungen
- **Geplante Fahrpläne**: Zugriff auf geplante Fahrplandaten für einen bestimmten Zeitpunkt
- **Stationssuche**: Suche nach Bahnhofsstationen anhand von Namen oder Codes

## Voraussetzungen

- Node.js 18 oder höher
- API-Zugangsdaten für die DB Timetable API (Client-ID und Client-Secret)

## Installation

1. Repository klonen:
   ```
   git clone <repository-url>
   cd db-mcp
   ```

2. Abhängigkeiten installieren:
   ```
   npm install
   ```

3. TypeScript-Code kompilieren:
   ```
   npm run build
   ```

## Konfiguration

Erstelle eine `.env`-Datei im Root-Verzeichnis des Projekts mit folgenden Umgebungsvariablen:

```
DB_TIMETABLE_CLIENT_ID=deine-client-id
DB_TIMETABLE_CLIENT_SECRET=dein-client-secret
TRANSPORT_TYPE=stdio
PORT=8080
SSE_ENDPOINT=/sse
LOG_LEVEL=info
```

### Konfigurationsoptionen

- `DB_TIMETABLE_CLIENT_ID`: Client-ID für die DB API (erforderlich)
- `DB_TIMETABLE_CLIENT_SECRET`: Client-Secret für die DB API (erforderlich)
- `TRANSPORT_TYPE`: Transporttyp für den MCP-Server (`stdio` oder `sse`, Standard: `stdio`)
- `PORT`: Port für den SSE-Server (Standard: `8080`)
- `SSE_ENDPOINT`: Endpunkt für SSE-Verbindungen (Standard: `/sse`)
- `LOG_LEVEL`: Logging-Level (`debug`, `info`, `warn`, `error`, Standard: `info`)

## Verwendung

### Server starten

Im stdio-Modus (für CLI-Tests und Debugging):

```bash
npm start
```

Im SSE-Modus (für Webclients):

```bash
TRANSPORT_TYPE=sse npm start
```

### Mit Inspect-Modus testen

Der Server kann mit dem FastMCP Inspector getestet werden:

```bash
npx fastmcp inspect path/to/index.js
```

### MCP-Tools

Der Server stellt folgende Tools bereit:

1. **getCurrentTimetable**: Ruft aktuelle Fahrplandaten für eine Station ab
   - Parameter: `evaNo` - EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)

2. **getRecentChanges**: Ruft aktuelle Änderungen für eine Station ab
   - Parameter: `evaNo` - EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)

3. **getPlannedTimetable**: Ruft geplante Fahrplandaten für eine Station ab
   - Parameter: 
     - `evaNo` - EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)
     - `date` - Datum im Format YYMMDD (z.B. 230401 für 01.04.2023)
     - `hour` - Stunde im Format HH (z.B. 14 für 14 Uhr)

4. **findStations**: Sucht nach Stationen anhand eines Suchmusters
   - Parameter: `pattern` - Suchmuster (z.B. "Frankfurt" oder "BLS")

### MCP-Ressourcen

Der Server stellt folgende Ressourcen bereit:

1. **Aktuelle Fahrplandaten**: `db-api:timetable/current/{evaNo}`
2. **Aktuelle Fahrplanänderungen**: `db-api:timetable/changes/{evaNo}`
3. **Geplante Fahrplandaten**: `db-api:timetable/planned/{evaNo}/{date}/{hour}`
4. **Stationssuche**: `db-api:station/{pattern}`

## Entwicklung

### Projekt-Struktur

```
db-mcp/
├── src/
│   ├── api/             # API-Client und Typen
│   ├── tools/           # MCP-Tools
│   ├── resources/       # MCP-Ressourcen
│   ├── utils/           # Hilfsfunktionen
│   ├── config.ts        # Konfiguration
│   └── index.ts         # Haupteinstiegspunkt
├── dist/                # Kompilierte Dateien
├── .env                 # Umgebungsvariablen
├── package.json
├── tsconfig.json
└── README.md
```

### NPM-Skripte

- `npm run build`: Kompiliert den TypeScript-Code
- `npm start`: Startet den Server
- `npm run dev`: Startet den Server im Entwicklungsmodus mit automatischem Neuladen
- `npm test`: Führt Tests aus

## Erweiterbarkeit

Potenzielle Erweiterungen
1. Datenverarbeitung und -anreicherung
   - Semantische Fahrplandatenverarbeitung: XML zu strukturiertem JSON mit semantischer Anreicherung
   - Historische Datenanalyse für Verspätungen und Betriebsstörungen
   - Integration multimodaler Verkehrsverbindungen
2. Erweiterte MCP-Tools
   - Routenplanung zwischen Stationen
   - KI-basierte Verspätungs- und Auslastungsprognosen
   - Reisestörungsanalyse
   - Barrierefreiheitscheck für Stationen und Verbindungen

## Lizenz

MCP Server: [MIT Lizenz](LICENSE)

DB Timetable API: [Creative Commons Namensnennung 4.0 International Lizenz](https://developers.deutschebahn.com/db-api-marketplace/apis/product/timetables)

```

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

```markdown
MIT License

Copyright (c) [2025] [Nils Jorek]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

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

```typescript
import {
	findStationsTool,
	getCurrentTimetableTool,
	getPlannedTimetableTool,
	getRecentChangesTool,
} from "../tools/timetableTools.js";

export const tools = [
	getCurrentTimetableTool,
	getRecentChangesTool,
	getPlannedTimetableTool,
	findStationsTool,
];

export {
	getCurrentTimetableTool,
	getRecentChangesTool,
	getPlannedTimetableTool,
	findStationsTool,
};

```

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

```dockerfile
FROM node:22.12-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
COPY package-lock.json ./
COPY tsconfig.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source files
COPY . .

# Build TypeScript
RUN npm run build

# Set environment variables
ENV NODE_ENV=production
ENV TRANSPORT_TYPE=stdio

# Run in production mode
CMD ["node", "dist/index.js"]



```

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

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
	test: {
		globals: true,
		environment: "node",
		include: ["src/tests/**/*.test.ts"],
		exclude: ["node_modules/", "dist/**", "vitest.config.ts", "**/types.ts"],
		coverage: {
			provider: "v8",
			reporter: ["text", "json", "html"],
			exclude: [
				"node_modules/",
				"src/tests/**",
				"dist/**",
				"vitest.config.ts",
				"**/types.ts",
			],
		},
	},
});

```

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

```typescript
import {
	PlannedTimetableResourceArgs,
	StationResourceArgs,
	TimetableResourceArgs,
	currentTimetableResource,
	plannedTimetableResource,
	recentChangesResource,
	stationResource,
} from "./timetableResources.js";

export const resources = [
	currentTimetableResource,
	recentChangesResource,
	plannedTimetableResource,
	stationResource,
];

export {
	currentTimetableResource,
	recentChangesResource,
	plannedTimetableResource,
	stationResource,
	TimetableResourceArgs,
	PlannedTimetableResourceArgs,
	StationResourceArgs,
};

```

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

```json
{
	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
	"vcs": {
		"enabled": false,
		"clientKind": "git",
		"useIgnoreFile": false
	},
	"files": {
		"ignoreUnknown": false,
		"ignore": [
			"dist/**",
			"node_modules/**",
			"**/*.test.ts",
			"**/*.spec.ts",
			"coverage/**"
		]
	},
	"formatter": {
		"enabled": true,
		"indentStyle": "tab"
	},
	"organizeImports": {
		"enabled": true
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	},
	"javascript": {
		"formatter": {
			"quoteStyle": "double"
		}
	}
}

```

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

```json
{
	"compilerOptions": {
		"target": "ES2022",
		"module": "NodeNext",
		"moduleResolution": "NodeNext",
		"esModuleInterop": true,
		"allowSyntheticDefaultImports": true,
		"strict": true,
		"outDir": "dist",
		"sourceMap": true,
		"declaration": true,
		"resolveJsonModule": true,
		"skipLibCheck": true,
		"forceConsistentCasingInFileNames": true,
		"noImplicitAny": true,
		"noImplicitThis": true,
		"strictNullChecks": true,
		"strictFunctionTypes": true,
		"strictPropertyInitialization": true,
		"types": ["node", "vitest/globals"],
		"baseUrl": "."
	},
	"include": ["src/**/*"],
	"exclude": ["node_modules", "dist"]
}

```

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

```json
{
	"name": "db-mcp",
	"version": "1.0.0",
	"type": "module",
	"main": "dist/index.js",
	"bin": {
		"db-mcp": "dist/index.js"
	},
	"scripts": {
		"build": "tsc",
		"start": "node dist/index.js",
		"dev": "node --loader ts-node/esm src/index.ts",
		"dev:sse": "cross-env TRANSPORT_TYPE=sse node --loader ts-node/esm src/index.ts",
		"test": "vitest run",
		"test-ci": "vitest run --coverage.enabled --coverage.reporter='text-summary'",
		"lint": "npx biome check src",
		"coverage": "npm run test -- --coverage"
	},
	"keywords": ["mcp", "deutsche-bahn", "timetable", "api-client"],
	"author": "Nils Jorek <[email protected]>",
	"license": "MIT",
	"description": "MCP Server für die Deutsche Bahn Timetable API",
	"dependencies": {
		"dotenv": "^16.4.7",
		"fastmcp": "^1.20.5",
		"node-fetch": "^2.7.0",
		"zod": "^3.24.2"
	},
	"devDependencies": {
		"@biomejs/biome": "1.9.4",
		"@types/node": "^22.13.11",
		"@types/node-fetch": "^2.6.12",
		"@vitest/coverage-v8": "^3.0.9",
		"eslint": "^8.57.0",
		"ts-node": "^10.9.2",
		"typescript": "^5.8.2",
		"vitest": "^3.0.9"
	}
}

```

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

```typescript
import path from "node:path";
import { fileURLToPath } from "node:url";
import dotenv from "dotenv";

// Lade Umgebungsvariablen
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, "../../.env") });

// Konfigurationswerte
export const config = {
	server: {
		name: "DB Timetables MCP Server",
		version: "1.0.0",
		transportType: process.env.TRANSPORT_TYPE || "stdio", // 'stdio' oder 'sse'
		port: Number.parseInt(process.env.PORT || "8080", 10),
		endpoint: process.env.SSE_ENDPOINT || "/sse",
	},
	api: {
		baseUrl:
			"https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1",
		clientId: process.env.DB_TIMETABLE_CLIENT_ID || "",
		clientSecret: process.env.DB_TIMETABLE_CLIENT_SECRET || "",
	},
	logging: {
		level: process.env.LOG_LEVEL || "info",
	},
};

if (!config.api.clientId || !config.api.clientSecret) {
	console.error(
		"API-Zugangsdaten fehlen! Bitte setze DB_TIMETABLE_CLIENT_ID und DB_TIMETABLE_CLIENT_SECRET in .env",
	);
	process.exit(1);
}

export default config;

```

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

```typescript
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { timetableApi } from '../api/timetableApi.js';

vi.mock('node-fetch', async () => {
    const actual = await vi.importActual('node-fetch');
    return {
        ...actual,
        default: vi.fn(() =>
            Promise.resolve({
                ok: true,
                text: () => Promise.resolve('<timetable>Test XML</timetable>'),
            })
        )
    };
});

describe('TimetableApiClient', () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });

    test('getCurrentTimetable ruft die richtige API-Endpoint auf', async () => {
        const result = await timetableApi.getCurrentTimetable({ evaNo: '8000105' });
        expect(result).toBe('<timetable>Test XML</timetable>');
    });

    test('getRecentChanges ruft die richtige API-Endpoint auf', async () => {
        const result = await timetableApi.getRecentChanges({ evaNo: '8000105' });
        expect(result).toBe('<timetable>Test XML</timetable>');
    });

    test('getPlannedTimetable ruft die richtige API-Endpoint auf', async () => {
        const result = await timetableApi.getPlannedTimetable({
            evaNo: '8000105',
            date: '230401',
            hour: '14'
        });
        expect(result).toBe('<timetable>Test XML</timetable>');
    });

    test('findStations ruft die richtige API-Endpoint auf', async () => {
        const result = await timetableApi.findStations({ pattern: 'Frankfurt' });
        expect(result).toBe('<timetable>Test XML</timetable>');
    });
}); 
```

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

```typescript
import { config } from "../config.js";

export enum LogLevel {
	DEBUG = "debug",
	INFO = "info",
	WARN = "warn",
	ERROR = "error",
}

const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
	[LogLevel.DEBUG]: 0,
	[LogLevel.INFO]: 1,
	[LogLevel.WARN]: 2,
	[LogLevel.ERROR]: 3,
};

export type LogMetadata = Record<string, unknown>;

class Logger {
	private level: LogLevel;

	constructor() {
		this.level = (config.logging.level as LogLevel) || LogLevel.INFO;
	}

	private shouldLog(level: LogLevel): boolean {
		return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level];
	}

	private formatMessage(
		level: LogLevel,
		message: string,
		meta?: LogMetadata,
	): string {
		const timestamp = new Date().toISOString();
		const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
		return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr}`;
	}

	private log(level: LogLevel, message: string, meta?: LogMetadata): void {
		if (!this.shouldLog(level)) return;

		const formattedMessage = this.formatMessage(level, message, meta);

		switch (level) {
			case LogLevel.ERROR:
				console.error(formattedMessage);
				break;
			case LogLevel.WARN:
				console.warn(formattedMessage);
				break;
			case LogLevel.INFO:
				console.info(formattedMessage);
				break;
			case LogLevel.DEBUG:
				console.debug(formattedMessage);
				break;
			default:
				console.log(formattedMessage);
		}
	}

	debug(message: string, meta?: LogMetadata): void {
		this.log(LogLevel.DEBUG, message, meta);
	}

	info(message: string, meta?: LogMetadata): void {
		this.log(LogLevel.INFO, message, meta);
	}

	warn(message: string, meta?: LogMetadata): void {
		this.log(LogLevel.WARN, message, meta);
	}

	error(message: string, meta?: LogMetadata): void {
		this.log(LogLevel.ERROR, message, meta);
	}

	setLevel(level: LogLevel): void {
		this.level = level;
	}
}

export const logger = new Logger();

```

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

```typescript
import { FastMCP } from "fastmcp";
import { config } from "./config.js";
import { resources } from "./resources/index.js";
import { tools } from "./tools/index.js";
import { logger } from "./utils/logger.js";

const server = new FastMCP({
	name: config.server.name,
	version: config.server.version as `${number}.${number}.${number}`,
});

for (const tool of tools) {
	logger.info(`Tool hinzugefügt: ${tool.name}`);
	// @ts-ignore
	server.addTool(tool);
}

for (const resource of resources) {
	logger.info(`Ressource hinzugefügt: ${resource.name}`);
	// @ts-ignore
	server.addResourceTemplate(resource);
}

logger.info(`Starte ${config.server.name} v${config.server.version}`);

if (config.server.transportType === "sse") {
	logger.info(`Server startet im SSE-Modus auf Port ${config.server.port}`);

	const endpoint = config.server.endpoint.startsWith("/")
		? config.server.endpoint
		: `/${config.server.endpoint}`;

	server.start({
		transportType: "sse",
		sse: {
			endpoint: endpoint as `/${string}`,
			port: config.server.port,
		},
	});
} else {
	logger.info("Server startet im stdio-Modus");
	server.start({
		transportType: "stdio",
	});
}

server.on("connect", (event) => {
	// @ts-ignore - FastMCP Session-Typ hat möglicherweise keine id-Eigenschaft
	const sessionId = event.session?.id || "unbekannt";
	logger.info("Client verbunden", { sessionId });
});

server.on("disconnect", (event) => {
	// @ts-ignore - FastMCP Session-Typ hat möglicherweise keine id-Eigenschaft
	const sessionId = event.session?.id || "unbekannt";
	logger.info("Client getrennt", { sessionId });
});

process.on("SIGINT", () => {
	logger.info("Server wird beendet (SIGINT)");
	process.exit(0);
});

process.on("SIGTERM", () => {
	logger.info("Server wird beendet (SIGTERM)");
	process.exit(0);
});

process.on("uncaughtException", (error) => {
	logger.error("Unbehandelte Ausnahme", { error });
	process.exit(1);
});

process.on("unhandledRejection", (reason) => {
	logger.error("Unbehandelte Promise-Ablehnung", { reason });
});

export default server;

```

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

```typescript
import { logger } from "../utils/logger.js";

export class AppError extends Error {
	constructor(
		public message: string,
		public code = "INTERNAL_ERROR",
		public statusCode = 500,
		public details?: Record<string, unknown>,
	) {
		super(message);
		this.name = this.constructor.name;
		Error.captureStackTrace(this, this.constructor);
	}
}

export class ApiError extends AppError {
	constructor(
		message: string,
		code = "API_ERROR",
		statusCode = 500,
		details?: Record<string, unknown>,
	) {
		super(message, code, statusCode, details);
	}
}

export class ValidationError extends AppError {
	constructor(message: string, details?: Record<string, unknown>) {
		super(message, "VALIDATION_ERROR", 400, details);
	}
}

export class AuthenticationError extends AppError {
	constructor(message: string) {
		super(message, "AUTHENTICATION_ERROR", 401);
	}
}
export class ResourceNotFoundError extends AppError {
	constructor(message: string) {
		super(message, "RESOURCE_NOT_FOUND", 404);
	}
}

export function asyncErrorHandler<T>(
	fn: (...args: unknown[]) => Promise<T>,
): (...args: unknown[]) => Promise<T> {
	return async (...args: unknown[]): Promise<T> => {
		try {
			return await fn(...args);
		} catch (error) {
			if (error instanceof AppError) {
				logger.error(`${error.name}: ${error.message}`, {
					code: error.code,
					statusCode: error.statusCode,
					details: error.details,
				});
				throw error;
			}
			const appError = new AppError(
				error instanceof Error ? error.message : "Unbekannter Fehler",
				"INTERNAL_ERROR",
				500,
				{ originalError: error },
			);
			logger.error(`${appError.name}: ${appError.message}`, {
				code: appError.code,
				statusCode: appError.statusCode,
				details: appError.details,
			});
			throw appError;
		}
	};
}

export function withErrorHandling<T, R>(
	fn: (input: T) => Promise<R> | R,
	errorTransformer?: (error: unknown) => string,
): (input: T) => Promise<R> {
	return async (input: T): Promise<R> => {
		try {
			return await fn(input);
		} catch (error) {
			const errorMessage = errorTransformer
				? errorTransformer(error)
				: error instanceof Error
					? error.message
					: "Ein unbekannter Fehler ist aufgetreten";

			logger.error("Fehler bei der Ausführung:", { error, input });

			throw new Error(errorMessage);
		}
	};
}

```

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

```typescript
import fetch from "node-fetch";
import config from "../config.js";
import type { PlanParams, StationParams, TimetableParams } from "./types.js";

/**
 * API-Client für die DB Timetable API
 */
export class TimetableApiClient {
	private baseUrl: string;
	private clientId: string;
	private clientSecret: string;

	constructor() {
		this.baseUrl = config.api.baseUrl;
		this.clientId = config.api.clientId;
		this.clientSecret = config.api.clientSecret;
	}

	/**
	 * Sendet eine Anfrage an die API mit entsprechenden Authentifizierungsheadern
	 */
	private async request<T>(endpoint: string): Promise<T> {
		try {
			const response = await fetch(`${this.baseUrl}${endpoint}`, {
				method: "GET",
				headers: {
					"DB-Client-Id": this.clientId,
					"DB-Api-Key": this.clientSecret,
					Accept: "application/xml",
				},
			});

			if (!response.ok) {
				throw new Error(
					`API-Fehler: ${response.status} ${response.statusText}`,
				);
			}

			// Die API gibt XML zurück, aber wir behandeln es für MCP als Text
			// In einer erweiterten Implementierung könnte man hier einen XML-Parser verwenden
			const data = await response.text();

			// Für eine einfache Implementierung geben wir den XML-Text direkt zurück
			// In einer produktiven Implementierung würde man hier XML nach JSON konvertieren
			return data as unknown as T;
		} catch (error) {
			console.error("Fehler bei der API-Anfrage:", error);
			throw error;
		}
	}

	/**
	 * Ruft den aktuellen Fahrplan für eine Station ab
	 */
	async getCurrentTimetable({ evaNo }: TimetableParams): Promise<string> {
		return this.request<string>(`/fchg/${evaNo}`);
	}

	/**
	 * Ruft die letzten Änderungen für eine Station ab
	 */
	async getRecentChanges({ evaNo }: TimetableParams): Promise<string> {
		return this.request<string>(`/rchg/${evaNo}`);
	}

	/**
	 * Ruft geplante Fahrplandaten für eine bestimmte Station und Zeitspanne ab
	 */
	async getPlannedTimetable({
		evaNo,
		date,
		hour,
	}: PlanParams): Promise<string> {
		return this.request<string>(`/plan/${evaNo}/${date}/${hour}`);
	}

	/**
	 * Sucht nach Stationen, die dem angegebenen Muster entsprechen
	 */
	async findStations({ pattern }: StationParams): Promise<string> {
		return this.request<string>(`/station/${pattern}`);
	}
}

// Export Singleton
export const timetableApi = new TimetableApiClient();

```

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

```typescript
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';

// Mock von process und dotenv
vi.mock('node:path', () => ({
    default: {
        dirname: vi.fn(() => '/mock/dir'),
        resolve: vi.fn(() => '/mock/path/.env')
    }
}));

vi.mock('dotenv', () => ({
    default: {
        config: vi.fn()
    }
}));

describe('Konfiguration', () => {
    // Speichere originale Umgebungsvariablen
    const originalEnv = { ...process.env };
    const originalExit = process.exit;

    beforeEach(() => {
        // Mock der process.exit
        process.exit = vi.fn() as unknown as (code?: number) => never;

        // Zuvor geladene Module löschen
        vi.resetModules();

        // Umgebungsvariablen zurücksetzen
        process.env = { ...originalEnv };

        // Grundlegende API-Credentials setzen, die für das Laden der Konfiguration benötigt werden
        process.env.DB_TIMETABLE_CLIENT_ID = 'test-client-id';
        process.env.DB_TIMETABLE_CLIENT_SECRET = 'test-client-secret';
    });

    afterEach(() => {
        // Originale Umgebungsvariablen und Exit-Funktion wiederherstellen
        process.env = originalEnv;
        process.exit = originalExit;

        vi.clearAllMocks();
    });

    test('lädt die Standardkonfiguration, wenn keine Umgebungsvariablen gesetzt sind', async () => {
        const { config } = await import('../config.js');

        expect(config.server.transportType).toBe('stdio');
        expect(config.server.port).toBe(8080);
        expect(config.server.endpoint).toBe('/sse');
        expect(config.logging.level).toBe('info');
        expect(config.api.baseUrl).toBe('https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1');
    });

    test('nutzt Umgebungsvariablen, wenn gesetzt', async () => {
        process.env.TRANSPORT_TYPE = 'sse';
        process.env.PORT = '9000';
        process.env.SSE_ENDPOINT = '/custom-endpoint';
        process.env.LOG_LEVEL = 'debug';

        const { config } = await import('../config.js');

        expect(config.server.transportType).toBe('sse');
        expect(config.server.port).toBe(9000);
        expect(config.server.endpoint).toBe('/custom-endpoint');
        expect(config.logging.level).toBe('debug');
    });

    test('beendet den Prozess, wenn API-Credentials fehlen', async () => {
        // Entferne API-Credentials
        process.env.DB_TIMETABLE_CLIENT_ID = '';
        process.env.DB_TIMETABLE_CLIENT_SECRET = '';

        // Ausgabe umlenken, um Fehlermeldungen zu unterdrücken
        const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });

        try {
            await import('../config.js');
        } catch (e) {
            // Ignorieren, da der Prozess beendet werden würde
        }

        expect(process.exit).toHaveBeenCalledWith(1);
        expect(consoleSpy).toHaveBeenCalled();

        consoleSpy.mockRestore();
    });
}); 
```

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

```typescript
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { getCurrentTimetableTool, getRecentChangesTool, getPlannedTimetableTool, findStationsTool } from '../tools/timetableTools.js';
import { timetableApi } from '../api/timetableApi.js';

vi.mock('../api/timetableApi.js', () => ({
    timetableApi: {
        getCurrentTimetable: vi.fn().mockResolvedValue('<timetable>Current Data</timetable>'),
        getRecentChanges: vi.fn().mockResolvedValue('<timetable>Recent Changes</timetable>'),
        getPlannedTimetable: vi.fn().mockResolvedValue('<timetable>Planned Data</timetable>'),
        findStations: vi.fn().mockResolvedValue('<stations>Station Data</stations>')
    }
}));

describe('Timetable Tools', () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });

    describe('getCurrentTimetableTool', () => {
        test('ruft timetableApi.getCurrentTimetable auf', async () => {
            const args = { evaNo: '8000105' };
            const result = await getCurrentTimetableTool.execute(args);

            expect(timetableApi.getCurrentTimetable).toHaveBeenCalledWith(args);
            expect(result).toBe('<timetable>Current Data</timetable>');
        });

        test('validiert Eingabeparameter', async () => {
            const invalidArgs = { evaNo: '' };
            await expect(getCurrentTimetableTool.execute(invalidArgs)).rejects.toThrow();
        });
    });

    describe('getRecentChangesTool', () => {
        test('ruft timetableApi.getRecentChanges auf', async () => {
            const args = { evaNo: '8000105' };
            const result = await getRecentChangesTool.execute(args);

            expect(timetableApi.getRecentChanges).toHaveBeenCalledWith(args);
            expect(result).toBe('<timetable>Recent Changes</timetable>');
        });
    });

    describe('getPlannedTimetableTool', () => {
        test('ruft timetableApi.getPlannedTimetable auf', async () => {
            const args = { evaNo: '8000105', date: '230401', hour: '14' };
            const result = await getPlannedTimetableTool.execute(args);

            expect(timetableApi.getPlannedTimetable).toHaveBeenCalledWith(args);
            expect(result).toBe('<timetable>Planned Data</timetable>');
        });

        test('validiert Datumsformat', async () => {
            const invalidArgs = { evaNo: '8000105', date: '2304', hour: '14' };
            await expect(getPlannedTimetableTool.execute(invalidArgs)).rejects.toThrow();
        });

        test('validiert Stundenformat', async () => {
            const invalidArgs = { evaNo: '8000105', date: '230401', hour: '24' };
            await expect(getPlannedTimetableTool.execute(invalidArgs)).rejects.toThrow();
        });
    });

    describe('findStationsTool', () => {
        test('ruft timetableApi.findStations auf', async () => {
            const args = { pattern: 'Frankfurt' };
            const result = await findStationsTool.execute(args);

            expect(timetableApi.findStations).toHaveBeenCalledWith(args);
            expect(result).toBe('<stations>Station Data</stations>');
        });
    });
}); 
```

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

```typescript
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { tools } from '../tools/index.js';
import { resources } from '../resources/index.js';

vi.mock('../api/timetableApi.js', () => ({
    timetableApi: {
        getCurrentTimetable: vi.fn().mockResolvedValue('<timetable>Current Data</timetable>'),
        getRecentChanges: vi.fn().mockResolvedValue('<timetable>Recent Changes</timetable>'),
        getPlannedTimetable: vi.fn().mockResolvedValue('<timetable>Planned Data</timetable>'),
        findStations: vi.fn().mockResolvedValue('<stations>Station Data</stations>')
    }
}));

const mockMcpServer = {
    addTool: vi.fn(),
    addResource: vi.fn(),
    executeTool: vi.fn(),
    loadResource: vi.fn()
};

mockMcpServer.executeTool.mockImplementation((toolId, _args) => {
    if (toolId === 'db-timetable:getCurrentTimetable') {
        return Promise.resolve('<timetable>Current Data</timetable>');
    }
    if (toolId === 'db-timetable:findStations') {
        return Promise.resolve('<stations>Station Data</stations>');
    }
    return Promise.resolve(null);
});

mockMcpServer.loadResource.mockImplementation((uri) => {
    if (uri === 'db-api:timetable/current/8000105') {
        return Promise.resolve('<timetable>Current Data</timetable>');
    }
    if (uri === 'db-api:station/Frankfurt') {
        return Promise.resolve('<stations>Station Data</stations>');
    }
    return Promise.reject(new Error(`Resource nicht gefunden: ${uri}`));
});

describe('MCP Server Integration', () => {
    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    let server: any;

    beforeEach(() => {
        server = mockMcpServer;

        vi.clearAllMocks();
    });

    describe('Tool Ausführung', () => {
        test('kann getCurrentTimetable ausführen', async () => {
            for (const tool of tools) {
                server.addTool(tool);
            }

            const result = await server.executeTool('db-timetable:getCurrentTimetable', { evaNo: '8000105' });
            expect(result).toBe('<timetable>Current Data</timetable>');
            expect(server.addTool).toHaveBeenCalled();
        });

        test('kann findStations ausführen', async () => {
            const result = await server.executeTool('db-timetable:findStations', { pattern: 'Frankfurt' });
            expect(result).toBe('<stations>Station Data</stations>');
        });
    });

    describe('Resource Anfragen', () => {
        test('kann die aktuelle Fahrplantafel abrufen', async () => {
            for (const resource of resources) {
                server.addResource(resource);
            }

            const result = await server.loadResource('db-api:timetable/current/8000105');
            expect(result).toBe('<timetable>Current Data</timetable>');
            expect(server.addResource).toHaveBeenCalled();
        });

        test('kann Stationen suchen', async () => {
            const result = await server.loadResource('db-api:station/Frankfurt');
            expect(result).toBe('<stations>Station Data</stations>');
        });

        test('liefert einen Fehler bei ungültigem Ressourcen-URI', async () => {
            await expect(server.loadResource('db-api:invalid')).rejects.toThrow();
        });
    });
}); 
```

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

```typescript
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import { logger, LogLevel } from '../../utils/logger.js';

describe('Logger', () => {
    const originalConsole = {
        error: console.error,
        warn: console.warn,
        info: console.info,
        debug: console.debug,
        log: console.log
    };

    beforeEach(() => {
        console.error = vi.fn();
        console.warn = vi.fn();
        console.info = vi.fn();
        console.debug = vi.fn();
        console.log = vi.fn();
    });

    afterEach(() => {
        console.error = originalConsole.error;
        console.warn = originalConsole.warn;
        console.info = originalConsole.info;
        console.debug = originalConsole.debug;
        console.log = originalConsole.log;
    });

    describe('Log Methoden', () => {
        test('debug() ruft console.debug auf', () => {
            logger.setLevel(LogLevel.DEBUG);
            logger.debug('Debug Nachricht');

            expect(console.debug).toHaveBeenCalled();
            const callArg = (console.debug as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
            expect(callArg).toContain('DEBUG');
            expect(callArg).toContain('Debug Nachricht');
        });

        test('info() ruft console.info auf', () => {
            logger.info('Info Nachricht');

            expect(console.info).toHaveBeenCalled();
            const callArg = (console.info as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
            expect(callArg).toContain('INFO');
            expect(callArg).toContain('Info Nachricht');
        });

        test('warn() ruft console.warn auf', () => {
            logger.warn('Warn Nachricht');

            expect(console.warn).toHaveBeenCalled();
            const callArg = (console.warn as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
            expect(callArg).toContain('WARN');
            expect(callArg).toContain('Warn Nachricht');
        });

        test('error() ruft console.error auf', () => {
            logger.error('Error Nachricht');

            expect(console.error).toHaveBeenCalled();
            const callArg = (console.error as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
            expect(callArg).toContain('ERROR');
            expect(callArg).toContain('Error Nachricht');
        });
    });

    describe('Log Level', () => {
        test('zeigt keine DEBUG-Nachrichten bei INFO Level', () => {
            logger.setLevel(LogLevel.INFO);
            logger.debug('Debug sollte nicht angezeigt werden');

            expect(console.debug).not.toHaveBeenCalled();
        });

        test('zeigt keine INFO-Nachrichten bei WARN Level', () => {
            logger.setLevel(LogLevel.WARN);
            logger.info('Info sollte nicht angezeigt werden');

            expect(console.info).not.toHaveBeenCalled();
        });

        test('zeigt keine WARN-Nachrichten bei ERROR Level', () => {
            logger.setLevel(LogLevel.ERROR);
            logger.warn('Warn sollte nicht angezeigt werden');

            expect(console.warn).not.toHaveBeenCalled();
        });

        test('zeigt ERROR-Nachrichten bei jedem Level', () => {
            logger.setLevel(LogLevel.DEBUG);
            logger.error('Error sollte angezeigt werden');

            expect(console.error).toHaveBeenCalled();
        });
    });

    describe('Metadaten', () => {
        test('formatiert Metadaten korrekt', () => {
            const metadata = { user: 'test', action: 'login' };
            logger.info('Info mit Metadaten', metadata);

            const callArg = (console.info as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
            expect(callArg).toContain('INFO');
            expect(callArg).toContain('Info mit Metadaten');
            expect(callArg).toContain('"user":"test"');
            expect(callArg).toContain('"action":"login"');
        });
    });
}); 
```

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

```markdown
# Testing des DB Timetable MCP Servers

Dieses Dokument beschreibt verschiedene Methoden zum Testen und Debuggen des DB Timetable MCP Servers.

## Automatische Tests

Der Server enthält automatische Tests, die mit Vitest ausgeführt werden können:

```bash
npm test
```

Die Tests überprüfen:
- API-Client-Funktionalität
- Tool-Implementierung
- Server-Integration

## Manuelles Testen

### Mit FastMCP Inspector

Der FastMCP Inspector ist ein Befehlszeilenwerkzeug, mit dem MCP-Server interaktiv getestet werden können.

1. Installiere den FastMCP Inspector global:
   ```bash
   npm install -g fastmcp
   ```

2. Starte den Server im stdio-Modus:
   ```bash
   npm start
   ```

3. In einem anderen Terminal-Fenster führe den Inspector aus:
   ```bash
   fastmcp inspect
   ```

4. Verwende den Inspector, um mit den Tools und Ressourcen zu interagieren.

### Testen im stdio-Modus

Im stdio-Modus arbeitet der Server über die Standardein- und -ausgabe. Dies ist nützlich für die Entwicklung und das Debugging.

1. Starte den Server:
   ```bash
   npm start
   ```

2. Sende MCP-formatierte Nachrichten, z.B.:
   ```json
   {"type":"request","id":"test-1","method":"tool","params":{"tool":"getCurrentTimetable","input":{"evaNo":"8000105"}}}
   ```

### Testen im SSE-Modus

Im SSE-Modus (Server-Sent Events) erstellt der Server einen HTTP-Endpunkt, der für Webanwendungen zugänglich ist.

1. Starte den Server im SSE-Modus:
   ```bash
   TRANSPORT_TYPE=sse npm start
   ```

2. Verwende einen SSE-Client, um zu testen (z.B. mit einer Browserkonsole oder einem Tool wie curl).

## Test-Beispiele

### Aktuelle Fahrplandaten abrufen

```json
{
  "type": "request",
  "id": "test-1",
  "method": "tool",
  "params": {
    "tool": "getCurrentTimetable",
    "input": {
      "evaNo": "8000105"
    }
  }
}
```

### Stationssuche

```json
{
  "type": "request",
  "id": "test-2",
  "method": "tool",
  "params": {
    "tool": "findStations",
    "input": {
      "pattern": "Frankfurt"
    }
  }
}
```

### Geplante Fahrplandaten abrufen

```json
{
  "type": "request",
  "id": "test-3",
  "method": "tool",
  "params": {
    "tool": "getPlannedTimetable",
    "input": {
      "evaNo": "8000105",
      "date": "230401",
      "hour": "14"
    }
  }
}
```

### Ressource abrufen

```json
{
  "type": "request",
  "id": "test-4",
  "method": "resource",
  "params": {
    "uri": "db-api:timetable/current/8000105"
  }
}
```

## Debugging

### Logging

Der Server verwendet einen strukturierten Logger mit verschiedenen Log-Levels:

- DEBUG: Detaillierte Debugging-Informationen
- INFO: Allgemeine Informationen (Standard)
- WARN: Warnungen
- ERROR: Fehlermeldungen

Das Log-Level kann in der .env-Datei eingestellt werden:

```
LOG_LEVEL=debug
```

### Fehlerbehandlung testen

Um die Fehlerbehandlung zu testen, können Sie ungültige Parameter an die Tools übergeben:

```json
{
  "type": "request",
  "id": "test-error",
  "method": "tool",
  "params": {
    "tool": "getCurrentTimetable",
    "input": {
      "evaNo": ""
    }
  }
}
```

### Bekannte Fehlercodes

- `VALIDATION_ERROR`: Ungültige Eingabeparameter
- `API_ERROR`: Fehler bei der API-Anfrage
- `INTERNAL_ERROR`: Interner Serverfehler
- `AUTHENTICATION_ERROR`: Authentifizierungsfehler
- `RESOURCE_NOT_FOUND`: Ressource nicht gefunden

## Typische Teststationen

Hier sind einige gültige EVA-Nummern für Tests:

- 8000105: Frankfurt (Main) Hbf
- 8000096: Berlin Hbf
- 8000152: Hamburg Hbf
- 8000244: München Hbf
- 8000098: Köln Hbf

## Fehlerbehebung

### API-Zugangsdaten

Stellen Sie sicher, dass in der .env-Datei gültige API-Zugangsdaten für die DB Timetable API konfiguriert sind:

```
DB_TIMETABLE_CLIENT_ID=your-client-id
DB_TIMETABLE_CLIENT_SECRET=your-client-secret
```

### Netzwerkfehler

Bei Netzwerkfehlern überprüfen Sie:

1. Internetverbindung
2. API-Erreichbarkeit
3. Gültigkeit der API-Zugangsdaten

### Server startet nicht

1. Prüfen Sie, ob die erforderlichen Abhängigkeiten installiert sind: `npm install`
2. Prüfen Sie, ob der TypeScript-Code kompiliert wurde: `npm run build`
3. Überprüfen Sie die Log-Ausgabe auf Fehlermeldungen 
```

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

```typescript
import { z } from "zod";
import { timetableApi } from "../api/timetableApi.js";
import type {
	PlanParams,
	StationParams,
	TimetableParams,
} from "../api/types.js";
import { ValidationError, asyncErrorHandler } from "../utils/errorHandling.js";
import { type LogMetadata, logger } from "../utils/logger.js";

const TimetableParamsSchema = z.object({
	evaNo: z
		.string()
		.min(1)
		.describe("EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)"),
});

const PlanParamsSchema = z.object({
	evaNo: z
		.string()
		.min(1)
		.describe("EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)"),
	date: z
		.string()
		.regex(/^\d{6}$/)
		.describe("Datum im Format YYMMDD (z.B. 230401 für 01.04.2023)"),
	hour: z
		.string()
		.regex(/^([0-1][0-9]|2[0-3])$/)
		.describe("Stunde im Format HH (z.B. 14 für 14 Uhr)"),
});

const StationParamsSchema = z.object({
	pattern: z
		.string()
		.min(1)
		.describe("Suchmuster für Stationen (z.B. Frankfurt oder 8000105)"),
});

export const getCurrentTimetableTool = {
	name: "getCurrentTimetable",
	description:
		"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.",
	parameters: TimetableParamsSchema,
	execute: asyncErrorHandler(async (args) => {
		logger.info("Rufe aktuelle Fahrplandaten ab", args as LogMetadata);

		const validateResult = TimetableParamsSchema.safeParse(args);
		if (!validateResult.success) {
			throw new ValidationError(
				"Ungültige Parameter für getCurrentTimetable",
				validateResult.error.format(),
			);
		}

		const result = await timetableApi.getCurrentTimetable(
			args as TimetableParams,
		);
		return result;
	}),
};

export const getRecentChangesTool = {
	name: "getRecentChanges",
	description:
		"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.",
	parameters: TimetableParamsSchema,
	execute: asyncErrorHandler(async (args) => {
		logger.info("Rufe aktuelle Änderungen ab", args as LogMetadata);

		const validateResult = TimetableParamsSchema.safeParse(args);
		if (!validateResult.success) {
			throw new ValidationError(
				"Ungültige Parameter für getRecentChanges",
				validateResult.error.format(),
			);
		}

		const result = await timetableApi.getRecentChanges(args as TimetableParams);
		return result;
	}),
};

export const getPlannedTimetableTool = {
	name: "getPlannedTimetable",
	description:
		"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.",
	parameters: PlanParamsSchema,
	execute: asyncErrorHandler(async (args) => {
		logger.info("Rufe geplante Fahrplandaten ab", args as LogMetadata);

		const validateResult = PlanParamsSchema.safeParse(args);
		if (!validateResult.success) {
			throw new ValidationError(
				"Ungültige Parameter für getPlannedTimetable",
				validateResult.error.format(),
			);
		}

		const result = await timetableApi.getPlannedTimetable(args as PlanParams);
		return result;
	}),
};

export const findStationsTool = {
	name: "findStations",
	description:
		"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.",
	parameters: StationParamsSchema,
	execute: asyncErrorHandler(async (args) => {
		logger.info("Suche nach Stationen", args as LogMetadata);

		const validateResult = StationParamsSchema.safeParse(args);
		if (!validateResult.success) {
			throw new ValidationError(
				"Ungültige Parameter für findStations",
				validateResult.error.format(),
			);
		}

		const result = await timetableApi.findStations(args as StationParams);
		return result;
	}),
};

```

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

```typescript
import { timetableApi } from "../api/timetableApi.js";
import { ValidationError } from "../utils/errorHandling.js";
import { logger } from "../utils/logger.js";

export interface TimetableResourceArgs {
	evaNo: string;
}

export interface PlannedTimetableResourceArgs {
	evaNo: string;
	date: string;
	hour: string;
}

export interface StationResourceArgs {
	pattern: string;
}

export const currentTimetableResource = {
	uriTemplate: "db-api:timetable/current/{evaNo}",
	name: "Aktuelle Fahrplandaten",
	description:
		"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.",
	mimeType: "application/xml",
	arguments: [
		{
			name: "evaNo",
			description: "EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)",
			required: true,
		},
	],
	async load({ evaNo }: TimetableResourceArgs) {
		logger.info("Lade aktuelle Fahrplandaten", { evaNo });

		if (!evaNo) {
			throw new ValidationError("EVA-Nummer ist erforderlich");
		}

		try {
			const data = await timetableApi.getCurrentTimetable({ evaNo });
			return {
				text: data,
			};
		} catch (error) {
			logger.error("Fehler beim Laden der aktuellen Fahrplandaten", {
				error,
				evaNo,
			});
			throw error;
		}
	},
};

export const recentChangesResource = {
	uriTemplate: "db-api:timetable/changes/{evaNo}",
	name: "Aktuelle Fahrplanänderungen",
	description:
		"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.",
	mimeType: "application/xml",
	arguments: [
		{
			name: "evaNo",
			description: "EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)",
			required: true,
		},
	],
	async load({ evaNo }: TimetableResourceArgs) {
		logger.info("Lade aktuelle Fahrplanänderungen", { evaNo });

		if (!evaNo) {
			throw new ValidationError("EVA-Nummer ist erforderlich");
		}

		try {
			const data = await timetableApi.getRecentChanges({ evaNo });
			return {
				text: data,
			};
		} catch (error) {
			logger.error("Fehler beim Laden der aktuellen Fahrplanänderungen", {
				error,
				evaNo,
			});
			throw error;
		}
	},
};

export const plannedTimetableResource = {
	uriTemplate: "db-api:timetable/planned/{evaNo}/{date}/{hour}",
	name: "Geplante Fahrplandaten",
	description:
		"Geplante Fahrplandaten für eine Bahnhofsstation zu einer bestimmten Zeit",
	mimeType: "application/xml",
	arguments: [
		{
			name: "evaNo",
			description: "EVA-Nummer der Station (z.B. 8000105 für Frankfurt Hbf)",
			required: true,
		},
		{
			name: "date",
			description: "Datum im Format YYMMDD (z.B. 230401 für 01.04.2023)",
			required: true,
		},
		{
			name: "hour",
			description: "Stunde im Format HH (z.B. 14 für 14 Uhr)",
			required: true,
		},
	],
	async load({ evaNo, date, hour }: PlannedTimetableResourceArgs) {
		logger.info("Lade geplante Fahrplandaten", { evaNo, date, hour });

		if (!evaNo || !date || !hour) {
			throw new ValidationError(
				"Alle Parameter (evaNo, date, hour) sind erforderlich",
			);
		}

		if (!/^\d{6}$/.test(date)) {
			throw new ValidationError("Datum muss im Format YYMMDD sein");
		}

		if (!/^([0-1][0-9]|2[0-3])$/.test(hour)) {
			throw new ValidationError(
				"Stunde muss im Format HH sein und zwischen 00 und 23 liegen",
			);
		}

		try {
			const data = await timetableApi.getPlannedTimetable({
				evaNo,
				date,
				hour,
			});
			return {
				text: data,
			};
		} catch (error) {
			logger.error("Fehler beim Laden der geplanten Fahrplandaten", {
				error,
				evaNo,
				date,
				hour,
			});
			throw error;
		}
	},
};

export const stationResource = {
	uriTemplate: "db-api:station/{pattern}",
	name: "Stationssuche",
	description: "Suche nach Bahnhofsstationen anhand eines Musters",
	mimeType: "application/xml",
	arguments: [
		{
			name: "pattern",
			description: "Suchmuster für Stationen (z.B. Frankfurt oder 8000105)",
			required: true,
		},
	],
	async load({ pattern }: StationResourceArgs) {
		logger.info("Suche nach Stationen", { pattern });

		if (!pattern) {
			throw new ValidationError("Suchmuster ist erforderlich");
		}

		try {
			const data = await timetableApi.findStations({ pattern });
			return {
				text: data,
			};
		} catch (error) {
			logger.error("Fehler bei der Stationssuche", { error, pattern });
			throw error;
		}
	},
};

```

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

```typescript
import { describe, test, expect, vi, beforeEach } from 'vitest';
import {
    AppError,
    ApiError,
    ValidationError,
    AuthenticationError,
    ResourceNotFoundError,
    asyncErrorHandler,
    withErrorHandling
} from '../../utils/errorHandling.js';
import { logger } from '../../utils/logger.js';

vi.mock('../../utils/logger.js', () => ({
    logger: {
        error: vi.fn(),
        warn: vi.fn(),
        info: vi.fn(),
        debug: vi.fn()
    }
}));

describe('Error Handling Utilities', () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });

    describe('Error Klassen', () => {
        test('AppError hat korrekte Eigenschaften', () => {
            const error = new AppError('Test Fehler', 'TEST_CODE', 400, { test: 'details' });

            expect(error.message).toBe('Test Fehler');
            expect(error.code).toBe('TEST_CODE');
            expect(error.statusCode).toBe(400);
            expect(error.details).toEqual({ test: 'details' });
            expect(error.name).toBe('AppError');
        });

        test('ApiError erbt von AppError mit korrekten Standardwerten', () => {
            const error = new ApiError('API Fehler');

            expect(error.message).toBe('API Fehler');
            expect(error.code).toBe('API_ERROR');
            expect(error.statusCode).toBe(500);
            expect(error instanceof AppError).toBe(true);
        });

        test('ValidationError hat korrekte Eigenschaften', () => {
            const error = new ValidationError('Validierungsfehler', { field: 'test' });

            expect(error.message).toBe('Validierungsfehler');
            expect(error.code).toBe('VALIDATION_ERROR');
            expect(error.statusCode).toBe(400);
            expect(error.details).toEqual({ field: 'test' });
            expect(error instanceof AppError).toBe(true);
        });

        test('AuthenticationError hat korrekte Eigenschaften', () => {
            const error = new AuthenticationError('Auth Fehler');

            expect(error.message).toBe('Auth Fehler');
            expect(error.code).toBe('AUTHENTICATION_ERROR');
            expect(error.statusCode).toBe(401);
            expect(error instanceof AppError).toBe(true);
        });

        test('ResourceNotFoundError hat korrekte Eigenschaften', () => {
            const error = new ResourceNotFoundError('Resource nicht gefunden');

            expect(error.message).toBe('Resource nicht gefunden');
            expect(error.code).toBe('RESOURCE_NOT_FOUND');
            expect(error.statusCode).toBe(404);
            expect(error instanceof AppError).toBe(true);
        });
    });

    describe('asyncErrorHandler', () => {
        test('gibt das Ergebnis zurück, wenn keine Fehler auftreten', async () => {
            const successFn = async () => 'Erfolg';
            const handledFn = asyncErrorHandler(successFn);

            const result = await handledFn();
            expect(result).toBe('Erfolg');
            expect(logger.error).not.toHaveBeenCalled();
        });

        test('loggt und wirft AppError weiter', async () => {
            const errorFn = async () => {
                throw new ValidationError('Test Validierungsfehler');
            };
            const handledFn = asyncErrorHandler(errorFn);

            await expect(handledFn()).rejects.toThrow(ValidationError);
            expect(logger.error).toHaveBeenCalled();
        });

        test('wandelt Standard-Fehler in AppError um', async () => {
            const errorFn = async () => {
                throw new Error('Standard Fehler');
            };
            const handledFn = asyncErrorHandler(errorFn);

            await expect(handledFn()).rejects.toThrow(AppError);
            expect(logger.error).toHaveBeenCalled();
        });
    });

    describe('withErrorHandling', () => {
        test('gibt das Ergebnis zurück, wenn keine Fehler auftreten', async () => {
            const successFn = () => 'Erfolg';
            const handledFn = withErrorHandling(successFn);

            const result = await handledFn({});
            expect(result).toBe('Erfolg');
            expect(logger.error).not.toHaveBeenCalled();
        });

        test('verarbeitet Fehler und wirft neue Error-Instanz', async () => {
            const errorFn = () => {
                throw new Error('Originalfehler');
            };
            const handledFn = withErrorHandling(errorFn);

            await expect(handledFn({})).rejects.toThrow('Originalfehler');
            expect(logger.error).toHaveBeenCalled();
        });

        test('verwendet errorTransformer, wenn angegeben', async () => {
            const errorFn = () => {
                throw new Error('Originalfehler');
            };
            const errorTransformer = () => 'Transformierter Fehler';
            const handledFn = withErrorHandling(errorFn, errorTransformer);

            await expect(handledFn({})).rejects.toThrow('Transformierter Fehler');
            expect(logger.error).toHaveBeenCalled();
        });
    });
}); 
```

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

```typescript
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';

vi.mock('fastmcp', () => ({
    FastMCP: vi.fn().mockImplementation(() => ({
        addTool: vi.fn(),
        addResourceTemplate: vi.fn(),
        start: vi.fn(),
        on: vi.fn()
    }))
}));

vi.mock('../config.js', () => ({
    config: {
        server: {
            name: 'Test MCP Server',
            version: '1.0.0',
            transportType: 'stdio',
            port: 8080,
            endpoint: '/test'
        },
        api: {
            baseUrl: 'https://test-api.example.com',
            clientId: 'test-id',
            clientSecret: 'test-secret'
        },
        logging: {
            level: 'info'
        }
    }
}));

vi.mock('../resources/index.js', () => ({
    resources: [
        { name: 'TestResource1', pattern: 'test:resource1' },
        { name: 'TestResource2', pattern: 'test:resource2' }
    ]
}));

vi.mock('../tools/index.js', () => ({
    tools: [
        { name: 'TestTool1', description: 'Test Tool 1' },
        { name: 'TestTool2', description: 'Test Tool 2' }
    ]
}));

vi.mock('../utils/logger.js', () => ({
    logger: {
        info: vi.fn(),
        error: vi.fn(),
        warn: vi.fn(),
        debug: vi.fn()
    }
}));

describe('MCP Server', () => {
    const originalListeners = {
        SIGINT: process.listeners('SIGINT'),
        SIGTERM: process.listeners('SIGTERM'),
        uncaughtException: process.listeners('uncaughtException'),
        unhandledRejection: process.listeners('unhandledRejection')
    };

    beforeEach(() => {
        process.removeAllListeners('SIGINT');
        process.removeAllListeners('SIGTERM');
        process.removeAllListeners('uncaughtException');
        process.removeAllListeners('unhandledRejection');

        process.exit = vi.fn() as unknown as (code?: number) => never;

        vi.resetModules();
    });

    afterEach(() => {
        process.removeAllListeners('SIGINT');
        process.removeAllListeners('SIGTERM');
        process.removeAllListeners('uncaughtException');
        process.removeAllListeners('unhandledRejection');

        for (const listener of originalListeners.SIGINT) {
            process.on('SIGINT', listener);
        }

        for (const listener of originalListeners.SIGTERM) {
            process.on('SIGTERM', listener);
        }

        for (const listener of originalListeners.uncaughtException) {
            process.on('uncaughtException', listener);
        }

        for (const listener of originalListeners.unhandledRejection) {
            process.on('unhandledRejection', listener);
        }

        vi.clearAllMocks();
    });

    test('Initialisiert FastMCP Server mit den korrekten Parametern', async () => {
        const { FastMCP } = await import('fastmcp');
        await import('../index.js');

        expect(FastMCP).toHaveBeenCalledWith({
            name: 'Test MCP Server',
            version: '1.0.0'
        });
    });

    test('Fügt alle Tools und Ressourcen hinzu', async () => {
        const { logger } = await import('../utils/logger.js');
        const serverModule = await import('../index.js');
        const server = serverModule.default;

        expect(server.addTool).toHaveBeenCalledTimes(2);
        expect(server.addResourceTemplate).toHaveBeenCalledTimes(2);
        expect(logger.info).toHaveBeenCalledWith('Tool hinzugefügt: TestTool1');
        expect(logger.info).toHaveBeenCalledWith('Tool hinzugefügt: TestTool2');
        expect(logger.info).toHaveBeenCalledWith('Ressource hinzugefügt: TestResource1');
        expect(logger.info).toHaveBeenCalledWith('Ressource hinzugefügt: TestResource2');
    });

    test('Startet den Server im stdio-Modus', async () => {
        const serverModule = await import('../index.js');
        const server = serverModule.default;

        expect(server.start).toHaveBeenCalledWith({
            transportType: 'stdio'
        });
    });

    test('Startet den Server im SSE-Modus, wenn konfiguriert', async () => {
        vi.doMock('../config.js', () => ({
            config: {
                server: {
                    name: 'Test MCP Server',
                    version: '1.0.0',
                    transportType: 'sse',
                    port: 9000,
                    endpoint: 'test-endpoint'
                },
                api: {
                    baseUrl: 'https://test-api.example.com',
                    clientId: 'test-id',
                    clientSecret: 'test-secret'
                },
                logging: {
                    level: 'info'
                }
            }
        }));

        vi.resetModules();

        const serverModule = await import('../index.js');
        const server = serverModule.default;

        expect(server.start).toHaveBeenCalledWith({
            transportType: 'sse',
            sse: {
                endpoint: '/test-endpoint',
                port: 9000
            }
        });
    });

    test('Registriert Event-Handler für connect und disconnect', async () => {
        const serverModule = await import('../index.js');
        const server = serverModule.default;

        expect(server.on).toHaveBeenCalledTimes(2);
        expect(server.on).toHaveBeenCalledWith('connect', expect.any(Function));
        expect(server.on).toHaveBeenCalledWith('disconnect', expect.any(Function));
    });

    test('Registriert Prozess-Event-Handler', async () => {
        await import('../index.js');

        expect(process.listeners('SIGINT').length).toBeGreaterThan(0);
        expect(process.listeners('SIGTERM').length).toBeGreaterThan(0);
        expect(process.listeners('uncaughtException').length).toBeGreaterThan(0);
        expect(process.listeners('unhandledRejection').length).toBeGreaterThan(0);

        process.emit('SIGINT');
        const { logger } = await import('../utils/logger.js');
        expect(logger.info).toHaveBeenCalledWith('Server wird beendet (SIGINT)');
        expect(process.exit).toHaveBeenCalledWith(0);
    });
}); 
```

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

```typescript
/**
 * Type-Definitionen für die DB Timetable API
 * Basierend auf der OpenAPI-Spezifikation
 */

export enum EventStatus {
	PLANNED = "p", // Geplantes Ereignis
	ADDED = "a", // Hinzugefügtes Ereignis
	CANCELLED = "c", // Storniertes Ereignis
}

export enum MessageType {
	HIM = "h", // Störungsmeldung (HIM - Hafas Information Manager)
	QUALITY_CHANGE = "q", // Qualitätsänderung
	FREE = "f", // Freie Meldung
	CAUSE_OF_DELAY = "d", // Verspätungsursache
	IBIS = "i", // IBIS-Meldung (Integriertes Bord-Informations-System)
	UNASSIGNED_IBIS_MESSAGE = "u", // Nicht zugeordnete IBIS-Meldung
	DISRUPTION = "r", // Betriebsstörung
	CONNECTION = "c", // Anschlussinformation
}

export enum Priority {
	HIGH = "1", // Hohe Priorität
	MEDIUM = "2", // Mittlere Priorität
	LOW = "3", // Niedrige Priorität
	DONE = "4", // Erledigt
}

export enum ConnectionStatus {
	WAITING = "w", // Wartend
	TRANSITION = "n", // Übergang
	ALTERNATIVE = "a", // Alternative
}

export enum DelaySource {
	LEIBIT = "L", // LEIBIT (Leit- und Informationssystem für den Betriebsdienst)
	RISNE_AUT = "NA", // RIS::NE automatisch (Reisendeninformationssystem Nahverkehr)
	RISNE_MAN = "NM", // RIS::NE manuell
	VDV = "V", // VDV (Verband Deutscher Verkehrsunternehmen)
	ISTP_AUT = "IA", // ISTP automatisch (Integrierte Steuerung Transportprozesse)
	ISTP_MAN = "IM", // ISTP manuell
	AUTOMATIC_PROGNOSIS = "A", // Automatische Prognose
}

export enum DistributorType {
	CITY = "s", // Stadt
	REGION = "r", // Region
	LONG_DISTANCE = "f", // Fernverkehr
	OTHER = "x", // Sonstige
}

export enum TripType {
	P = "p", // Personenzug (Personentransport)
	E = "e", // Eilzug (Schnellerer Personenzug)
	Z = "z", // Zusatzzug (Sonderzug)
	S = "s", // S-Bahn (Stadtschnellbahn)
	H = "h", // Hilfszug (Notfall- oder Wartungszug)
	N = "n", // Nachtzug (Nachtverkehr)
}

export enum ReferenceTripRelationToStop {
	BEFORE = "b", // Vor dem Halt
	END = "e", // Ende des Halts
	BETWEEN = "c", // Zwischen Halten
	START = "s", // Beginn des Halts
	AFTER = "a", // Nach dem Halt
}

export interface Event {
	/** Geplante Ankunftszeit (changed) */
	cde?: string;
	/** Letzte Änderungszeit */
	clt?: string;
	/** Geänderter Bahnsteig */
	cp?: string;
	/** Geänderter Bahnsteigpfad */
	cpth?: string;
	/** Status des geänderten Ereignisses */
	cs?: EventStatus;
	/** Geänderte Zeit */
	ct?: string;
	/** Verzögerungscode */
	dc?: number;
	/** Haltinformations-ID */
	hi?: number;
	/** Linie */
	l?: string;
	/** Zugehörige Meldungen */
	m?: Message[];
	/** Geplante Abfahrtszeit */
	pde?: string;
	/** Geplanter Bahnsteig */
	pp?: string;
	/** Geplanter Bahnsteigpfad */
	ppth?: string;
	/** Status des geplanten Ereignisses */
	ps?: EventStatus;
	/** Geplante Zeit */
	pt?: string;
	/** Zugattribut */
	tra?: string;
	/** Flügelzüge (durch Komma getrennte IDs) */
	wings?: string;
}

export interface Message {
	/** Code */
	c?: number;
	/** Kategorie */
	cat?: string;
	/** Verzögerung in Minuten */
	del?: number;
	/** Verteiler-Meldungen */
	dm?: DistributorMessage[];
	/** Ereigniscode */
	ec?: string;
	/** Externer Link */
	elnk?: string;
	/** Externer Text */
	ext?: string;
	/** Gültig von (Zeitstempel) */
	from?: string;
	/** Eindeutige Meldungs-ID */
	id: string;
	/** Interner Text */
	int?: string;
	/** Besitzer/Ersteller der Meldung */
	o?: string;
	/** Priorität der Meldung */
	pr?: Priority;
	/** Typ der Meldung */
	t: MessageType;
	/** Zugehörige Zugbezeichnungen */
	tl?: TripLabel[];
	/** Gültig bis (Zeitstempel) */
	to?: string;
	/** Zeitstempel der Meldungserstellung */
	ts: string;
}

export interface DistributorMessage {
	/** Interner Text */
	int?: string;
	/** Name des Verteilers */
	n?: string;
	/** Typ des Verteilers */
	t?: DistributorType;
	/** Zeitstempel */
	ts?: string;
}

export interface TripLabel {
	/** Kategorie des Zuges (z.B. ICE, RE, S) */
	c: string;
	/** Zusätzliche Flags */
	f?: string;
	/** Zugnummer */
	n: string;
	/** Betreiber-ID */
	o: string;
	/** Zugtyp */
	t?: TripType;
}

export interface TripReference {
	/** Referenzierte Zugbezeichnungen */
	rt?: TripLabel[];
	/** Zugbezeichnung */
	tl: TripLabel;
}

export interface Connection {
	/** Status der Verbindung */
	cs: ConnectionStatus;
	/** EVA-Nummer (eindeutige Stationskennung) */
	eva?: number;
	/** Verbindungs-ID */
	id: string;
	/** Referenzierter Halt */
	ref?: TimetableStop;
	/** Quell-Halt */
	s: TimetableStop;
	/** Zeitstempel */
	ts: string;
}

export interface HistoricDelay {
	/** Ankunftsverspätung */
	ar?: string;
	/** Verspätungsursache */
	cod?: string;
	/** Abfahrtsverspätung */
	dp?: string;
	/** Quelle der Verspätungsinformation */
	src?: DelaySource;
	/** Zeitstempel */
	ts?: string;
}

export interface HistoricPlatformChange {
	/** Ankunftsgleiswechsel */
	ar?: string;
	/** Ursache des Gleiswechsels */
	cot?: string;
	/** Abfahrtsgleiswechsel */
	dp?: string;
	/** Zeitstempel */
	ts?: string;
}

export interface ReferenceTrip {
	/** Ist abgeschlossen */
	c: boolean;
	/** Endpunkt */
	ea: ReferenceTripStopLabel;
	/** Referenz-ID */
	id: string;
	/** Referenz-Zugbezeichnung */
	rtl: ReferenceTripLabel;
	/** Startpunkt */
	sd: ReferenceTripStopLabel;
}

export interface ReferenceTripLabel {
	/** Kategorie des Zuges */
	c: string;
	/** Zugnummer */
	n: string;
}

export interface ReferenceTripStopLabel {
	/** EVA-Nummer der Station */
	eva: number;
	/** Index im Fahrplan */
	i: number;
	/** Name der Station */
	n: string;
	/** Geplante Zeit */
	pt: string;
}

export interface ReferenceTripRelation {
	/** Referenzfahrt */
	rt: ReferenceTrip;
	/** Beziehung zum Halt */
	rts: ReferenceTripRelationToStop;
}

export interface TimetableStop {
	/** Ankunftsereignis */
	ar?: Event;
	/** Verbindungen */
	conn?: Connection[];
	/** Abfahrtsereignis */
	dp?: Event;
	/** EVA-Nummer der Station */
	eva: number;
	/** Historische Verspätungen */
	hd?: HistoricDelay[];
	/** Historische Gleiswechsel */
	hpc?: HistoricPlatformChange[];
	/** Eindeutige ID des Halts */
	id: string;
	/** Meldungen */
	m?: Message[];
	/** Referenz zu einem anderen Zug */
	ref?: TripReference;
	/** Referenzfahrtbeziehungen */
	rtr?: ReferenceTripRelation[];
	/** Zugbezeichnung */
	tl?: TripLabel;
}

export interface Timetable {
	/** EVA-Nummer der Station */
	eva?: number;
	/** Meldungen */
	m?: Message[];
	/** Liste der Halte */
	s?: TimetableStop[];
	/** Name der Station */
	station?: string;
}

export interface StationData {
	/** DS100-Code (betriebliche Abkürzung) */
	ds100: string;
	/** EVA-Nummer (eindeutige Stationskennung) */
	eva: number;
	/** Metadaten */
	meta?: string;
	/** Name der Station */
	name: string;
	/** Plattform-Information */
	p?: string;
}

export interface MultipleStationData {
	/** Liste von Stationsdaten */
	station: StationData[];
}

export interface TimetableParams {
	/** EVA-Nummer der Station */
	evaNo: string;
}

export interface PlanParams extends TimetableParams {
	date: string; // Format YYMMDD
	hour: string; // Format HH
}

export interface StationParams {
	pattern: string;
}

export interface TimetableResponse {
	timetable: Timetable;
}

export interface StationResponse {
	stations: MultipleStationData;
}

```