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 +57 -21
- package/README.md +40 -8
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +58 -0
- package/dist/cache.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +58 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +4 -145
- package/dist/index.js.map +1 -1
- package/dist/rate-limit.d.ts +5 -0
- package/dist/rate-limit.js +44 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +215 -0
- package/dist/tools.js.map +1 -0
- package/package.json +6 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,57 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
The
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/sbb-mcp)
|
|
4
|
+
[](https://fsl.software/)
|
|
5
|
+
[](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.
|
package/dist/cache.d.ts
ADDED
|
@@ -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
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
|
package/dist/http.js.map
ADDED
|
@@ -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 {
|
|
5
|
-
import {
|
|
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 (
|
|
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":";AAEA,OAAO,EAAE,
|
|
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,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"}
|
package/dist/tools.d.ts
ADDED
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.
|
|
4
|
-
"mcpName": "io.github.
|
|
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"
|