Skip to content

Commit 205765d

Browse files
committed
Implement Deutsche Bahn API integration with configuration and service classes
1 parent b14239a commit 205765d

5 files changed

Lines changed: 244 additions & 4 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
*.userosscache
1111
*.sln.docstates
1212
*.env
13+
AbeckDev.DbTimetable.Mcp/appsettings.Development.json
14+
1315

1416
# User-specific files (MonoDevelop/Xamarin Studio)
1517
*.userprefs
@@ -416,4 +418,3 @@ FodyWeavers.xsd
416418
*.msix
417419
*.msm
418420
*.msp
419-
AbeckDev.DbTimetable.Mcp/appsettings.Development.json
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace AbeckDev.DbTimetable.Mcp.Models;
4+
5+
public class Configuration
6+
{
7+
public const string SectionName = "DeutscheBahnApi";
8+
public string BaseUrl { get; set; } = "https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1/";
9+
public string ClientId { get; set; } = string.Empty;
10+
public string ApiKey { get; set; } = string.Empty;
11+
}

AbeckDev.DbTimetable.Mcp/Program.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
//Minimal API approach
2+
using AbeckDev.DbTimetable.Mcp.Models;
3+
using AbeckDev.DbTimetable.Mcp.Services;
24
using ModelContextProtocol.Protocol;
35

46
var builder = WebApplication.CreateBuilder(args);
57

