open-meteo-mcp-server 1.6.1 → 2.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/CHANGELOG.md +97 -0
- package/README.md +46 -10
- package/dist/client.d.ts +7 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -146
- package/dist/index.js.map +1 -1
- package/dist/security.d.ts +22 -0
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +76 -0
- package/dist/security.js.map +1 -1
- package/dist/tools.d.ts +25 -13
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +78 -1116
- package/dist/tools.js.map +1 -1
- package/dist/truncation.d.ts +20 -0
- package/dist/truncation.d.ts.map +1 -0
- package/dist/truncation.js +84 -0
- package/dist/truncation.js.map +1 -0
- package/dist/types.d.ts +750 -92
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +802 -32
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Releases before 2.0.0 predate this file; see the
|
|
9
|
+
[GitHub releases](https://github.com/cmer81/open-meteo-mcp/releases) for their notes.
|
|
10
|
+
|
|
11
|
+
## [2.0.0] - 2026-07-25
|
|
12
|
+
|
|
13
|
+
Security and correctness release. Everything breaking here is confined to the
|
|
14
|
+
**Streamable HTTP transport**. If you use the default stdio transport (npx,
|
|
15
|
+
Claude Desktop) or the Docker image, no action is required.
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
- **The HTTP transport now binds to `127.0.0.1` instead of all interfaces.** A
|
|
20
|
+
server started with `TRANSPORT=http` is reachable only from the local machine
|
|
21
|
+
unless you set `HOST=0.0.0.0`. The failure mode is silent: the process starts
|
|
22
|
+
normally and its internal health check still passes, but no external client can
|
|
23
|
+
reach it. The Docker image sets `HOST=0.0.0.0` itself, so published ports keep
|
|
24
|
+
working unchanged.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# before
|
|
28
|
+
TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
29
|
+
# after, for anything other than local-only use
|
|
30
|
+
HOST=0.0.0.0 TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Requests carrying an `Origin` header are rejected with `403` unless the origin
|
|
34
|
+
is allow-listed.** This is DNS rebinding protection. Clients that send no
|
|
35
|
+
`Origin` — CLI tools, SDK transports, container probes — are unaffected. Browser
|
|
36
|
+
based clients must now be declared:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ALLOWED_ORIGINS=https://app.example,http://localhost:5173
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **`API_KEY` is now enforced on `GET` and `DELETE /mcp`, not just `POST`.** Any
|
|
43
|
+
client that reached those two verbs without a key was relying on the flaw fixed
|
|
44
|
+
below and will now receive `401`.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **`weather_archive` and `climate_projection` published an empty input schema.**
|
|
49
|
+
Both use a `.refine()` for their `start_date <= end_date` rule, which produces a
|
|
50
|
+
`ZodEffects` the SDK cannot introspect: it advertised `{}` as the input schema
|
|
51
|
+
while still validating strictly, so clients had no way to learn the parameters
|
|
52
|
+
and any guess was rejected. Both tools now publish their full schema (16 and 13
|
|
53
|
+
properties, with `latitude`, `longitude`, `start_date` and `end_date` required),
|
|
54
|
+
and cross-field rules remain enforced.
|
|
55
|
+
|
|
56
|
+
- **Response truncation overshot its own limit by roughly 2.5x.** Sizes were
|
|
57
|
+
measured on compact JSON while the response was emitted pretty-printed. A
|
|
58
|
+
16-day, 20-variable forecast was "truncated" to 24,904 characters and emitted at
|
|
59
|
+
61,279, which clients rejected outright. Truncation now measures the text as it
|
|
60
|
+
is actually emitted, including the truncation notice, and the same request
|
|
61
|
+
returns 24,959 characters.
|
|
62
|
+
|
|
63
|
+
- **Authentication and rate limiting never ran on `GET` and `DELETE /mcp`.** The
|
|
64
|
+
middlewares were registered after those routes, and Express runs middleware in
|
|
65
|
+
declaration order. With `API_KEY` set, `POST` answered `401` while `GET` and
|
|
66
|
+
`DELETE` answered `404` having skipped authentication entirely; 70
|
|
67
|
+
unauthenticated `DELETE`s drew no `429`. A caller holding a session id could
|
|
68
|
+
terminate another client's session without a key. Session ids are UUIDv4 and
|
|
69
|
+
cannot be guessed, so exploitation required a leaked id — but the key protected
|
|
70
|
+
only one verb of three.
|
|
71
|
+
|
|
72
|
+
- **Clients sending `Accept: */*` received `406 Not Acceptable`.** The header
|
|
73
|
+
normalizer mutated `req.headers`, but the SDK rebuilds the request from Node's
|
|
74
|
+
raw header array through Hono, so its work was invisible to the transport. It
|
|
75
|
+
now rewrites `rawHeaders` as well.
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- `HOST` — interface the HTTP transport binds to (default `127.0.0.1`).
|
|
80
|
+
- `ALLOWED_ORIGINS` — comma-separated browser origins permitted to reach the
|
|
81
|
+
server. Empty by default.
|
|
82
|
+
- 14 regression tests covering the published schema of every tool, the emitted
|
|
83
|
+
response size, and the auth, origin and `Accept` behaviour of the HTTP pipeline.
|
|
84
|
+
|
|
85
|
+
### Documentation
|
|
86
|
+
|
|
87
|
+
- Documented `HOST`, `ALLOWED_ORIGINS` and the 25,000-character response cap,
|
|
88
|
+
none of which were described anywhere despite appearing in every truncated
|
|
89
|
+
payload.
|
|
90
|
+
- Corrected the README's production example, which the new binding default left
|
|
91
|
+
unreachable, and its claim of 7-day forecasts (the schema allows 16).
|
|
92
|
+
- `CLAUDE.md` now records the three traps behind the fixes above, since none are
|
|
93
|
+
apparent from reading the code: middleware mounted after a route silently skips
|
|
94
|
+
it, truncation must measure the text as emitted, and `.refine()` yields a
|
|
95
|
+
`ZodEffects` the SDK cannot introspect.
|
|
96
|
+
|
|
97
|
+
[2.0.0]: https://github.com/cmer81/open-meteo-mcp/compare/v1.7.0...v2.0.0
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A comprehensive [Model Context Protocol (MCP)](https://modelcontextprotocol.io)
|
|
|
11
11
|
This MCP server provides complete access to Open-Meteo APIs, including:
|
|
12
12
|
|
|
13
13
|
### Core Weather APIs
|
|
14
|
-
- **Weather Forecast** (`weather_forecast`) - 7
|
|
14
|
+
- **Weather Forecast** (`weather_forecast`) - Forecasts up to 16 days (7 by default) with hourly and daily resolution
|
|
15
15
|
- **Weather Archive** (`weather_archive`) - Historical ERA5 data from 1940 to present
|
|
16
16
|
- **Air Quality** (`air_quality`) - PM2.5, PM10, ozone, nitrogen dioxide, pollen, European/US AQI indices, UV index and other pollutants
|
|
17
17
|
- **Marine Weather** (`marine_weather`) - Wave height, wave period, wave direction and sea surface temperature
|
|
@@ -29,7 +29,7 @@ This MCP server provides complete access to Open-Meteo APIs, including:
|
|
|
29
29
|
|
|
30
30
|
### Advanced Forecasting Tools
|
|
31
31
|
- **Flood Forecast** (`flood_forecast`) - River discharge and flood forecasts from GloFAS (Global Flood Awareness System)
|
|
32
|
-
- **Seasonal Forecast** (`seasonal_forecast`) - Long-range forecasts up to
|
|
32
|
+
- **Seasonal Forecast** (`seasonal_forecast`) - Long-range forecasts up to ~7 months ahead
|
|
33
33
|
- **Climate Projections** (`climate_projection`) - CMIP6 climate change projections for different warming scenarios
|
|
34
34
|
- **Ensemble Forecast** (`ensemble_forecast`) - Multiple model runs showing forecast uncertainty
|
|
35
35
|
|
|
@@ -168,12 +168,16 @@ TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
|
168
168
|
|
|
169
169
|
This starts an Express server on the specified port (default: 3000) with the MCP endpoint at `/mcp`. The HTTP transport supports session management with unique session IDs per client.
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
> **The server binds to `127.0.0.1` by default**, so it is reachable only from the local machine. To accept connections from other hosts, set `HOST=0.0.0.0` explicitly. The Docker image already does this, so published ports work without extra configuration.
|
|
172
|
+
|
|
173
|
+
For production deployments, bind to a reachable interface and enable authentication and rate limiting:
|
|
172
174
|
|
|
173
175
|
```bash
|
|
174
|
-
API_KEY=your-secret-key RATE_LIMIT_RPM=60 TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
176
|
+
HOST=0.0.0.0 API_KEY=your-secret-key RATE_LIMIT_RPM=60 TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
175
177
|
```
|
|
176
178
|
|
|
179
|
+
If a browser-based client connects to the server, list its origin in `ALLOWED_ORIGINS` — requests carrying an unlisted `Origin` header are rejected with `403` as DNS rebinding protection.
|
|
180
|
+
|
|
177
181
|
Clients must then include the key in every request:
|
|
178
182
|
```
|
|
179
183
|
Authorization: Bearer your-secret-key
|
|
@@ -213,11 +217,11 @@ docker run -d \
|
|
|
213
217
|
curl http://localhost:3000/health
|
|
214
218
|
```
|
|
215
219
|
|
|
216
|
-
Available tags:
|
|
220
|
+
Available tags (no `v` prefix — the git tag `v2.0.0` publishes the image as `2.0.0`):
|
|
217
221
|
- `latest` - Latest stable release
|
|
218
|
-
- `
|
|
219
|
-
- `
|
|
220
|
-
- `
|
|
222
|
+
- `2.0.0` - Specific version
|
|
223
|
+
- `2.0` - Latest 2.0.x release
|
|
224
|
+
- `2` - Latest 2.x.x release
|
|
221
225
|
|
|
222
226
|
#### Using Docker Compose
|
|
223
227
|
|
|
@@ -297,12 +301,16 @@ All environment variables are optional and have sensible defaults:
|
|
|
297
301
|
- `OPEN_METEO_CLIMATE_API_URL` - Climate projection API URL (default: https://climate-api.open-meteo.com)
|
|
298
302
|
- `TRANSPORT` - Transport mode: `http` for Streamable HTTP, omit for stdio (default: stdio)
|
|
299
303
|
- `PORT` - HTTP server port when using HTTP transport (default: 3000)
|
|
304
|
+
- `HOST` - Interface the HTTP transport binds to (default: `127.0.0.1`, loopback only). Set to `0.0.0.0` to accept connections from other machines. The Docker image sets this to `0.0.0.0` already, so published ports work out of the box.
|
|
300
305
|
|
|
301
306
|
#### HTTP Transport Security (optional)
|
|
302
307
|
|
|
303
|
-
- `API_KEY` - When set, all requests to `/mcp` must include this key via `Authorization: Bearer <key>` or `X-API-Key: <key>`. Leave unset for open access (local/dev mode).
|
|
308
|
+
- `API_KEY` - When set, all requests to `/mcp` must include this key via `Authorization: Bearer <key>` or `X-API-Key: <key>`. Leave unset for open access (local/dev mode). Enforced on `GET`, `POST` and `DELETE` alike.
|
|
304
309
|
- `RATE_LIMIT_RPM` - Maximum requests per minute per IP (default: `60`). HTTP transport only.
|
|
305
310
|
- `TRUSTED_PROXIES` - Comma-separated list of trusted proxy IPs or CIDR ranges (e.g. `10.0.0.0/8,172.16.0.0/12`). When set, `X-Forwarded-For` is honoured only for requests originating from these addresses. Leave unset to always use the direct connection IP.
|
|
311
|
+
- `ALLOWED_ORIGINS` - Comma-separated list of browser origins permitted to reach the server (e.g. `http://localhost:5173,https://app.example`). Protects against DNS rebinding: any request carrying an `Origin` header that is not listed is rejected with `403`. Requests without an `Origin` header — CLI clients and SDK transports — are unaffected. Empty by default.
|
|
312
|
+
|
|
313
|
+
`/health` stays reachable without a key and without rate limiting, so container probes keep working.
|
|
306
314
|
|
|
307
315
|
## Skills
|
|
308
316
|
|
|
@@ -372,6 +380,10 @@ What were the temperatures in London during January 2023?
|
|
|
372
380
|
What's the current air quality in Beijing with PM2.5 and ozone levels?
|
|
373
381
|
```
|
|
374
382
|
|
|
383
|
+
```
|
|
384
|
+
Give me the current European AQI, UV index, and pollen levels (birch, grass, ragweed) in Paris.
|
|
385
|
+
```
|
|
386
|
+
|
|
375
387
|
### Marine Weather
|
|
376
388
|
```
|
|
377
389
|
Get me the wave height and sea surface temperature for coordinates 45.0, -125.0 for the next 5 days.
|
|
@@ -382,6 +394,16 @@ Get me the wave height and sea surface temperature for coordinates 45.0, -125.0
|
|
|
382
394
|
Check the river discharge forecast for coordinates 52.5, 13.4 for the next 30 days.
|
|
383
395
|
```
|
|
384
396
|
|
|
397
|
+
### Seasonal Forecast
|
|
398
|
+
```
|
|
399
|
+
Give me the weekly and monthly temperature outlook for Madrid over the next 4 months.
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Ensemble Forecast
|
|
403
|
+
```
|
|
404
|
+
Compare the ICON and GFS ensemble forecasts for Berlin over the next 5 days and show the spread across members.
|
|
405
|
+
```
|
|
406
|
+
|
|
385
407
|
### Climate Projections
|
|
386
408
|
```
|
|
387
409
|
Show me temperature projections for New York from 2050 to 2070 using CMIP6 models.
|
|
@@ -494,7 +516,8 @@ src/
|
|
|
494
516
|
├── client.ts # HTTP client for Open-Meteo API
|
|
495
517
|
├── tools.ts # MCP tool definitions
|
|
496
518
|
├── types.ts # Zod validation schemas
|
|
497
|
-
|
|
519
|
+
├── truncation.ts # Response size capping and serialization
|
|
520
|
+
└── security.ts # Auth, origin validation, rate limiter, IP extraction
|
|
498
521
|
```
|
|
499
522
|
|
|
500
523
|
## API Coverage
|
|
@@ -533,6 +556,19 @@ The server provides comprehensive error handling with detailed error messages fo
|
|
|
533
556
|
- Network connectivity issues
|
|
534
557
|
- Invalid date ranges
|
|
535
558
|
|
|
559
|
+
### Response Size Limits
|
|
560
|
+
|
|
561
|
+
Tool responses are capped at 25,000 characters so a single wide query cannot overflow an LLM's context. When a response exceeds the limit, the time-series arrays (`hourly`, `daily`, `minutely_15`) are shortened by an equal ratio — keeping every parallel series aligned on the same timestamps — and two fields are added:
|
|
562
|
+
|
|
563
|
+
```json
|
|
564
|
+
{
|
|
565
|
+
"truncated": true,
|
|
566
|
+
"truncation_message": "Response truncated from 95538 characters to stay within the 25000-character limit. Narrow the request (start_date/end_date, forecast_days, past_days, or fewer variables) to retrieve the full data."
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
To get complete data, narrow the request: shorter date range, fewer `forecast_days`/`past_days`, or fewer variables.
|
|
571
|
+
|
|
536
572
|
## Performance
|
|
537
573
|
|
|
538
574
|
- Efficient HTTP client with connection pooling
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AirQualityParams, ArchiveParams, ClimateParams, EcmwfParams, ElevationParams, ElevationResponse, EnsembleParams, FloodParams, ForecastParams, GeocodingParams, GeocodingResponse, MarineParams, SeasonalParams, WeatherResponse } from './types.js';
|
|
1
|
+
import type { AirQualityParams, ArchiveParams, ClimateParams, DwdIconParams, EcmwfParams, ElevationParams, ElevationResponse, EnsembleParams, FloodParams, ForecastParams, GemParams, GeocodingParams, GeocodingResponse, GfsParams, JmaParams, MarineParams, MeteoFranceParams, MetnoParams, SeasonalParams, WeatherResponse } from './types.js';
|
|
2
2
|
export declare class OpenMeteoClient {
|
|
3
3
|
private client;
|
|
4
4
|
private airQualityClient;
|
|
@@ -15,13 +15,13 @@ export declare class OpenMeteoClient {
|
|
|
15
15
|
private buildParams;
|
|
16
16
|
getForecast(params: ForecastParams): Promise<WeatherResponse>;
|
|
17
17
|
getArchive(params: ArchiveParams): Promise<WeatherResponse>;
|
|
18
|
-
getDwdIcon(params:
|
|
19
|
-
getGfs(params:
|
|
20
|
-
getMeteoFrance(params:
|
|
18
|
+
getDwdIcon(params: DwdIconParams): Promise<WeatherResponse>;
|
|
19
|
+
getGfs(params: GfsParams): Promise<WeatherResponse>;
|
|
20
|
+
getMeteoFrance(params: MeteoFranceParams): Promise<WeatherResponse>;
|
|
21
21
|
getEcmwf(params: EcmwfParams): Promise<WeatherResponse>;
|
|
22
|
-
getJma(params:
|
|
23
|
-
getMetno(params:
|
|
24
|
-
getGem(params:
|
|
22
|
+
getJma(params: JmaParams): Promise<WeatherResponse>;
|
|
23
|
+
getMetno(params: MetnoParams): Promise<WeatherResponse>;
|
|
24
|
+
getGem(params: GemParams): Promise<WeatherResponse>;
|
|
25
25
|
getAirQuality(params: AirQualityParams): Promise<WeatherResponse>;
|
|
26
26
|
getMarine(params: MarineParams): Promise<WeatherResponse>;
|
|
27
27
|
getEnsemble(params: EnsembleParams): Promise<WeatherResponse>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,aAAa,CAAgB;gBAGnC,OAAO,GAAE,MAAuE,EAChF,OAAO,SAAY;IAuCrB,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,MAAM,CAAC,YAAY;IAkB3B,OAAO,CAAC,WAAW;IAgBb,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC;IAO3D,UAAU,CAAC,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,aAAa,CAAgB;gBAGnC,OAAO,GAAE,MAAuE,EAChF,OAAO,SAAY;IAuCrB,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,MAAM,CAAC,YAAY;IAkB3B,OAAO,CAAC,WAAW;IAgBb,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC;IAO3D,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC;IAO3D,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC;IAOnD,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IAOnE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAOvD,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC;IAOnD,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAOvD,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC;IAOnD,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAOjE,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAOzD,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAOjE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAOvD,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC;IAO3D,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAMxE"}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAA6B,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AA6FvB,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,cAAc,CAGR;IAEd,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAkB;IACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAO;IAC3C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;;IAiB5D,OAAO,CAAC,oBAAoB;IAgE5B,OAAO,CAAC,YAAY;IA8GpB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,eAAe;IAmPvB,OAAO,CAAC,kBAAkB;IAiBpB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAiB3B"}
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,16 @@ import 'dotenv/config';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
9
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
10
9
|
import express from 'express';
|
|
10
|
+
import { z } from 'zod';
|
|
11
11
|
import { OpenMeteoClient } from './client.js';
|
|
12
|
-
import { createAuthMiddleware, createRateLimiter, generateSessionId, getClientIp, sanitizeErrorMessage, } from './security.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { createAcceptNormalizer, createAuthMiddleware, createOriginValidator, createRateLimiter, generateSessionId, getClientIp, sanitizeErrorMessage, } from './security.js';
|
|
13
|
+
import { AIR_QUALITY_TOOL, CLIMATE_PROJECTION_TOOL, DWD_ICON_FORECAST_TOOL, ECMWF_FORECAST_TOOL, ELEVATION_TOOL, ENSEMBLE_FORECAST_TOOL, FLOOD_FORECAST_TOOL, GEM_FORECAST_TOOL, GEOCODING_TOOL, GFS_FORECAST_TOOL, JMA_FORECAST_TOOL, MARINE_WEATHER_TOOL, METEOFRANCE_FORECAST_TOOL, METNO_FORECAST_TOOL, SEASONAL_FORECAST_TOOL, WEATHER_ARCHIVE_TOOL, WEATHER_FORECAST_TOOL, } from './tools.js';
|
|
14
|
+
import { serializeToolResponse } from './truncation.js';
|
|
15
|
+
import { AirQualityParamsSchema, ArchiveParamsSchema, ClimateParamsSchema, DwdIconParamsSchema, EcmwfParamsSchema, ElevationParamsSchema, EnsembleParamsSchema, FloodParamsSchema, ForecastParamsSchema, GemParamsSchema, GeocodingParamsSchema, GfsParamsSchema, JmaParamsSchema, MarineParamsSchema, MeteoFranceParamsSchema, MetnoParamsSchema, SeasonalParamsSchema, } from './types.js';
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = dirname(__filename);
|
|
17
18
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -29,117 +30,47 @@ export class OpenMeteoMCPServer {
|
|
|
29
30
|
const baseURL = process.env.OPEN_METEO_API_URL || 'https://api.open-meteo.com';
|
|
30
31
|
this.client = new OpenMeteoClient(baseURL, pkg.version);
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
// Registers a read-only tool: wires the Zod schema (validation + generated
|
|
34
|
+
// JSON schema), and wraps the handler with logging, response truncation,
|
|
35
|
+
// and MCP-style error results shared across all 17 tools.
|
|
36
|
+
//
|
|
37
|
+
// The schemas arrive here as a common `z.ZodTypeAny` rather than the concrete
|
|
38
|
+
// per-tool types registerTool's generic overloads expect, so the call below
|
|
39
|
+
// goes through an untyped signature; each call site's `handler` still gets a
|
|
40
|
+
// precise, explicitly-annotated params type. Note this cast also silences the
|
|
41
|
+
// SDK's compile-time check on `inputSchema` — see the ZodEffects handling
|
|
42
|
+
// below for what that check would otherwise have caught.
|
|
43
|
+
registerReadOnlyTool(server, meta, schema, handler) {
|
|
44
|
+
const registerToolUntyped = server.registerTool.bind(server);
|
|
45
|
+
// `.refine()` wraps the object in a ZodEffects, which the SDK cannot
|
|
46
|
+
// introspect: it would publish an empty `{}` input schema while still
|
|
47
|
+
// validating strictly, leaving clients with no idea what to send. Publish
|
|
48
|
+
// the underlying object and re-apply the effects below.
|
|
49
|
+
const objectSchema = schema instanceof z.ZodEffects ? schema.innerType() : schema;
|
|
50
|
+
registerToolUntyped(meta.name, {
|
|
51
|
+
title: meta.title,
|
|
52
|
+
description: meta.description,
|
|
53
|
+
inputSchema: objectSchema,
|
|
54
|
+
annotations: meta.annotations,
|
|
55
|
+
}, async (params) => {
|
|
47
56
|
const start = Date.now();
|
|
48
|
-
log('info', 'tool_call', { tool: name, args });
|
|
57
|
+
log('info', 'tool_call', { tool: meta.name, args: params });
|
|
58
|
+
// Cross-field rules (e.g. start_date <= end_date) live in the effects
|
|
59
|
+
// the SDK never sees, so they are enforced here.
|
|
60
|
+
const refined = schema.safeParse(params);
|
|
61
|
+
if (!refined.success) {
|
|
62
|
+
const message = refined.error.issues.map((issue) => issue.message).join('; ');
|
|
63
|
+
log('error', 'tool_error', { tool: meta.name, error: message, duration_ms: 0 });
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
49
69
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
case 'weather_forecast': {
|
|
53
|
-
const params = ForecastParamsSchema.parse(args);
|
|
54
|
-
result = await this.client.getForecast(params);
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
case 'weather_archive': {
|
|
58
|
-
const params = ArchiveParamsSchema.parse(args);
|
|
59
|
-
result = await this.client.getArchive(params);
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
case 'air_quality': {
|
|
63
|
-
const params = AirQualityParamsSchema.parse(args);
|
|
64
|
-
result = await this.client.getAirQuality(params);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
case 'marine_weather': {
|
|
68
|
-
const params = MarineParamsSchema.parse(args);
|
|
69
|
-
result = await this.client.getMarine(params);
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
case 'elevation': {
|
|
73
|
-
const params = ElevationParamsSchema.parse(args);
|
|
74
|
-
result = await this.client.getElevation(params);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
case 'flood_forecast': {
|
|
78
|
-
const params = FloodParamsSchema.parse(args);
|
|
79
|
-
result = await this.client.getFlood(params);
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
case 'geocoding': {
|
|
83
|
-
const params = GeocodingParamsSchema.parse(args);
|
|
84
|
-
result = await this.client.getGeocoding(params);
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
case 'dwd_icon_forecast': {
|
|
88
|
-
const params = ForecastParamsSchema.parse(args);
|
|
89
|
-
result = await this.client.getDwdIcon(params);
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
case 'gfs_forecast': {
|
|
93
|
-
const params = ForecastParamsSchema.parse(args);
|
|
94
|
-
result = await this.client.getGfs(params);
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
case 'meteofrance_forecast': {
|
|
98
|
-
const params = ForecastParamsSchema.parse(args);
|
|
99
|
-
result = await this.client.getMeteoFrance(params);
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
case 'ecmwf_forecast': {
|
|
103
|
-
const params = EcmwfParamsSchema.parse(args);
|
|
104
|
-
result = await this.client.getEcmwf(params);
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
case 'jma_forecast': {
|
|
108
|
-
const params = ForecastParamsSchema.parse(args);
|
|
109
|
-
result = await this.client.getJma(params);
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
case 'metno_forecast': {
|
|
113
|
-
const params = ForecastParamsSchema.parse(args);
|
|
114
|
-
result = await this.client.getMetno(params);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case 'gem_forecast': {
|
|
118
|
-
const params = ForecastParamsSchema.parse(args);
|
|
119
|
-
result = await this.client.getGem(params);
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
case 'seasonal_forecast': {
|
|
123
|
-
const params = SeasonalParamsSchema.parse(args);
|
|
124
|
-
result = await this.client.getSeasonal(params);
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
case 'climate_projection': {
|
|
128
|
-
const params = ClimateParamsSchema.parse(args);
|
|
129
|
-
result = await this.client.getClimate(params);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case 'ensemble_forecast': {
|
|
133
|
-
const params = EnsembleParamsSchema.parse(args);
|
|
134
|
-
result = await this.client.getEnsemble(params);
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
default:
|
|
138
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
139
|
-
}
|
|
140
|
-
const responseText = JSON.stringify(result, null, 2);
|
|
70
|
+
const result = await handler(refined.data);
|
|
71
|
+
const responseText = serializeToolResponse(result);
|
|
141
72
|
log('info', 'tool_success', {
|
|
142
|
-
tool: name,
|
|
73
|
+
tool: meta.name,
|
|
143
74
|
response_size: responseText.length,
|
|
144
75
|
duration_ms: Date.now() - start,
|
|
145
76
|
});
|
|
@@ -147,10 +78,41 @@ export class OpenMeteoMCPServer {
|
|
|
147
78
|
}
|
|
148
79
|
catch (err) {
|
|
149
80
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
150
|
-
log('error', 'tool_error', {
|
|
81
|
+
log('error', 'tool_error', {
|
|
82
|
+
tool: meta.name,
|
|
83
|
+
error: message,
|
|
84
|
+
duration_ms: Date.now() - start,
|
|
85
|
+
});
|
|
151
86
|
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
152
87
|
}
|
|
153
88
|
});
|
|
89
|
+
}
|
|
90
|
+
createServer() {
|
|
91
|
+
const server = new McpServer({
|
|
92
|
+
name: 'open-meteo-mcp-server',
|
|
93
|
+
version: pkg.version,
|
|
94
|
+
}, {
|
|
95
|
+
capabilities: {
|
|
96
|
+
tools: {},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
this.registerReadOnlyTool(server, WEATHER_FORECAST_TOOL, ForecastParamsSchema, (params) => this.client.getForecast(params));
|
|
100
|
+
this.registerReadOnlyTool(server, WEATHER_ARCHIVE_TOOL, ArchiveParamsSchema, (params) => this.client.getArchive(params));
|
|
101
|
+
this.registerReadOnlyTool(server, AIR_QUALITY_TOOL, AirQualityParamsSchema, (params) => this.client.getAirQuality(params));
|
|
102
|
+
this.registerReadOnlyTool(server, MARINE_WEATHER_TOOL, MarineParamsSchema, (params) => this.client.getMarine(params));
|
|
103
|
+
this.registerReadOnlyTool(server, ELEVATION_TOOL, ElevationParamsSchema, (params) => this.client.getElevation(params));
|
|
104
|
+
this.registerReadOnlyTool(server, FLOOD_FORECAST_TOOL, FloodParamsSchema, (params) => this.client.getFlood(params));
|
|
105
|
+
this.registerReadOnlyTool(server, GEOCODING_TOOL, GeocodingParamsSchema, (params) => this.client.getGeocoding(params));
|
|
106
|
+
this.registerReadOnlyTool(server, DWD_ICON_FORECAST_TOOL, DwdIconParamsSchema, (params) => this.client.getDwdIcon(params));
|
|
107
|
+
this.registerReadOnlyTool(server, GFS_FORECAST_TOOL, GfsParamsSchema, (params) => this.client.getGfs(params));
|
|
108
|
+
this.registerReadOnlyTool(server, METEOFRANCE_FORECAST_TOOL, MeteoFranceParamsSchema, (params) => this.client.getMeteoFrance(params));
|
|
109
|
+
this.registerReadOnlyTool(server, ECMWF_FORECAST_TOOL, EcmwfParamsSchema, (params) => this.client.getEcmwf(params));
|
|
110
|
+
this.registerReadOnlyTool(server, JMA_FORECAST_TOOL, JmaParamsSchema, (params) => this.client.getJma(params));
|
|
111
|
+
this.registerReadOnlyTool(server, METNO_FORECAST_TOOL, MetnoParamsSchema, (params) => this.client.getMetno(params));
|
|
112
|
+
this.registerReadOnlyTool(server, GEM_FORECAST_TOOL, GemParamsSchema, (params) => this.client.getGem(params));
|
|
113
|
+
this.registerReadOnlyTool(server, SEASONAL_FORECAST_TOOL, SeasonalParamsSchema, (params) => this.client.getSeasonal(params));
|
|
114
|
+
this.registerReadOnlyTool(server, CLIMATE_PROJECTION_TOOL, ClimateParamsSchema, (params) => this.client.getClimate(params));
|
|
115
|
+
this.registerReadOnlyTool(server, ENSEMBLE_FORECAST_TOOL, EnsembleParamsSchema, (params) => this.client.getEnsemble(params));
|
|
154
116
|
return server;
|
|
155
117
|
}
|
|
156
118
|
getSession(sessionId) {
|
|
@@ -181,26 +143,14 @@ export class OpenMeteoMCPServer {
|
|
|
181
143
|
app.get('/health', (_req, res) => {
|
|
182
144
|
res.status(200).json({ status: 'ok' });
|
|
183
145
|
});
|
|
184
|
-
app.use((
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const normalized = new Set(tokens.map((value) => value.toLowerCase()));
|
|
193
|
-
const ensureHeader = (value) => {
|
|
194
|
-
if (!normalized.has(value)) {
|
|
195
|
-
tokens.push(value);
|
|
196
|
-
normalized.add(value);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
ensureHeader('application/json');
|
|
200
|
-
ensureHeader('text/event-stream');
|
|
201
|
-
req.headers.accept = tokens.join(', ');
|
|
202
|
-
next();
|
|
203
|
-
});
|
|
146
|
+
app.use(createAcceptNormalizer());
|
|
147
|
+
// Every guard below must be registered BEFORE the routes it protects:
|
|
148
|
+
// Express runs middleware in declaration order, so anything mounted after a
|
|
149
|
+
// route never runs for it. These three previously sat between the DELETE and
|
|
150
|
+
// POST handlers, leaving GET and DELETE unauthenticated and unthrottled.
|
|
151
|
+
app.use(createOriginValidator());
|
|
152
|
+
app.use(createRateLimiter());
|
|
153
|
+
app.use(createAuthMiddleware());
|
|
204
154
|
// GET /mcp — SSE streaming for server-to-client notifications
|
|
205
155
|
app.get('/mcp', async (req, res) => {
|
|
206
156
|
const remoteIp = getClientIp(req);
|
|
@@ -297,8 +247,6 @@ export class OpenMeteoMCPServer {
|
|
|
297
247
|
}
|
|
298
248
|
}
|
|
299
249
|
});
|
|
300
|
-
app.use(createRateLimiter());
|
|
301
|
-
app.use(createAuthMiddleware());
|
|
302
250
|
app.post('/mcp', async (req, res) => {
|
|
303
251
|
const remoteIp = getClientIp(req);
|
|
304
252
|
const userAgent = req.headers['user-agent'] ?? 'unknown';
|
|
@@ -331,20 +279,24 @@ export class OpenMeteoMCPServer {
|
|
|
331
279
|
const newSessionId = generateSessionId();
|
|
332
280
|
log('info', 'session_created', { session_id: newSessionId.substring(0, 8) });
|
|
333
281
|
// Create server and transport for this new session
|
|
334
|
-
const
|
|
282
|
+
const mcpServer = this.createServer();
|
|
335
283
|
const transport = new StreamableHTTPServerTransport({
|
|
336
284
|
enableJsonResponse: true,
|
|
337
285
|
sessionIdGenerator: () => newSessionId,
|
|
338
286
|
});
|
|
339
|
-
server.oninitialized = () => {
|
|
287
|
+
mcpServer.server.oninitialized = () => {
|
|
340
288
|
log('info', 'session_initialized', { session_id: newSessionId.substring(0, 8) });
|
|
341
289
|
};
|
|
342
|
-
server.onclose = () => {
|
|
290
|
+
mcpServer.server.onclose = () => {
|
|
343
291
|
this.sessionServers.delete(newSessionId);
|
|
344
292
|
log('info', 'session_closed', { session_id: newSessionId.substring(0, 8) });
|
|
345
293
|
};
|
|
346
|
-
await
|
|
347
|
-
this.sessionServers.set(newSessionId, {
|
|
294
|
+
await mcpServer.connect(transport);
|
|
295
|
+
this.sessionServers.set(newSessionId, {
|
|
296
|
+
server: mcpServer,
|
|
297
|
+
transport,
|
|
298
|
+
lastActivity: Date.now(),
|
|
299
|
+
});
|
|
348
300
|
// Set session ID in response header before handling request
|
|
349
301
|
res.setHeader('mcp-session-id', newSessionId);
|
|
350
302
|
// Handle the initialize request
|
|
@@ -403,9 +355,13 @@ export class OpenMeteoMCPServer {
|
|
|
403
355
|
startHttpTransport() {
|
|
404
356
|
const app = this.buildExpressApp();
|
|
405
357
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
358
|
+
// Loopback by default so a local server is not silently exposed on the LAN.
|
|
359
|
+
// Container images set HOST=0.0.0.0 explicitly, since the container boundary
|
|
360
|
+
// is what limits reachability there.
|
|
361
|
+
const host = process.env.HOST || '127.0.0.1';
|
|
406
362
|
app
|
|
407
|
-
.listen(port, () => {
|
|
408
|
-
log('info', 'server_start', { transport: 'http', port });
|
|
363
|
+
.listen(port, host, () => {
|
|
364
|
+
log('info', 'server_start', { transport: 'http', host, port });
|
|
409
365
|
})
|
|
410
366
|
.on('error', (err) => {
|
|
411
367
|
log('error', 'server_error', { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -420,12 +376,12 @@ export class OpenMeteoMCPServer {
|
|
|
420
376
|
}
|
|
421
377
|
else {
|
|
422
378
|
// For stdio mode, create a single server instance
|
|
423
|
-
const
|
|
379
|
+
const mcpServer = this.createServer();
|
|
424
380
|
const stdioTransport = new StdioServerTransport();
|
|
425
|
-
server.oninitialized = () => {
|
|
381
|
+
mcpServer.server.oninitialized = () => {
|
|
426
382
|
log('info', 'session_initialized', { transport: 'stdio' });
|
|
427
383
|
};
|
|
428
|
-
await
|
|
384
|
+
await mcpServer.connect(stdioTransport);
|
|
429
385
|
log('info', 'server_start', { transport: 'stdio' });
|
|
430
386
|
}
|
|
431
387
|
}
|