mcp-swiss 0.1.2 → 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.
@@ -4,6 +4,45 @@ exports.geodataTools = void 0;
4
4
  exports.handleGeodata = handleGeodata;
5
5
  const http_js_1 = require("../utils/http.js");
6
6
  const BASE = "https://api3.geo.admin.ch";
7
+ // ── Helpers ─────────────────────────────────────────────────────────────────
8
+ function slimSearchResult(r) {
9
+ const a = r.attrs;
10
+ return {
11
+ label: a.label,
12
+ lat: a.lat,
13
+ lon: a.lon,
14
+ type: a.origin,
15
+ detail: a.detail,
16
+ };
17
+ }
18
+ function slimSolarResult(r) {
19
+ const a = r.attributes;
20
+ return {
21
+ buildingId: a.building_id,
22
+ roofSurface: a.df_nummer,
23
+ class: a.klasse,
24
+ classText: a.klasse_text,
25
+ area_m2: a.flaeche,
26
+ orientation: a.ausrichtung,
27
+ tilt_deg: a.neigung,
28
+ electricityYield_kWh: a.stromertrag,
29
+ winterYield_kWh: a.stromertrag_winterhalbjahr,
30
+ summerYield_kWh: a.stromertrag_sommerhalbjahr,
31
+ financialReturn_CHF: a.finanzertrag,
32
+ radiation_kWh_m2: a.gstrahlung,
33
+ egid: a.gwr_egid,
34
+ label: a.label,
35
+ };
36
+ }
37
+ function slimIdentifyResult(r) {
38
+ return {
39
+ layer: r.layerName,
40
+ layerId: r.layerBodId,
41
+ id: r.featureId,
42
+ attributes: r.attributes,
43
+ };
44
+ }
45
+ // ── Tool definitions ────────────────────────────────────────────────────────
7
46
  exports.geodataTools = [
8
47
  {
9
48
  name: "geocode",
@@ -77,24 +116,7 @@ exports.geodataTools = [
77
116
  },
78
117
  },
79
118
  ];
80
- // Convert WGS84 to LV95 (Swiss projection) — approximate
81
- function wgs84ToLV95(lat, lng) {
82
- // Approx conversion for API calls that need Swiss coords
83
- const phiPrime = (lat * 3600 - 169028.66) / 10000;
84
- const lambdaPrime = (lng * 3600 - 26782.5) / 10000;
85
- const E = 2600072.37
86
- + 211455.93 * lambdaPrime
87
- - 10938.51 * lambdaPrime * phiPrime
88
- - 0.36 * lambdaPrime * phiPrime ** 2
89
- - 44.54 * lambdaPrime ** 3;
90
- const N = 1200147.07
91
- + 308807.95 * phiPrime
92
- + 3745.25 * lambdaPrime ** 2
93
- + 76.63 * phiPrime ** 2
94
- - 194.56 * lambdaPrime ** 2 * phiPrime
95
- + 119.79 * phiPrime ** 3;
96
- return { x: E, y: N };
97
- }
119
+ // ── Handler ─────────────────────────────────────────────────────────────────
98
120
  async function handleGeodata(name, args) {
99
121
  switch (name) {
100
122
  case "geocode":
@@ -106,44 +128,70 @@ async function handleGeodata(name, args) {
106
128
  limit: 10,
107
129
  });
108
130
  const data = await (0, http_js_1.fetchJSON)(url);
109
- return JSON.stringify(data, null, 2);
131
+ return JSON.stringify({
132
+ count: data.results.length,
133
+ results: data.results.map(slimSearchResult),
134
+ });
110
135
  }
