@transit-se/mcp 1.0.0
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 +21 -0
- package/README.md +289 -0
- package/dist/formatting.js +341 -0
- package/dist/index.js +153 -0
- package/dist/tools/combined/nearby-vehicles.js +33 -0
- package/dist/tools/gtfs/operators.js +1 -0
- package/dist/tools/gtfs/service-alerts.js +27 -0
- package/dist/tools/gtfs/trip-updates.js +25 -0
- package/dist/tools/gtfs/vehicle-positions.js +26 -0
- package/dist/tools/sl/deviations.js +60 -0
- package/dist/tools/sl/transport.js +43 -0
- package/dist/tools/trafiklab/stop-lookup.js +27 -0
- package/dist/tools/trafiklab/timetables.js +43 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rafael Belliard
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# @transit-se/mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for Swedish public transit. Exposes [Trafiklab](https://www.trafiklab.se/) real-time APIs as tools that AI assistants like Claude can call directly.
|
|
4
|
+
|
|
5
|
+
Built on top of [`@transit-se/sdk`](../sdk/).
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Available Tools](#available-tools)
|
|
11
|
+
- [SL tools (no API key required)](#sl-tools-no-api-key-required)
|
|
12
|
+
- [Trafiklab tools (require `TRAFIKLAB_API_KEY`)](#trafiklab-tools-require-trafiklab_api_key)
|
|
13
|
+
- [GTFS tools (require `TRAFIKLAB_GTFS_KEY`)](#gtfs-tools-require-trafiklab_gtfs_key)
|
|
14
|
+
- [Combined tools (require `TRAFIKLAB_GTFS_KEY`)](#combined-tools-require-trafiklab_gtfs_key)
|
|
15
|
+
- [SDK methods not exposed as MCP tools](#sdk-methods-not-exposed-as-mcp-tools)
|
|
16
|
+
- [Typical workflow](#typical-workflow)
|
|
17
|
+
- [Environment Variables](#environment-variables)
|
|
18
|
+
- [Development](#development)
|
|
19
|
+
- [How It Works](#how-it-works)
|
|
20
|
+
- [Project Structure](#project-structure)
|
|
21
|
+
- [License](#license)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Get API keys (optional)
|
|
28
|
+
|
|
29
|
+
Some tools require [Trafiklab](https://developer.trafiklab.se) API keys. Stockholm (SL) tools work without any keys.
|
|
30
|
+
|
|
31
|
+
1. Sign up at [developer.trafiklab.se](https://developer.trafiklab.se)
|
|
32
|
+
2. Create a project
|
|
33
|
+
3. Enable the API products you need:
|
|
34
|
+
- **Trafiklab Realtime APIs** → for `trafiklab_search_stops`, `trafiklab_get_departures`, `trafiklab_get_arrivals`
|
|
35
|
+
- **GTFS Sweden 3 Realtime** → for `gtfs_service_alerts`, `gtfs_trip_updates`, `gtfs_vehicle_positions`, `combined_nearby_vehicles`
|
|
36
|
+
4. Copy each API key
|
|
37
|
+
|
|
38
|
+
### 2. Try it out
|
|
39
|
+
|
|
40
|
+
To interactively test tools in a web UI, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# From the repo root
|
|
44
|
+
bun run --filter @transit-se/mcp inspect
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Configure your MCP client
|
|
48
|
+
|
|
49
|
+
Add the server to your MCP client configuration. Both keys are optional — without them, only the SL tools (Stockholm) are available.
|
|
50
|
+
|
|
51
|
+
#### Via npx (recommended)
|
|
52
|
+
|
|
53
|
+
No installation needed — `npx` downloads and runs the package automatically. Works with any MCP client that supports the `command` + `args` format.
|
|
54
|
+
|
|
55
|
+
- **Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
|
|
56
|
+
- **Claude Code** — add to `.mcp.json` in your project root or `~/.claude/settings.json`.
|
|
57
|
+
- **Cursor** — add to `.cursor/mcp.json` in your project root:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"transit-se": {
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "@transit-se/mcp"],
|
|
65
|
+
"env": {
|
|
66
|
+
"TRAFIKLAB_API_KEY": "your-key-here",
|
|
67
|
+
"TRAFIKLAB_GTFS_KEY": "your-key-here"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### From source (for development)
|
|
75
|
+
|
|
76
|
+
If you've cloned the repo and want to run from source:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"transit-se": {
|
|
82
|
+
"command": "node",
|
|
83
|
+
"args": ["/absolute/path/to/packages/mcp/dist/index.js"],
|
|
84
|
+
"env": {
|
|
85
|
+
"TRAFIKLAB_API_KEY": "your-key-here",
|
|
86
|
+
"TRAFIKLAB_GTFS_KEY": "your-key-here"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> Build first with `bun run --filter @transit-se/mcp build`.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Available Tools
|
|
98
|
+
|
|
99
|
+
### SL tools (no API key required)
|
|
100
|
+
|
|
101
|
+
| Tool | Description | Key Parameters |
|
|
102
|
+
| --------------- | ------------------------------ | ------------------------------------------------------------------------- |
|
|
103
|
+
| `sl_departures` | Stockholm (SL) departures | `site_id`: SL site ID (e.g. 9192) |
|
|
104
|
+
| `sl_sites` | Search SL stations (cached) | `query?`: station name |
|
|
105
|
+
| `sl_deviations` | Service disruptions and alerts | `transport_modes?`: e.g. `["METRO"]`, `line_ids?`, `site_ids?`, `future?` |
|
|
106
|
+
|
|
107
|
+
### Trafiklab tools (require `TRAFIKLAB_API_KEY`)
|
|
108
|
+
|
|
109
|
+
| Tool | Description | Key Parameters |
|
|
110
|
+
| -------------------------- | -------------------- | --------------------------------------------- |
|
|
111
|
+
| `trafiklab_search_stops` | Search stops by name | `query`: stop name (e.g. "T-Centralen") |
|
|
112
|
+
| `trafiklab_get_departures` | Real-time departures | `area_id`: stop ID, `time?`: YYYY-MM-DDTHH:mm |
|
|
113
|
+
| `trafiklab_get_arrivals` | Real-time arrivals | `area_id`: stop ID, `time?`: YYYY-MM-DDTHH:mm |
|
|
114
|
+
|
|
115
|
+
### GTFS tools (require `TRAFIKLAB_GTFS_KEY`)
|
|
116
|
+
|
|
117
|
+
| Tool | Description | Key Parameters |
|
|
118
|
+
| ------------------------ | -------------------------------------------------------- | ---------------------------------------------------- |
|
|
119
|
+
| `gtfs_service_alerts` | Service alerts for any Swedish operator (GTFS-RT) | `operator`: e.g. `"ul"` (Uppsala), `"skane"` (Skåne) |
|
|
120
|
+
| `gtfs_trip_updates` | Real-time trip delays and cancellations for any operator | `operator`: e.g. `"ul"` (Uppsala), `"skane"` (Skåne) |
|
|
121
|
+
| `gtfs_vehicle_positions` | Real-time GPS positions for vehicles in service | `operator`: e.g. `"ul"` (Uppsala), `"skane"` (Skåne) |
|
|
122
|
+
|
|
123
|
+
> **Note:** For Stockholm (SL) disruptions, prefer `sl_deviations` — it provides richer data and requires no API key.
|
|
124
|
+
|
|
125
|
+
### Combined tools (require `TRAFIKLAB_GTFS_KEY`)
|
|
126
|
+
|
|
127
|
+
| Tool | Description | Key Parameters |
|
|
128
|
+
| -------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
129
|
+
| `combined_nearby_vehicles` | Find vehicles near a Stockholm location with transport mode classification | `site_name?`, `site_id?`, `latitude?`+`longitude?`, `radius_km?` (0–20, default 1.0) |
|
|
130
|
+
|
|
131
|
+
> Combines GTFS-RT vehicle positions with SL stop point data to classify each vehicle as metro, bus, tram, train, ferry, or ship.
|
|
132
|
+
>
|
|
133
|
+
> **Location resolution** — provide exactly one of these (if multiple are given, precedence is `site_id` > `site_name` > `latitude`+`longitude`):
|
|
134
|
+
>
|
|
135
|
+
> - `site_name` — SL station name (e.g. `"Solna centrum"`), resolved via cached site data
|
|
136
|
+
> - `site_id` — SL site ID (e.g. `9305`), use `sl_sites` to find IDs
|
|
137
|
+
> - `latitude` + `longitude` — raw coordinates; the nearest SL site is auto-detected
|
|
138
|
+
|
|
139
|
+
### SDK methods not exposed as MCP tools
|
|
140
|
+
|
|
141
|
+
The following SDK methods are available for programmatic use but intentionally excluded from the MCP server. They return bulk data that is useful for apps (UI lists, caches) but wasteful for AI agents, which reason better with focused, filtered results.
|
|
142
|
+
|
|
143
|
+
| SDK Method | Why not exposed |
|
|
144
|
+
| ------------------------------ | --------------------------------------------------------------------------- |
|
|
145
|
+
| `sl.getLines()` | Returns all SL lines. Departures already include line info per station. |
|
|
146
|
+
| `sl.getSites()` | Full site response. `sl_sites` uses the cached/lightweight version instead. |
|
|
147
|
+
| `sl.getStopPoints()` | Bulk stop point data. Useful for maps, not for agent queries. |
|
|
148
|
+
| `sl.getTransportAuthorities()` | Admin metadata. No agent use case. |
|
|
149
|
+
| `stops.listAll()` | Returns thousands of stops. Use `trafiklab_search_stops` to search by name. |
|
|
150
|
+
|
|
151
|
+
### Typical workflow
|
|
152
|
+
|
|
153
|
+
The AI will typically chain tools like this:
|
|
154
|
+
|
|
155
|
+
1. **User**: "When does the next train leave from Slussen?"
|
|
156
|
+
2. **AI calls** `sl_sites` with `query: "Slussen"` → gets site ID 9192
|
|
157
|
+
3. **AI calls** `sl_departures` with `site_id: 9192` → gets real-time departures
|
|
158
|
+
4. **AI responds** with formatted departure information
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Environment Variables
|
|
163
|
+
|
|
164
|
+
| Variable | Trafiklab product | Required | Description |
|
|
165
|
+
| -------------------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------ |
|
|
166
|
+
| `TRAFIKLAB_API_KEY` | Trafiklab Realtime APIs | No | Stop lookup, departures, arrivals. Without it, only the SL tools work. |
|
|
167
|
+
| `TRAFIKLAB_GTFS_KEY` | GTFS Sweden 3 Realtime | No | GTFS-RT feeds (service alerts, trip updates, vehicle positions, nearby vehicles). |
|
|
168
|
+
| `TRAFIKLAB_VALIDATE` | — | No | Set to `"true"` to enable runtime response validation via Valibot schemas. Off by default. |
|
|
169
|
+
|
|
170
|
+
### Without API Keys
|
|
171
|
+
|
|
172
|
+
If no API keys are set, the server still starts and registers all tools. The 3 SL tools work normally. The Trafiklab and GTFS tools return helpful messages explaining how to get and configure keys.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
### Build
|
|
179
|
+
|
|
180
|
+
The MCP server is compiled to JavaScript for Node.js compatibility, so it can be run via `npx` without requiring Bun.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Build the MCP package (compiles src/ → dist/)
|
|
184
|
+
bun run --filter @transit-se/mcp build
|
|
185
|
+
|
|
186
|
+
# Build is also run automatically before npm publish via prepublishOnly
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Type-check
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Type-check just the MCP package
|
|
193
|
+
bun run --filter @transit-se/mcp tc
|
|
194
|
+
|
|
195
|
+
# Type-check everything (SDK + MCP)
|
|
196
|
+
bun run tc
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Run tests
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# All tests (SDK + MCP)
|
|
203
|
+
bun test
|
|
204
|
+
|
|
205
|
+
# MCP tests only
|
|
206
|
+
bun test packages/mcp/
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Lint
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
bun run lint
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Format
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
bun run format
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## How It Works
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
┌─────────────────┐ stdio ┌──────────────────┐ HTTPS ┌──────────────┐
|
|
227
|
+
│ Claude / IDE │ ◄──────────► │ @transit-se/mcp │ ──────────► │ Trafiklab │
|
|
228
|
+
│ (MCP Client) │ JSON-RPC │ (this package) │ REST API │ APIs │
|
|
229
|
+
└─────────────────┘ └──────────────────┘ └──────────────┘
|
|
230
|
+
│
|
|
231
|
+
▼
|
|
232
|
+
@transit-se/sdk
|
|
233
|
+
(TransitClient)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The MCP server is a thin bridge between AI assistants and the transit APIs:
|
|
237
|
+
|
|
238
|
+
1. **MCP Client** (Claude, Cursor, etc.) discovers the available tools via the MCP protocol
|
|
239
|
+
2. **Tool calls** arrive as JSON-RPC messages over stdin
|
|
240
|
+
3. **This server** validates parameters (via Zod schemas), calls the appropriate SDK method, and formats the response as human-readable text
|
|
241
|
+
4. **Formatted results** go back to the AI, which uses them to answer the user
|
|
242
|
+
|
|
243
|
+
### Why formatted text instead of raw JSON?
|
|
244
|
+
|
|
245
|
+
AI assistants work better with concise, structured text than massive JSON blobs. The formatters turn API responses into clean departure boards and station lists that the model can reason about efficiently.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Project Structure
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
packages/mcp/
|
|
253
|
+
├── src/
|
|
254
|
+
│ ├── index.ts # Server entry — creates McpServer, registers tools, connects stdio
|
|
255
|
+
│ ├── index.test.ts # Server integration tests
|
|
256
|
+
│ ├── formatting.ts # Human-readable output formatters
|
|
257
|
+
│ ├── formatting.test.ts # Formatter tests
|
|
258
|
+
│ └── tools/
|
|
259
|
+
│ ├── sl/
|
|
260
|
+
│ │ ├── transport.ts # sl_departures, sl_sites
|
|
261
|
+
│ │ ├── transport.test.ts
|
|
262
|
+
│ │ ├── deviations.ts # sl_deviations
|
|
263
|
+
│ │ └── deviations.test.ts
|
|
264
|
+
│ ├── trafiklab/
|
|
265
|
+
│ │ ├── stop-lookup.ts # trafiklab_search_stops
|
|
266
|
+
│ │ ├── stop-lookup.test.ts
|
|
267
|
+
│ │ ├── timetables.ts # trafiklab_get_departures, trafiklab_get_arrivals
|
|
268
|
+
│ │ └── timetables.test.ts
|
|
269
|
+
│ ├── gtfs/
|
|
270
|
+
│ │ ├── operators.ts # Shared GTFS operator list (re-exported from SDK)
|
|
271
|
+
│ │ ├── service-alerts.ts # gtfs_service_alerts
|
|
272
|
+
│ │ ├── service-alerts.test.ts
|
|
273
|
+
│ │ ├── trip-updates.ts # gtfs_trip_updates
|
|
274
|
+
│ │ ├── trip-updates.test.ts
|
|
275
|
+
│ │ ├── vehicle-positions.ts # gtfs_vehicle_positions
|
|
276
|
+
│ │ └── vehicle-positions.test.ts
|
|
277
|
+
│ └── combined/
|
|
278
|
+
│ ├── nearby-vehicles.ts # combined_nearby_vehicles
|
|
279
|
+
│ └── nearby-vehicles.test.ts
|
|
280
|
+
├── package.json
|
|
281
|
+
├── tsconfig.json
|
|
282
|
+
└── README.md # This file
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable formatters for SDK responses.
|
|
3
|
+
*
|
|
4
|
+
* These turn raw API data into concise text that AI assistants can
|
|
5
|
+
* reason about efficiently. Raw JSON is large and noisy — formatted
|
|
6
|
+
* text lets the model focus on the information that matters.
|
|
7
|
+
*/
|
|
8
|
+
import { GTFS_OPERATOR_NAMES } from '@transit-se/sdk/gtfs';
|
|
9
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
10
|
+
function time(iso) {
|
|
11
|
+
return new Date(iso).toLocaleTimeString('sv-SE', {
|
|
12
|
+
hour: '2-digit',
|
|
13
|
+
minute: '2-digit',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function delayTag(seconds) {
|
|
17
|
+
if (seconds <= 0) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
const mins = Math.round(seconds / 60);
|
|
21
|
+
return ` (+${mins} min late)`;
|
|
22
|
+
}
|
|
23
|
+
// ─── Stop Lookup ────────────────────────────────────────────────────
|
|
24
|
+
export function formatTrafiklabStopLookup(res) {
|
|
25
|
+
if (res.stop_groups.length === 0) {
|
|
26
|
+
return 'No stops found.';
|
|
27
|
+
}
|
|
28
|
+
const lines = [`Found ${res.stop_groups.length} stop(s):\n`];
|
|
29
|
+
for (const group of res.stop_groups) {
|
|
30
|
+
lines.push(` ${group.name} (ID: ${group.id})`);
|
|
31
|
+
lines.push(` Type: ${group.area_type}`);
|
|
32
|
+
lines.push(` Modes: ${group.transport_modes.join(', ')}`);
|
|
33
|
+
lines.push(` Avg. daily departures: ${group.average_daily_stop_times}`);
|
|
34
|
+
if (group.stops.length > 0) {
|
|
35
|
+
lines.push(` Child stops:`);
|
|
36
|
+
for (const stop of group.stops) {
|
|
37
|
+
lines.push(` - ${stop.name} (${stop.id}) @ ${stop.lat}, ${stop.lon}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
lines.push('');
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
44
|
+
// ─── Timetable Departures / Arrivals ────────────────────────────────
|
|
45
|
+
function formatCallAtLocation(entry, _label) {
|
|
46
|
+
const sched = time(entry.scheduled);
|
|
47
|
+
const real = time(entry.realtime);
|
|
48
|
+
const delay = delayTag(entry.delay);
|
|
49
|
+
const canceled = entry.canceled ? ' [CANCELED]' : '';
|
|
50
|
+
const rt = entry.is_realtime ? '' : ' (scheduled only)';
|
|
51
|
+
const platform = entry.realtime_platform?.designation ?? entry.scheduled_platform?.designation;
|
|
52
|
+
const platStr = platform ? ` Platform ${platform}` : '';
|
|
53
|
+
const alerts = entry.alerts.length > 0
|
|
54
|
+
? '\n Alerts: ' + entry.alerts.map((a) => a.header).join('; ')
|
|
55
|
+
: '';
|
|
56
|
+
const routeName = entry.route.name ? `${entry.route.name} | ` : '';
|
|
57
|
+
return (` ${real}${delay}${canceled} Line ${entry.route.designation} → ${entry.route.direction}` +
|
|
58
|
+
`${platStr}${rt}` +
|
|
59
|
+
`\n ${routeName}Scheduled: ${sched} | ${entry.stop.name}` +
|
|
60
|
+
alerts);
|
|
61
|
+
}
|
|
62
|
+
export function formatTrafiklabDepartures(res) {
|
|
63
|
+
if (res.departures.length === 0) {
|
|
64
|
+
return `No departures found for stop ${res.query.query}.`;
|
|
65
|
+
}
|
|
66
|
+
const stopName = res.stops[0]?.name ?? res.query.query;
|
|
67
|
+
const lines = [
|
|
68
|
+
`Departures from ${stopName} (${res.departures.length} results):\n`,
|
|
69
|
+
];
|
|
70
|
+
// Show stop-level alerts
|
|
71
|
+
for (const stop of res.stops) {
|
|
72
|
+
for (const alert of stop.alerts) {
|
|
73
|
+
lines.push(` ⚠ ${alert.header}: ${alert.details}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const dep of res.departures) {
|
|
77
|
+
lines.push(formatCallAtLocation(dep, 'departs'));
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
export function formatTrafiklabArrivals(res) {
|
|
82
|
+
if (res.arrivals.length === 0) {
|
|
83
|
+
return `No arrivals found for stop ${res.query.query}.`;
|
|
84
|
+
}
|
|
85
|
+
const stopName = res.stops[0]?.name ?? res.query.query;
|
|
86
|
+
const lines = [`Arrivals at ${stopName} (${res.arrivals.length} results):\n`];
|
|
87
|
+
for (const stop of res.stops) {
|
|
88
|
+
for (const alert of stop.alerts) {
|
|
89
|
+
lines.push(` ⚠ ${alert.header}: ${alert.details}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const arr of res.arrivals) {
|
|
93
|
+
lines.push(formatCallAtLocation(arr, 'arrives'));
|
|
94
|
+
}
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
// ─── SL Departures ──────────────────────────────────────────────────
|
|
98
|
+
export function formatSLDepartures(res, siteId) {
|
|
99
|
+
if (res.departures.length === 0) {
|
|
100
|
+
return `No departures found for SL site ${siteId}.`;
|
|
101
|
+
}
|
|
102
|
+
const stopName = res.departures[0]?.stop_area.name ?? `site ${siteId}`;
|
|
103
|
+
const lines = [
|
|
104
|
+
`SL Departures from ${stopName} (${res.departures.length} results):\n`,
|
|
105
|
+
];
|
|
106
|
+
// Stop-level deviations
|
|
107
|
+
for (const dev of res.stop_deviations) {
|
|
108
|
+
lines.push(` ⚠ ${dev.message}`);
|
|
109
|
+
}
|
|
110
|
+
for (const dep of res.departures) {
|
|
111
|
+
const mode = dep.line.transport_mode.toUpperCase();
|
|
112
|
+
const deviations = dep.deviations.length > 0
|
|
113
|
+
? '\n Disruptions: ' + dep.deviations.map((d) => d.message).join('; ')
|
|
114
|
+
: '';
|
|
115
|
+
const passenger = dep.journey.passenger_level !== 'UNKNOWN' ? ` Crowding: ${dep.journey.passenger_level}` : '';
|
|
116
|
+
lines.push(` ${dep.display.padEnd(8)} ${mode} ${dep.line.designation} → ${dep.destination}` +
|
|
117
|
+
` (Platform ${dep.stop_point.designation})${passenger}` +
|
|
118
|
+
deviations);
|
|
119
|
+
}
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
// ─── SL Sites (cached) ─────────────────────────────────────────────
|
|
123
|
+
export function formatSLSites(sites, query) {
|
|
124
|
+
if (sites.length === 0) {
|
|
125
|
+
return query ? `No SL sites matching "${query}".` : 'No SL sites found.';
|
|
126
|
+
}
|
|
127
|
+
const header = query
|
|
128
|
+
? `SL sites matching "${query}" (${sites.length} results):`
|
|
129
|
+
: `All SL sites (${sites.length} total):`;
|
|
130
|
+
const lines = [header, ''];
|
|
131
|
+
const display = sites.slice(0, 100); // cap to avoid huge output
|
|
132
|
+
for (const site of display) {
|
|
133
|
+
lines.push(` ${site.name} (ID: ${site.id}) @ ${site.lat}, ${site.lon}`);
|
|
134
|
+
}
|
|
135
|
+
if (sites.length > 100) {
|
|
136
|
+
lines.push(`\n ... and ${sites.length - 100} more. Use a search query to narrow results.`);
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
// ─── SL Deviations ──────────────────────────────────────────────────
|
|
141
|
+
export function formatSLDeviations(messages, context) {
|
|
142
|
+
if (messages.length === 0) {
|
|
143
|
+
return context
|
|
144
|
+
? `No service deviations found for ${context}.`
|
|
145
|
+
: 'No active service deviations.';
|
|
146
|
+
}
|
|
147
|
+
const header = context
|
|
148
|
+
? `Service deviations for ${context} (${messages.length}):`
|
|
149
|
+
: `Service deviations (${messages.length}):`;
|
|
150
|
+
const lines = [header, ''];
|
|
151
|
+
for (const msg of messages) {
|
|
152
|
+
const variant = msg.message_variants[0];
|
|
153
|
+
if (!variant) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const affectedLines = msg.scope.lines?.map((l) => `${l.name} ${l.designation}`).join(', ') ?? '';
|
|
157
|
+
const affectedStops = msg.scope.stop_areas?.map((s) => s.name).join(', ') ?? '';
|
|
158
|
+
const until = new Date(msg.publish.upto).toLocaleDateString('sv-SE');
|
|
159
|
+
const categoryTags = msg.categories && msg.categories.length > 0
|
|
160
|
+
? ` [${msg.categories.map((c) => c.type).join(', ')}]`
|
|
161
|
+
: '';
|
|
162
|
+
lines.push(` ${variant.header}${categoryTags}`);
|
|
163
|
+
lines.push(` ${variant.details.replace(/\n+/g, ' ').trim()}`);
|
|
164
|
+
if (affectedStops) {
|
|
165
|
+
lines.push(` Affects: ${affectedStops}`);
|
|
166
|
+
}
|
|
167
|
+
if (affectedLines) {
|
|
168
|
+
lines.push(` Lines: ${affectedLines}`);
|
|
169
|
+
}
|
|
170
|
+
lines.push(` Until: ${until}`);
|
|
171
|
+
lines.push('');
|
|
172
|
+
}
|
|
173
|
+
return lines.join('\n').trimEnd();
|
|
174
|
+
}
|
|
175
|
+
// ─── GTFS Trip Updates ──────────────────────────────────────────────
|
|
176
|
+
export function formatGtfsTripUpdates(updates, operator) {
|
|
177
|
+
const operatorName = GTFS_OPERATOR_NAMES[operator] ?? operator;
|
|
178
|
+
if (updates.length === 0) {
|
|
179
|
+
return `No active trip updates for ${operatorName}.`;
|
|
180
|
+
}
|
|
181
|
+
const lines = [`Trip updates for ${operatorName} (${updates.length}):\n`];
|
|
182
|
+
for (const tu of updates) {
|
|
183
|
+
const route = tu.trip.routeId ? `Route ${tu.trip.routeId}` : 'Unknown route';
|
|
184
|
+
const tripId = tu.trip.tripId ? ` (trip ${tu.trip.tripId})` : '';
|
|
185
|
+
const status = tu.trip.scheduleRelationship !== 'SCHEDULED' ? ` [${tu.trip.scheduleRelationship}]` : '';
|
|
186
|
+
const delayStr = tu.delay != null ? ` — ${tu.delay > 0 ? '+' : ''}${Math.round(tu.delay / 60)} min` : '';
|
|
187
|
+
lines.push(` ${route}${tripId}${status}${delayStr}`);
|
|
188
|
+
if (tu.vehicle) {
|
|
189
|
+
const vLabel = tu.vehicle.label ?? tu.vehicle.id ?? 'unknown';
|
|
190
|
+
lines.push(` Vehicle: ${vLabel}`);
|
|
191
|
+
}
|
|
192
|
+
if (tu.trip.startTime) {
|
|
193
|
+
const date = tu.trip.startDate
|
|
194
|
+
? `${tu.trip.startDate.slice(0, 4)}-${tu.trip.startDate.slice(4, 6)}-${tu.trip.startDate.slice(6, 8)} `
|
|
195
|
+
: '';
|
|
196
|
+
lines.push(` Departure: ${date}${tu.trip.startTime}`);
|
|
197
|
+
}
|
|
198
|
+
if (tu.stopTimeUpdates.length > 0) {
|
|
199
|
+
const skipped = tu.stopTimeUpdates.filter((s) => s.scheduleRelationship === 'SKIPPED');
|
|
200
|
+
const delayed = tu.stopTimeUpdates.filter((s) => s.scheduleRelationship === 'SCHEDULED' &&
|
|
201
|
+
(s.arrival?.delay ?? s.departure?.delay ?? 0) > 0);
|
|
202
|
+
if (skipped.length > 0) {
|
|
203
|
+
const stopIds = skipped.map((s) => s.stopId ?? `#${s.stopSequence}`).join(', ');
|
|
204
|
+
lines.push(` Skipped stops: ${stopIds}`);
|
|
205
|
+
}
|
|
206
|
+
if (delayed.length > 0) {
|
|
207
|
+
const maxDelay = Math.max(...delayed.map((s) => Math.max(s.arrival?.delay ?? 0, s.departure?.delay ?? 0)));
|
|
208
|
+
lines.push(` ${delayed.length} stop(s) delayed, max +${Math.round(maxDelay / 60)} min`);
|
|
209
|
+
}
|
|
210
|
+
const nextStop = tu.stopTimeUpdates.find((s) => s.scheduleRelationship === 'SCHEDULED');
|
|
211
|
+
if (nextStop) {
|
|
212
|
+
const stopLabel = nextStop.stopId ?? `stop #${nextStop.stopSequence}`;
|
|
213
|
+
const arrDelay = nextStop.arrival?.delay;
|
|
214
|
+
const depDelay = nextStop.departure?.delay;
|
|
215
|
+
const stopDelay = arrDelay ?? depDelay;
|
|
216
|
+
const stopDelayStr = stopDelay != null
|
|
217
|
+
? ` (${stopDelay > 0 ? '+' : ''}${Math.round(stopDelay / 60)} min)`
|
|
218
|
+
: '';
|
|
219
|
+
lines.push(` Next: ${stopLabel}${stopDelayStr}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
224
|
+
return lines.join('\n').trimEnd();
|
|
225
|
+
}
|
|
226
|
+
// ─── GTFS Vehicle Positions ─────────────────────────────────────────
|
|
227
|
+
export function formatGtfsVehiclePositions(positions, operator) {
|
|
228
|
+
const operatorName = GTFS_OPERATOR_NAMES[operator] ?? operator;
|
|
229
|
+
if (positions.length === 0) {
|
|
230
|
+
return `No active vehicles for ${operatorName}.`;
|
|
231
|
+
}
|
|
232
|
+
const lines = [`Vehicle positions for ${operatorName} (${positions.length}):\n`];
|
|
233
|
+
for (const vp of positions) {
|
|
234
|
+
const route = vp.trip?.routeId ? `Route ${vp.trip.routeId}` : 'Unknown route';
|
|
235
|
+
const vehicleLabel = vp.vehicle?.label ?? vp.vehicle?.id ?? 'unknown vehicle';
|
|
236
|
+
const status = vp.currentStatus ? ` [${vp.currentStatus.replace(/_/g, ' ')}]` : '';
|
|
237
|
+
lines.push(` ${route} — ${vehicleLabel}${status}`);
|
|
238
|
+
if (vp.position) {
|
|
239
|
+
const bearing = vp.position.bearing != null ? ` bearing ${vp.position.bearing}°` : '';
|
|
240
|
+
const speed = vp.position.speed != null ? ` at ${Math.round(vp.position.speed * 3.6)} km/h` : '';
|
|
241
|
+
lines.push(` Position: ${vp.position.latitude.toFixed(4)}, ${vp.position.longitude.toFixed(4)}${bearing}${speed}`);
|
|
242
|
+
}
|
|
243
|
+
if (vp.stopId) {
|
|
244
|
+
lines.push(` Stop: ${vp.stopId}${vp.currentStopSequence != null ? ` (seq ${vp.currentStopSequence})` : ''}`);
|
|
245
|
+
}
|
|
246
|
+
if (vp.occupancyStatus && vp.occupancyStatus !== 'NO_DATA_AVAILABLE') {
|
|
247
|
+
const pct = vp.occupancyPercentage != null ? ` (${vp.occupancyPercentage}%)` : '';
|
|
248
|
+
lines.push(` Occupancy: ${vp.occupancyStatus.replace(/_/g, ' ').toLowerCase()}${pct}`);
|
|
249
|
+
}
|
|
250
|
+
if (vp.congestionLevel && vp.congestionLevel !== 'UNKNOWN_CONGESTION_LEVEL') {
|
|
251
|
+
lines.push(` Congestion: ${vp.congestionLevel.replace(/_/g, ' ').toLowerCase()}`);
|
|
252
|
+
}
|
|
253
|
+
lines.push('');
|
|
254
|
+
}
|
|
255
|
+
return lines.join('\n').trimEnd();
|
|
256
|
+
}
|
|
257
|
+
// ─── GTFS Service Alerts ────────────────────────────────────────────
|
|
258
|
+
export function formatGtfsServiceAlerts(alerts, operator) {
|
|
259
|
+
const operatorName = GTFS_OPERATOR_NAMES[operator] ?? operator;
|
|
260
|
+
if (alerts.length === 0) {
|
|
261
|
+
return `No active service alerts for ${operatorName}.`;
|
|
262
|
+
}
|
|
263
|
+
const lines = [`Service alerts for ${operatorName} (${alerts.length}):\n`];
|
|
264
|
+
for (const alert of alerts) {
|
|
265
|
+
const header = alert.headerText ?? '(no title)';
|
|
266
|
+
const effect = alert.effect !== 'UNKNOWN_EFFECT' ? ` [${alert.effect}]` : '';
|
|
267
|
+
const cause = alert.cause !== 'UNKNOWN_CAUSE' ? ` (${alert.cause})` : '';
|
|
268
|
+
lines.push(` ${header}${effect}`);
|
|
269
|
+
if (alert.descriptionText) {
|
|
270
|
+
lines.push(` ${alert.descriptionText.replace(/\n+/g, ' ').trim()}`);
|
|
271
|
+
}
|
|
272
|
+
if (alert.cause !== 'UNKNOWN_CAUSE') {
|
|
273
|
+
lines.push(` Cause: ${alert.cause.replace(/_/g, ' ').toLowerCase()}${cause ? '' : ''}`);
|
|
274
|
+
}
|
|
275
|
+
const entities = alert.informedEntities;
|
|
276
|
+
if (entities.length > 0) {
|
|
277
|
+
const routes = entities
|
|
278
|
+
.filter((e) => !!e.routeId)
|
|
279
|
+
.map((e) => e.routeId);
|
|
280
|
+
const stops = entities
|
|
281
|
+
.filter((e) => !!e.stopId)
|
|
282
|
+
.map((e) => e.stopId);
|
|
283
|
+
if (routes.length > 0) {
|
|
284
|
+
lines.push(` Routes: ${[...new Set(routes)].join(', ')}`);
|
|
285
|
+
}
|
|
286
|
+
if (stops.length > 0) {
|
|
287
|
+
lines.push(` Stops: ${[...new Set(stops)].join(', ')}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (alert.activePeriods.length > 0) {
|
|
291
|
+
const period = alert.activePeriods[0];
|
|
292
|
+
const from = period.start ? new Date(period.start * 1000).toLocaleDateString('sv-SE') : '?';
|
|
293
|
+
const to = period.end ? new Date(period.end * 1000).toLocaleDateString('sv-SE') : 'ongoing';
|
|
294
|
+
lines.push(` Period: ${from} → ${to}`);
|
|
295
|
+
}
|
|
296
|
+
if (alert.url) {
|
|
297
|
+
lines.push(` Info: ${alert.url}`);
|
|
298
|
+
}
|
|
299
|
+
lines.push('');
|
|
300
|
+
}
|
|
301
|
+
return lines.join('\n').trimEnd();
|
|
302
|
+
}
|
|
303
|
+
// ─── Combined SL Nearby Vehicles ────────────────────────────────────
|
|
304
|
+
export function formatCombinedSLNearbyVehicles(result) {
|
|
305
|
+
const { location, radiusKm, vehicles, activeModes } = result;
|
|
306
|
+
if (vehicles.length === 0) {
|
|
307
|
+
return `No vehicles found within ${radiusKm} km of ${location.name}.`;
|
|
308
|
+
}
|
|
309
|
+
const lines = [
|
|
310
|
+
`Vehicles near ${location.name} (ID: ${location.siteId}) — ${vehicles.length} within ${radiusKm} km`,
|
|
311
|
+
`Active modes: ${activeModes.join(', ')}`,
|
|
312
|
+
'',
|
|
313
|
+
];
|
|
314
|
+
for (const v of vehicles) {
|
|
315
|
+
const mode = v.transportMode.toUpperCase().padEnd(7);
|
|
316
|
+
const dist = v.distanceMeters < 1000
|
|
317
|
+
? `${v.distanceMeters} m`
|
|
318
|
+
: `${(v.distanceMeters / 1000).toFixed(2)} km`;
|
|
319
|
+
const label = v.vehicleLabel ?? v.vehicleId ?? '—';
|
|
320
|
+
const speed = v.position.speed != null ? `${Math.round(v.position.speed * 3.6)} km/h` : '—';
|
|
321
|
+
const bearing = v.position.bearing != null ? `${v.position.bearing}°` : '—';
|
|
322
|
+
const status = v.currentStatus?.replace(/_/g, ' ') ?? '—';
|
|
323
|
+
const ts = v.timestamp
|
|
324
|
+
? new Date(v.timestamp * 1000).toLocaleTimeString('sv-SE', {
|
|
325
|
+
hour: '2-digit',
|
|
326
|
+
minute: '2-digit',
|
|
327
|
+
})
|
|
328
|
+
: '—';
|
|
329
|
+
const trip = v.trip?.tripId ? `trip ${v.trip.tripId}` : '';
|
|
330
|
+
const stopPt = v.nearestStopPoint
|
|
331
|
+
? `near ${v.nearestStopPoint.name}${v.nearestStopPoint.designation ? ` (${v.nearestStopPoint.designation})` : ''}`
|
|
332
|
+
: '';
|
|
333
|
+
lines.push(` ${mode} ${dist.padEnd(8)} ${label}` +
|
|
334
|
+
` ${status} speed ${speed} bearing ${bearing} updated ${ts}`);
|
|
335
|
+
const details = [stopPt, trip].filter(Boolean);
|
|
336
|
+
if (details.length > 0) {
|
|
337
|
+
lines.push(` ${details.join(' | ')}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return lines.join('\n');
|
|
341
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @transit-se/mcp — MCP server for Swedish public transit.
|
|
4
|
+
*
|
|
5
|
+
* This is the entry point. It:
|
|
6
|
+
* 1. Reads the TRAFIKLAB_API_KEY from the environment (passed via MCP config)
|
|
7
|
+
* 2. Creates a TransitClient from @transit-se/sdk
|
|
8
|
+
* 3. Registers all tools with the MCP server
|
|
9
|
+
* 4. Connects via stdio transport so Claude / Cursor / any MCP client can call the tools
|
|
10
|
+
*
|
|
11
|
+
* Run it directly:
|
|
12
|
+
* TRAFIKLAB_API_KEY=xxx bun run packages/mcp/src/index.ts
|
|
13
|
+
*
|
|
14
|
+
* Or configure it in your MCP client — see README.md for details.
|
|
15
|
+
*/
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { TransitClient } from '@transit-se/sdk';
|
|
19
|
+
import { CombinedSLNearbyVehiclesApi } from '@transit-se/sdk/combined';
|
|
20
|
+
import { GtfsServiceAlertsApi, GtfsTripUpdatesApi, GtfsVehiclePositionsApi, } from '@transit-se/sdk/gtfs';
|
|
21
|
+
import { SLDeviationsApi, SLTransportApi } from '@transit-se/sdk/sl';
|
|
22
|
+
import pkg from '../package.json';
|
|
23
|
+
import { registerCombinedSLNearbyVehiclesTools } from './tools/combined/nearby-vehicles.js';
|
|
24
|
+
import { registerGtfsServiceAlertsTools } from './tools/gtfs/service-alerts.js';
|
|
25
|
+
import { registerGtfsTripUpdatesTools } from './tools/gtfs/trip-updates.js';
|
|
26
|
+
import { registerGtfsVehiclePositionsTools } from './tools/gtfs/vehicle-positions.js';
|
|
27
|
+
import { registerSLDeviationsTools } from './tools/sl/deviations.js';
|
|
28
|
+
import { registerSLTransportTools } from './tools/sl/transport.js';
|
|
29
|
+
import { registerTrafiklabStopLookupTools } from './tools/trafiklab/stop-lookup.js';
|
|
30
|
+
import { registerTrafiklabTimetableTools } from './tools/trafiklab/timetables.js';
|
|
31
|
+
// ─── Configuration ──────────────────────────────────────────────────
|
|
32
|
+
const API_KEY = process.env.TRAFIKLAB_API_KEY;
|
|
33
|
+
const GTFS_KEY = process.env.TRAFIKLAB_GTFS_KEY;
|
|
34
|
+
const VALIDATE = process.env.TRAFIKLAB_VALIDATE === 'true';
|
|
35
|
+
// ─── SDK Clients ────────────────────────────────────────────────────
|
|
36
|
+
// SL Transport API works without a key — always available
|
|
37
|
+
const sl = new SLTransportApi({ validate: VALIDATE });
|
|
38
|
+
// SL Deviations API works without a key — always available
|
|
39
|
+
const deviationsApi = new SLDeviationsApi({ validate: VALIDATE });
|
|
40
|
+
// Full TransitClient requires an API key. If missing, we still start
|
|
41
|
+
// the server but only register the SL tools (which are keyless).
|
|
42
|
+
const client = API_KEY ? new TransitClient({ apiKey: API_KEY, validate: VALIDATE }) : null;
|
|
43
|
+
// GTFS-RT APIs require a separate GTFS Sweden 3 key.
|
|
44
|
+
const gtfsAlerts = GTFS_KEY ? new GtfsServiceAlertsApi({ apiKey: GTFS_KEY }) : null;
|
|
45
|
+
const gtfsTripUpdates = GTFS_KEY ? new GtfsTripUpdatesApi({ apiKey: GTFS_KEY }) : null;
|
|
46
|
+
const gtfsVehiclePositions = GTFS_KEY ? new GtfsVehiclePositionsApi({ apiKey: GTFS_KEY }) : null;
|
|
47
|
+
// Combined APIs merge GTFS + SL data (needs GTFS key for vehicle positions).
|
|
48
|
+
const combinedNearbyVehicles = gtfsVehiclePositions
|
|
49
|
+
? new CombinedSLNearbyVehiclesApi({
|
|
50
|
+
vehiclePositionsApi: gtfsVehiclePositions,
|
|
51
|
+
slTransportApi: sl,
|
|
52
|
+
})
|
|
53
|
+
: null;
|
|
54
|
+
// ─── MCP Server ─────────────────────────────────────────────────────
|
|
55
|
+
const server = new McpServer({
|
|
56
|
+
name: pkg.name,
|
|
57
|
+
version: pkg.version,
|
|
58
|
+
});
|
|
59
|
+
// Always register SL tools (no key needed)
|
|
60
|
+
registerSLTransportTools(server, sl);
|
|
61
|
+
registerSLDeviationsTools(server, deviationsApi);
|
|
62
|
+
if (client) {
|
|
63
|
+
// Full API key available — register all tools
|
|
64
|
+
registerTrafiklabStopLookupTools(server, client);
|
|
65
|
+
registerTrafiklabTimetableTools(server, client);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// No API key — register placeholder tools that explain the situation
|
|
69
|
+
const noKeyMessage = [
|
|
70
|
+
'This tool requires a TRAFIKLAB_API_KEY which is not configured.',
|
|
71
|
+
'',
|
|
72
|
+
'To fix this, add the key to your MCP server configuration:',
|
|
73
|
+
'',
|
|
74
|
+
' "env": { "TRAFIKLAB_API_KEY": "your-key-here" }',
|
|
75
|
+
'',
|
|
76
|
+
'Get a free key at: https://developer.trafiklab.se',
|
|
77
|
+
' 1. Create an account',
|
|
78
|
+
' 2. Create a project',
|
|
79
|
+
' 3. Enable "Trafiklab Realtime APIs"',
|
|
80
|
+
' 4. Copy the API key',
|
|
81
|
+
'',
|
|
82
|
+
'Meanwhile, the sl_departures, sl_sites, and sl_deviations tools work without a key.',
|
|
83
|
+
].join('\n');
|
|
84
|
+
for (const [name, desc] of [
|
|
85
|
+
[
|
|
86
|
+
'trafiklab_search_stops',
|
|
87
|
+
'Search for Swedish public transport stops by name. (Requires TRAFIKLAB_API_KEY)',
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
'trafiklab_get_departures',
|
|
91
|
+
'Get real-time departures from a Swedish transit stop. (Requires TRAFIKLAB_API_KEY)',
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
'trafiklab_get_arrivals',
|
|
95
|
+
'Get real-time arrivals at a Swedish transit stop. (Requires TRAFIKLAB_API_KEY)',
|
|
96
|
+
],
|
|
97
|
+
]) {
|
|
98
|
+
server.tool(name, desc, async () => ({
|
|
99
|
+
content: [{ type: 'text', text: noKeyMessage }],
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// GTFS-RT tools (separate key)
|
|
104
|
+
if (gtfsAlerts && gtfsTripUpdates && gtfsVehiclePositions) {
|
|
105
|
+
registerGtfsServiceAlertsTools(server, gtfsAlerts);
|
|
106
|
+
registerGtfsTripUpdatesTools(server, gtfsTripUpdates);
|
|
107
|
+
registerGtfsVehiclePositionsTools(server, gtfsVehiclePositions);
|
|
108
|
+
if (combinedNearbyVehicles) {
|
|
109
|
+
registerCombinedSLNearbyVehiclesTools(server, combinedNearbyVehicles);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const noGtfsKeyMessage = [
|
|
114
|
+
'This tool requires a TRAFIKLAB_GTFS_KEY which is not configured.',
|
|
115
|
+
'',
|
|
116
|
+
'To fix this, add the key to your MCP server configuration:',
|
|
117
|
+
'',
|
|
118
|
+
' "env": { "TRAFIKLAB_GTFS_KEY": "your-key-here" }',
|
|
119
|
+
'',
|
|
120
|
+
'Get a free key at: https://developer.trafiklab.se',
|
|
121
|
+
' 1. Create an account',
|
|
122
|
+
' 2. Create a project',
|
|
123
|
+
' 3. Enable "GTFS Sweden 3" (under GTFS Datasets)',
|
|
124
|
+
' 4. Copy the API key',
|
|
125
|
+
'',
|
|
126
|
+
'For Stockholm disruptions, sl_deviations works without any key.',
|
|
127
|
+
].join('\n');
|
|
128
|
+
for (const [name, desc] of [
|
|
129
|
+
[
|
|
130
|
+
'gtfs_service_alerts',
|
|
131
|
+
'Get service alerts for Swedish transit operators (Requires TRAFIKLAB_GTFS_KEY)',
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
'gtfs_trip_updates',
|
|
135
|
+
'Get real-time trip updates for Swedish transit operators (Requires TRAFIKLAB_GTFS_KEY)',
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
'gtfs_vehicle_positions',
|
|
139
|
+
'Get real-time vehicle positions for Swedish transit operators (Requires TRAFIKLAB_GTFS_KEY)',
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
'combined_nearby_vehicles',
|
|
143
|
+
'Find vehicles near a Stockholm location with transport mode (Requires TRAFIKLAB_GTFS_KEY)',
|
|
144
|
+
],
|
|
145
|
+
]) {
|
|
146
|
+
server.tool(name, desc, async () => ({
|
|
147
|
+
content: [{ type: 'text', text: noGtfsKeyMessage }],
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ─── Connect ────────────────────────────────────────────────────────
|
|
152
|
+
const transport = new StdioServerTransport();
|
|
153
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool for finding vehicles near a Stockholm location.
|
|
3
|
+
*
|
|
4
|
+
* Combines GTFS-RT vehicle positions with SL stop point data to
|
|
5
|
+
* return a table of nearby vehicles classified by transport mode
|
|
6
|
+
* (metro, bus, tram, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Requires TRAFIKLAB_GTFS_KEY.
|
|
9
|
+
*/
|
|
10
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { formatCombinedSLNearbyVehicles } from '../../formatting.js';
|
|
13
|
+
export function registerCombinedSLNearbyVehiclesTools(server, nearbyVehiclesApi) {
|
|
14
|
+
const desc = API_DESCRIPTIONS.combined_nearby_vehicles;
|
|
15
|
+
server.tool('combined_nearby_vehicles', getApiDescription(desc), {
|
|
16
|
+
site_name: z.string().optional().describe(desc.params.site_name),
|
|
17
|
+
site_id: z.number().optional().describe(desc.params.site_id),
|
|
18
|
+
latitude: z.number().optional().describe(desc.params.latitude),
|
|
19
|
+
longitude: z.number().optional().describe(desc.params.longitude),
|
|
20
|
+
radius_km: z.number().min(0).max(20).optional().describe(desc.params.radius_km),
|
|
21
|
+
}, async ({ site_name, site_id, latitude, longitude, radius_km }) => {
|
|
22
|
+
const result = await nearbyVehiclesApi.getNearbyVehicles({
|
|
23
|
+
siteName: site_name,
|
|
24
|
+
siteId: site_id,
|
|
25
|
+
latitude,
|
|
26
|
+
longitude,
|
|
27
|
+
radiusKm: radius_km,
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: 'text', text: formatCombinedSLNearbyVehicles(result) }],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GTFS_OPERATORS } from '@transit-se/sdk/gtfs';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool for GTFS-RT ServiceAlerts.
|
|
3
|
+
*
|
|
4
|
+
* Exposes service alerts from GTFS Sweden 3 feeds, covering all Swedish
|
|
5
|
+
* transit operators with real-time data (UL, Skånetrafiken, etc.).
|
|
6
|
+
*
|
|
7
|
+
* For Stockholm (SL) disruptions, the sl_deviations tool is preferred —
|
|
8
|
+
* it provides richer data and requires no API key.
|
|
9
|
+
*
|
|
10
|
+
* Requires TRAFIKLAB_GTFS_KEY.
|
|
11
|
+
*/
|
|
12
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { formatGtfsServiceAlerts } from '../../formatting.js';
|
|
15
|
+
import { GTFS_OPERATORS } from './operators.js';
|
|
16
|
+
export function registerGtfsServiceAlertsTools(server, alertsApi) {
|
|
17
|
+
server.tool('gtfs_service_alerts', getApiDescription(API_DESCRIPTIONS.gtfs_service_alerts), {
|
|
18
|
+
operator: z
|
|
19
|
+
.enum(GTFS_OPERATORS)
|
|
20
|
+
.describe(API_DESCRIPTIONS.gtfs_service_alerts.params.operator),
|
|
21
|
+
}, async ({ operator }) => {
|
|
22
|
+
const result = await alertsApi.getServiceAlerts(operator);
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: formatGtfsServiceAlerts(result, operator) }],
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool for GTFS-RT TripUpdates.
|
|
3
|
+
*
|
|
4
|
+
* Exposes real-time trip updates from GTFS Sweden 3 feeds, covering all
|
|
5
|
+
* Swedish transit operators with real-time data (UL, Skånetrafiken, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Provides per-trip delay predictions, cancellations, and per-stop
|
|
8
|
+
* arrival/departure times. Updated every 15 seconds.
|
|
9
|
+
*
|
|
10
|
+
* Requires TRAFIKLAB_GTFS_KEY.
|
|
11
|
+
*/
|
|
12
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { formatGtfsTripUpdates } from '../../formatting.js';
|
|
15
|
+
import { GTFS_OPERATORS } from './operators.js';
|
|
16
|
+
export function registerGtfsTripUpdatesTools(server, tripUpdatesApi) {
|
|
17
|
+
server.tool('gtfs_trip_updates', getApiDescription(API_DESCRIPTIONS.gtfs_trip_updates), {
|
|
18
|
+
operator: z.enum(GTFS_OPERATORS).describe(API_DESCRIPTIONS.gtfs_trip_updates.params.operator),
|
|
19
|
+
}, async ({ operator }) => {
|
|
20
|
+
const result = await tripUpdatesApi.getTripUpdates(operator);
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: 'text', text: formatGtfsTripUpdates(result, operator) }],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool for GTFS-RT VehiclePositions.
|
|
3
|
+
*
|
|
4
|
+
* Exposes real-time vehicle positions from GTFS Sweden 3 feeds, covering
|
|
5
|
+
* all Swedish transit operators with real-time data. Returns GPS locations,
|
|
6
|
+
* bearing, speed, and occupancy for vehicles currently in service.
|
|
7
|
+
* Updated every 3 seconds.
|
|
8
|
+
*
|
|
9
|
+
* Requires TRAFIKLAB_GTFS_KEY.
|
|
10
|
+
*/
|
|
11
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { formatGtfsVehiclePositions } from '../../formatting.js';
|
|
14
|
+
import { GTFS_OPERATORS } from './operators.js';
|
|
15
|
+
export function registerGtfsVehiclePositionsTools(server, vehiclePositionsApi) {
|
|
16
|
+
server.tool('gtfs_vehicle_positions', getApiDescription(API_DESCRIPTIONS.gtfs_vehicle_positions), {
|
|
17
|
+
operator: z
|
|
18
|
+
.enum(GTFS_OPERATORS)
|
|
19
|
+
.describe(API_DESCRIPTIONS.gtfs_vehicle_positions.params.operator),
|
|
20
|
+
}, async ({ operator }) => {
|
|
21
|
+
const result = await vehiclePositionsApi.getVehiclePositions(operator);
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: 'text', text: formatGtfsVehiclePositions(result, operator) }],
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool for the SL Deviations API.
|
|
3
|
+
*
|
|
4
|
+
* Exposes service alerts and disruptions for Stockholm's transit network.
|
|
5
|
+
* No API key required.
|
|
6
|
+
*/
|
|
7
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { formatSLDeviations } from '../../formatting.js';
|
|
10
|
+
const TRANSPORT_MODES = ['BUS', 'METRO', 'TRAM', 'TRAIN', 'SHIP', 'FERRY', 'TAXI'];
|
|
11
|
+
export function registerSLDeviationsTools(server, deviations) {
|
|
12
|
+
/**
|
|
13
|
+
* sl_deviations — Active service alerts across SL's network.
|
|
14
|
+
*
|
|
15
|
+
* Returns disruption messages: elevator/escalator outages, track works,
|
|
16
|
+
* cancellations, timetable changes, and other incidents.
|
|
17
|
+
* Filter by transport mode, line ID, or site ID to narrow results.
|
|
18
|
+
*/
|
|
19
|
+
server.tool('sl_deviations', getApiDescription(API_DESCRIPTIONS.sl_deviations), {
|
|
20
|
+
transport_modes: z
|
|
21
|
+
.array(z.enum(TRANSPORT_MODES))
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(API_DESCRIPTIONS.sl_deviations.params.transport_modes),
|
|
24
|
+
line_ids: z
|
|
25
|
+
.array(z.string())
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(API_DESCRIPTIONS.sl_deviations.params.line_ids),
|
|
28
|
+
site_ids: z
|
|
29
|
+
.array(z.string())
|
|
30
|
+
.optional()
|
|
31
|
+
.describe(API_DESCRIPTIONS.sl_deviations.params.site_ids),
|
|
32
|
+
future: z.boolean().optional().describe(API_DESCRIPTIONS.sl_deviations.params.future),
|
|
33
|
+
}, async ({ transport_modes, line_ids, site_ids, future }) => {
|
|
34
|
+
const parsedLineIds = line_ids?.map(Number);
|
|
35
|
+
const parsedSiteIds = site_ids?.map(Number);
|
|
36
|
+
const result = await deviations.getDeviations({
|
|
37
|
+
transportModes: transport_modes,
|
|
38
|
+
lineIds: parsedLineIds,
|
|
39
|
+
siteIds: parsedSiteIds,
|
|
40
|
+
future,
|
|
41
|
+
});
|
|
42
|
+
const context = buildContext(transport_modes, parsedLineIds, parsedSiteIds);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: formatSLDeviations(result, context) }],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function buildContext(modes, lineIds, siteIds) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (modes && modes.length > 0) {
|
|
51
|
+
parts.push(modes.join('/'));
|
|
52
|
+
}
|
|
53
|
+
if (lineIds && lineIds.length > 0) {
|
|
54
|
+
parts.push(`lines ${lineIds.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
if (siteIds && siteIds.length > 0) {
|
|
57
|
+
parts.push(`sites ${siteIds.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
return parts.length > 0 ? parts.join(', ') : undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools for the SL Transport API (Stockholm-specific).
|
|
3
|
+
*
|
|
4
|
+
* These tools work WITHOUT an API key since the SL Transport API is public.
|
|
5
|
+
* They provide Stockholm-specific data: departures and site lookups.
|
|
6
|
+
*
|
|
7
|
+
* sl_sites fetches from the SL Transport API and caches results in memory
|
|
8
|
+
* for the lifetime of the server — fast lookups after the initial fetch.
|
|
9
|
+
*/
|
|
10
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { formatSLDepartures, formatSLSites } from '../../formatting.js';
|
|
13
|
+
export function registerSLTransportTools(server, sl) {
|
|
14
|
+
/**
|
|
15
|
+
* sl_departures — Real-time SL departures from a Stockholm station.
|
|
16
|
+
*
|
|
17
|
+
* Includes display times ("3 min"), crowding levels, platform info,
|
|
18
|
+
* and disruption messages. Use sl_sites to find site IDs.
|
|
19
|
+
*/
|
|
20
|
+
server.tool('sl_departures', getApiDescription(API_DESCRIPTIONS.sl_departures), {
|
|
21
|
+
site_id: z.string().describe(API_DESCRIPTIONS.sl_departures.params.site_id),
|
|
22
|
+
}, async ({ site_id }) => {
|
|
23
|
+
const id = Number(site_id);
|
|
24
|
+
const result = await sl.getDepartures(id);
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: 'text', text: formatSLDepartures(result, id) }],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* sl_sites — Search SL stations.
|
|
31
|
+
*
|
|
32
|
+
* Fetches from the SL API on first call, then cached. Great for
|
|
33
|
+
* resolving "Slussen" → site ID 9192 before calling sl_departures.
|
|
34
|
+
*/
|
|
35
|
+
server.tool('sl_sites', getApiDescription(API_DESCRIPTIONS.sl_sites), {
|
|
36
|
+
query: z.string().optional().describe(API_DESCRIPTIONS.sl_sites.params.query),
|
|
37
|
+
}, async ({ query }) => {
|
|
38
|
+
const sites = query ? await sl.searchSitesByName(query) : await sl.getCachedSites();
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: formatSLSites(sites, query) }],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool for the Trafiklab Stop Lookup API.
|
|
3
|
+
*
|
|
4
|
+
* Lets the AI search for Swedish public transport stops by name
|
|
5
|
+
* and discover stop IDs needed by other tools (like trafiklab_get_departures).
|
|
6
|
+
*
|
|
7
|
+
* Requires a TRAFIKLAB_API_KEY.
|
|
8
|
+
*/
|
|
9
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { formatTrafiklabStopLookup } from '../../formatting.js';
|
|
12
|
+
export function registerTrafiklabStopLookupTools(server, client) {
|
|
13
|
+
/**
|
|
14
|
+
* trafiklab_search_stops — Find stops by name.
|
|
15
|
+
*
|
|
16
|
+
* Use this as the first step when a user mentions a station or stop name.
|
|
17
|
+
* Returns stop IDs that can be passed to trafiklab_get_departures / trafiklab_get_arrivals.
|
|
18
|
+
*/
|
|
19
|
+
server.tool('trafiklab_search_stops', getApiDescription(API_DESCRIPTIONS.trafiklab_search_stops), {
|
|
20
|
+
query: z.string().describe(API_DESCRIPTIONS.trafiklab_search_stops.params.query),
|
|
21
|
+
}, async ({ query }) => {
|
|
22
|
+
const result = await client.stops.searchByName(query);
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: formatTrafiklabStopLookup(result) }],
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools for the Trafiklab Timetables API.
|
|
3
|
+
*
|
|
4
|
+
* These tools provide real-time departures and arrivals for any Swedish
|
|
5
|
+
* public transport stop. They need a stop area ID — use trafiklab_search_stops
|
|
6
|
+
* first if you only have a station name.
|
|
7
|
+
*
|
|
8
|
+
* Both tools require a TRAFIKLAB_API_KEY.
|
|
9
|
+
*/
|
|
10
|
+
import { API_DESCRIPTIONS, getApiDescription } from '@transit-se/sdk';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { formatTrafiklabArrivals, formatTrafiklabDepartures } from '../../formatting.js';
|
|
13
|
+
export function registerTrafiklabTimetableTools(server, client) {
|
|
14
|
+
/**
|
|
15
|
+
* trafiklab_get_departures — Real-time departures from a stop.
|
|
16
|
+
*
|
|
17
|
+
* Returns the next 60 minutes of departures including real-time delays,
|
|
18
|
+
* cancellations, platform info, and service alerts.
|
|
19
|
+
*/
|
|
20
|
+
server.tool('trafiklab_get_departures', getApiDescription(API_DESCRIPTIONS.trafiklab_get_departures), {
|
|
21
|
+
area_id: z.string().describe(API_DESCRIPTIONS.trafiklab_get_departures.params.area_id),
|
|
22
|
+
time: z.string().optional().describe(API_DESCRIPTIONS.trafiklab_get_departures.params.time),
|
|
23
|
+
}, async ({ area_id, time }) => {
|
|
24
|
+
const result = await client.timetables.getDepartures(area_id, time);
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: 'text', text: formatTrafiklabDepartures(result) }],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* trafiklab_get_arrivals — Real-time arrivals at a stop.
|
|
31
|
+
*
|
|
32
|
+
* Same as trafiklab_get_departures but for incoming vehicles.
|
|
33
|
+
*/
|
|
34
|
+
server.tool('trafiklab_get_arrivals', getApiDescription(API_DESCRIPTIONS.trafiklab_get_arrivals), {
|
|
35
|
+
area_id: z.string().describe(API_DESCRIPTIONS.trafiklab_get_arrivals.params.area_id),
|
|
36
|
+
time: z.string().optional().describe(API_DESCRIPTIONS.trafiklab_get_arrivals.params.time),
|
|
37
|
+
}, async ({ area_id, time }) => {
|
|
38
|
+
const result = await client.timetables.getArrivals(area_id, time);
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: formatTrafiklabArrivals(result) }],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@transit-se/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Swedish public transit (Trafiklab). Real-time departures, service alerts, and stop lookups as AI-callable tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Rafael Belliard",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/rebelliard/transit-se.git",
|
|
11
|
+
"directory": "packages/mcp"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
15
|
+
"prepublishOnly": "bun run build",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"inspect": "set -a && . ../../.env 2>/dev/null; set +a; DANGEROUSLY_OMIT_AUTH=1 npx @modelcontextprotocol/inspector ${TRAFIKLAB_API_KEY:+-e TRAFIKLAB_API_KEY=$TRAFIKLAB_API_KEY} ${TRAFIKLAB_GTFS_KEY:+-e TRAFIKLAB_GTFS_KEY=$TRAFIKLAB_GTFS_KEY} bun run src/index.ts",
|
|
18
|
+
"tc": "tsc --noEmit",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"lint": "eslint src/",
|
|
21
|
+
"version:patch": "bun ../../scripts/bump-version.ts patch",
|
|
22
|
+
"version:minor": "bun ../../scripts/bump-version.ts minor",
|
|
23
|
+
"version:major": "bun ../../scripts/bump-version.ts major",
|
|
24
|
+
"version:pre": "bun ../../scripts/bump-version.ts prerelease"
|
|
25
|
+
},
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"bin": {
|
|
28
|
+
"transit-mcp": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"model-context-protocol",
|
|
38
|
+
"trafiklab",
|
|
39
|
+
"sl",
|
|
40
|
+
"sverige",
|
|
41
|
+
"sweden",
|
|
42
|
+
"stockholm",
|
|
43
|
+
"lokaltrafik"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
47
|
+
"@transit-se/sdk": "^1.0.0",
|
|
48
|
+
"zod": "^3.25.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "^1.3.9"
|
|
52
|
+
}
|
|
53
|
+
}
|