sbb-mcp 0.1.1 → 0.1.3

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 CHANGED
@@ -1,21 +1,57 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 SwissTrip
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ # Functional Source License, Version 1.1, MIT Future License
2
+
3
+ ## Abbreviation
4
+ FSL-1.1-MIT
5
+
6
+ ## Notice
7
+ Copyright 2026 SwissTrip (F. Weinhappl)
8
+
9
+ ## Terms and Conditions
10
+
11
+ ### Licensor ("We")
12
+ The party offering the Software under these Terms and Conditions.
13
+
14
+ ### The Software
15
+ The "Software" is each version of the software that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software.
16
+
17
+ ### License Grant
18
+ Subject to your compliance with this License Grant and the Patents, Redistribution and Trademark clauses below, we hereby grant you the right to use, copy, modify, create derivative works, publicly perform, publicly display and redistribute the Software for any Permitted Purpose identified below.
19
+
20
+ ### Permitted Purpose
21
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use means making the Software available to others in a commercial product or service that:
22
+
23
+ 1. substitutes for the Software;
24
+ 2. substitutes for any other product or service we offer using the Software that exists as of the date we make the Software available; or
25
+ 3. offers the same or substantially similar functionality as the Software.
26
+
27
+ Permitted Purposes specifically include using the Software:
28
+
29
+ 1. for your internal use and access;
30
+ 2. for non-commercial education;
31
+ 3. for non-commercial research; and
32
+ 4. in connection with professional services that you provide to a licensee using the Software in accordance with these Terms and Conditions.
33
+
34
+ ### Patents
35
+ To the extent your use for a Permitted Purpose would necessarily infringe our patents, the license grant above includes a license under our patents. If you make a claim against any party that the Software infringes or contributes to the infringement of any patent, then your patent license to the Software ends immediately.
36
+
37
+ ### Redistribution
38
+ The Terms and Conditions apply to all copies, modifications and derivatives of the Software.
39
+
40
+ If you redistribute any copies, modifications or derivatives of the Software, you must include a copy of or a link to these Terms and Conditions and not remove any copyright notices provided in or with the Software.
41
+
42
+ ### Disclaimer
43
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
44
+
45
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
46
+
47
+ ### Trademarks
48
+ Except for displaying the License Details and identifying us as the origin of the Software, you have no right under these Terms and Conditions to use our trademarks, trade names, service marks or product names.
49
+
50
+ ## Grant of Future License
51
+ We hereby irrevocably grant you an additional license to use the Software under the MIT license that is effective on the second anniversary of the date we make the Software available. On or after that date, you may use the Software under the MIT license, in which case the following will apply:
52
+
53
+ 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:
54
+
55
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
56
+
57
+ 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.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # sbb-mcp
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/sbb-mcp.svg)](https://www.npmjs.com/package/sbb-mcp)
4
+ [![License: FSL-1.1-MIT](https://img.shields.io/badge/License-FSL--1.1--MIT-blue.svg)](https://fsl.software/)
5
+ [![smithery badge](https://smithery.ai/badge/fabsforward2-zhoi/sbb-mcp)](https://smithery.ai/servers/fabsforward2-zhoi/sbb-mcp)
6
+
3
7
  MCP server for **Swiss Federal Railways** (SBB/CFF/FFS) -- real-time train schedules, prices, and ticket purchase links for any AI assistant.
4
8
 
5
- Works with Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot, and any MCP-compatible AI client.
9
+ Works with Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot, ChatGPT, and any MCP-compatible AI client.
6
10
 
7
11
  ## Features
8
12
 
@@ -12,6 +16,7 @@ Works with Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot, and a
12
16
  - **Purchase links** -- direct deep links to buy tickets on SBB.ch
13
17
  - **Pagination** -- browse earlier/later connections
14
18
  - **Trip details** -- intermediate stops, occupancy, platform changes
19
+ - **Journey planning** -- built-in prompt for end-to-end trip planning
15
20
  - **Mock mode** -- works without credentials for testing and demos
16
21
 
17
22
  ## Quick Start
@@ -106,6 +111,14 @@ Add to `.vscode/mcp.json`:
106
111
  }
107
112
  ```
108
113
 
114
+ ### Smithery
115
+
116
+ Install via [Smithery](https://smithery.ai/servers/fabsforward2-zhoi/sbb-mcp):
117
+
118
+ ```bash
119
+ npx @smithery/cli mcp add fabsforward2-zhoi/sbb-mcp
120
+ ```
121
+
109
122
  ### Demo Mode (No Credentials)
110
123
 
111
124
  Run without any environment variables to use mock data:
@@ -120,7 +133,7 @@ This lets you test the MCP server with realistic Swiss station data without SBB
120
133
 
121
134
  ### search_stations
122
135
 
123
- Search for Swiss train stations by name.
136
+ Search for Swiss train stations by name. Returns station IDs needed for other tools.
124
137
 
125
138
  **Input:**
126
139
  - `query` (string, required) -- Station name, e.g. "Zurich", "Bern", "Interlaken"
@@ -130,10 +143,10 @@ Search for Swiss train stations by name.
130
143
 
131
144
  ### search_connections
132
145
 
133
- Find train connections between two stations.
146
+ Find train connections between two stations. Automatically resolves station names to IDs.
134
147
 
135
148
  **Input:**
136
- - `from` (string, required) -- Origin station name or ID
149
+ - `from` (string, required) -- Origin station name or ID (e.g. "Zurich HB" or "8503000")
137
150
  - `to` (string, required) -- Destination station name or ID
138
151
  - `date` (string, optional) -- Travel date YYYY-MM-DD
139
152
  - `time` (string, optional) -- Departure time HH:MM
@@ -143,7 +156,7 @@ Find train connections between two stations.
143
156
 
144
157
  ### get_trip_details
145
158
 
146
- Get detailed stop-by-stop information for a connection.
159
+ Get detailed stop-by-stop information for a connection including intermediate stops, platforms, and occupancy forecasts.
147
160
 
148
161
  **Input:**
149
162
  - `trip_id` (string, required) -- Trip ID from search_connections
@@ -158,7 +171,7 @@ Load earlier or later trains for a previous search.
158
171
 
159
172
  ### get_prices
160
173
 
161
- Get ticket prices with reduction card support.
174
+ Get ticket prices with Swiss reduction card support.
162
175
 
163
176
  **Input:**
164
177
  - `trip_ids` (string[], required) -- Trip IDs from search_connections
@@ -169,13 +182,24 @@ Get ticket prices with reduction card support.
169
182
 
170
183
  ### get_ticket_link
171
184
 
172
- Get a direct purchase link to buy the ticket on SBB.ch.
185
+ Get a direct purchase link to buy the ticket on SBB.ch. Only call when the user wants to buy.
173
186
 
174
187
  **Input:**
175
188
  - `trip_id` (string, required) -- Trip ID to purchase
176
189
  - `traveler_type` ("ADULT" | "CHILD", default: "ADULT")
177
190
  - `reduction_card` ("HALF_FARE" | "GA" | "NONE", default: "HALF_FARE")
178
191
 
192
+ ## Prompts
193
+
194
+ ### plan_journey
195
+
196
+ End-to-end journey planning prompt. Searches connections, compares prices, and provides ticket links.
197
+
198
+ **Input:**
199
+ - `from` (string, required) -- Origin station
200
+ - `to` (string, required) -- Destination station
201
+ - `date` (string, optional) -- Travel date
202
+
179
203
  ## Environment Variables
180
204
 
181
205
  | Variable | Required | Description |
@@ -189,6 +213,14 @@ Get a direct purchase link to buy the ticket on SBB.ch.
189
213
 
190
214
  *Without credentials, the server runs in mock mode with realistic demo data.
191
215
 
216
+ ## Available on
217
+
218
+ - [npm](https://www.npmjs.com/package/sbb-mcp)
219
+ - [Official MCP Registry](https://registry.modelcontextprotocol.io)
220
+ - [Smithery](https://smithery.ai/servers/fabsforward2-zhoi/sbb-mcp)
221
+
222
+ Also available as: [sbb-mcp-official](https://www.npmjs.com/package/sbb-mcp-official) | [swiss-rail-mcp](https://www.npmjs.com/package/swiss-rail-mcp) | [sbb-cff-ffs-mcp](https://www.npmjs.com/package/sbb-cff-ffs-mcp) | [swiss-train-mcp](https://www.npmjs.com/package/swiss-train-mcp) | [swiss-railways-mcp](https://www.npmjs.com/package/swiss-railways-mcp)
223
+
192
224
  ## About
193
225
 
194
226
  Built on the official SBB Swiss Mobility API (SMAPI) with OSDM-compliant journey planning and pricing. Covers the entire Swiss public transport network including SBB, BLS, SOB, and regional operators.
@@ -197,4 +229,4 @@ Built on the official SBB Swiss Mobility API (SMAPI) with OSDM-compliant journey
197
229
 
198
230
  ## License
199
231
 
200
- MIT
232
+ [FSL-1.1-MIT](https://fsl.software/) -- Functional Source License. Free to use for any non-competing purpose. Converts to MIT after 2 years. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Simple in-memory TTL cache with LRU eviction to reduce SMAPI calls.
3
+ * Station searches cached 24h, connections cached 5min, prices cached 2min.
4
+ */
5
+ export declare function cacheGet<T>(key: string): T | undefined;
6
+ export declare function cacheSet<T>(key: string, data: T, ttlMs: number): void;
7
+ export declare const TTL: {
8
+ readonly STATIONS: number;
9
+ readonly CONNECTIONS: number;
10
+ readonly PRICES: number;
11
+ readonly TRIP_DETAILS: number;
12
+ };
package/dist/cache.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Simple in-memory TTL cache with LRU eviction to reduce SMAPI calls.
3
+ * Station searches cached 24h, connections cached 5min, prices cached 2min.
4
+ */
5
+ const store = new Map();
6
+ const MAX_ENTRIES = 10000;
7
+ function evictExpired() {
8
+ const now = Date.now();
9
+ let evicted = 0;
10
+ for (const [key, entry] of store) {
11
+ if (now >= entry.expiresAt) {
12
+ store.delete(key);
13
+ evicted++;
14
+ }
15
+ }
16
+ return evicted;
17
+ }
18
+ function evictOldest(count) {
19
+ // Map iteration order is insertion order — oldest first
20
+ let removed = 0;
21
+ for (const key of store.keys()) {
22
+ if (removed >= count)
23
+ break;
24
+ store.delete(key);
25
+ removed++;
26
+ }
27
+ }
28
+ export function cacheGet(key) {
29
+ const entry = store.get(key);
30
+ if (!entry)
31
+ return undefined;
32
+ if (Date.now() >= entry.expiresAt) {
33
+ store.delete(key);
34
+ return undefined;
35
+ }
36
+ // Move to end for LRU (re-insert refreshes position)
37
+ store.delete(key);
38
+ store.set(key, entry);
39
+ return entry.data;
40
+ }
41
+ export function cacheSet(key, data, ttlMs) {
42
+ if (store.size >= MAX_ENTRIES) {
43
+ const evicted = evictExpired();
44
+ // If expired eviction didn't free enough, drop oldest 10%
45
+ if (evicted < MAX_ENTRIES * 0.1) {
46
+ evictOldest(Math.floor(MAX_ENTRIES * 0.1));
47
+ }
48
+ }
49
+ store.set(key, { data, expiresAt: Date.now() + ttlMs });
50
+ }
51
+ // TTLs
52
+ export const TTL = {
53
+ STATIONS: 24 * 60 * 60 * 1000, // 24 hours
54
+ CONNECTIONS: 5 * 60 * 1000, // 5 minutes
55
+ PRICES: 2 * 60 * 1000, // 2 minutes
56
+ TRIP_DETAILS: 5 * 60 * 1000, // 5 minutes
57
+ };
58
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAA;AAEpD,MAAM,WAAW,GAAG,KAAK,CAAA;AAEzB,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;QACjC,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACjB,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,wDAAwD;IACxD,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/B,IAAI,OAAO,IAAI,KAAK;YAAE,MAAK;QAC3B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACjB,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAI,GAAW;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAClC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACjB,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,qDAAqD;IACrD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACjB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IACrB,OAAO,KAAK,CAAC,IAAS,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAI,GAAW,EAAE,IAAO,EAAE,KAAa;IAC7D,IAAI,KAAK,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,YAAY,EAAE,CAAA;QAC9B,0DAA0D;QAC1D,IAAI,OAAO,GAAG,WAAW,GAAG,GAAG,EAAE,CAAC;YAChC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;AACzD,CAAC;AAED,OAAO;AACP,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAK,WAAW;IAC7C,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAS,YAAY;IAC/C,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAc,YAAY;IAC/C,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAQ,YAAY;CACvC,CAAA"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/http.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
4
+ import { isSmapiConfigured } from './transport/index.js';
5
+ import { createSbbMcpServer } from './tools.js';
6
+ import { isRateLimited } from './rate-limit.js';
7
+ const useMock = !isSmapiConfigured();
8
+ async function main() {
9
+ const port = parseInt(process.env.PORT || '3001');
10
+ const app = createMcpExpressApp({ host: '0.0.0.0' });
11
+ app.post('/mcp', async (req, res) => {
12
+ const ip = req.ip || req.socket.remoteAddress || 'unknown';
13
+ if (isRateLimited(ip)) {
14
+ res.status(429).json({ error: 'Too many requests. Max 60 per minute.' });
15
+ return;
16
+ }
17
+ const server = createSbbMcpServer();
18
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
19
+ try {
20
+ await server.connect(transport);
21
+ await transport.handleRequest(req, res, req.body);
22
+ }
23
+ finally {
24
+ await transport.close();
25
+ await server.close();
26
+ }
27
+ });
28
+ app.get('/health', (_req, res) => {
29
+ res.json({ status: 'ok', mode: useMock ? 'mock' : 'live' });
30
+ });
31
+ app.get('/.well-known/mcp/server-card.json', (_req, res) => {
32
+ res.json({
33
+ name: 'sbb-mcp',
34
+ description: 'Swiss Federal Railways (SBB/CFF/FFS) — train schedules, prices, and ticket links',
35
+ homepage: 'https://github.com/Fabsbags/sbb-mcp',
36
+ configSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ SMAPI_CLIENT_ID: { type: 'string', description: 'SBB SMAPI OAuth 2.0 client ID' },
40
+ SMAPI_CLIENT_SECRET: { type: 'string', description: 'SBB SMAPI OAuth 2.0 client secret' },
41
+ SMAPI_SCOPE: { type: 'string', description: 'SBB SMAPI OAuth scope' },
42
+ SMAPI_CONTRACT_ID: { type: 'string', description: 'SBB business contract ID' },
43
+ },
44
+ },
45
+ });
46
+ });
47
+ app.listen(port, '0.0.0.0', () => {
48
+ console.log(`[sbb-mcp] HTTP server listening on port ${port}`);
49
+ console.log(`[sbb-mcp] MCP endpoint: http://0.0.0.0:${port}/mcp`);
50
+ console.log(`[sbb-mcp] Mode: ${useMock ? 'MOCK' : 'LIVE'}`);
51
+ console.log(`[sbb-mcp] Rate limit: 60 req/min per IP`);
52
+ });
53
+ }
54
+ main().catch((err) => {
55
+ console.error('[sbb-mcp] Fatal error:', err);
56
+ process.exit(1);
57
+ });
58
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAA;AAClG,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAA;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAE/C,MAAM,OAAO,GAAG,CAAC,iBAAiB,EAAE,CAAA;AAEpC,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAC,CAAA;IACjD,MAAM,GAAG,GAAG,mBAAmB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;IAEpD,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAA;QAC1D,IAAI,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;YACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uCAAuC,EAAE,CAAC,CAAA;YACxE,OAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;QACnC,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC,EAAE,kBAAkB,EAAE,SAAS,EAAE,CAAC,CAAA;QACtF,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;YAC/B,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;QACnD,CAAC;gBAAS,CAAC;YACT,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;YACvB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACtB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,GAAG,CAAC,mCAAmC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACzD,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,kFAAkF;YAC/F,QAAQ,EAAE,qCAAqC;YAC/C,YAAY,EAAE;gBACZ,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,eAAe,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;oBACjF,mBAAmB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,mCAAmC,EAAE;oBACzF,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,uBAAuB,EAAE;oBACrE,iBAAiB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE;iBAC/E;aACF;SACF,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;QAC/B,OAAO,CAAC,GAAG,CAAC,2CAA2C,IAAI,EAAE,CAAC,CAAA;QAC9D,OAAO,CAAC,GAAG,CAAC,0CAA0C,IAAI,MAAM,CAAC,CAAA;QACjE,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;QAC3D,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAA;IACxD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAA;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
package/dist/index.js CHANGED
@@ -1,153 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { z } from 'zod';
5
- import { isSmapiConfigured, searchPlaces, searchTrips, getTrip, paginateTrips, getTripPrices, getTripOffers, mockSearchPlaces, mockSearchTrips, mockGetTripPrices, mockGetTripOffers, } from './transport/index.js';
6
- import { formatStations, formatConnections, formatTripDetails, formatPrices, formatTicketLink, } from './formatters.js';
7
- const useMock = !isSmapiConfigured();
8
- const server = new McpServer({
9
- name: 'sbb-mcp',
10
- version: '0.1.0',
11
- description: 'Swiss Federal Railways (SBB/CFF/FFS) — real-time train schedules, prices, and ticket purchase links for Switzerland',
12
- });
13
- // ─── Tool 1: search_stations ────────────────────────────────────────────────
14
- server.tool('search_stations', 'Search for Swiss train stations, addresses, or points of interest by name. Returns station IDs needed for other tools.', {
15
- query: z.string().describe('Station name to search for (e.g. "Zurich", "Bern", "Interlaken")'),
16
- limit: z.number().min(1).max(20).default(10).describe('Maximum number of results'),
17
- }, async ({ query, limit }) => {
18
- const stations = useMock
19
- ? await mockSearchPlaces({ name: query })
20
- : await searchPlaces({ name: query, type: 'STOP', numberOfResults: limit });
21
- return { content: [{ type: 'text', text: formatStations(stations) }] };
22
- });
23
- // ─── Tool 2: search_connections ─────────────────────────────────────────────
24
- server.tool('search_connections', 'Find train connections between two Swiss stations. Returns schedules with departure/arrival times, duration, transfers, and trip IDs for pricing.', {
25
- from: z.string().describe('Origin station name or ID (e.g. "Zurich HB" or "8503000")'),
26
- to: z.string().describe('Destination station name or ID (e.g. "Bern" or "8507000")'),
27
- date: z.string().optional().describe('Travel date in YYYY-MM-DD format (default: today)'),
28
- time: z.string().optional().describe('Departure time in HH:MM format (default: now)'),
29
- arrival_time: z.boolean().optional().describe('If true, the time parameter is treated as desired arrival time'),
30
- }, async ({ from, to, date, time, arrival_time }) => {
31
- // Resolve station names to IDs if needed
32
- let fromId = from;
33
- let toId = to;
34
- if (!/^\d{7}$/.test(from)) {
35
- const stations = useMock
36
- ? await mockSearchPlaces({ name: from })
37
- : await searchPlaces({ name: from, type: 'STOP', numberOfResults: 1 });
38
- if (stations.length === 0) {
39
- return { content: [{ type: 'text', text: `No station found matching "${from}". Try a different name.` }] };
40
- }
41
- fromId = stations[0].id;
42
- }
43
- if (!/^\d{7}$/.test(to)) {
44
- const stations = useMock
45
- ? await mockSearchPlaces({ name: to })
46
- : await searchPlaces({ name: to, type: 'STOP', numberOfResults: 1 });
47
- if (stations.length === 0) {
48
- return { content: [{ type: 'text', text: `No station found matching "${to}". Try a different name.` }] };
49
- }
50
- toId = stations[0].id;
51
- }
52
- // Build datetime
53
- let departureTime;
54
- let arrivalTime;
55
- if (date || time) {
56
- const d = date || new Date().toISOString().split('T')[0];
57
- const t = time || '08:00';
58
- const dt = new Date(`${d}T${t}:00+02:00`).toISOString();
59
- if (arrival_time) {
60
- arrivalTime = dt;
61
- }
62
- else {
63
- departureTime = dt;
64
- }
65
- }
66
- const collection = useMock
67
- ? await mockSearchTrips({ origin: fromId, destination: toId, departureTime })
68
- : await searchTrips({
69
- origin: fromId,
70
- destination: toId,
71
- ...(departureTime && { departureTime }),
72
- ...(arrivalTime && { arrivalTime }),
73
- });
74
- return { content: [{ type: 'text', text: formatConnections(collection) }] };
75
- });
76
- // ─── Tool 3: get_trip_details ───────────────────────────────────────────────
77
- server.tool('get_trip_details', 'Get detailed information about a specific train connection including all intermediate stops, platforms, and occupancy. Use a trip ID from search_connections results.', {
78
- trip_id: z.string().describe('Trip ID from search_connections results'),
79
- }, async ({ trip_id }) => {
80
- if (useMock) {
81
- return {
82
- content: [{
83
- type: 'text',
84
- text: `Detailed trip information is available with live API access. In mock mode, use search_connections to see trip summaries.`,
85
- }],
86
- };
87
- }
88
- const trip = await getTrip(trip_id, 'REAL_BOARDING_ALIGHTING');
89
- return { content: [{ type: 'text', text: formatTripDetails(trip) }] };
90
- });
91
- // ─── Tool 4: get_more_connections ───────────────────────────────────────────
92
- server.tool('get_more_connections', 'Load earlier or later train connections for a previous search. Use the collection ID from search_connections results.', {
93
- collection_id: z.string().describe('Collection ID from search_connections results'),
94
- direction: z.enum(['next', 'previous']).describe('"next" for later trains, "previous" for earlier trains'),
95
- }, async ({ collection_id, direction }) => {
96
- if (useMock) {
97
- // Generate new mock trips with offset times
98
- const offset = direction === 'next' ? 3 * 60 * 60 * 1000 : -3 * 60 * 60 * 1000;
99
- const baseTime = new Date(Date.now() + offset).toISOString();
100
- const collection = await mockSearchTrips({
101
- origin: '8503000',
102
- destination: '8507000',
103
- departureTime: baseTime,
104
- });
105
- return { content: [{ type: 'text', text: formatConnections(collection) }] };
106
- }
107
- const collection = await paginateTrips(collection_id, direction);
108
- return { content: [{ type: 'text', text: formatConnections(collection) }] };
109
- });
110
- // ─── Tool 5: get_prices ────────────────────────────────────────────────────
111
- server.tool('get_prices', 'Get ticket prices for one or more train connections. Supports Half-Fare card (Halbtax) and GA travelcard discounts.', {
112
- trip_ids: z.array(z.string()).min(1).max(10).describe('Trip IDs from search_connections results'),
113
- traveler_type: z.enum(['ADULT', 'CHILD']).default('ADULT').describe('Traveler type'),
114
- reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).default('HALF_FARE').describe('Swiss reduction card (most travelers have Half-Fare)'),
115
- }, async ({ trip_ids, traveler_type, reduction_card }) => {
116
- if (useMock) {
117
- const prices = await mockGetTripPrices(trip_ids);
118
- return { content: [{ type: 'text', text: formatPrices(prices) }] };
119
- }
120
- const travelers = [{
121
- id: 'traveler-1',
122
- type: traveler_type,
123
- reductionCard: reduction_card,
124
- }];
125
- const prices = await getTripPrices(trip_ids, travelers);
126
- return { content: [{ type: 'text', text: formatPrices(prices) }] };
127
- });
128
- // ─── Tool 6: get_ticket_link ───────────────────────────────────────────────
129
- server.tool('get_ticket_link', '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.', {
130
- trip_id: z.string().describe('Trip ID to purchase'),
131
- traveler_type: z.enum(['ADULT', 'CHILD']).default('ADULT').describe('Traveler type'),
132
- reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).default('HALF_FARE').describe('Swiss reduction card'),
133
- }, async ({ trip_id, traveler_type, reduction_card }) => {
134
- const travelers = [{
135
- id: 'traveler-1',
136
- type: traveler_type,
137
- reductionCard: reduction_card,
138
- }];
139
- if (useMock) {
140
- const result = await mockGetTripOffers(trip_id, travelers);
141
- return { content: [{ type: 'text', text: formatTicketLink(trip_id, result.affiliateDeepLink) }] };
142
- }
143
- const result = await getTripOffers(trip_id, travelers);
144
- return { content: [{ type: 'text', text: formatTicketLink(trip_id, result.affiliateDeepLink) }] };
145
- });
146
- // ─── Start server ───────────────────────────────────────────────────────────
3
+ import { isSmapiConfigured } from './transport/index.js';
4
+ import { createSbbMcpServer } from './tools.js';
147
5
  async function main() {
6
+ const server = createSbbMcpServer();
148
7
  const transport = new StdioServerTransport();
149
8
  await server.connect(transport);
150
- if (useMock) {
9
+ if (!isSmapiConfigured()) {
151
10
  console.error('[sbb-mcp] Running in MOCK mode (no SMAPI credentials configured)');
152
11
  console.error('[sbb-mcp] Set SMAPI_CLIENT_ID, SMAPI_CLIENT_SECRET, SMAPI_SCOPE for live data');
153
12
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;IACnC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAA;IAC5C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAE/B,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAA;QACjF,OAAO,CAAC,KAAK,CAAC,+EAA+E,CAAC,CAAA;IAChG,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACnD,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAA;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Simple in-memory rate limiter per IP.
3
+ * 60 requests per minute per IP. Max 50k tracked IPs.
4
+ */
5
+ export declare function isRateLimited(ip: string): boolean;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Simple in-memory rate limiter per IP.
3
+ * 60 requests per minute per IP. Max 50k tracked IPs.
4
+ */
5
+ const requests = new Map();
6
+ const WINDOW_MS = 60 * 1000; // 1 minute
7
+ const MAX_REQUESTS = 60; // per window
8
+ const MAX_IPS = 50000; // max tracked IPs
9
+ export function isRateLimited(ip) {
10
+ const now = Date.now();
11
+ const entry = requests.get(ip);
12
+ if (!entry || now >= entry.resetAt) {
13
+ // Evict if map is too large
14
+ if (requests.size >= MAX_IPS) {
15
+ for (const [k, v] of requests) {
16
+ if (now >= v.resetAt)
17
+ requests.delete(k);
18
+ }
19
+ // If still too large after cleanup, drop oldest entries
20
+ if (requests.size >= MAX_IPS) {
21
+ let toDrop = Math.floor(MAX_IPS * 0.1);
22
+ for (const k of requests.keys()) {
23
+ if (toDrop-- <= 0)
24
+ break;
25
+ requests.delete(k);
26
+ }
27
+ }
28
+ }
29
+ requests.set(ip, { count: 1, resetAt: now + WINDOW_MS });
30
+ return false;
31
+ }
32
+ entry.count++;
33
+ return entry.count > MAX_REQUESTS;
34
+ }
35
+ // Cleanup old entries every 5 minutes
36
+ setInterval(() => {
37
+ const now = Date.now();
38
+ for (const [ip, entry] of requests) {
39
+ if (now >= entry.resetAt) {
40
+ requests.delete(ip);
41
+ }
42
+ }
43
+ }, 5 * 60 * 1000).unref();
44
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA8C,CAAA;AAEtE,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,CAAA,CAAE,WAAW;AACxC,MAAM,YAAY,GAAG,EAAE,CAAA,CAAO,aAAa;AAC3C,MAAM,OAAO,GAAG,KAAK,CAAA,CAAS,kBAAkB;AAEhD,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAE9B,IAAI,CAAC,KAAK,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,4BAA4B;QAC5B,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;YAC7B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC;gBAC9B,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;oBAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YAC1C,CAAC;YACD,wDAAwD;YACxD,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,CAAA;gBACtC,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;oBAChC,IAAI,MAAM,EAAE,IAAI,CAAC;wBAAE,MAAK;oBACxB,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;gBACpB,CAAC;YACH,CAAC;QACH,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,SAAS,EAAE,CAAC,CAAA;QACxD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,KAAK,CAAC,KAAK,EAAE,CAAA;IACb,OAAO,KAAK,CAAC,KAAK,GAAG,YAAY,CAAA;AACnC,CAAC;AAED,sCAAsC;AACtC,WAAW,CAAC,GAAG,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;QACnC,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;AACH,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,EAAE,CAAA"}
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createSbbMcpServer(): McpServer;
package/dist/tools.js ADDED
@@ -0,0 +1,215 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { isSmapiConfigured, searchPlaces, searchTrips, getTrip, paginateTrips, getTripPrices, getTripOffers, mockSearchPlaces, mockSearchTrips, mockGetTripPrices, mockGetTripOffers, } from './transport/index.js';
4
+ import { formatStations, formatConnections, formatTripDetails, formatPrices, formatTicketLink, } from './formatters.js';
5
+ import { cacheGet, cacheSet, TTL } from './cache.js';
6
+ import { SmapiError } from './transport/smapi-client.js';
7
+ const useMock = !isSmapiConfigured();
8
+ function errorResult(err) {
9
+ if (err instanceof SmapiError) {
10
+ const msgs = err.displayMessages;
11
+ const detail = msgs.length ? msgs.join('. ') : err.message;
12
+ return { content: [{ type: 'text', text: `Error: ${detail} (HTTP ${err.status})` }], isError: true };
13
+ }
14
+ const message = err instanceof Error ? err.message : String(err);
15
+ return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
16
+ }
17
+ export function createSbbMcpServer() {
18
+ const server = new McpServer({
19
+ name: 'sbb-mcp',
20
+ version: '0.1.2',
21
+ description: 'Swiss Federal Railways (SBB/CFF/FFS) — real-time train schedules, prices, and ticket purchase links for Switzerland',
22
+ });
23
+ // ─── Prompts ───────────────────────────────────────────────────────────
24
+ server.prompt('plan_journey', 'Plan a train journey in Switzerland — finds connections, compares prices, and provides ticket links', {
25
+ from: z.string().describe('Origin station (e.g. "Zurich")'),
26
+ to: z.string().describe('Destination station (e.g. "Bern")'),
27
+ date: z.string().optional().describe('Travel date YYYY-MM-DD'),
28
+ }, async ({ from, to, date }) => ({
29
+ messages: [{
30
+ role: 'user',
31
+ content: {
32
+ type: 'text',
33
+ text: `Plan a train journey from ${from} to ${to}${date ? ` on ${date}` : ' today'}. Search for connections, get prices with Half-Fare card, and provide a ticket purchase link for the best option.`,
34
+ },
35
+ }],
36
+ }));
37
+ // ─── Tool 1: search_stations ──────────────────────────────────────────
38
+ server.tool('search_stations', 'Search for Swiss train stations, addresses, or points of interest by name. Returns station IDs needed for other tools.', {
39
+ query: z.string().describe('Station name to search for (e.g. "Zurich", "Bern", "Interlaken")'),
40
+ limit: z.number().min(1).max(20).default(10).describe('Maximum number of results'),
41
+ }, { title: 'Search Stations', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query, limit }) => {
42
+ try {
43
+ const cacheKey = `stations:${query.toLowerCase()}:${limit}`;
44
+ const cached = cacheGet(cacheKey);
45
+ if (cached)
46
+ return { content: [{ type: 'text', text: cached }] };
47
+ const stations = useMock
48
+ ? await mockSearchPlaces({ name: query })
49
+ : await searchPlaces({ name: query, type: 'STOP', numberOfResults: limit });
50
+ const text = formatStations(stations);
51
+ cacheSet(cacheKey, text, TTL.STATIONS);
52
+ return { content: [{ type: 'text', text }] };
53
+ }
54
+ catch (err) {
55
+ return errorResult(err);
56
+ }
57
+ });
58
+ // ─── Tool 2: search_connections ───────────────────────────────────────
59
+ server.tool('search_connections', 'Find train connections between two Swiss stations. Returns schedules with departure/arrival times, duration, transfers, and trip IDs for pricing.', {
60
+ from: z.string().describe('Origin station name or ID (e.g. "Zurich HB" or "8503000")'),
61
+ to: z.string().describe('Destination station name or ID (e.g. "Bern" or "8507000")'),
62
+ date: z.string().optional().describe('Travel date in YYYY-MM-DD format (default: today)'),
63
+ time: z.string().optional().describe('Departure time in HH:MM format (default: now)'),
64
+ arrival_time: z.boolean().optional().describe('If true, the time parameter is treated as desired arrival time'),
65
+ }, { title: 'Search Connections', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ from, to, date, time, arrival_time }) => {
66
+ try {
67
+ let fromId = from;
68
+ let toId = to;
69
+ if (!/^\d{5,8}$/.test(from)) {
70
+ const stations = useMock
71
+ ? await mockSearchPlaces({ name: from })
72
+ : await searchPlaces({ name: from, type: 'STOP', numberOfResults: 1 });
73
+ if (stations.length === 0) {
74
+ return { content: [{ type: 'text', text: `No station found matching "${from}". Try a different name.` }] };
75
+ }
76
+ fromId = stations[0].id;
77
+ }
78
+ if (!/^\d{5,8}$/.test(to)) {
79
+ const stations = useMock
80
+ ? await mockSearchPlaces({ name: to })
81
+ : await searchPlaces({ name: to, type: 'STOP', numberOfResults: 1 });
82
+ if (stations.length === 0) {
83
+ return { content: [{ type: 'text', text: `No station found matching "${to}". Try a different name.` }] };
84
+ }
85
+ toId = stations[0].id;
86
+ }
87
+ let departureTime;
88
+ let arrivalTime;
89
+ if (date || time) {
90
+ const d = date || new Date().toISOString().split('T')[0];
91
+ const t = time || '08:00';
92
+ // Compute correct Europe/Zurich offset (CET +01:00 or CEST +02:00)
93
+ const probe = new Date(`${d}T${t}:00Z`);
94
+ const zurichStr = probe.toLocaleString('en-US', { timeZone: 'Europe/Zurich' });
95
+ const zurichTime = new Date(zurichStr + ' UTC');
96
+ const offsetMs = zurichTime.getTime() - probe.getTime();
97
+ const offsetHours = Math.round(offsetMs / (60 * 60 * 1000));
98
+ const offsetStr = `${offsetHours >= 0 ? '+' : '-'}${String(Math.abs(offsetHours)).padStart(2, '0')}:00`;
99
+ const dt = new Date(`${d}T${t}:00${offsetStr}`).toISOString();
100
+ if (arrival_time) {
101
+ arrivalTime = dt;
102
+ }
103
+ else {
104
+ departureTime = dt;
105
+ }
106
+ }
107
+ const cacheKey = `connections:${fromId}:${toId}:${departureTime || ''}:${arrivalTime || ''}`;
108
+ const cached = cacheGet(cacheKey);
109
+ if (cached)
110
+ return { content: [{ type: 'text', text: cached }] };
111
+ const collection = useMock
112
+ ? await mockSearchTrips({ origin: fromId, destination: toId, departureTime })
113
+ : await searchTrips({
114
+ origin: fromId,
115
+ destination: toId,
116
+ ...(departureTime && { departureTime }),
117
+ ...(arrivalTime && { arrivalTime }),
118
+ });
119
+ const text = formatConnections(collection);
120
+ cacheSet(cacheKey, text, TTL.CONNECTIONS);
121
+ return { content: [{ type: 'text', text }] };
122
+ }
123
+ catch (err) {
124
+ return errorResult(err);
125
+ }
126
+ });
127
+ // ─── Tool 3: get_trip_details ─────────────────────────────────────────
128
+ server.tool('get_trip_details', 'Get detailed information about a specific train connection including all intermediate stops, platforms, and occupancy. Use a trip ID from search_connections results.', {
129
+ trip_id: z.string().describe('Trip ID from search_connections results'),
130
+ }, { title: 'Get Trip Details', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ trip_id }) => {
131
+ try {
132
+ if (useMock) {
133
+ return { content: [{ type: 'text', text: 'Detailed trip information is available with live API access. In mock mode, use search_connections to see trip summaries.' }] };
134
+ }
135
+ const cacheKey = `trip:${trip_id}`;
136
+ const cached = cacheGet(cacheKey);
137
+ if (cached)
138
+ return { content: [{ type: 'text', text: cached }] };
139
+ const trip = await getTrip(trip_id, 'REAL_BOARDING_ALIGHTING');
140
+ const text = formatTripDetails(trip);
141
+ cacheSet(cacheKey, text, TTL.TRIP_DETAILS);
142
+ return { content: [{ type: 'text', text }] };
143
+ }
144
+ catch (err) {
145
+ return errorResult(err);
146
+ }
147
+ });
148
+ // ─── Tool 4: get_more_connections ─────────────────────────────────────
149
+ server.tool('get_more_connections', 'Load earlier or later train connections for a previous search. Use the collection ID from search_connections results.', {
150
+ collection_id: z.string().describe('Collection ID from search_connections results'),
151
+ direction: z.enum(['next', 'previous']).describe('"next" for later trains, "previous" for earlier trains'),
152
+ }, { title: 'Get More Connections', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ collection_id, direction }) => {
153
+ try {
154
+ if (useMock) {
155
+ const offset = direction === 'next' ? 3 * 60 * 60 * 1000 : -3 * 60 * 60 * 1000;
156
+ const baseTime = new Date(Date.now() + offset).toISOString();
157
+ const collection = await mockSearchTrips({ origin: '8503000', destination: '8507000', departureTime: baseTime });
158
+ return { content: [{ type: 'text', text: formatConnections(collection) }] };
159
+ }
160
+ const collection = await paginateTrips(collection_id, direction);
161
+ return { content: [{ type: 'text', text: formatConnections(collection) }] };
162
+ }
163
+ catch (err) {
164
+ return errorResult(err);
165
+ }
166
+ });
167
+ // ─── Tool 5: get_prices ───────────────────────────────────────────────
168
+ server.tool('get_prices', 'Get ticket prices for one or more train connections. Supports Half-Fare card (Halbtax) and GA travelcard discounts.', {
169
+ trip_ids: z.array(z.string()).min(1).max(10).describe('Trip IDs from search_connections results'),
170
+ traveler_type: z.enum(['ADULT', 'CHILD']).default('ADULT').describe('Traveler type'),
171
+ reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).default('HALF_FARE').describe('Swiss reduction card (most travelers have Half-Fare)'),
172
+ }, { title: 'Get Prices', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ trip_ids, traveler_type, reduction_card }) => {
173
+ try {
174
+ const cacheKey = `prices:${trip_ids.join(',')}:${traveler_type}:${reduction_card}`;
175
+ const cached = cacheGet(cacheKey);
176
+ if (cached)
177
+ return { content: [{ type: 'text', text: cached }] };
178
+ if (useMock) {
179
+ const prices = await mockGetTripPrices(trip_ids);
180
+ const text = formatPrices(prices);
181
+ cacheSet(cacheKey, text, TTL.PRICES);
182
+ return { content: [{ type: 'text', text }] };
183
+ }
184
+ const travelers = [{ id: 'traveler-1', type: traveler_type, reductionCard: reduction_card }];
185
+ const prices = await getTripPrices(trip_ids, travelers);
186
+ const text = formatPrices(prices);
187
+ cacheSet(cacheKey, text, TTL.PRICES);
188
+ return { content: [{ type: 'text', text }] };
189
+ }
190
+ catch (err) {
191
+ return errorResult(err);
192
+ }
193
+ });
194
+ // ─── Tool 6: get_ticket_link ──────────────────────────────────────────
195
+ server.tool('get_ticket_link', '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.', {
196
+ trip_id: z.string().describe('Trip ID to purchase'),
197
+ traveler_type: z.enum(['ADULT', 'CHILD']).default('ADULT').describe('Traveler type'),
198
+ reduction_card: z.enum(['HALF_FARE', 'GA', 'NONE']).default('HALF_FARE').describe('Swiss reduction card'),
199
+ }, { title: 'Get Ticket Link', readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ trip_id, traveler_type, reduction_card }) => {
200
+ try {
201
+ const travelers = [{ id: 'traveler-1', type: traveler_type, reductionCard: reduction_card }];
202
+ if (useMock) {
203
+ const result = await mockGetTripOffers(trip_id, travelers);
204
+ return { content: [{ type: 'text', text: formatTicketLink(trip_id, result.affiliateDeepLink) }] };
205
+ }
206
+ const result = await getTripOffers(trip_id, travelers);
207
+ return { content: [{ type: 'text', text: formatTicketLink(trip_id, result.affiliateDeepLink) }] };
208
+ }
209
+ catch (err) {
210
+ return errorResult(err);
211
+ }
212
+ });
213
+ return server;
214
+ }
215
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sbb-mcp",
3
- "version": "0.1.1",
4
- "mcpName": "io.github.fabsbags/sbb",
3
+ "version": "0.1.3",
4
+ "mcpName": "io.github.Fabsbags/sbb-mcp",
5
5
  "description": "MCP server for Swiss Federal Railways (SBB/CFF/FFS) — real-time train schedules, prices, and ticket purchase links for any AI assistant",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc -p tsconfig.build.json",
18
18
  "dev": "tsc -w -p tsconfig.build.json",
19
+ "start:http": "node dist/http.js",
19
20
  "test": "vitest run",
20
21
  "prepublishOnly": "npm run build"
21
22
  },
@@ -37,7 +38,7 @@
37
38
  "model-context-protocol"
38
39
  ],
39
40
  "author": "SwissTrip <fabsforward2@gmail.com>",
40
- "license": "MIT",
41
+ "license": "FSL-1.1-MIT",
41
42
  "repository": {
42
43
  "type": "git",
43
44
  "url": "https://github.com/Fabsbags/sbb-mcp"
@@ -51,9 +52,11 @@
51
52
  },
52
53
  "dependencies": {
53
54
  "@modelcontextprotocol/sdk": "^1.12.1",
55
+ "express": "^5.1.0",
54
56
  "zod": "^3.24.4"
55
57
  },
56
58
  "devDependencies": {
59
+ "@types/express": "^5.0.2",
57
60
  "@types/node": "^22.15.3",
58
61
  "typescript": "^5.8.3",
59
62
  "vitest": "^4.1.0"