111
136
  case "reverse_geocode": {
112
137
  const lat = args.lat;
113
138
  const lng = args.lng;
114
- const { x, y } = wgs84ToLV95(lat, lng);
115
- const extent = `${x - 100},${y - 100},${x + 100},${y + 100}`;
116
139
  const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/api/SearchServer`, {
117
140
  searchText: `${lat},${lng}`,
118
141
  type: "locations",
119
142
  sr: 4326,
120
143
  limit: 5,
121
144
  });
122
- void extent; // extent used in identify, not reverse geocode search
123
145
  const data = await (0, http_js_1.fetchJSON)(url);
124
- return JSON.stringify(data, null, 2);
146
+ return JSON.stringify({
147
+ count: data.results.length,
148
+ results: data.results.map(slimSearchResult),
149
+ });
125
150
  }
126
151
  case "get_solar_potential": {
127
152
  const lat = args.lat;
128
153
  const lng = args.lng;
129
- const extent = `${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}`;
154
+ // Use tight tolerance to get only the closest building
155
+ const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
130
156
  const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/all/MapServer/identify`, {
131
157
  geometry: `${lng},${lat}`,
132
158
  geometryType: "esriGeometryPoint",
133
159
  layers: "all:ch.bfe.solarenergie-eignung-daecher",
134
160
  mapExtent: extent,
135
161
  imageDisplay: "500,500,96",
136
- tolerance: 100,
162
+ tolerance: 10,
137
163
  sr: 4326,
138
164
  returnGeometry: false,
139
165
  });
140
166
  const data = await (0, http_js_1.fetchJSON)(url);
141
- return JSON.stringify(data, null, 2);
167
+ const roofs = data.results.map(slimSolarResult);
168
+ // Group by building and summarize
169
+ const buildingMap = new Map();
170
+ for (const r of roofs) {
171
+ const id = r.buildingId;
172
+ if (!buildingMap.has(id))
173
+ buildingMap.set(id, []);
174
+ buildingMap.get(id).push(r);
175
+ }
176
+ const buildings = [...buildingMap.entries()].map(([buildingId, surfaces]) => ({
177
+ buildingId,
178
+ totalArea_m2: Math.round(surfaces.reduce((s, r) => s + (r.area_m2 ?? 0), 0)),
179
+ totalElectricity_kWh: Math.round(surfaces.reduce((s, r) => s + (r.electricityYield_kWh ?? 0), 0)),
180
+ totalFinancialReturn_CHF: Math.round(surfaces.reduce((s, r) => s + (r.financialReturn_CHF ?? 0), 0)),
181
+ roofSurfaces: surfaces.length,
182
+ bestClass: Math.min(...surfaces.map((r) => r.class).filter((c) => c != null)),
183
+ surfaces: surfaces.slice(0, 5), // Cap at 5 surfaces per building
184
+ }));
185
+ return JSON.stringify({
186
+ count: buildings.length,
187
+ buildings: buildings.slice(0, 10), // Cap at 10 buildings
188
+ source: "Swiss Federal Office of Energy (BFE)",
189
+ });
142
190
  }
