sbb-mcp 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +57 -216
- package/README.md +314 -153
- package/dist/formatters.d.ts +8 -8
- package/dist/formatters.js +54 -46
- package/dist/formatters.js.map +1 -1
- package/dist/i18n.d.ts +22 -0
- package/dist/i18n.js +36 -0
- package/dist/i18n.js.map +1 -0
- package/dist/profile.d.ts +3 -1
- package/dist/profile.js +10 -3
- package/dist/profile.js.map +1 -1
- package/dist/structured.d.ts +119 -0
- package/dist/structured.js +133 -0
- package/dist/structured.js.map +1 -0
- package/dist/tools.js +183 -117
- package/dist/tools.js.map +1 -1
- package/dist/transport/smapi-client.js +1 -0
- package/dist/transport/smapi-client.js.map +1 -1
- package/dist/transport/smapi-journey.d.ts +9 -4
- package/dist/transport/smapi-journey.js +7 -6
- package/dist/transport/smapi-journey.js.map +1 -1
- package/dist/transport/smapi-mock.d.ts +6 -2
- package/dist/transport/smapi-mock.js +9 -2
- package/dist/transport/smapi-mock.js.map +1 -1
- package/dist/transport/smapi-prices.d.ts +3 -2
- package/dist/transport/smapi-prices.js +5 -3
- package/dist/transport/smapi-prices.js.map +1 -1
- package/dist/widgets.d.ts +33 -0
- package/dist/widgets.js +120 -0
- package/dist/widgets.js.map +1 -0
- package/package.json +11 -3
- package/web/dist/widgets.css +1 -0
- package/web/dist/widgets.js +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure extractors from SMAPI types to widget-friendly DTOs.
|
|
3
|
+
* Shape must stay in sync with web/src/types.ts.
|
|
4
|
+
*
|
|
5
|
+
* Keep these side-effect free. They're tested in unit tests and run on
|
|
6
|
+
* every tool invocation to populate `structuredContent` on responses.
|
|
7
|
+
*/
|
|
8
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
9
|
+
function toStation(p) {
|
|
10
|
+
return {
|
|
11
|
+
id: p.id,
|
|
12
|
+
name: p.name,
|
|
13
|
+
lat: p.geoPosition?.latitude,
|
|
14
|
+
lon: p.geoPosition?.longitude,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function parseIsoDurationMinutes(iso) {
|
|
18
|
+
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?/);
|
|
19
|
+
if (!m)
|
|
20
|
+
return 0;
|
|
21
|
+
return parseInt(m[1] ?? '0', 10) * 60 + parseInt(m[2] ?? '0', 10);
|
|
22
|
+
}
|
|
23
|
+
function legToDTO(leg) {
|
|
24
|
+
if (leg.type === 'timed') {
|
|
25
|
+
const tl = leg;
|
|
26
|
+
return {
|
|
27
|
+
type: 'train',
|
|
28
|
+
line: tl.service.publishedLineName,
|
|
29
|
+
platform: tl.board.platform,
|
|
30
|
+
minutes: parseIsoDurationMinutes(tl.duration),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (leg.type === 'transfer') {
|
|
34
|
+
const tr = leg;
|
|
35
|
+
return { type: 'walk', minutes: parseIsoDurationMinutes(tr.duration) };
|
|
36
|
+
}
|
|
37
|
+
return { type: 'walk', minutes: parseIsoDurationMinutes(leg.duration) };
|
|
38
|
+
}
|
|
39
|
+
export function toStationsListDTO(query, places) {
|
|
40
|
+
return { query, stations: places.map(toStation) };
|
|
41
|
+
}
|
|
42
|
+
export function toConnectionListDTO(collection, weather) {
|
|
43
|
+
const first = collection.trips[0];
|
|
44
|
+
const fallbackStation = { id: '', name: '—' };
|
|
45
|
+
return {
|
|
46
|
+
origin: first ? toStation(first.origin) : fallbackStation,
|
|
47
|
+
destination: first ? toStation(first.destination) : fallbackStation,
|
|
48
|
+
date: first?.startTime ?? '',
|
|
49
|
+
collectionId: collection.id,
|
|
50
|
+
connections: collection.trips.map((t) => ({
|
|
51
|
+
tripId: t.id,
|
|
52
|
+
departureTime: t.startTime,
|
|
53
|
+
arrivalTime: t.endTime,
|
|
54
|
+
durationMinutes: parseIsoDurationMinutes(t.duration),
|
|
55
|
+
transfers: t.transfers,
|
|
56
|
+
legs: t.legs.map(legToDTO),
|
|
57
|
+
})),
|
|
58
|
+
...(weather ? { weather } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function toTripDetailsDTO(trip) {
|
|
62
|
+
return {
|
|
63
|
+
tripId: trip.id,
|
|
64
|
+
origin: toStation(trip.origin),
|
|
65
|
+
destination: toStation(trip.destination),
|
|
66
|
+
departureTime: trip.startTime,
|
|
67
|
+
arrivalTime: trip.endTime,
|
|
68
|
+
durationMinutes: parseIsoDurationMinutes(trip.duration),
|
|
69
|
+
transfers: trip.transfers,
|
|
70
|
+
status: trip.tripStatus,
|
|
71
|
+
legs: trip.legs.map((leg) => {
|
|
72
|
+
if (leg.type === 'timed') {
|
|
73
|
+
const tl = leg;
|
|
74
|
+
return {
|
|
75
|
+
type: 'train',
|
|
76
|
+
line: tl.service.publishedLineName,
|
|
77
|
+
operator: tl.service.operatorName,
|
|
78
|
+
durationMinutes: parseIsoDurationMinutes(tl.duration),
|
|
79
|
+
from: {
|
|
80
|
+
name: tl.board.stopPlace.name,
|
|
81
|
+
time: tl.board.departureTime,
|
|
82
|
+
platform: tl.board.platform,
|
|
83
|
+
},
|
|
84
|
+
to: {
|
|
85
|
+
name: tl.alight.stopPlace.name,
|
|
86
|
+
time: tl.alight.arrivalTime,
|
|
87
|
+
platform: tl.alight.platform,
|
|
88
|
+
},
|
|
89
|
+
intermediateStops: tl.intermediateStops?.map((s) => ({
|
|
90
|
+
name: s.stopPlace.name,
|
|
91
|
+
arrivalTime: s.arrivalTime,
|
|
92
|
+
departureTime: s.departureTime,
|
|
93
|
+
})),
|
|
94
|
+
occupancy: tl.occupancy
|
|
95
|
+
? {
|
|
96
|
+
firstClass: tl.occupancy.firstClass,
|
|
97
|
+
secondClass: tl.occupancy.secondClass,
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
type: 'walk',
|
|
104
|
+
durationMinutes: parseIsoDurationMinutes(leg.duration),
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function toPricesTableDTO(results) {
|
|
110
|
+
return {
|
|
111
|
+
prices: results.map((r) => {
|
|
112
|
+
const second = r.prices.find((p) => p.class === '2');
|
|
113
|
+
const first = r.prices.find((p) => p.class === '1');
|
|
114
|
+
return {
|
|
115
|
+
tripId: r.tripId,
|
|
116
|
+
...(second ? { secondClass: { amount: second.amount, currency: second.currency } } : {}),
|
|
117
|
+
...(first ? { firstClass: { amount: first.amount, currency: first.currency } } : {}),
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function toTicketCardDTO(params) {
|
|
123
|
+
return {
|
|
124
|
+
tripId: params.tripId,
|
|
125
|
+
origin: { id: params.fromId, name: params.fromName },
|
|
126
|
+
destination: { id: params.toId, name: params.toName },
|
|
127
|
+
departureTime: params.departureTime,
|
|
128
|
+
arrivalTime: params.arrivalTime,
|
|
129
|
+
primaryLink: params.primaryLink,
|
|
130
|
+
...(params.affiliateLink ? { affiliateLink: params.affiliateLink } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=structured.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured.js","sourceRoot":"","sources":["../src/structured.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { isSmapiConfigured, searchPlaces, searchTrips, getTrip, paginateTrips, getTripPrices, getTripOffers, mockSearchPlaces, mockSearchTrips, mockGetTripPrices, } from './transport/index.js';
|
|
4
|
-
import { formatStations, formatConnections, formatTripDetails, formatPrices, formatTicketLink, buildSbbDeepLink,
|
|
4
|
+
import { formatStations, formatConnections, formatTripDetails, formatPrices, formatTicketLink, buildSbbDeepLink, } from './formatters.js';
|
|
5
5
|
import { cacheGet, cacheSet, TTL } from './cache.js';
|
|
6
6
|
import { SmapiError } from './transport/smapi-client.js';
|
|
7
7
|
import { loadProfile, saveProfile, formatProfileSummary, isReductionCardExpired } from './profile.js';
|
|
8
8
|
import { isSwissTripConfigured, fetchProfile as fetchCloudProfile, fetchTravelers, toSmapiTraveler, formatTravelersList, formatCloudProfile, } from './swisstrip.js';
|
|
9
|
+
import { getWeatherSummary } from 'swiss-weather-mcp';
|
|
10
|
+
import { toStationsListDTO, toConnectionListDTO, toTripDetailsDTO, toPricesTableDTO, toTicketCardDTO, } from './structured.js';
|
|
11
|
+
import { registerWidgets, widgetResponseMeta, widgetToolMeta, WIDGETS } from './widgets.js';
|
|
9
12
|
const useMock = !isSmapiConfigured();
|
|
10
13
|
const useCloud = isSwissTripConfigured();
|
|
11
14
|
/** Match SwissTrip travelers by first name (case-insensitive, fuzzy) */
|
|
@@ -13,7 +16,6 @@ function matchTravelersByName(all, names) {
|
|
|
13
16
|
const matched = [];
|
|
14
17
|
for (const name of names) {
|
|
15
18
|
const lower = name.toLowerCase();
|
|
16
|
-
// Try exact first_name match, then label match, then partial match
|
|
17
19
|
const found = all.find(t => t.first_name?.toLowerCase() === lower ||
|
|
18
20
|
t.label?.toLowerCase() === lower) || all.find(t => t.first_name?.toLowerCase().startsWith(lower) ||
|
|
19
21
|
t.last_name?.toLowerCase() === lower);
|
|
@@ -32,12 +34,24 @@ function errorResult(err) {
|
|
|
32
34
|
const message = err instanceof Error ? err.message : String(err);
|
|
33
35
|
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
34
36
|
}
|
|
37
|
+
/** Compute Europe/Zurich ISO timestamp for a (date, HH:MM) pair. */
|
|
38
|
+
function zurichIso(date, time) {
|
|
39
|
+
const probe = new Date(`${date}T${time}:00Z`);
|
|
40
|
+
const zurichStr = probe.toLocaleString('en-US', { timeZone: 'Europe/Zurich' });
|
|
41
|
+
const zurichTime = new Date(zurichStr + ' UTC');
|
|
42
|
+
const offsetMs = zurichTime.getTime() - probe.getTime();
|
|
43
|
+
const offsetHours = Math.round(offsetMs / (60 * 60 * 1000));
|
|
44
|
+
const offsetStr = `${offsetHours >= 0 ? '+' : '-'}${String(Math.abs(offsetHours)).padStart(2, '0')}:00`;
|
|
45
|
+
return new Date(`${date}T${time}:00${offsetStr}`).toISOString();
|
|
46
|
+
}
|
|
35
47
|
export function createSbbMcpServer() {
|
|
36
48
|
const server = new McpServer({
|
|
37
49
|
name: 'sbb-mcp',
|
|
38
|
-
version: '0.
|
|
50
|
+
version: '0.4.0',
|
|
39
51
|
description: 'Swiss Federal Railways (SBB/CFF/FFS) — real-time train schedules, prices, and ticket purchase links for Switzerland',
|
|
40
52
|
});
|
|
53
|
+
// ─── Widget resources (ChatGPT Apps SDK) ───────────────────────────────
|
|
54
|
+
registerWidgets(server);
|
|
41
55
|
// ─── Prompts ───────────────────────────────────────────────────────────
|
|
42
56
|
server.prompt('plan_journey', 'Plan a train journey in Switzerland — finds connections, compares prices, and provides ticket links', {
|
|
43
57
|
from: z.string().describe('Origin station (e.g. "Zurich")'),
|
|
@@ -57,7 +71,6 @@ export function createSbbMcpServer() {
|
|
|
57
71
|
description: 'The user\'s saved travel profile (name, DOB, reduction card). Read this before showing prices or generating ticket links. If empty, ask the user for their details and save with save_profile.',
|
|
58
72
|
mimeType: 'text/plain',
|
|
59
73
|
}, async () => {
|
|
60
|
-
// Cloud profile takes precedence when SWISSTRIP_TOKEN is set
|
|
61
74
|
if (useCloud) {
|
|
62
75
|
try {
|
|
63
76
|
const cloudProfile = await fetchCloudProfile();
|
|
@@ -70,7 +83,7 @@ export function createSbbMcpServer() {
|
|
|
70
83
|
};
|
|
71
84
|
}
|
|
72
85
|
catch {
|
|
73
|
-
// Fall through
|
|
86
|
+
// Fall through
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
const profile = loadProfile();
|
|
@@ -97,13 +110,18 @@ export function createSbbMcpServer() {
|
|
|
97
110
|
};
|
|
98
111
|
});
|
|
99
112
|
// ─── Tool: save_profile ──────────────────────────────────────────────
|
|
100
|
-
server.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
server.registerTool('save_profile', {
|
|
114
|
+
title: 'Save Travel Profile',
|
|
115
|
+
description: 'Save the user\'s travel profile locally for future sessions. Call this after asking the user for their details (name, date of birth, reduction card). Data is stored at ~/.sbb-mcp/profile.json.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
first_name: z.string().optional().describe('First name (for ticket booking)'),
|
|
118
|
+
last_name: z.string().optional().describe('Last name (for ticket booking)'),
|
|
119
|
+
date_of_birth: z.string().optional().describe('Date of birth YYYY-MM-DD (for age-based pricing)'),
|
|
120
|
+
reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).optional().describe('Swiss reduction card: HALF_FARE (Halbtax), GA (General Abonnement), or NONE'),
|
|
121
|
+
reduction_card_valid_until: z.string().optional().describe('Reduction card expiry date YYYY-MM-DD (ask the user when their Halbtax/GA expires)'),
|
|
122
|
+
},
|
|
123
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
124
|
+
}, async ({ first_name, last_name, date_of_birth, reduction_card, reduction_card_valid_until }) => {
|
|
107
125
|
try {
|
|
108
126
|
const updates = {};
|
|
109
127
|
if (first_name)
|
|
@@ -125,15 +143,19 @@ export function createSbbMcpServer() {
|
|
|
125
143
|
}
|
|
126
144
|
});
|
|
127
145
|
// ─── Tool: get_profile ───────────────────────────────────────────────
|
|
128
|
-
server.
|
|
129
|
-
|
|
146
|
+
server.registerTool('get_profile', {
|
|
147
|
+
title: 'Get Travel Profile',
|
|
148
|
+
description: 'Read the user\'s saved travel profile. Check this before showing prices to use correct reduction card. If no profile exists, ask the user for their details.',
|
|
149
|
+
inputSchema: {},
|
|
150
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
151
|
+
}, async () => {
|
|
130
152
|
if (useCloud) {
|
|
131
153
|
try {
|
|
132
154
|
const cloudProfile = await fetchCloudProfile();
|
|
133
155
|
return { content: [{ type: 'text', text: formatCloudProfile(cloudProfile) }] };
|
|
134
156
|
}
|
|
135
157
|
catch {
|
|
136
|
-
// Fall through
|
|
158
|
+
// Fall through
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
const profile = loadProfile();
|
|
@@ -143,7 +165,12 @@ export function createSbbMcpServer() {
|
|
|
143
165
|
return { content: [{ type: 'text', text: formatProfileSummary(profile) }] };
|
|
144
166
|
});
|
|
145
167
|
// ─── Tool: list_travelers (SwissTrip cloud only) ─────────────────────
|
|
146
|
-
server.
|
|
168
|
+
server.registerTool('list_travelers', {
|
|
169
|
+
title: 'List Travelers',
|
|
170
|
+
description: 'List all travelers in the user\'s SwissTrip account (self, partner, kids). Each traveler has their own reduction card. Use their names with get_prices for family trip pricing. Requires SWISSTRIP_TOKEN.',
|
|
171
|
+
inputSchema: {},
|
|
172
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
173
|
+
}, async () => {
|
|
147
174
|
if (!useCloud) {
|
|
148
175
|
return { content: [{ type: 'text', text: 'list_travelers requires a SwissTrip account connection (SWISSTRIP_TOKEN). Without it, use save_profile/get_profile for single-traveler local profiles.' }] };
|
|
149
176
|
}
|
|
@@ -154,7 +181,7 @@ export function createSbbMcpServer() {
|
|
|
154
181
|
return { content: [{ type: 'text', text: cached }] };
|
|
155
182
|
const travelers = await fetchTravelers();
|
|
156
183
|
const text = formatTravelersList(travelers);
|
|
157
|
-
cacheSet(cacheKey, text, TTL.STATIONS);
|
|
184
|
+
cacheSet(cacheKey, text, TTL.STATIONS);
|
|
158
185
|
return { content: [{ type: 'text', text }] };
|
|
159
186
|
}
|
|
160
187
|
catch (err) {
|
|
@@ -162,34 +189,44 @@ export function createSbbMcpServer() {
|
|
|
162
189
|
}
|
|
163
190
|
});
|
|
164
191
|
// ─── Tool 1: search_stations ──────────────────────────────────────────
|
|
165
|
-
server.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
server.registerTool('search_stations', {
|
|
193
|
+
title: 'Search Stations',
|
|
194
|
+
description: 'Search for Swiss train stations, addresses, or points of interest by name. Returns station IDs needed for other tools.',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
query: z.string().describe('Station name to search for (e.g. "Zurich", "Bern", "Interlaken")'),
|
|
197
|
+
limit: z.number().min(1).max(20).default(10).describe('Maximum number of results'),
|
|
198
|
+
},
|
|
199
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
200
|
+
_meta: widgetToolMeta(WIDGETS.STATIONS_LIST, 'Searching stations…', 'Stations ready'),
|
|
201
|
+
}, async ({ query, limit }) => {
|
|
169
202
|
try {
|
|
170
|
-
const cacheKey = `stations:${query.toLowerCase()}:${limit}`;
|
|
171
|
-
const cached = cacheGet(cacheKey);
|
|
172
|
-
if (cached)
|
|
173
|
-
return { content: [{ type: 'text', text: cached }] };
|
|
174
203
|
const stations = useMock
|
|
175
204
|
? await mockSearchPlaces({ name: query })
|
|
176
205
|
: await searchPlaces({ name: query, type: 'STOP', numberOfResults: limit });
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: formatStations(stations) }],
|
|
208
|
+
structuredContent: toStationsListDTO(query, stations),
|
|
209
|
+
_meta: widgetResponseMeta(WIDGETS.STATIONS_LIST),
|
|
210
|
+
};
|
|
180
211
|
}
|
|
181
212
|
catch (err) {
|
|
182
213
|
return errorResult(err);
|
|
183
214
|
}
|
|
184
215
|
});
|
|
185
216
|
// ─── Tool 2: search_connections ───────────────────────────────────────
|
|
186
|
-
server.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
server.registerTool('search_connections', {
|
|
218
|
+
title: 'Search Connections',
|
|
219
|
+
description: 'Find train connections between two Swiss stations. Returns schedules with departure/arrival times, duration, transfers, and trip IDs for pricing.',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
from: z.string().describe('Origin station name or ID (e.g. "Zurich HB" or "8503000")'),
|
|
222
|
+
to: z.string().describe('Destination station name or ID (e.g. "Bern" or "8507000")'),
|
|
223
|
+
date: z.string().optional().describe('Travel date in YYYY-MM-DD format (default: today)'),
|
|
224
|
+
time: z.string().optional().describe('Departure time in HH:MM format (default: now)'),
|
|
225
|
+
arrival_time: z.boolean().optional().describe('If true, the time parameter is treated as desired arrival time'),
|
|
226
|
+
},
|
|
227
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
228
|
+
_meta: widgetToolMeta(WIDGETS.CONNECTION_LIST, 'Finding trains…', 'Connections ready'),
|
|
229
|
+
}, async ({ from, to, date, time, arrival_time }) => {
|
|
193
230
|
try {
|
|
194
231
|
let fromId = from;
|
|
195
232
|
let toId = to;
|
|
@@ -216,25 +253,12 @@ export function createSbbMcpServer() {
|
|
|
216
253
|
if (date || time) {
|
|
217
254
|
const d = date || new Date().toISOString().split('T')[0];
|
|
218
255
|
const t = time || '08:00';
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const zurichStr = probe.toLocaleString('en-US', { timeZone: 'Europe/Zurich' });
|
|
222
|
-
const zurichTime = new Date(zurichStr + ' UTC');
|
|
223
|
-
const offsetMs = zurichTime.getTime() - probe.getTime();
|
|
224
|
-
const offsetHours = Math.round(offsetMs / (60 * 60 * 1000));
|
|
225
|
-
const offsetStr = `${offsetHours >= 0 ? '+' : '-'}${String(Math.abs(offsetHours)).padStart(2, '0')}:00`;
|
|
226
|
-
const dt = new Date(`${d}T${t}:00${offsetStr}`).toISOString();
|
|
227
|
-
if (arrival_time) {
|
|
256
|
+
const dt = zurichIso(d, t);
|
|
257
|
+
if (arrival_time)
|
|
228
258
|
arrivalTime = dt;
|
|
229
|
-
|
|
230
|
-
else {
|
|
259
|
+
else
|
|
231
260
|
departureTime = dt;
|
|
232
|
-
}
|
|
233
261
|
}
|
|
234
|
-
const cacheKey = `connections:${fromId}:${toId}:${departureTime || ''}:${arrivalTime || ''}`;
|
|
235
|
-
const cached = cacheGet(cacheKey);
|
|
236
|
-
if (cached)
|
|
237
|
-
return { content: [{ type: 'text', text: cached }] };
|
|
238
262
|
const collection = useMock
|
|
239
263
|
? await mockSearchTrips({ origin: fromId, destination: toId, departureTime })
|
|
240
264
|
: await searchTrips({
|
|
@@ -244,117 +268,146 @@ export function createSbbMcpServer() {
|
|
|
244
268
|
...(arrivalTime && { arrivalTime }),
|
|
245
269
|
});
|
|
246
270
|
let text = formatConnections(collection);
|
|
247
|
-
|
|
271
|
+
let weatherSummary;
|
|
248
272
|
if (collection.trips.length > 0) {
|
|
249
273
|
const dest = collection.trips[0].destination;
|
|
250
274
|
if (dest.geoPosition) {
|
|
251
275
|
const travelDate = date || new Date().toISOString().split('T')[0];
|
|
252
|
-
|
|
276
|
+
const name = dest.name || to;
|
|
277
|
+
try {
|
|
278
|
+
const summary = await getWeatherSummary(dest.geoPosition.latitude, dest.geoPosition.longitude, travelDate);
|
|
279
|
+
if (summary) {
|
|
280
|
+
weatherSummary = summary;
|
|
281
|
+
text = text + `\n\n**${name} weather:** ${summary}`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Non-fatal
|
|
286
|
+
}
|
|
253
287
|
}
|
|
254
288
|
}
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: 'text', text }],
|
|
291
|
+
structuredContent: toConnectionListDTO(collection, weatherSummary ? { summary: weatherSummary } : undefined),
|
|
292
|
+
_meta: widgetResponseMeta(WIDGETS.CONNECTION_LIST),
|
|
293
|
+
};
|
|
257
294
|
}
|
|
258
295
|
catch (err) {
|
|
259
296
|
return errorResult(err);
|
|
260
297
|
}
|
|
261
298
|
});
|
|
262
299
|
// ─── Tool 3: get_trip_details ─────────────────────────────────────────
|
|
263
|
-
server.
|
|
264
|
-
|
|
265
|
-
|
|
300
|
+
server.registerTool('get_trip_details', {
|
|
301
|
+
title: 'Get Trip Details',
|
|
302
|
+
description: 'Get detailed information about a specific train connection including all intermediate stops, platforms, and occupancy. Use a trip ID from search_connections results.',
|
|
303
|
+
inputSchema: {
|
|
304
|
+
trip_id: z.string().describe('Trip ID from search_connections results'),
|
|
305
|
+
},
|
|
306
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
307
|
+
_meta: widgetToolMeta(WIDGETS.TRIP_DETAILS, 'Loading trip details…', 'Trip details ready'),
|
|
308
|
+
}, async ({ trip_id }) => {
|
|
266
309
|
try {
|
|
267
310
|
if (useMock) {
|
|
268
311
|
return { content: [{ type: 'text', text: 'Detailed trip information is available with live API access. In mock mode, use search_connections to see trip summaries.' }] };
|
|
269
312
|
}
|
|
270
|
-
const cacheKey = `trip:${trip_id}`;
|
|
271
|
-
const cached = cacheGet(cacheKey);
|
|
272
|
-
if (cached)
|
|
273
|
-
return { content: [{ type: 'text', text: cached }] };
|
|
274
313
|
const trip = await getTrip(trip_id, 'REAL_BOARDING_ALIGHTING');
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: 'text', text: formatTripDetails(trip) }],
|
|
316
|
+
structuredContent: toTripDetailsDTO(trip),
|
|
317
|
+
_meta: widgetResponseMeta(WIDGETS.TRIP_DETAILS),
|
|
318
|
+
};
|
|
278
319
|
}
|
|
279
320
|
catch (err) {
|
|
280
321
|
return errorResult(err);
|
|
281
322
|
}
|
|
282
323
|
});
|
|
283
324
|
// ─── Tool 4: get_more_connections ─────────────────────────────────────
|
|
284
|
-
server.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
325
|
+
server.registerTool('get_more_connections', {
|
|
326
|
+
title: 'Get More Connections',
|
|
327
|
+
description: 'Load earlier or later train connections for a previous search. Use the collection ID from search_connections results.',
|
|
328
|
+
inputSchema: {
|
|
329
|
+
collection_id: z.string().describe('Collection ID from search_connections results'),
|
|
330
|
+
direction: z.enum(['next', 'previous']).describe('"next" for later trains, "previous" for earlier trains'),
|
|
331
|
+
},
|
|
332
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
333
|
+
_meta: widgetToolMeta(WIDGETS.CONNECTION_LIST, 'Loading more trains…', 'More connections ready'),
|
|
334
|
+
}, async ({ collection_id, direction }) => {
|
|
288
335
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return {
|
|
336
|
+
const collection = useMock
|
|
337
|
+
? await mockSearchTrips({
|
|
338
|
+
origin: '8503000',
|
|
339
|
+
destination: '8507000',
|
|
340
|
+
departureTime: new Date(Date.now() + (direction === 'next' ? 3 : -3) * 60 * 60 * 1000).toISOString(),
|
|
341
|
+
})
|
|
342
|
+
: await paginateTrips(collection_id, direction);
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: 'text', text: formatConnections(collection) }],
|
|
345
|
+
structuredContent: toConnectionListDTO(collection),
|
|
346
|
+
_meta: widgetResponseMeta(WIDGETS.CONNECTION_LIST),
|
|
347
|
+
};
|
|
297
348
|
}
|
|
298
349
|
catch (err) {
|
|
299
350
|
return errorResult(err);
|
|
300
351
|
}
|
|
301
352
|
});
|
|
302
353
|
// ─── Tool 5: get_prices ───────────────────────────────────────────────
|
|
303
|
-
server.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
354
|
+
server.registerTool('get_prices', {
|
|
355
|
+
title: 'Get Prices',
|
|
356
|
+
description: 'Get ticket prices for one or more train connections. Supports Half-Fare card (Halbtax) and GA travelcard discounts. When connected to SwissTrip (SWISSTRIP_TOKEN), pass traveler_names to get family pricing for multiple travelers.',
|
|
357
|
+
inputSchema: {
|
|
358
|
+
trip_ids: z.array(z.string()).min(1).max(10).describe('Trip IDs from search_connections results'),
|
|
359
|
+
traveler_type: z.enum(['ADULT', 'CHILD']).default('ADULT').describe('Traveler type (used when no traveler_names given)'),
|
|
360
|
+
reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).default('HALF_FARE').describe('Swiss reduction card (used when no traveler_names given)'),
|
|
361
|
+
traveler_names: z.array(z.string()).optional().describe('Names of SwissTrip travelers to price for (e.g. ["Fabian", "Anna"]). Requires SWISSTRIP_TOKEN. Each traveler\'s reduction card is applied automatically.'),
|
|
362
|
+
},
|
|
363
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
364
|
+
_meta: widgetToolMeta(WIDGETS.PRICES_TABLE, 'Fetching prices…', 'Prices ready'),
|
|
365
|
+
}, async ({ trip_ids, traveler_type, reduction_card, traveler_names }) => {
|
|
309
366
|
try {
|
|
310
|
-
// Build travelers list — cloud multi-traveler or single manual
|
|
311
367
|
let travelers;
|
|
312
|
-
let cacheKey;
|
|
313
368
|
if (traveler_names && traveler_names.length > 0 && useCloud) {
|
|
314
|
-
// Fetch travelers from SwissTrip and match by name
|
|
315
369
|
const allTravelers = await fetchTravelers();
|
|
316
370
|
const matched = matchTravelersByName(allTravelers, traveler_names);
|
|
317
371
|
if (matched.length === 0) {
|
|
318
372
|
return { content: [{ type: 'text', text: `No matching travelers found. Available: ${allTravelers.map(t => t.first_name).filter(Boolean).join(', ')}. Use list_travelers to see all.` }] };
|
|
319
373
|
}
|
|
320
374
|
travelers = matched.map((t, i) => toSmapiTraveler(t, i));
|
|
321
|
-
cacheKey = `prices:${trip_ids.join(',')}:cloud:${matched.map(t => t.id).join(',')}`;
|
|
322
375
|
}
|
|
323
376
|
else {
|
|
324
377
|
travelers = [{ id: 'traveler-1', type: traveler_type, reductionCard: reduction_card }];
|
|
325
|
-
cacheKey = `prices:${trip_ids.join(',')}:${traveler_type}:${reduction_card}`;
|
|
326
378
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
const prices = await getTripPrices(trip_ids, travelers);
|
|
337
|
-
const text = formatPrices(prices);
|
|
338
|
-
cacheSet(cacheKey, text, TTL.PRICES);
|
|
339
|
-
return { content: [{ type: 'text', text }] };
|
|
379
|
+
const prices = useMock
|
|
380
|
+
? await mockGetTripPrices(trip_ids)
|
|
381
|
+
: await getTripPrices(trip_ids, travelers);
|
|
382
|
+
return {
|
|
383
|
+
content: [{ type: 'text', text: formatPrices(prices) }],
|
|
384
|
+
structuredContent: toPricesTableDTO(prices),
|
|
385
|
+
_meta: widgetResponseMeta(WIDGETS.PRICES_TABLE),
|
|
386
|
+
};
|
|
340
387
|
}
|
|
341
388
|
catch (err) {
|
|
342
389
|
return errorResult(err);
|
|
343
390
|
}
|
|
344
391
|
});
|
|
345
392
|
// ─── Tool 6: get_ticket_link ──────────────────────────────────────────
|
|
346
|
-
server.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
393
|
+
server.registerTool('get_ticket_link', {
|
|
394
|
+
title: 'Get Ticket Link',
|
|
395
|
+
description: 'Get a direct purchase link to buy a train ticket on SBB.ch. Only call this when the user wants to buy a specific ticket. On mobile with SBB app installed, opens directly in the app with Halbtax/GA applied automatically. Pass traveler_names for family tickets when connected to SwissTrip.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
trip_id: z.string().describe('Trip ID to purchase'),
|
|
398
|
+
from_name: z.string().describe('Origin station name (e.g. "Zürich HB")'),
|
|
399
|
+
from_id: z.string().describe('Origin station ID (e.g. "8503000")'),
|
|
400
|
+
to_name: z.string().describe('Destination station name (e.g. "Bern")'),
|
|
401
|
+
to_id: z.string().describe('Destination station ID (e.g. "8507000")'),
|
|
402
|
+
date: z.string().describe('Travel date YYYY-MM-DD'),
|
|
403
|
+
time: z.string().describe('Departure time HH:MM'),
|
|
404
|
+
traveler_type: z.enum(['ADULT', 'CHILD']).default('ADULT').describe('Traveler type (used when no traveler_names given)'),
|
|
405
|
+
reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).default('HALF_FARE').describe('Swiss reduction card (used when no traveler_names given)'),
|
|
406
|
+
traveler_names: z.array(z.string()).optional().describe('SwissTrip traveler names for family tickets. Requires SWISSTRIP_TOKEN.'),
|
|
407
|
+
},
|
|
408
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
409
|
+
_meta: widgetToolMeta(WIDGETS.TICKET_CARD, 'Preparing ticket link…', 'Ticket link ready'),
|
|
410
|
+
}, async ({ trip_id, from_name, from_id, to_name, to_id, date, time, traveler_type, reduction_card, traveler_names }) => {
|
|
358
411
|
try {
|
|
359
412
|
const sbbLink = buildSbbDeepLink({
|
|
360
413
|
fromName: from_name,
|
|
@@ -364,7 +417,6 @@ export function createSbbMcpServer() {
|
|
|
364
417
|
date,
|
|
365
418
|
time,
|
|
366
419
|
});
|
|
367
|
-
// Build travelers — cloud multi-traveler or single manual
|
|
368
420
|
let travelers;
|
|
369
421
|
if (traveler_names && traveler_names.length > 0 && useCloud) {
|
|
370
422
|
const allTravelers = await fetchTravelers();
|
|
@@ -376,7 +428,6 @@ export function createSbbMcpServer() {
|
|
|
376
428
|
else {
|
|
377
429
|
travelers = [{ id: 'traveler-1', type: traveler_type, reductionCard: reduction_card }];
|
|
378
430
|
}
|
|
379
|
-
// Try to get affiliate deep link from SMAPI (if live)
|
|
380
431
|
let affiliateLink;
|
|
381
432
|
if (!useMock) {
|
|
382
433
|
try {
|
|
@@ -384,10 +435,25 @@ export function createSbbMcpServer() {
|
|
|
384
435
|
affiliateLink = result.affiliateDeepLink;
|
|
385
436
|
}
|
|
386
437
|
catch {
|
|
387
|
-
//
|
|
438
|
+
// Non-fatal
|
|
388
439
|
}
|
|
389
440
|
}
|
|
390
|
-
|
|
441
|
+
const depIso = zurichIso(date, time);
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: 'text', text: formatTicketLink(trip_id, affiliateLink, sbbLink) }],
|
|
444
|
+
structuredContent: toTicketCardDTO({
|
|
445
|
+
tripId: trip_id,
|
|
446
|
+
fromName: from_name,
|
|
447
|
+
fromId: from_id,
|
|
448
|
+
toName: to_name,
|
|
449
|
+
toId: to_id,
|
|
450
|
+
departureTime: depIso,
|
|
451
|
+
arrivalTime: depIso,
|
|
452
|
+
primaryLink: sbbLink,
|
|
453
|
+
affiliateLink,
|
|
454
|
+
}),
|
|
455
|
+
_meta: widgetResponseMeta(WIDGETS.TICKET_CARD),
|
|
456
|
+
};
|
|
391
457
|
}
|
|
392
458
|
catch (err) {
|
|
393
459
|
return errorResult(err);
|