mcp-swiss 0.1.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 +175 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -0
- package/dist/modules/companies.d.ts +83 -0
- package/dist/modules/companies.js +156 -0
- package/dist/modules/geodata.d.ts +109 -0
- package/dist/modules/geodata.js +174 -0
- package/dist/modules/transport.d.ts +141 -0
- package/dist/modules/transport.js +134 -0
- package/dist/modules/weather.d.ts +50 -0
- package/dist/modules/weather.js +128 -0
- package/dist/utils/http.d.ts +2 -0
- package/dist/utils/http.js +27 -0
- package/package.json +64 -0
- package/src/index.ts +70 -0
- package/src/modules/companies.ts +154 -0
- package/src/modules/geodata.ts +179 -0
- package/src/modules/transport.ts +138 -0
- package/src/modules/weather.ts +133 -0
- package/src/utils/http.ts +26 -0
- package/tsconfig.json +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-swiss",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Swiss open data MCP server — transport, weather, geodata, companies. Zero API keys.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-swiss": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"test": "vitest run tests/unit tests/mcp",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:coverage": "vitest run --coverage",
|
|
17
|
+
"test:unit": "vitest run tests/unit",
|
|
18
|
+
"test:integration": "vitest run tests/integration",
|
|
19
|
+
"lint": "eslint src/",
|
|
20
|
+
"lint:fix": "eslint src/ --fix",
|
|
21
|
+
"validate": "npm run lint && npm run build && npm test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"switzerland",
|
|
27
|
+
"swiss",
|
|
28
|
+
"sbb",
|
|
29
|
+
"transport",
|
|
30
|
+
"weather",
|
|
31
|
+
"meteoswiss",
|
|
32
|
+
"swisstopo",
|
|
33
|
+
"zefix",
|
|
34
|
+
"opendata",
|
|
35
|
+
"ai",
|
|
36
|
+
"claude",
|
|
37
|
+
"cursor"
|
|
38
|
+
],
|
|
39
|
+
"author": "Vikram Gorla",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/vikramgorla/mcp-swiss.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/vikramgorla/mcp-swiss/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/vikramgorla/mcp-swiss#readme",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^10.0.1",
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
56
|
+
"eslint": "^10.0.3",
|
|
57
|
+
"typescript": "^5.3.0",
|
|
58
|
+
"typescript-eslint": "^8.56.1",
|
|
59
|
+
"vitest": "^4.0.18"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
import { transportTools, handleTransport } from "./modules/transport.js";
|
|
10
|
+
import { weatherTools, handleWeather } from "./modules/weather.js";
|
|
11
|
+
import { geodataTools, handleGeodata } from "./modules/geodata.js";
|
|
12
|
+
import { companiesTools, handleCompanies } from "./modules/companies.js";
|
|
13
|
+
|
|
14
|
+
const server = new Server(
|
|
15
|
+
{ name: "mcp-swiss", version: "0.1.0" },
|
|
16
|
+
{ capabilities: { tools: {} } }
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const allTools = [
|
|
20
|
+
...transportTools,
|
|
21
|
+
...weatherTools,
|
|
22
|
+
...geodataTools,
|
|
23
|
+
...companiesTools,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
27
|
+
tools: allTools,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
31
|
+
const { name, arguments: args } = request.params;
|
|
32
|
+
const safeArgs = (args ?? {}) as Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
let result: string;
|
|
36
|
+
|
|
37
|
+
if (transportTools.some((t) => t.name === name)) {
|
|
38
|
+
result = await handleTransport(name, safeArgs);
|
|
39
|
+
} else if (weatherTools.some((t) => t.name === name)) {
|
|
40
|
+
result = await handleWeather(name, safeArgs);
|
|
41
|
+
} else if (geodataTools.some((t) => t.name === name)) {
|
|
42
|
+
result = await handleGeodata(name, safeArgs);
|
|
43
|
+
} else if (companiesTools.some((t) => t.name === name)) {
|
|
44
|
+
result = await handleCompanies(name, safeArgs);
|
|
45
|
+
} else {
|
|
46
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: result }],
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const transport = new StdioServerTransport();
|
|
63
|
+
await server.connect(transport);
|
|
64
|
+
process.stderr.write("mcp-swiss running on stdio\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((err) => {
|
|
68
|
+
process.stderr.write(`Fatal: ${err}\n`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { fetchJSON } from "../utils/http.js";
|
|
2
|
+
|
|
3
|
+
const BASE = "https://www.zefix.admin.ch/ZefixREST/api/v1";
|
|
4
|
+
|
|
5
|
+
export const companiesTools = [
|
|
6
|
+
{
|
|
7
|
+
name: "search_companies",
|
|
8
|
+
description: "Search Swiss company registry (ZEFIX) by name, canton, or legal form",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
required: ["name"],
|
|
12
|
+
properties: {
|
|
13
|
+
name: { type: "string", description: "Company name or partial name to search" },
|
|
14
|
+
canton: { type: "string", description: "Canton abbreviation (e.g. ZH, BE, GE, ZG)" },
|
|
15
|
+
legal_form: { type: "string", description: "Legal form code (e.g. 0106=GmbH, 0101=AG)" },
|
|
16
|
+
limit: { type: "number", description: "Max results (default: 20)" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "get_company",
|
|
22
|
+
description: "Get full details of a Swiss company by its ZEFIX internal ID (ehraid). Use search_companies first to find the ehraid — it is returned in company search results.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
required: ["ehraid"],
|
|
26
|
+
properties: {
|
|
27
|
+
ehraid: { type: "number", description: "Company internal ZEFIX ID (ehraid integer, e.g. 119283). Returned by search_companies." },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "search_companies_by_address",
|
|
33
|
+
description: "Search Swiss companies registered at a specific address or locality",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
required: ["address"],
|
|
37
|
+
properties: {
|
|
38
|
+
address: { type: "string", description: "Address or locality name" },
|
|
39
|
+
limit: { type: "number", description: "Max results (default: 20)" },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "list_cantons",
|
|
45
|
+
description: "List all Swiss cantons with their codes",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "list_legal_forms",
|
|
53
|
+
description: "List all Swiss company legal forms (AG, GmbH, etc.)",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export async function handleCompanies(name: string, args: Record<string, unknown>): Promise<string> {
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "search_companies": {
|
|
64
|
+
const body: Record<string, unknown> = {
|
|
65
|
+
name: args.name as string,
|
|
66
|
+
maxEntries: (args.limit as number) ?? 20,
|
|
67
|
+
languageKey: "en",
|
|
68
|
+
};
|
|
69
|
+
if (args.canton) body.cantonAbbreviation = [args.canton as string];
|
|
70
|
+
if (args.legal_form) body.legalFormCode = args.legal_form as string;
|
|
71
|
+
|
|
72
|
+
const response = await fetch(`${BASE}/firm/search.json`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
75
|
+
body: JSON.stringify(body),
|
|
76
|
+
});
|
|
77
|
+
if (response.status === 404) {
|
|
78
|
+
return JSON.stringify({ companies: [], hasMoreResults: false }, null, 2);
|
|
79
|
+
}
|
|
80
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
81
|
+
const data = await response.json() as { list?: unknown[]; hasMoreResults?: boolean; error?: unknown };
|
|
82
|
+
if (data.error) return JSON.stringify({ companies: [], hasMoreResults: false }, null, 2);
|
|
83
|
+
return JSON.stringify({ companies: data.list ?? [], hasMoreResults: data.hasMoreResults ?? false }, null, 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "get_company": {
|
|
87
|
+
// ZEFIX firm/{id}.json uses the internal ehraid integer (not CHE uid)
|
|
88
|
+
const ehraid = args.ehraid as number;
|
|
89
|
+
const url = `${BASE}/firm/${ehraid}.json`;
|
|
90
|
+
const data = await fetchJSON<unknown>(url);
|
|
91
|
+
return JSON.stringify(data, null, 2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "search_companies_by_address": {
|
|
95
|
+
const body = {
|
|
96
|
+
name: args.address as string,
|
|
97
|
+
maxEntries: (args.limit as number) ?? 20,
|
|
98
|
+
languageKey: "en",
|
|
99
|
+
};
|
|
100
|
+
const response = await fetch(`${BASE}/firm/search.json`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
if (response.status === 404) return JSON.stringify({ companies: [], hasMoreResults: false }, null, 2);
|
|
106
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
107
|
+
const data = await response.json() as { list?: unknown[]; hasMoreResults?: boolean; error?: unknown };
|
|
108
|
+
if (data.error) return JSON.stringify({ companies: [], hasMoreResults: false }, null, 2);
|
|
109
|
+
return JSON.stringify({ companies: data.list ?? [], hasMoreResults: data.hasMoreResults ?? false }, null, 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "list_cantons": {
|
|
113
|
+
// ZEFIX /cantons and /legalForms endpoints require authentication (403).
|
|
114
|
+
// Return hardcoded authoritative list instead.
|
|
115
|
+
const cantons = [
|
|
116
|
+
{ code: "AG", name: "Aargau" }, { code: "AI", name: "Appenzell Innerrhoden" },
|
|
117
|
+
{ code: "AR", name: "Appenzell Ausserrhoden" }, { code: "BE", name: "Bern" },
|
|
118
|
+
{ code: "BL", name: "Basel-Landschaft" }, { code: "BS", name: "Basel-Stadt" },
|
|
119
|
+
{ code: "FR", name: "Fribourg" }, { code: "GE", name: "Geneva" },
|
|
120
|
+
{ code: "GL", name: "Glarus" }, { code: "GR", name: "Graubünden" },
|
|
121
|
+
{ code: "JU", name: "Jura" }, { code: "LU", name: "Lucerne" },
|
|
122
|
+
{ code: "NE", name: "Neuchâtel" }, { code: "NW", name: "Nidwalden" },
|
|
123
|
+
{ code: "OW", name: "Obwalden" }, { code: "SG", name: "St. Gallen" },
|
|
124
|
+
{ code: "SH", name: "Schaffhausen" }, { code: "SO", name: "Solothurn" },
|
|
125
|
+
{ code: "SZ", name: "Schwyz" }, { code: "TG", name: "Thurgau" },
|
|
126
|
+
{ code: "TI", name: "Ticino" }, { code: "UR", name: "Uri" },
|
|
127
|
+
{ code: "VD", name: "Vaud" }, { code: "VS", name: "Valais" },
|
|
128
|
+
{ code: "ZG", name: "Zug" }, { code: "ZH", name: "Zürich" },
|
|
129
|
+
];
|
|
130
|
+
return JSON.stringify(cantons, null, 2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case "list_legal_forms": {
|
|
134
|
+
// ZEFIX /legalForms requires authentication (403). Return common Swiss legal forms.
|
|
135
|
+
const forms = [
|
|
136
|
+
{ code: "0101", name: "Einzelunternehmen", nameEn: "Sole proprietorship" },
|
|
137
|
+
{ code: "0103", name: "Kollektivgesellschaft", nameEn: "General partnership" },
|
|
138
|
+
{ code: "0104", name: "Kommanditgesellschaft", nameEn: "Limited partnership" },
|
|
139
|
+
{ code: "0105", name: "Aktiengesellschaft (AG)", nameEn: "Corporation (AG)" },
|
|
140
|
+
{ code: "0106", name: "Gesellschaft mit beschränkter Haftung (GmbH)", nameEn: "Limited liability company (GmbH)" },
|
|
141
|
+
{ code: "0107", name: "Genossenschaft", nameEn: "Cooperative" },
|
|
142
|
+
{ code: "0108", name: "Verein", nameEn: "Association" },
|
|
143
|
+
{ code: "0109", name: "Stiftung", nameEn: "Foundation" },
|
|
144
|
+
{ code: "0110", name: "Kommanditaktiengesellschaft", nameEn: "Partnership limited by shares" },
|
|
145
|
+
{ code: "0113", name: "Filiale ausländischer Gesellschaft", nameEn: "Branch of foreign company" },
|
|
146
|
+
{ code: "0114", name: "Institut des öffentlichen Rechts", nameEn: "Public law institution" },
|
|
147
|
+
];
|
|
148
|
+
return JSON.stringify(forms, null, 2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unknown companies tool: ${name}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { fetchJSON, buildUrl } from "../utils/http.js";
|
|
2
|
+
|
|
3
|
+
const BASE = "https://api3.geo.admin.ch";
|
|
4
|
+
|
|
5
|
+
export const geodataTools = [
|
|
6
|
+
{
|
|
7
|
+
name: "geocode",
|
|
8
|
+
description: "Convert a Swiss address or place name to coordinates (swisstopo)",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
required: ["address"],
|
|
12
|
+
properties: {
|
|
13
|
+
address: { type: "string", description: "Swiss address or place name" },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "reverse_geocode",
|
|
19
|
+
description: "Convert coordinates to a Swiss address (swisstopo)",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
required: ["lat", "lng"],
|
|
23
|
+
properties: {
|
|
24
|
+
lat: { type: "number", description: "Latitude (WGS84)" },
|
|
25
|
+
lng: { type: "number", description: "Longitude (WGS84)" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "search_places",
|
|
31
|
+
description: "Search Swiss place names, localities, mountains, and geographic features",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
required: ["query"],
|
|
35
|
+
properties: {
|
|
36
|
+
query: { type: "string", description: "Place name to search" },
|
|
37
|
+
type: { type: "string", description: "Type filter: locations, featuresearch" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "get_solar_potential",
|
|
43
|
+
description: "Get rooftop solar energy potential for a location in Switzerland",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
required: ["lat", "lng"],
|
|
47
|
+
properties: {
|
|
48
|
+
lat: { type: "number", description: "Latitude (WGS84)" },
|
|
49
|
+
lng: { type: "number", description: "Longitude (WGS84)" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "identify_location",
|
|
55
|
+
description: "Identify geographic features and data layers at a specific Swiss location",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
required: ["lat", "lng"],
|
|
59
|
+
properties: {
|
|
60
|
+
lat: { type: "number", description: "Latitude (WGS84)" },
|
|
61
|
+
lng: { type: "number", description: "Longitude (WGS84)" },
|
|
62
|
+
layers: { type: "string", description: "Comma-separated layer ids (default: all visible)" },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "get_municipality",
|
|
68
|
+
description: "Get information about a Swiss municipality by name",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
required: ["name"],
|
|
72
|
+
properties: {
|
|
73
|
+
name: { type: "string", description: "Municipality name" },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
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
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function handleGeodata(name: string, args: Record<string, unknown>): Promise<string> {
|
|
99
|
+
switch (name) {
|
|
100
|
+
case "geocode":
|
|
101
|
+
case "search_places": {
|
|
102
|
+
const url = buildUrl(`${BASE}/rest/services/api/SearchServer`, {
|
|
103
|
+
searchText: args.address as string ?? args.query as string,
|
|
104
|
+
type: args.type as string ?? "locations",
|
|
105
|
+
sr: 4326,
|
|
106
|
+
limit: 10,
|
|
107
|
+
});
|
|
108
|
+
const data = await fetchJSON<unknown>(url);
|
|
109
|
+
return JSON.stringify(data, null, 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "reverse_geocode": {
|
|
113
|
+
const lat = args.lat as number;
|
|
114
|
+
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
|
+
const url = buildUrl(`${BASE}/rest/services/api/SearchServer`, {
|
|
118
|
+
searchText: `${lat},${lng}`,
|
|
119
|
+
type: "locations",
|
|
120
|
+
sr: 4326,
|
|
121
|
+
limit: 5,
|
|
122
|
+
});
|
|
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);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "get_solar_potential": {
|
|
129
|
+
const lat = args.lat as number;
|
|
130
|
+
const lng = args.lng as number;
|
|
131
|
+
const extent = `${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}`;
|
|
132
|
+
const url = buildUrl(`${BASE}/rest/services/all/MapServer/identify`, {
|
|
133
|
+
geometry: `${lng},${lat}`,
|
|
134
|
+
geometryType: "esriGeometryPoint",
|
|
135
|
+
layers: "all:ch.bfe.solarenergie-eignung-daecher",
|
|
136
|
+
mapExtent: extent,
|
|
137
|
+
imageDisplay: "500,500,96",
|
|
138
|
+
tolerance: 100,
|
|
139
|
+
sr: 4326,
|
|
140
|
+
returnGeometry: false,
|
|
141
|
+
});
|
|
142
|
+
const data = await fetchJSON<unknown>(url);
|
|
143
|
+
return JSON.stringify(data, null, 2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "identify_location": {
|
|
147
|
+
const lat = args.lat as number;
|
|
148
|
+
const lng = args.lng as number;
|
|
149
|
+
const extent = `${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}`;
|
|
150
|
+
const layers = args.layers ? `all:${args.layers}` : "all";
|
|
151
|
+
const url = buildUrl(`${BASE}/rest/services/all/MapServer/identify`, {
|
|
152
|
+
geometry: `${lng},${lat}`,
|
|
153
|
+
geometryType: "esriGeometryPoint",
|
|
154
|
+
layers,
|
|
155
|
+
mapExtent: extent,
|
|
156
|
+
imageDisplay: "500,500,96",
|
|
157
|
+
tolerance: 5,
|
|
158
|
+
sr: 4326,
|
|
159
|
+
returnGeometry: false,
|
|
160
|
+
});
|
|
161
|
+
const data = await fetchJSON<unknown>(url);
|
|
162
|
+
return JSON.stringify(data, null, 2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "get_municipality": {
|
|
166
|
+
const url = buildUrl(`${BASE}/rest/services/api/SearchServer`, {
|
|
167
|
+
searchText: args.name as string,
|
|
168
|
+
type: "locations",
|
|
169
|
+
sr: 4326,
|
|
170
|
+
limit: 5,
|
|
171
|
+
});
|
|
172
|
+
const data = await fetchJSON<unknown>(url);
|
|
173
|
+
return JSON.stringify(data, null, 2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`Unknown geodata tool: ${name}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { fetchJSON, buildUrl } from "../utils/http.js";
|
|
2
|
+
|
|
3
|
+
const BASE = "https://transport.opendata.ch/v1";
|
|
4
|
+
|
|
5
|
+
export const transportTools = [
|
|
6
|
+
{
|
|
7
|
+
name: "search_stations",
|
|
8
|
+
description: "Search for Swiss public transport stations/stops by name or coordinates",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
query: { type: "string", description: "Station name to search for" },
|
|
13
|
+
x: { type: "number", description: "Longitude (WGS84)" },
|
|
14
|
+
y: { type: "number", description: "Latitude (WGS84)" },
|
|
15
|
+
type: { type: "string", description: "Filter: all, station, poi, address" },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "get_connections",
|
|
21
|
+
description: "Get train/bus connections between two Swiss locations",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
required: ["from", "to"],
|
|
25
|
+
properties: {
|
|
26
|
+
from: { type: "string", description: "Departure station/address" },
|
|
27
|
+
to: { type: "string", description: "Arrival station/address" },
|
|
28
|
+
date: { type: "string", description: "Date YYYY-MM-DD (default: today)" },
|
|
29
|
+
time: { type: "string", description: "Time HH:MM (default: now)" },
|
|
30
|
+
limit: { type: "number", description: "Number of connections (1-16, default: 4)" },
|
|
31
|
+
isArrivalTime: { type: "boolean", description: "True if time is arrival time" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "get_departures",
|
|
37
|
+
description: "Get live departures from a Swiss transport station",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
required: ["station"],
|
|
41
|
+
properties: {
|
|
42
|
+
station: { type: "string", description: "Station name" },
|
|
43
|
+
limit: { type: "number", description: "Number of departures (default: 10)" },
|
|
44
|
+
datetime: { type: "string", description: "DateTime YYYY-MM-DDTHH:MM (default: now)" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "get_arrivals",
|
|
50
|
+
description: "Get live arrivals at a Swiss transport station",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
required: ["station"],
|
|
54
|
+
properties: {
|
|
55
|
+
station: { type: "string", description: "Station name" },
|
|
56
|
+
limit: { type: "number", description: "Number of arrivals (default: 10)" },
|
|
57
|
+
datetime: { type: "string", description: "DateTime YYYY-MM-DDTHH:MM (default: now)" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_nearby_stations",
|
|
63
|
+
description: "Find Swiss public transport stations near given coordinates",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
required: ["x", "y"],
|
|
67
|
+
properties: {
|
|
68
|
+
x: { type: "number", description: "Longitude (WGS84)" },
|
|
69
|
+
y: { type: "number", description: "Latitude (WGS84)" },
|
|
70
|
+
limit: { type: "number", description: "Number of results (default: 10)" },
|
|
71
|
+
distance: { type: "number", description: "Max distance in meters" },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export async function handleTransport(name: string, args: Record<string, unknown>): Promise<string> {
|
|
78
|
+
switch (name) {
|
|
79
|
+
case "search_stations": {
|
|
80
|
+
const url = buildUrl(`${BASE}/locations`, {
|
|
81
|
+
query: args.query as string,
|
|
82
|
+
x: args.x as number,
|
|
83
|
+
y: args.y as number,
|
|
84
|
+
type: args.type as string,
|
|
85
|
+
});
|
|
86
|
+
const data = await fetchJSON<{ stations: unknown[] }>(url);
|
|
87
|
+
return JSON.stringify(data.stations, null, 2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "get_connections": {
|
|
91
|
+
const url = buildUrl(`${BASE}/connections`, {
|
|
92
|
+
from: args.from as string,
|
|
93
|
+
to: args.to as string,
|
|
94
|
+
date: args.date as string,
|
|
95
|
+
time: args.time as string,
|
|
96
|
+
limit: args.limit as number,
|
|
97
|
+
isArrivalTime: args.isArrivalTime ? 1 : undefined,
|
|
98
|
+
});
|
|
99
|
+
const data = await fetchJSON<{ connections: unknown[] }>(url);
|
|
100
|
+
return JSON.stringify(data.connections, null, 2);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case "get_departures": {
|
|
104
|
+
const url = buildUrl(`${BASE}/stationboard`, {
|
|
105
|
+
station: args.station as string,
|
|
106
|
+
limit: args.limit as number,
|
|
107
|
+
datetime: args.datetime as string,
|
|
108
|
+
type: "departure",
|
|
109
|
+
});
|
|
110
|
+
const data = await fetchJSON<{ station: unknown; stationboard: unknown[] }>(url);
|
|
111
|
+
return JSON.stringify({ station: data.station, departures: data.stationboard }, null, 2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "get_arrivals": {
|
|
115
|
+
const url = buildUrl(`${BASE}/stationboard`, {
|
|
116
|
+
station: args.station as string,
|
|
117
|
+
limit: args.limit as number,
|
|
118
|
+
datetime: args.datetime as string,
|
|
119
|
+
type: "arrival",
|
|
120
|
+
});
|
|
121
|
+
const data = await fetchJSON<{ station: unknown; stationboard: unknown[] }>(url);
|
|
122
|
+
return JSON.stringify({ station: data.station, arrivals: data.stationboard }, null, 2);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "get_nearby_stations": {
|
|
126
|
+
const url = buildUrl(`${BASE}/locations`, {
|
|
127
|
+
x: args.x as number,
|
|
128
|
+
y: args.y as number,
|
|
129
|
+
type: "station",
|
|
130
|
+
});
|
|
131
|
+
const data = await fetchJSON<{ stations: unknown[] }>(url);
|
|
132
|
+
return JSON.stringify(data.stations, null, 2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
default:
|
|
136
|
+
throw new Error(`Unknown transport tool: ${name}`);
|
|
137
|
+
}
|
|
138
|
+
}
|