143
191
  case "identify_location": {
144
192
  const lat = args.lat;
145
193
  const lng = args.lng;
146
- const extent = `${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}`;
194
+ const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
147
195
  const layers = args.layers ? `all:${args.layers}` : "all";
148
196
  const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/all/MapServer/identify`, {
149
197
  geometry: `${lng},${lat}`,
@@ -156,7 +204,10 @@ async function handleGeodata(name, args) {
156
204
  returnGeometry: false,
157
205
  });
158
206
  const data = await (0, http_js_1.fetchJSON)(url);
159
- return JSON.stringify(data, null, 2);
207
+ return JSON.stringify({
208
+ count: data.results.length,
209
+ results: data.results.slice(0, 20).map(slimIdentifyResult),
210
+ });
160
211
  }
161
212
  case "get_municipality": {
162
213
  const url = (0, http_js_1.buildUrl)(`${BASE}/rest/services/api/SearchServer`, {
@@ -166,7 +217,10 @@ async function handleGeodata(name, args) {
166
217
  limit: 5,
167
218
  });
168
219
  const data = await (0, http_js_1.fetchJSON)(url);
169
- return JSON.stringify(data, null, 2);
220
+ return JSON.stringify({
221
+ count: data.results.length,
222
+ results: data.results.map(slimSearchResult),
223
+ });
170
224
  }
171
225
  default:
172
226
  throw new Error(`Unknown geodata tool: ${name}`);
@@ -4,6 +4,46 @@ exports.transportTools = void 0;
4
4
  exports.handleTransport = handleTransport;
5
5
  const http_js_1 = require("../utils/http.js");
6
6
  const BASE = "https://transport.opendata.ch/v1";
7
+ // ── Helpers ─────────────────────────────────────────────────────────────────
8
+ function slimBoardEntry(entry) {
9
+ return {
10
+ line: `${entry.category}${entry.number}`,
11
+ to: entry.to,
12
+ departure: entry.stop.departure,
13
+ delay: entry.stop.delay,
14
+ platform: entry.stop.platform,
15
+ operator: entry.operator,
16
+ };
17
+ }
18
+ function slimConnection(conn) {
19
+ return {
20
+ from: conn.from.station?.name,
21
+ to: conn.to.station?.name,
22
+ departure: conn.from.departure,
23
+ arrival: conn.to.arrival,
24
+ duration: conn.duration,
25
+ transfers: conn.transfers,
26
+ sections: conn.sections.map((s) => ({
27
+ type: s.journey ? "journey" : "walk",
28
+ line: s.journey ? `${s.journey.category}${s.journey.number}` : undefined,
29
+ from: s.departure.station?.name,
30
+ departure: s.departure.departure,
31
+ platform: s.departure.platform,
32
+ to: s.arrival.station?.name,
33
+ arrival: s.arrival.arrival,
34
+ })),
35
+ };
36
+ }
37
+ function slimStation(s) {
38
+ return {
39
+ id: s.id,
40
+ name: s.name,
41
+ lat: s.coordinate?.x,
42
+ lon: s.coordinate?.y,
43
+ distance: s.distance,
44
+ };
45
+ }
46
+ // ── Tool definitions ────────────────────────────────────────────────────────
7
47
  exports.transportTools = [
8
48
  {
9
49
  name: "search_stations",
@@ -75,6 +115,7 @@ exports.transportTools = [
75
115
  },
76
116
  },
77
117
  ];
118
+ // ── Handler ─────────────────────────────────────────────────────────────────
78
119
  async function handleTransport(name, args) {
79
120
  switch (name) {
80
121
  case "search_stations": {
@@ -85,7 +126,7 @@ async function handleTransport(name, args) {
85
126
  type: args.type,
86
127
  });
87
128
  const data = await (0, http_js_1.fetchJSON)(url);
88
- return JSON.stringify(data.stations, null, 2);
129
+ return JSON.stringify(data.stations.map(slimStation));
89
130
  }
90
131
  case "get_connections": {
91
132
  const url = (0, http_js_1.buildUrl)(`${BASE}/connections`, {
@@ -97,7 +138,7 @@ async function handleTransport(name, args) {
97
138
  isArrivalTime: args.isArrivalTime ? 1 : undefined,
98
139
  });
99
140
  const data = await (0, http_js_1.fetchJSON)(url);
100
- return JSON.stringify(data.connections, null, 2);
141
+ return JSON.stringify(data.connections.map(slimConnection));
101
142
  }
102
143
  case "get_departures": {
103
144
  const url = (0, http_js_1.buildUrl)(`${BASE}/stationboard`, {
@@ -107,7 +148,10 @@ async function handleTransport(name, args) {
107
148
  type: "departure",
108
149
  });
109
150
  const data = await (0, http_js_1.fetchJSON)(url);
110
- return JSON.stringify({ station: data.station, departures: data.stationboard }, null, 2);
151
+ return JSON.stringify({
152
+ station: data.station?.name,
153
+ departures: data.stationboard.map(slimBoardEntry),
154
+ });
111
155
  }
112
156
  case "get_arrivals": {
113
157
  const url = (0, http_js_1.buildUrl)(`${BASE}/stationboard`, {
@@ -117,7 +161,10 @@ async function handleTransport(name, args) {
117
161
  type: "arrival",
118
162
  });
119
163
  const data = await (0, http_js_1.fetchJSON)(url);
120
- return JSON.stringify({ station: data.station, arrivals: data.stationboard }, null, 2);
164
+ return JSON.stringify({
165
+ station: data.station?.name,
166
+ arrivals: data.stationboard.map(slimBoardEntry),
167
+ });
121
168
  }
122
169
  case "get_nearby_stations": {
123
170
  const url = (0, http_js_1.buildUrl)(`${BASE}/locations`, {
@@ -126,7 +173,7 @@ async function handleTransport(name, args) {
126
173
  type: "station",
127
174
  });
128
175
  const data = await (0, http_js_1.fetchJSON)(url);
129
- return JSON.stringify(data.stations, null, 2);
176
+ return JSON.stringify(data.stations.map(slimStation));
130
177
  }
131
178
  default:
132
179
  throw new Error(`Unknown transport tool: ${name}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-swiss",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Swiss open data MCP server — transport, weather, geodata, companies. Zero API keys.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -2,6 +2,91 @@ import { fetchJSON, buildUrl } from "../utils/http.js";
2
2
 
3
3
  const BASE = "https://api3.geo.admin.ch";
4
4
 
5
+ // ── Types ───────────────────────────────────────────────────────────────────
6
+
7
+ interface SearchResult {
8
+ id: number;
9
+ weight: number;
10
+ attrs: Record<string, unknown>;
11
+ }
12
+
13
+ interface SearchResponse {
14
+ results: SearchResult[];
15
+ }
16
+
17
+ interface SolarAttributes {
18
+ building_id: number;
19
+ klasse: number;
20
+ klasse_text: string;
21
+ flaeche: number;
22
+ ausrichtung: string;
23
+ neigung: number;
24
+ stromertrag: number;
25
+ stromertrag_winterhalbjahr: number;
26
+ stromertrag_sommerhalbjahr: number;
27
+ finanzertrag: number;
28
+ gstrahlung: number;
29
+ gwr_egid: number;
30
+ df_nummer: number;
31
+ label: string;
32
+ }
33
+
34
+ interface IdentifyResult {
35
+ layerBodId: string;
36
+ layerName: string;
37
+ featureId: number;
38
+ id: number;
39
+ attributes: Record<string, unknown>;
40
+ }
41
+
42
+ interface IdentifyResponse {
43
+ results: IdentifyResult[];
44
+ }
45
+
46
+ // ── Helpers ─────────────────────────────────────────────────────────────────
47
+
48
+ function slimSearchResult(r: SearchResult) {
49
+ const a = r.attrs;
50
+ return {
51
+ label: a.label,
52
+ lat: a.lat,
53
+ lon: a.lon,
54
+ type: a.origin,
55
+ detail: a.detail,
56
+ };
57
+ }
58
+
59
+ function slimSolarResult(r: IdentifyResult) {
60
+ const a = r.attributes as unknown as SolarAttributes;
61
+ return {
62
+ buildingId: a.building_id,
63
+ roofSurface: a.df_nummer,
64
+ class: a.klasse,
65
+ classText: a.klasse_text,
66
+ area_m2: a.flaeche,
67
+ orientation: a.ausrichtung,
68
+ tilt_deg: a.neigung,
69
+ electricityYield_kWh: a.stromertrag,
70
+ winterYield_kWh: a.stromertrag_winterhalbjahr,
71
+ summerYield_kWh: a.stromertrag_sommerhalbjahr,
72
+ financialReturn_CHF: a.finanzertrag,
73
+ radiation_kWh_m2: a.gstrahlung,
74
+ egid: a.gwr_egid,
75
+ label: a.label,
76
+ };
77
+ }
78
+
79
+ function slimIdentifyResult(r: IdentifyResult) {
80
+ return {
81
+ layer: r.layerName,
82
+ layerId: r.layerBodId,
83
+ id: r.featureId,
84
+ attributes: r.attributes,
85
+ };
86
+ }
87
+
88
+ // ── Tool definitions ────────────────────────────────────────────────────────
89
+
5
90
  export const geodataTools = [
6
91
  {
7
92
  name: "geocode",
@@ -76,24 +161,7 @@ export const geodataTools = [
76
161
  },
77
162
  ];
78
163
 
79
- // Convert WGS84 to LV95 (Swiss projection) — approximate
80
- function wgs84ToLV95(lat: number, lng: number): { x: number; y: number } {
81
- // Approx conversion for API calls that need Swiss coords
82
- const phiPrime = (lat * 3600 - 169028.66) / 10000;
83
- const lambdaPrime = (lng * 3600 - 26782.5) / 10000;
84
- const E = 2600072.37
85
- + 211455.93 * lambdaPrime
86
- - 10938.51 * lambdaPrime * phiPrime
87
- - 0.36 * lambdaPrime * phiPrime ** 2
88
- - 44.54 * lambdaPrime ** 3;
89
- const N = 1200147.07
90
- + 308807.95 * phiPrime
91
- + 3745.25 * lambdaPrime ** 2
92
- + 76.63 * phiPrime ** 2
93
- - 194.56 * lambdaPrime ** 2 * phiPrime
94
- + 119.79 * phiPrime ** 3;
95
- return { x: E, y: N };
96
- }
164
+ // ── Handler ─────────────────────────────────────────────────────────────────
97
165
 
98
166
  export async function handleGeodata(name: string, args: Record<string, unknown>): Promise<string> {
99
167
  switch (name) {
@@ -105,48 +173,76 @@ export async function handleGeodata(name: string, args: Record<string, unknown>)
105
173
  sr: 4326,
106
174
  limit: 10,
107
175
  });
108
- const data = await fetchJSON<unknown>(url);
109
- return JSON.stringify(data, null, 2);
176
+ const data = await fetchJSON<SearchResponse>(url);
177
+ return JSON.stringify({
178
+ count: data.results.length,
179
+ results: data.results.map(slimSearchResult),
180
+ });
110
181
  }
111
182
 
112
183
  case "reverse_geocode": {
113
184
  const lat = args.lat as number;
114
185
  const lng = args.lng as number;
115
- const { x, y } = wgs84ToLV95(lat, lng);
116
- const extent = `${x - 100},${y - 100},${x + 100},${y + 100}`;
117
186
  const url = buildUrl(`${BASE}/rest/services/api/SearchServer`, {
118
187
  searchText: `${lat},${lng}`,
119
188
  type: "locations",
120
189
  sr: 4326,
121
190
  limit: 5,
122
191
  });
123
- void extent; // extent used in identify, not reverse geocode search
124
- const data = await fetchJSON<unknown>(url);
125
- return JSON.stringify(data, null, 2);
192
+ const data = await fetchJSON<SearchResponse>(url);
193
+ return JSON.stringify({
194
+ count: data.results.length,
195
+ results: data.results.map(slimSearchResult),
196
+ });
126
197
  }
127
198
 
128
199
  case "get_solar_potential": {
129
200
  const lat = args.lat as number;
130
201
  const lng = args.lng as number;
131
- const extent = `${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}`;
202
+ // Use tight tolerance to get only the closest building
203
+ const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
132
204
  const url = buildUrl(`${BASE}/rest/services/all/MapServer/identify`, {
133
205
  geometry: `${lng},${lat}`,
134
206
  geometryType: "esriGeometryPoint",
135
207
  layers: "all:ch.bfe.solarenergie-eignung-daecher",
136
208
  mapExtent: extent,
137
209
  imageDisplay: "500,500,96",
138
- tolerance: 100,
210
+ tolerance: 10,
139
211
  sr: 4326,
140
212
  returnGeometry: false,
141
213
  });
142
- const data = await fetchJSON<unknown>(url);
143
- return JSON.stringify(data, null, 2);
214
+ const data = await fetchJSON<IdentifyResponse>(url);
215
+ const roofs = data.results.map(slimSolarResult);
216
+
217
+ // Group by building and summarize
218
+ const buildingMap = new Map<number, typeof roofs>();
219
+ for (const r of roofs) {
220
+ const id = r.buildingId;
221
+ if (!buildingMap.has(id)) buildingMap.set(id, []);
222
+ buildingMap.get(id)!.push(r);
223
+ }
224
+
225
+ const buildings = [...buildingMap.entries()].map(([buildingId, surfaces]) => ({
226
+ buildingId,
227
+ totalArea_m2: Math.round(surfaces.reduce((s, r) => s + (r.area_m2 ?? 0), 0)),
228
+ totalElectricity_kWh: Math.round(surfaces.reduce((s, r) => s + (r.electricityYield_kWh ?? 0), 0)),
229
+ totalFinancialReturn_CHF: Math.round(surfaces.reduce((s, r) => s + (r.financialReturn_CHF ?? 0), 0)),
230
+ roofSurfaces: surfaces.length,
231
+ bestClass: Math.min(...surfaces.map((r) => r.class).filter((c) => c != null)),
232
+ surfaces: surfaces.slice(0, 5), // Cap at 5 surfaces per building
233
+ }));
234
+
235
+ return JSON.stringify({
236
+ count: buildings.length,
237
+ buildings: buildings.slice(0, 10), // Cap at 10 buildings
238
+ source: "Swiss Federal Office of Energy (BFE)",
239
+ });
144
240
  }
145
241
 
146
242
  case "identify_location": {
147
243
  const lat = args.lat as number;
148
244
  const lng = args.lng as number;
149
- const extent = `${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}`;
245
+ const extent = `${lng - 0.001},${lat - 0.001},${lng + 0.001},${lat + 0.001}`;
150
246
  const layers = args.layers ? `all:${args.layers}` : "all";
151
247
  const url = buildUrl(`${BASE}/rest/services/all/MapServer/identify`, {
152
248
  geometry: `${lng},${lat}`,
@@ -158,8 +254,11 @@ export async function handleGeodata(name: string, args: Record<string, unknown>)
158
254
  sr: 4326,
159
255
  returnGeometry: false,
160
256
  });
161
- const data = await fetchJSON<unknown>(url);
162
- return JSON.stringify(data, null, 2);
257
+ const data = await fetchJSON<IdentifyResponse>(url);
258
+ return JSON.stringify({
259
+ count: data.results.length,
260
+ results: data.results.slice(0, 20).map(slimIdentifyResult),
261
+ });
163
262
  }
164
263
 
165
264
  case "get_municipality": {
@@ -169,8 +268,11 @@ export async function handleGeodata(name: string, args: Record<string, unknown>)
169
268
  sr: 4326,
170
269
  limit: 5,
171
270
  });
