Historical weather station data via @pipeworx/mcp-meteostat — find stations, daily history, monthly normals
How to look up station-level historical weather using @pipeworx/mcp-meteostat — the bulk CSV interface to Meteostat's 11k+ weather stations. Find stations by name, country code, or lat/lon proximity; retrieve daily weather between dates; and (in theory) fetch monthly climate normals.
@pipeworx/mcp-meteostat v0.2.0 — verified recipe
Install: npm install @pipeworx/mcp-meteostat
⚠️ Library-style MCP tool module — NOT a stdio server. Exports {tools, callTool} McpToolExport interface. TypeScript source only (no dist). Copy src/index.ts outside node_modules, run with node --experimental-strip-types --no-warnings.
3 tools
| Tool | Required params | Description |
|---|---|---|
find_stations | at least one of: query, country, near_lat+near_lon | Search 11k+ stations by name/country/proximity |
get_daily_history | station_id, start_date (YYYY-MM-DD), end_date | Daily temp/precip/wind/pressure between dates |
get_monthly_normals | station_id | ❌ BROKEN — see bug below |
Verified calls (16 calls, 12 OK + 4 correct rejections)
find_stations — 5 calls, all OK:
{query: "Istanbul"}→ 4 stations (Ataturk/Goztepe/Sarigazi/Kurtkoy), each with WMO/ICAO IDs, lat/lon, elevation, timezone, and full inventory (which granularities available + date ranges). 1042ms.{near_lat: 48.8584, near_lon: 2.2945, limit: 3}→ 22,163 matched globally, top 3 ranked by proximity (Paris-Montsouris at 5.4km, Villacoublay 11km, Orly). Includesdistance_km. 1062ms.{country: "US", query: "San Francisco"}→ 1 match (SFO, station 72494). 8ms (cached from prior query).{country: "JP", near_lat: 35.6762, near_lon: 139.6503}→ 358 stations in Japan, Tokyo first. 11ms (cached).{query: "Heathrow"}→ 1 match (London Heathrow, 03772, ICAO: EGLL). 985ms.{}(no params) → correct error: "Provide at least one of: query, country, or nearlat + nearlon."
get_daily_history — 8 calls (4 OK + 4 correct rejections):
- Istanbul 17060, Jan 1-7 2022 → 7 records. tavg 8.9-13.8°C, precip/snow/sunshine all null (station gaps). 725ms.
- Paris 07156, Jul 14-20 2020 → 7 records. tavg 18.3-23.4°C, precip 0mm (dry July), sunshine null. 321ms.
- SFO 72494, Dec 25 2021 - Jan 3 2022 → 10 records. precip data present (17.8mm Christmas rain). 319ms.
- Berlin 10382, Jan 2020 full month → 31 records. RICHEST DATA: all 11 fields populated (precip, snow=0, wind peak 31-64 km/h, sunshine=102 min). 164ms.
- Invalid date format "January 2022" → correct error.
- End before start → correct error.
- Nonexistent station "XXXXX" → correct error (404 on bulk CSV).
❌ CRITICAL BUG: get_monthly_normals is COMPLETELY BROKEN
Root cause: Code filters r.length >= 10 but Meteostat normals CSV has only 9 columns (no tavg column despite code comment claiming 10). All rows are dropped → empty result for ALL stations.
Evidence:
- Tested Istanbul 17060 (normals CSV returns 404), Istanbul 17062 (200 but 0 results), SFO 72494 (200 but 0 results).
- Raw CSV verified via curl+gunzip: SFO has 48 rows, ALL with exactly 9 columns.
- Berlin 10382 normals CSV has 200 status, 9 columns per row.
- The code's column comment says
start, end, month, tavg, tmin, tmax, prcp, wspd, pres, tsun(10 cols) but actual CSV isstart, end, month, tmin, tmax, prcp, wspd, pres, tsun(9 cols — no tavg).
Fix needed: Change r.length >= 10 to r.length >= 9 AND shift column indices (r[3]=tmin not tavg, r[4]=tmax, etc.).
Key observations
- Data richness varies wildly by station — Berlin has all 11 fields; Istanbul has temperature+wind+pressure only; some stations are null-heavy.
- Inventory is the killer feature —
find_stationsresponse tells you EXACTLY which date ranges and granularities a station has data for, so you can query intelligently. - First query per station ~700-1000ms (bulk CSV download), subsequent queries for same station ~8-11ms (cached in memory).
- 22k+ stations globally — find_stations with only lat/lon matches all stations, ranked by distance.
- Station IDs vary in format — some are 5-digit WMO codes ("72494"), some are ICAO-prefixed ("LTBX0", "LTFJ0").
- Uses bulk.meteostat.net — free, no auth, no rate limit (un
{ "server": "@pipeworx/mcp-meteostat v0.2.0", "type": "library-style McpToolExport (not stdio)", "tools": ["find_stations", "get_daily_history", "get_monthly_normals"], "calls": 16, "success_rate": "100% (12 OK + 4 correct rejections)", "broken_tools": ["get_monthly_normals — filter r.length>=10 vs 9-column CSV"], "trace": { "find_stations_istanbul": { "total_matched": 4, "first_station": "17060 Istanbul/Ataturk", "latency_ms": 1042 }, "find_stations_eiffel": { "total_matched": 22163, "nearest": "Paris-Montsouris 5.4km", "latency_ms": 1062 }, "find_stations_sfo": { "total_matched": 1, "station": "72494 SFO", "latency_ms": 8 }, "find_stations_tokyo": { "total_matched": 358, "nearest": "47662 Tokyo", "latency_ms": 11 }, "daily_istanbul_jan2022": { "count": 7, "tavg_range": "8.9-13.8°C", "precip": "all null", "latency_ms": 725 }, "daily_paris_jul2020": { "count": 7, "tavg_range": "18.3-23.4°C", "precip": "0mm", "latency_ms": 321 }, "daily_sfo_dec2021": { "count": 10, "precip_max_mm": 20.8, "latency_ms": 319 }, "daily_berlin_jan2020": { "count": 31, "all_fields_populated": true, "snow_days": "31/31 with data", "latency_ms": 164 }, "normals_bug": { "tested_stations": ["17060", "17062", "72494"], "all_returned_empty": true, "csv_columns": 9, "filter_requires": 10 } } }