mcp-meteoblue 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 +84 -0
- package/package.json +47 -0
- package/src/constants.js +73 -0
- package/src/index.js +11 -0
- package/src/meteoblue.js +125 -0
- package/src/server.js +124 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 unixfox
|
|
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,84 @@
|
|
|
1
|
+
# mcp-meteoblue
|
|
2
|
+
|
|
3
|
+
An MCP server for the [meteoblue Weather API](https://docs.meteoblue.com/en/weather-apis/introduction/overview), written in JavaScript. It lets MCP clients search for locations, fetch forecasts using packages available through the Free Weather API, and request forecast meteogram images.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
- `search_locations` resolves cities, postal codes, and IATA/ICAO airport codes with meteoblue Location Search.
|
|
8
|
+
- `get_forecast` accepts either a place name or coordinates and returns forecast JSON. It defaults to current, hourly, and daily weather.
|
|
9
|
+
- `get_forecast_image` accepts either a place name or coordinates and returns a forecast image directly to the MCP client.
|
|
10
|
+
|
|
11
|
+
Place names passed to forecast or image tools are always resolved through the official meteoblue Location Search API. Forecast package inputs are restricted to the packages listed in the Free Weather API documentation.
|
|
12
|
+
|
|
13
|
+
> [!NOTE]
|
|
14
|
+
> meteoblue's current Free Weather API documentation says that images require a higher access level. The image tool is included for keys with an Image API entitlement and will return a clear authorization error otherwise.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js 20 or newer
|
|
19
|
+
- A [meteoblue API key](https://www.meteoblue.com/en/weather-api)
|
|
20
|
+
|
|
21
|
+
Keep the key private. This server reads it from `METEOBLUE_API_KEY`; it is never part of the package or MCP tool arguments.
|
|
22
|
+
|
|
23
|
+
## Run from npm
|
|
24
|
+
|
|
25
|
+
Add this stdio server to your MCP client configuration:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"meteoblue": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "mcp-meteoblue"],
|
|
33
|
+
"env": {
|
|
34
|
+
"METEOBLUE_API_KEY": "your_api_key_here"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or run it directly:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
METEOBLUE_API_KEY=your_api_key_here npx -y mcp-meteoblue
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The server uses stdio, so it intentionally produces no normal terminal output while it waits for an MCP client.
|
|
48
|
+
|
|
49
|
+
## Develop locally
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm install
|
|
53
|
+
npm test
|
|
54
|
+
METEOBLUE_API_KEY=your_api_key_here npm start
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Point an MCP client at `node /absolute/path/to/mcp-meteoblue/src/index.js` and set the environment variable in that client's configuration.
|
|
58
|
+
|
|
59
|
+
## Releases
|
|
60
|
+
|
|
61
|
+
GitHub Actions runs the test suite and validates the npm tarball on Node.js 20, 22, 24, and 26 for every push and pull request.
|
|
62
|
+
|
|
63
|
+
Publishing a GitHub release triggers `.github/workflows/publish.yml`. The workflow uses npm trusted publishing (OIDC) and automatically attaches provenance. Configure the npm trusted publisher with:
|
|
64
|
+
|
|
65
|
+
- GitHub owner: `unixfox`
|
|
66
|
+
- Repository: `mcp-meteoblue`
|
|
67
|
+
- Workflow: `publish.yml`
|
|
68
|
+
- Environment: `npm`
|
|
69
|
+
|
|
70
|
+
No npm token is stored in GitHub when trusted publishing is configured.
|
|
71
|
+
|
|
72
|
+
## API scope
|
|
73
|
+
|
|
74
|
+
The server calls only:
|
|
75
|
+
|
|
76
|
+
- `https://www.meteoblue.com/{language}/server/search/query3`
|
|
77
|
+
- `https://my.meteoblue.com/packages/{packages}`
|
|
78
|
+
- `https://my.meteoblue.com/images/{forecast-image-type}`
|
|
79
|
+
|
|
80
|
+
It does not expose meteoblue History, Dataset, Maps, Measurements, or other APIs.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-meteoblue",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for meteoblue forecast, forecast image, and location search APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-meteoblue": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": "./src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"check": "node --check src/index.js && node --check src/meteoblue.js && npm test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"meteoblue",
|
|
24
|
+
"weather",
|
|
25
|
+
"forecast"
|
|
26
|
+
],
|
|
27
|
+
"author": "unixfox",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/unixfox/mcp-meteoblue.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/unixfox/mcp-meteoblue/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/unixfox/mcp-meteoblue#readme",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const FREE_FORECAST_PACKAGES = [
|
|
2
|
+
"basic-15min",
|
|
3
|
+
"basic-1h",
|
|
4
|
+
"basic-3h",
|
|
5
|
+
"basic-day",
|
|
6
|
+
"current",
|
|
7
|
+
"clouds-1h",
|
|
8
|
+
"clouds-3h",
|
|
9
|
+
"clouds-day",
|
|
10
|
+
"sunmoon",
|
|
11
|
+
"agro-1h",
|
|
12
|
+
"agro-3h",
|
|
13
|
+
"agro-day",
|
|
14
|
+
"agromodelleafwetness-1h",
|
|
15
|
+
"agromodelsowing-1h",
|
|
16
|
+
"agromodelspray-1h",
|
|
17
|
+
"solar-15min",
|
|
18
|
+
"solar-1h",
|
|
19
|
+
"solar-3h",
|
|
20
|
+
"solar-day",
|
|
21
|
+
"solarensemble-1h",
|
|
22
|
+
"wind-15min",
|
|
23
|
+
"wind-1h",
|
|
24
|
+
"wind-3h",
|
|
25
|
+
"wind-day",
|
|
26
|
+
"wind80ensemble-1h",
|
|
27
|
+
"sea-1h",
|
|
28
|
+
"sea-3h",
|
|
29
|
+
"sea-day",
|
|
30
|
+
"air-1h",
|
|
31
|
+
"air-3h",
|
|
32
|
+
"air-day",
|
|
33
|
+
"airquality-1h",
|
|
34
|
+
"airquality-3h",
|
|
35
|
+
"airquality-day",
|
|
36
|
+
"multimodel-1h",
|
|
37
|
+
"multimodeltemperature-1h",
|
|
38
|
+
"multimodelprecipitation-1h",
|
|
39
|
+
"multimodelrh-1h",
|
|
40
|
+
"multimodelwind-1h",
|
|
41
|
+
"multimodelwind80-1h",
|
|
42
|
+
"multimodelclouds-1h",
|
|
43
|
+
"multimodelradiation-1h",
|
|
44
|
+
"trend-1h",
|
|
45
|
+
"trend-day",
|
|
46
|
+
"trendpro-1h",
|
|
47
|
+
"trendpro-day",
|
|
48
|
+
"ensemble-1h"
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export const FORECAST_IMAGE_TYPES = [
|
|
52
|
+
"meteogram",
|
|
53
|
+
"meteogram_extended",
|
|
54
|
+
"meteogram_one",
|
|
55
|
+
"meteogram_snow",
|
|
56
|
+
"meteogram_agro",
|
|
57
|
+
"meteogram_sowing",
|
|
58
|
+
"meteogram_spraying",
|
|
59
|
+
"meteogram_soiltraffic",
|
|
60
|
+
"meteogram_air",
|
|
61
|
+
"meteogram_sounding",
|
|
62
|
+
"meteogram_stueve",
|
|
63
|
+
"meteogram_thermal",
|
|
64
|
+
"meteogram_14day",
|
|
65
|
+
"meteogram_multimodel",
|
|
66
|
+
"meteogram_multimodelensemble",
|
|
67
|
+
"meteogram_multimodelwind",
|
|
68
|
+
"meteogram_solar_forecast",
|
|
69
|
+
"meteogram_sea",
|
|
70
|
+
"meteogram_sea_7day",
|
|
71
|
+
"meteogram_surf",
|
|
72
|
+
"meteogram_seasonal"
|
|
73
|
+
];
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const server = createServer();
|
|
7
|
+
await server.connect(new StdioServerTransport());
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error(`mcp-meteoblue: ${error.message}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
package/src/meteoblue.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const API_BASE_URL = "https://my.meteoblue.com";
|
|
2
|
+
const LOCATION_BASE_URL = "https://www.meteoblue.com";
|
|
3
|
+
|
|
4
|
+
export class MeteoblueApiError extends Error {
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "MeteoblueApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MeteoblueClient {
|
|
13
|
+
constructor({ apiKey, fetchImpl = globalThis.fetch, timeoutMs = 30_000 } = {}) {
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw new Error("METEOBLUE_API_KEY is required");
|
|
16
|
+
}
|
|
17
|
+
if (typeof fetchImpl !== "function") {
|
|
18
|
+
throw new Error("A fetch implementation is required");
|
|
19
|
+
}
|
|
20
|
+
this.apiKey = apiKey;
|
|
21
|
+
this.fetch = fetchImpl;
|
|
22
|
+
this.timeoutMs = timeoutMs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async searchLocations(query, { language = "en", countryCode, page = 1, limit = 10 } = {}) {
|
|
26
|
+
const url = new URL(`/${encodeURIComponent(language)}/server/search/query3`, LOCATION_BASE_URL);
|
|
27
|
+
url.searchParams.set("query", query);
|
|
28
|
+
url.searchParams.set("apikey", this.apiKey);
|
|
29
|
+
url.searchParams.set("page", String(page));
|
|
30
|
+
url.searchParams.set("itemsPerPage", String(limit));
|
|
31
|
+
if (countryCode) url.searchParams.set("iso2", countryCode.toUpperCase());
|
|
32
|
+
|
|
33
|
+
return this.#requestJson(url, "location search");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async resolveLocation(query, options = {}) {
|
|
37
|
+
const data = await this.searchLocations(query, { ...options, page: 1, limit: 1 });
|
|
38
|
+
const location = data.results?.[0];
|
|
39
|
+
if (!location) {
|
|
40
|
+
throw new MeteoblueApiError(`No location found for “${query}”`, 404);
|
|
41
|
+
}
|
|
42
|
+
return location;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getForecast({ packages, latitude, longitude, elevation, forecastDays, historyDays, timezone, temperatureUnit, windSpeedUnit, precipitationUnit }) {
|
|
46
|
+
const url = new URL(`/packages/${packages.join(",")}`, API_BASE_URL);
|
|
47
|
+
this.#addCoordinates(url, latitude, longitude, elevation);
|
|
48
|
+
url.searchParams.set("apikey", this.apiKey);
|
|
49
|
+
if (forecastDays !== undefined) url.searchParams.set("forecastDays", String(forecastDays));
|
|
50
|
+
if (historyDays !== undefined) url.searchParams.set("historyDays", String(historyDays));
|
|
51
|
+
if (timezone) url.searchParams.set("tz", timezone);
|
|
52
|
+
if (temperatureUnit) url.searchParams.set("temperatureUnit", temperatureUnit);
|
|
53
|
+
if (windSpeedUnit) url.searchParams.set("windSpeedUnit", windSpeedUnit);
|
|
54
|
+
if (precipitationUnit) url.searchParams.set("precipitationUnit", precipitationUnit);
|
|
55
|
+
|
|
56
|
+
return this.#requestJson(url, "forecast");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getForecastImage({ type, latitude, longitude, elevation, options = {} }) {
|
|
60
|
+
const url = new URL(`/images/${type}`, API_BASE_URL);
|
|
61
|
+
this.#addCoordinates(url, latitude, longitude, elevation);
|
|
62
|
+
url.searchParams.set("apikey", this.apiKey);
|
|
63
|
+
for (const [key, value] of Object.entries(options)) {
|
|
64
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const response = await this.#fetch(url);
|
|
68
|
+
if (!response.ok) throw await this.#apiError(response, "forecast image");
|
|
69
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0] || "image/png";
|
|
70
|
+
if (!contentType.startsWith("image/")) {
|
|
71
|
+
throw new MeteoblueApiError("meteoblue returned a non-image response", response.status);
|
|
72
|
+
}
|
|
73
|
+
const data = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
74
|
+
return { data, mimeType: contentType };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#addCoordinates(url, latitude, longitude, elevation) {
|
|
78
|
+
url.searchParams.set("lat", String(latitude));
|
|
79
|
+
url.searchParams.set("lon", String(longitude));
|
|
80
|
+
if (elevation !== undefined) url.searchParams.set("asl", String(elevation));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async #requestJson(url, operation) {
|
|
84
|
+
const response = await this.#fetch(url);
|
|
85
|
+
if (!response.ok) throw await this.#apiError(response, operation);
|
|
86
|
+
try {
|
|
87
|
+
return await response.json();
|
|
88
|
+
} catch {
|
|
89
|
+
throw new MeteoblueApiError(`meteoblue returned invalid JSON for ${operation}`, response.status);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async #fetch(url) {
|
|
94
|
+
try {
|
|
95
|
+
return await this.fetch(url, {
|
|
96
|
+
headers: { "user-agent": "mcp-meteoblue/1.0.0" },
|
|
97
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.name === "TimeoutError" || error.name === "AbortError") {
|
|
101
|
+
throw new MeteoblueApiError("meteoblue request timed out");
|
|
102
|
+
}
|
|
103
|
+
throw new MeteoblueApiError(`Could not reach meteoblue: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #apiError(response, operation) {
|
|
108
|
+
let detail = "";
|
|
109
|
+
try {
|
|
110
|
+
const body = await response.text();
|
|
111
|
+
if (body && body.length < 500) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(body);
|
|
114
|
+
detail = parsed.message || parsed.error || parsed.detail || "";
|
|
115
|
+
} catch {
|
|
116
|
+
detail = body.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Keep the status-only error when the body cannot be read.
|
|
121
|
+
}
|
|
122
|
+
const suffix = detail ? `: ${detail}` : "";
|
|
123
|
+
return new MeteoblueApiError(`meteoblue ${operation} request failed (${response.status})${suffix}`, response.status);
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { FORECAST_IMAGE_TYPES, FREE_FORECAST_PACKAGES } from "./constants.js";
|
|
4
|
+
import { MeteoblueClient } from "./meteoblue.js";
|
|
5
|
+
|
|
6
|
+
const latitude = z.number().min(-90).max(90).optional().describe("WGS84 latitude; required with longitude when location is omitted");
|
|
7
|
+
const longitude = z.number().min(-180).max(180).optional().describe("WGS84 longitude; required with latitude when location is omitted");
|
|
8
|
+
const elevation = z.number().min(-500).max(9000).optional().describe("Elevation above sea level in metres");
|
|
9
|
+
const location = z.string().min(2).optional().describe("Place name, postal code, IATA, or ICAO code; resolved with meteoblue Location Search");
|
|
10
|
+
|
|
11
|
+
function textResult(value) {
|
|
12
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function errorResult(error) {
|
|
16
|
+
return { isError: true, content: [{ type: "text", text: error.message || "Unexpected error" }] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function coordinatesFor(client, input) {
|
|
20
|
+
if (input.location) {
|
|
21
|
+
const resolved = await client.resolveLocation(input.location, {
|
|
22
|
+
language: input.language,
|
|
23
|
+
countryCode: input.countryCode
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
latitude: resolved.lat,
|
|
27
|
+
longitude: resolved.lon,
|
|
28
|
+
elevation: input.elevation ?? resolved.asl,
|
|
29
|
+
resolvedLocation: resolved
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (input.latitude === undefined || input.longitude === undefined) {
|
|
33
|
+
throw new Error("Provide either location, or both latitude and longitude");
|
|
34
|
+
}
|
|
35
|
+
return { latitude: input.latitude, longitude: input.longitude, elevation: input.elevation };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createServer({ apiKey = process.env.METEOBLUE_API_KEY, client } = {}) {
|
|
39
|
+
const meteoblue = client || new MeteoblueClient({ apiKey });
|
|
40
|
+
const server = new McpServer({ name: "mcp-meteoblue", version: "1.0.0" });
|
|
41
|
+
|
|
42
|
+
server.registerTool(
|
|
43
|
+
"search_locations",
|
|
44
|
+
{
|
|
45
|
+
title: "Search meteoblue locations",
|
|
46
|
+
description: "Resolve a city, place, postal code, IATA code, or ICAO code to coordinates and elevation using the meteoblue Location Search API.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
query: z.string().min(2).describe("Location search text"),
|
|
49
|
+
language: z.string().regex(/^[a-z]{2}$/i).default("en").describe("Two-letter result language"),
|
|
50
|
+
countryCode: z.string().regex(/^[a-z]{2}$/i).optional().describe("Optional ISO 3166-1 alpha-2 country filter"),
|
|
51
|
+
page: z.number().int().min(1).default(1),
|
|
52
|
+
limit: z.number().int().min(1).max(100).default(10)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async (input) => {
|
|
56
|
+
try {
|
|
57
|
+
return textResult(await meteoblue.searchLocations(input.query, input));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return errorResult(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
server.registerTool(
|
|
65
|
+
"get_forecast",
|
|
66
|
+
{
|
|
67
|
+
title: "Get meteoblue weather forecast",
|
|
68
|
+
description: "Get meteoblue forecast JSON by place name or coordinates. Place names are resolved through the Location Search API. Only packages documented for the Free Weather API are accepted.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
location,
|
|
71
|
+
latitude,
|
|
72
|
+
longitude,
|
|
73
|
+
elevation,
|
|
74
|
+
language: z.string().regex(/^[a-z]{2}$/i).default("en"),
|
|
75
|
+
countryCode: z.string().regex(/^[a-z]{2}$/i).optional(),
|
|
76
|
+
packages: z.array(z.enum(FREE_FORECAST_PACKAGES)).min(1).max(10).default(["current", "basic-1h", "basic-day"]),
|
|
77
|
+
forecastDays: z.number().int().min(0).max(7).default(7),
|
|
78
|
+
historyDays: z.number().int().min(0).max(4).default(0),
|
|
79
|
+
timezone: z.string().optional().describe("IANA timezone such as Europe/Paris; auto-detected when omitted"),
|
|
80
|
+
temperatureUnit: z.enum(["C", "K", "F"]).default("C"),
|
|
81
|
+
windSpeedUnit: z.enum(["m/s", "km/h", "mph", "kn", "bft"]).default("km/h"),
|
|
82
|
+
precipitationUnit: z.enum(["metric", "imperial"]).default("metric")
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async (input) => {
|
|
86
|
+
try {
|
|
87
|
+
const coords = await coordinatesFor(meteoblue, input);
|
|
88
|
+
const forecast = await meteoblue.getForecast({ ...input, ...coords });
|
|
89
|
+
return textResult({ resolvedLocation: coords.resolvedLocation, forecast });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return errorResult(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
server.registerTool(
|
|
97
|
+
"get_forecast_image",
|
|
98
|
+
{
|
|
99
|
+
title: "Get meteoblue forecast image",
|
|
100
|
+
description: "Generate a meteoblue forecast meteogram by place name or coordinates. Returns the PNG directly. Image access depends on the API key's meteoblue entitlement.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: z.enum(FORECAST_IMAGE_TYPES).default("meteogram"),
|
|
103
|
+
location,
|
|
104
|
+
latitude,
|
|
105
|
+
longitude,
|
|
106
|
+
elevation,
|
|
107
|
+
language: z.string().regex(/^[a-z]{2}$/i).default("en"),
|
|
108
|
+
countryCode: z.string().regex(/^[a-z]{2}$/i).optional(),
|
|
109
|
+
options: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).default({}).describe("Image-specific meteoblue query parameters")
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async (input) => {
|
|
113
|
+
try {
|
|
114
|
+
const coords = await coordinatesFor(meteoblue, input);
|
|
115
|
+
const image = await meteoblue.getForecastImage({ ...input, ...coords });
|
|
116
|
+
return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }] };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return errorResult(error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return server;
|
|
124
|
+
}
|