8+
//Setup Config provider
9+
builder.Configuration
10+
.SetBasePath(Directory.GetCurrentDirectory())
11+
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
12+
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
13+
.AddEnvironmentVariables();
14+
15+
builder.Services.Configure<Configuration>(
16+
builder.Configuration.GetSection(Configuration.SectionName));
17+
18+
var dbConfig = builder.Configuration
19+
.GetSection(Configuration.SectionName)
20+
.Get<Configuration>() ?? new Configuration();
21+
22+
builder.Services.AddHttpClient<TimeTableService>(client =>
23+
{
24+
client.BaseAddress = new Uri(dbConfig.BaseUrl);
25+
client.DefaultRequestHeaders.Accept.Add(
26+
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/xml"));
27+
client.Timeout = TimeSpan.FromSeconds(30);
28+
});
29+
630
// Add services to the container.
731
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
832
builder.Services.AddMcpServer(options =>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
using AbeckDev.DbTimetable.Mcp.Models;
3+
using Microsoft.Extensions.Options;
4+
5+
namespace AbeckDev.DbTimetable.Mcp.Services;
6+
7+
public class TimeTableService
8+
{
9+
10+
private readonly HttpClient _httpClient;
11+
private readonly Configuration _config;
12+
13+
public TimeTableService(HttpClient httpClient, IOptions<Configuration> config)
14+
{
15+
_httpClient = httpClient;
16+
_config = config.Value;
17+
}
18+
19+
/// <summary>
20+
/// Get recent timetable changes for a specific event number
21+
/// </summary>
22+
public async Task<string> GetRecentTimetableChangesAsync(
23+
string eventNo,
24+
CancellationToken cancellationToken = default)
25+
{
26+
using var request = new HttpRequestMessage(HttpMethod.Get, $"rchg/{eventNo}");
27+
request.Headers.Add("DB-Client-Id", _config.ClientId);
28+
request.Headers.Add("DB-Api-Key", _config.ApiKey);
29+
request.Headers.Add("accept", "application/xml");
30+
31+
var response = await _httpClient.SendAsync(request, cancellationToken);
32+
33+
if (!response.IsSuccessStatusCode)
34+
{
35+
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
36+
throw new HttpRequestException(
37+
$"Deutsche Bahn API request failed with status {response.StatusCode}: {errorContent}");
38+
}
39+
40+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
41+
return content;
42+
}
43+
44+
/// <summary>
45+
/// Get station board (departures/arrivals) for a specific station
46+
/// </summary>
47+
public async Task<string> GetStationBoardAsync(
48+
string evaNo,
49+
DateTime? date = null,
50+
CancellationToken cancellationToken = default)
51+
{
52+
// Format: yyMMddHHmm (e.g., 2511051830 for 2025-11-05 18:30)
53+
var dateParam = date?.ToString("yyMMdd") ?? DateTime.UtcNow.ToString("yyMMdd");
54+
var hourParam = date?.ToString("HH") ?? DateTime.UtcNow.ToString("HH");
55+
56+
using var request = new HttpRequestMessage(HttpMethod.Get, $"plan/{evaNo}/{dateParam}/{hourParam}");
57+
request.Headers.Add("DB-Client-Id", _config.ClientId);
58+
request.Headers.Add("DB-Api-Key", _config.ApiKey);
59+
request.Headers.Add("accept", "application/xml");
60+
61+
var response = await _httpClient.SendAsync(request, cancellationToken);
62+
response.EnsureSuccessStatusCode();
63+
64+
return await response.Content.ReadAsStringAsync(cancellationToken);
65+
}
66+
67+
/// <summary>
68+
/// Get full changes for a station at a specific time
69+
/// </summary>
70+
public async Task<string> GetFullChangesAsync(
71+
string evaNo,
72+
CancellationToken cancellationToken = default)
73+
{
74+
using var request = new HttpRequestMessage(HttpMethod.Get, $"fchg/{evaNo}");
75+
request.Headers.Add("DB-Client-Id", _config.ClientId);
76+
request.Headers.Add("DB-Api-Key", _config.ApiKey);
77+
request.Headers.Add("accept", "application/xml");
78+
79+
var response = await _httpClient.SendAsync(request, cancellationToken);
80+
response.EnsureSuccessStatusCode();
81+
82+
return await response.Content.ReadAsStringAsync(cancellationToken);
83+
}
84+
85+
/// <summary>
86+
/// Get information about stations given either a station name (prefix), eva number, ds100/rl100 code, wildcard (*); doesn't seem to work with umlauten in station name (prefix)
87+
/// </summary>
88+
public async Task<string> GetStationInformation(
89+
string pattern,
90+
CancellationToken cancellationToken = default)
91+
{
92+
using var request = new HttpRequestMessage(HttpMethod.Get, $"station/{pattern}");
93+
request.Headers.Add("DB-Client-Id", _config.ClientId);
94+
request.Headers.Add("DB-Api-Key", _config.ApiKey);
95+
request.Headers.Add("accept", "application/xml");
96+
97+
var response = await _httpClient.SendAsync(request, cancellationToken);
98+
response.EnsureSuccessStatusCode();
99+
100+
return await response.Content.ReadAsStringAsync(cancellationToken);
101+
}
102+
103+
}

AbeckDev.DbTimetable.Mcp/Tools.cs

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.ComponentModel;
3+
using AbeckDev.DbTimetable.Mcp.Services;
34
using ModelContextProtocol.Server;
45

56

@@ -8,9 +9,109 @@ namespace AbeckDev.DbTimetable.Mcp;
89
[McpServerToolType]
910
public static class Tools
1011
{
11-
[McpServerTool, Description("Echoes the message back.")]
12-
public static string Echo(string message)
12+
/// <summary>
13+
/// MCP Server tools for accessing Deutsche Bahn Timetable API
14+
/// </summary>
15+
[McpServerToolType]
16+
public class TimetableTools
1317
{
14-
return $"Echo: {message}";
18+
private readonly TimeTableService _timeTableService;
19+
20+
public TimetableTools(TimeTableService timeTableService)
21+
{
22+
_timeTableService = timeTableService;
23+
}
24+
25+
[McpServerTool]
26+
[Description("Get full timetable changes for a specific train event. The data includes all known changes from now on until ndefinitely into the future. Once changes become obsolete (because their trip departs from the station) they are removed.")]
27+
public async Task<string> GetFullTimetableChanges(
28+
[Description("Event number (EVA number) of the train event")] string eventNo)
29+
{
30+
try
31+
{
32+
var result = await _timeTableService.GetFullChangesAsync(eventNo);
33+
return result;
34+
}
35+
catch (HttpRequestException ex)
36+
{
37+
return $"Error fetching timetable changes: {ex.Message}";
38+
}
39+
catch (Exception ex)
40+
{
41+
return $"Unexpected error: {ex.Message}";
42+
}
43+
}
44+
45+
[McpServerTool]
46+
[Description("Get station board (departures and arrivals) for a specific station. Returns XML data with train schedules.")]
47+
public async Task<string> GetStationBoard(
48+
[Description("EVA station number (e.g., 8000105 for Frankfurt Hauptbahnhof)")] string evaNo,
49+
[Description("Date and time in format 'yyyy-MM-dd HH:mm' (UTC). Leave empty for current time.")] string? dateTime = null)
50+
{
51+
try
52+
{
53+
DateTime? parsedDate = null;
54+
if (!string.IsNullOrEmpty(dateTime))
55+
{
56+
if (DateTime.TryParse(dateTime, out var dt))
57+
{
58+
parsedDate = dt;
59+
}
60+
else
61+
{
62+
return "Error: Invalid date format. Please use 'yyyy-MM-dd HH:mm' format.";
63+
}
64+
}
65+
66+
var result = await _timeTableService.GetStationBoardAsync(evaNo, parsedDate);
67+
return result;
68+
}
69+
catch (HttpRequestException ex)
70+
{
71+
return $"Error fetching station board: {ex.Message}";
72+
}
73+
catch (Exception ex)
74+
{
75+
return $"Unexpected error: {ex.Message}";
76+
}
77+
}
78+
79+
[McpServerTool]
80+
[Description("Get all current changes (delays, cancellations, platform changes) for a specific station. Recent changes are always a subset of the full changes. They may equal full changes but are typically much smaller. Data includes only those changes that became known within the last 2 minutes.")]
81+
public async Task<string> GetStationChanges([Description("EVA station number (e.g., 8000105 for Frankfurt Hauptbahnhof)")] string evaNo)
82+
{
83+
try
84+
{
85+
var result = await _timeTableService.GetRecentTimetableChangesAsync(evaNo);
86+
return result;
87+
}
88+
catch (HttpRequestException ex)
89+
{
90+
return $"Error fetching station changes: {ex.Message}";
91+
}
92+
catch (Exception ex)
93+
{
94+
return $"Unexpected error: {ex.Message}";
95+
}
96+
}
97+
98+
[McpServerTool]
99+
[Description("Get information about stations.")]
100+
public async Task<string> GetStationDetails([Description("Either a station name (prefix), eva number, ds100/rl100 code, wildcard (*); doesn't seem to work with umlauten in station name (prefix). If unsure use the Station Name e.g. \"Dresden Hbf\" ")] string pattern)
101+
{
102+
try
103+
{
104+
var result = await _timeTableService.GetStationInformation(pattern);
105+
return result;
106+
}
107+
catch (HttpRequestException ex)
108+
{
109+
return $"Error fetching station Details: {ex.Message}";
110+
}
111+
catch (Exception ex)
112+
{
113+
return $"Unexpected error: {ex.Message}";
114+
}
115+
}
15116
}
16117
}

0 commit comments

Comments
 (0)