172
- const data = await fetchJSON<unknown>(url);
173
- return JSON.stringify(data, null, 2);
271
+ const data = await fetchJSON<SearchResponse>(url);
272
+ return JSON.stringify({
273
+ count: data.results.length,
274
+ results: data.results.map(slimSearchResult),
275
+ });
174
276
  }
175
277
 
176
278
  default:
@@ -2,6 +2,116 @@ import { fetchJSON, buildUrl } from "../utils/http.js";
2
2
 
3
3
  const BASE = "https://transport.opendata.ch/v1";
4
4
 
5
+ // ── Types ───────────────────────────────────────────────────────────────────
6
+
7
+ interface Station {
8
+ id: string | null;
9
+ name: string | null;
10
+ score: number | null;
11
+ coordinate: { type: string; x: number | null; y: number | null };
12
+ distance: number | null;
13
+ }
14
+
15
+ interface Prognosis {
16
+ platform: string | null;
17
+ arrival: string | null;
18
+ departure: string | null;
19
+ capacity1st: number | null;
20
+ capacity2nd: number | null;
21
+ }
22
+
23
+ interface Stop {
24
+ station: Station;
25
+ arrival: string | null;
26
+ departure: string | null;
27
+ delay: number | null;
28
+ platform: string | null;
29
+ prognosis: Prognosis | null;
30
+ }
31
+
32
+ interface BoardEntry {
33
+ stop: Stop;
34
+ name: string;
35
+ category: string;
36
+ number: string;
37
+ operator: string;
38
+ to: string;
39
+ passList: Stop[];
40
+ capacity1st: number | null;
41
+ capacity2nd: number | null;
42
+ }
43
+
44
+ interface Section {
45
+ journey: {
46
+ name: string;
47
+ category: string;
48
+ number: string;
49
+ operator: string;
50
+ to: string;
51
+ passList: Stop[];
52
+ capacity1st: number | null;
53
+ capacity2nd: number | null;
54
+ } | null;
55
+ walk: unknown | null;
56
+ departure: Stop;
57
+ arrival: Stop;
58
+ }
59
+
60
+ interface Connection {
61
+ from: Stop;
62
+ to: Stop;
63
+ duration: string;
64
+ transfers: number;
65
+ sections: Section[];
66
+ capacity1st: number | null;
67
+ capacity2nd: number | null;
68
+ }
69
+
70
+ // ── Helpers ─────────────────────────────────────────────────────────────────
71
+
72
+ function slimBoardEntry(entry: BoardEntry) {
73
+ return {
74
+ line: `${entry.category}${entry.number}`,
75
+ to: entry.to,
76
+ departure: entry.stop.departure,
77
+ delay: entry.stop.delay,
78
+ platform: entry.stop.platform,
79
+ operator: entry.operator,
80
+ };
81
+ }
82
+
83
+ function slimConnection(conn: Connection) {
84
+ return {
85
+ from: conn.from.station?.name,
86
+ to: conn.to.station?.name,
87
+ departure: conn.from.departure,
88
+ arrival: conn.to.arrival,
89
+ duration: conn.duration,
90
+ transfers: conn.transfers,
91
+ sections: conn.sections.map((s) => ({
92
+ type: s.journey ? "journey" : "walk",
93
+ line: s.journey ? `${s.journey.category}${s.journey.number}` : undefined,
94
+ from: s.departure.station?.name,
95
+ departure: s.departure.departure,
96
+ platform: s.departure.platform,
97
+ to: s.arrival.station?.name,
98
+ arrival: s.arrival.arrival,
99
+ })),
100
+ };
101
+ }
102
+
103
+ function slimStation(s: Station) {
104
+ return {
105
+ id: s.id,
106
+ name: s.name,
107
+ lat: s.coordinate?.x,
108
+ lon: s.coordinate?.y,
109
+ distance: s.distance,
110
+ };
111
+ }
112
+
113
+ // ── Tool definitions ────────────────────────────────────────────────────────
114
+
5
115
  export const transportTools = [
6
116
  {
7
117
  name: "search_stations",
@@ -74,6 +184,8 @@ export const transportTools = [
74
184
  },
75
185
  ];
76
186
 
187
+ // ── Handler ─────────────────────────────────────────────────────────────────
188
+
77
189
  export async function handleTransport(name: string, args: Record<string, unknown>): Promise<string> {
78
190
  switch (name) {
79
191
  case "search_stations": {
@@ -83,8 +195,8 @@ export async function handleTransport(name: string, args: Record<string, unknown
83
195
  y: args.y as number,
84
196
  type: args.type as string,
85
197
  });
86
- const data = await fetchJSON<{ stations: unknown[] }>(url);
87
- return JSON.stringify(data.stations, null, 2);
198
+ const data = await fetchJSON<{ stations: Station[] }>(url);
199
+ return JSON.stringify(data.stations.map(slimStation));
88
200
  }
89
201
 
90
202
  case "get_connections": {
@@ -96,8 +208,8 @@ export async function handleTransport(name: string, args: Record<string, unknown
96
208
  limit: args.limit as number,
97
209
  isArrivalTime: args.isArrivalTime ? 1 : undefined,
98
210
  });
99
- const data = await fetchJSON<{ connections: unknown[] }>(url);
100
- return JSON.stringify(data.connections, null, 2);
211
+ const data = await fetchJSON<{ connections: Connection[] }>(url);
212
+ return JSON.stringify(data.connections.map(slimConnection));
101
213
  }
102
214
 
103
215
  case "get_departures": {
@@ -107,8 +219,11 @@ export async function handleTransport(name: string, args: Record<string, unknown
107
219
  datetime: args.datetime as string,
108
220
  type: "departure",
109
221
  });
110
- const data = await fetchJSON<{ station: unknown; stationboard: unknown[] }>(url);
111
- return JSON.stringify({ station: data.station, departures: data.stationboard }, null, 2);
222
+ const data = await fetchJSON<{ station: Station; stationboard: BoardEntry[] }>(url);
223
+ return JSON.stringify({
224
+ station: data.station?.name,
225
+ departures: data.stationboard.map(slimBoardEntry),
226
+ });
112
227
  }
113
228
 
114
229
  case "get_arrivals": {
@@ -118,8 +233,11 @@ export async function handleTransport(name: string, args: Record<string, unknown
118
233
  datetime: args.datetime as string,
119
234
  type: "arrival",
120
235
  });
121
- const data = await fetchJSON<{ station: unknown; stationboard: unknown[] }>(url);
122
- return JSON.stringify({ station: data.station, arrivals: data.stationboard }, null, 2);
236
+ const data = await fetchJSON<{ station: Station; stationboard: BoardEntry[] }>(url);
237
+ return JSON.stringify({
238
+ station: data.station?.name,
239
+ arrivals: data.stationboard.map(slimBoardEntry),
240
+ });
123
241
  }
124
242
 
125
243
  case "get_nearby_stations": {
@@ -128,8 +246,8 @@ export async function handleTransport(name: string, args: Record<string, unknown
128
246
  y: args.y as number,
129
247
  type: "station",
130
248
  });
131
- const data = await fetchJSON<{ stations: unknown[] }>(url);
132
- return JSON.stringify(data.stations, null, 2);
249
+ const data = await fetchJSON<{ stations: Station[] }>(url);
250
+ return JSON.stringify(data.stations.map(slimStation));
133
251
  }
134
252
 
135
253
  default: