<div align="center"> <h1>@cyanheads/open-meteo-mcp-server</h1> <p><b>Geocode places, fetch global weather forecasts, ERA5 historical climate, marine conditions, air quality, and terrain elevation via MCP. STDIO or Streamable HTTP.</b> <div>11 Tools</div> </p> </div>
<div align="center">
      
</div>
<div align="center">
  

</div>
<div align="center">
Public Hosted Server: https://open-meteo.caseyjhand.com/mcp
</div>
---
Tools
Eleven tools covering geocoding, weather forecasts, historical climate, probabilistic ensemble forecasts, marine conditions, air quality, terrain elevation, river discharge, CMIP6 climate projections, and SQL analytics over large datasets:
| Tool | Description | |:---|:---| | openmeteo_search_locations | Resolve a place name to ranked coordinate matches with country, region, elevation, timezone, and population | | openmeteo_get_forecast | Weather forecast for coordinates: hourly and/or daily variables for up to 16 days, with optional recent past data; wide windows spill to DataCanvas | | openmeteo_get_historical | Historical weather from the ERA5 reanalysis archive (1940–present); large ranges spill to DataCanvas | | openmeteo_get_marine | Marine wave and ocean conditions for coastal or ocean coordinates: wave height, period, direction, swell, and sea-surface temperature; up to 8 forecast days, past_days, or a start_date/end_date archive range; large windows spill to DataCanvas | | openmeteo_get_air_quality | Modeled CAMS air quality: PM2.5, PM10, NO2, O3, CO, dust, pollen, and European/US AQI indices; up to 7 forecast days, past_days, or a start_date/end_date archive range; large windows spill to DataCanvas | | openmeteo_get_elevation | Terrain elevation from Copernicus DEM (~90m resolution) for up to 100 coordinate pairs per call | | openmeteo_get_ensemble | Probabilistic ensemble forecast: per-member hourly/daily time series (up to 51 members, 16 days) for exceedance and uncertainty analysis | | openmeteo_get_flood | GloFAS river discharge forecast (up to 210 days) or reanalysis (1984–present); coordinate-based, snaps to nearest river; large ranges spill to DataCanvas | | openmeteo_get_climate | Bias-corrected daily CMIP6 climate projections (1950–2050) across up to 7 models; large ranges spill to DataCanvas | | openmeteo_dataframe_describe | List tables and columns on a DataCanvas staged by openmeteo_get_forecast, openmeteo_get_historical, openmeteo_get_marine, openmeteo_get_air_quality, openmeteo_get_ensemble, openmeteo_get_flood, or openmeteo_get_climate | | openmeteo_dataframe_query | Run a read-only SQL SELECT against tables staged on a DataCanvas |
openmeteo_search_locations
Resolve a free-text place name to ranked coordinate matches. Required first step for name-based queries — all weather tools accept latitude/longitude, not place names.
- Returns name, country, admin1/admin2, latitude, longitude, elevation, IANA timezone, population, and GeoNames feature code
- Search by a bare place name — a city, region, or landmark ("Baoding", not "Baoding Hebei"; "Paris", not "Paris, France"); a compound "City Region" or "City, Country" string matches nothing
- Disambiguate same-named places (e.g., "Springfield") with the optional
countryfilter (ISO 3166-1 alpha-2, e.g.US) or by raisingcount(default 5, up to 10) and reading theadmin1/countryfields on each result — those are output fields for choosing among matches, not search inputs - Pass the timezone from an
openmeteo_search_locationsresult directly to weather tools as thetimezoneparameter - Fails with a
no_resultserror (not an empty array) when nothing matches — retry the bare place name without qualifiers, or for a physical feature/landmark search the nearest populated place instead
---
openmeteo_get_forecast
Weather forecast for a coordinate pair with hourly and/or daily variable selection.
- Up to 16 forecast days ahead (
forecast_days 1–16, default 7) past_days(0–92) covers recent history via the forecast model — use instead ofopenmeteo_get_historicalfor dates within the last ~5 days to avoid ERA5 lag- Common hourly variables:
temperature_2m,precipitation,wind_speed_10m,relative_humidity_2m,cloud_cover,uv_index,apparent_temperature,precipitation_probability,weather_code,surface_pressure,visibility,wind_direction_10m,wind_gusts_10m,dew_point_2m - Common daily variables:
temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,sunrise,sunset,uv_index_max,precipitation_hours,weather_code - At least one of
hourly_variablesordaily_variablesis required - Hourly and daily are separate variable sets. A variable Open-Meteo documents under the other cadence is rejected before the request, by name, with the field it belongs in and the same-cadence alternatives (
cloud_coverindaily_variables→cloud_cover_max/_mean/_min). Names in neither set are passed upstream unchanged - Configurable temperature unit (Celsius/Fahrenheit), wind speed unit (km/h, mph, m/s, knots), and precipitation unit (mm/inch)
- Reshapes the API's columnar response into per-timestamp records with a parallel
hourly_units/daily_unitsmap - A wide window (a large
past_daysplus many hourly variables) spills to DataCanvas whenCANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: true; query withopenmeteo_dataframe_query
---
openmeteo_get_historical
Historical weather from the ERA5 reanalysis archive, covering 1940 to approximately 5 days ago.
- Requires
start_dateandend_date(YYYY-MM-DD); ERA5 has a variable ~1–5 day lag - Same variable vocabulary as
openmeteo_get_forecast— past and forecast data are directly comparable on one schema - At least one of
hourly_variablesordaily_variablesis required - Hourly and daily are separate variable sets; a variable documented under the other cadence is rejected before the request, by name, with the field it belongs in
- Large date ranges (multi-year hourly queries) spill to DataCanvas when
CANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: truewhenever a result is too large to return inline, which a wide multi-variable pull can be at any row count - Spill → query workflow: call
openmeteo_dataframe_describewith thecanvas_idto list tables, thenopenmeteo_dataframe_queryto run SQL SELECT against the staged data
---
openmeteo_get_marine
Marine wave and ocean conditions for coastal and open-ocean coordinates.
- Up to 8 forecast days (
forecast_days 1–8, upstream default 7) with optionalpast_days(0–92) - Or an archive range via
start_dateandend_date— real wave values go back to at least 2022 - One window per call: a date range is mutually exclusive with
forecast_days/past_days, and needs both ends — a lonestart_dateorend_dateis rejected - Common hourly variables:
wave_height,wave_direction,wave_period,wind_wave_height,wind_wave_direction,wind_wave_period,swell_wave_height,swell_wave_direction,swell_wave_period - Common daily variables:
wave_height_max,wave_direction_dominant,wave_period_max - At least one of
hourly_variablesordaily_variablesis required - Hourly and daily are separate variable sets; a variable documented under the other cadence is rejected before the request, by name, with the field it belongs in
- Inland or sheltered-water points return near-zero wave values (physically correct);
ocean_current_velocityis null for non-open-ocean coordinates - Wide windows spill to DataCanvas when
CANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: true; query withopenmeteo_dataframe_query
---
openmeteo_get_air_quality
Modeled CAMS air quality, forecast and archive.
- Up to 7 forecast days (
forecast_days 1–7, upstream default 5) with optionalpast_days(0–92) - Or an archive range via
start_dateandend_date— real CAMS values go back to at least2022-10-01; earlier dates return rows of nulls - One window per call: a date range is mutually exclusive with
forecast_days/past_days, and needs both ends — a lonestart_dateorend_dateis rejected - Common variables:
pm2_5,pm10,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone,dust,european_aqi,us_aqi,alder_pollen,birch_pollen,grass_pollen,mugwort_pollen,olive_pollen,ragweed_pollen - At least one variable from
hourly_variablesis required - Grid-modeled data from CAMS — resolution is coarser than ground stations; for measured station readings, cross-reference
openaq-mcp-server - Output includes
data_source: "CAMS"to distinguish modeled from measured data - Wide windows spill to DataCanvas when
CANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: true; query withopenmeteo_dataframe_query
---
openmeteo_get_elevation
Terrain elevation from the Copernicus Digital Elevation Model (~90m resolution).
- Accepts parallel
latitudes[]andlongitudes[]arrays; both must have equal length (up to 100 pairs) - Returns results in input order:
{ latitude, longitude, elevation_m } - Useful for geographic context, elevation-adjusted weather interpretation, or route planning
---
openmeteo_get_ensemble
Probabilistic ensemble weather forecast exposing all individual model member trajectories.
- Up to 16 forecast days (
forecast_days 1–16, default 7) with optionalpast_days(0–92) - Each requested variable is returned as per-member columns:
temperature_2m_member01,temperature_2m_member02, … Use the spread across members to compute exceedance probabilities, interquantile ranges, and decision thresholds - Available ensemble models (member counts include the control run):
- Global —
ecmwf_ifs025_ensemble(51),ecmwf_aifs025_ensemble(51),google_weathernext2_ensemble(64),ncep_gefs_seamless(31),ncep_gefs025(31),ncep_gefs05(31, 35-day horizon),ncep_aigefs025(31),icon_seamless_eps(20–40, global/Europe blend),icon_global_eps(40),gem_global_ensemble(21),bom_access_global_ensemble(18),ukmo_global_ensemble_20km(18) - Regional —
ecmwf_ifs_europe_ensemble(51),ecmwf_aifs_europe_ensemble(51),icon_eu_eps(40),icon_d2_eps(20),meteoswiss_icon_ch2_ensemble(21),meteoswiss_icon_ch1_ensemble(11),ukmo_uk_ensemble_2km(3). A regional model returns no data outside the area it covers. Upstream reports that two ways —No data is available for this locationfrom themeteoswiss_*pair, an HTTP 200 carryingnancoordinates from the rest — and both surface as a non-retryable input error naming the coverage gap, so switch to a global model rather than retrying - Omit
modelsto use the API default blend. The list is not an allowlist — a model name it does not carry is still sent upstream, so a model Open-Meteo adds later keeps working - Response includes
model(system used) andmember_count(perturbed members, excluding the control run) - At least one of
hourly_variablesordaily_variablesis required - Hourly and daily are separate variable sets; a variable documented under the other cadence is rejected before the request, by name, with the field it belongs in. The ensemble API's own catalog applies — it publishes
temperature_2m_maxandtemperature_2m_minas 3-hourly aggregations as well as daily, so those are accepted in either field - Large multi-member, multi-day pulls spill to DataCanvas when
CANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: true; query withopenmeteo_dataframe_query - Configurable temperature, wind speed, and precipitation units
---
openmeteo_get_flood
GloFAS (Global Flood Awareness System) river discharge forecast and reanalysis via the Open-Meteo Flood API.
- Coordinate-based — no river ID needed; the API snaps to the nearest river grid point automatically
- Forecast horizon up to 210 days; reanalysis history from 1984-01-01 to present
- One mode per call:
forecast_daysfor the future outlook, orstart_dateandend_datetogether for historical analysis. The two are mutually exclusive, and a date range needs both ends — a lonestart_dateorend_dateis rejected - Available daily variables:
river_discharge(ensemble mean),river_discharge_mean,river_discharge_min,river_discharge_max,river_discharge_median,river_discharge_p25(25th percentile),river_discharge_p75(75th percentile) — all in m³/s - Returns null for coordinates outside GloFAS coverage (e.g., open ocean or areas without river network data)
- Discharge values reflect the GloFAS ensemble — percentile variables expose the uncertainty spread
- Wide reanalysis ranges spill to DataCanvas when
CANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: true; query withopenmeteo_dataframe_query
---
openmeteo_get_climate
Long-range climate projections from bias-corrected daily CMIP6 models — the future-projection counterpart to openmeteo_get_historical.
- Coverage: 1950-01-01 to 2050-12-31, daily resolution only
- Available models:
CMCC_CM2_VHR4,FGOALS_f3_H,HiRAM_SIT_HR,MRI_AGCM3_2_S,EC_Earth3P_HR,MPI_ESM1_2_XR,NICAM16_8S. Not an allowlist — an unlisted name is still sent upstream; when upstream rejects a multi-model request, the error names only the model outside the documented set, not the whole list - With 2+ models, each variable appears once per model with the model name as column suffix (e.g.
temperature_2m_max_CMCC_CM2_VHR4); a single or omitted model returns plain variable names - Common daily variables:
temperature_2m_max,temperature_2m_min,temperature_2m_mean,precipitation_sum,rain_sum,snowfall_sum,wind_speed_10m_mean,wind_speed_10m_max,shortwave_radiation_sum,cloud_cover_mean,relative_humidity_2m_mean,pressure_msl_mean - Not all models carry all variables — missing combinations return null (e.g.
CMCC_CM2_VHR4has noshortwave_radiation_sum) - Multi-decade daily pulls across several models spill to DataCanvas when
CANVAS_PROVIDER_TYPE=duckdb— output includescanvas_idandtruncated: true; query withopenmeteo_dataframe_query - Configurable temperature, wind speed, and precipitation units
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool definitions — single file per tool, framework handles registration and validation
- Unified error handling — handlers throw, framework catches, classifies, and formats
- Pluggable auth:
none,jwt,oauth - Swappable storage backends:
in-memory,filesystem,Supabase,Cloudflare KV/R2/D1 - Structured logging with optional OpenTelemetry tracing
- STDIO and Streamable HTTP transports
Open-Meteo–specific:
- No API key required for non-commercial use — zero-config out of the box
- Self-contained geocoding:
openmeteo_search_locationsresolves place names so agents don't need a separate geocoder - ERA5 archive from 1940 to present with same variable schema as the forecast API — direct past/forecast comparisons on one schema
- Automatic columnar-to-record reshape: Open-Meteo returns parallel time/variable arrays; handlers convert to per-timestamp records with a
*_unitsmap - DataCanvas spillover for
openmeteo_get_forecast,openmeteo_get_historical,openmeteo_get_marine,openmeteo_get_air_quality,openmeteo_get_ensemble,openmeteo_get_flood, andopenmeteo_get_climate: a result too large to return inline registers a DuckDB dataframe for SQL querying, staging every hourly and daily row with its upstream numeric type intact. WithCANVAS_PROVIDER_TYPE=none(the default) the same size check still applies — those tools return a bounded preview withtruncated: trueand nocanvas_id, never an unbounded payload claiming to be complete - Configurable base URLs for all eight API endpoints (forecast, archive, marine, air quality, geocoding, ensemble, flood, climate) — override for testing or self-hosted deployments
- Attribution: Weather data by Open-Meteo.com (CC BY 4.0). Non-commercial use is free and keyless; commercial use requires Open-Meteo's paid API tier (~10,000 req/day, 5,000/hour fair-use ceiling for non-commercial)
Agent-friendly output:
- Location-first workflow:
openmeteo_search_locationsreturns the IANA timezone alongside coordinates — pass it directly astimezoneto any weather tool - Recovery hints on all error contracts — invalid variable names surface correction guidance with common variable examples
- Cadence-aware variable validation on
openmeteo_get_forecast,openmeteo_get_historical,openmeteo_get_marine, andopenmeteo_get_ensemble: a variable documented under the opposite cadence is rejected before the upstream call, naming the exact value and the field it belongs in, so the next attempt converges instead of re-guessing against an error that echoes the whole requested list. This is not an allowlist — a name in neither documented set goes upstream untouched - Unserved-variable notice on all seven weather tools: Open-Meteo answers a variable name it parses but does not serve with an all-null column and the unit
"undefined"rather than an error, so the result carries a notice naming those columns instead of presenting them as a data gap.openmeteo_get_air_quality,openmeteo_get_flood, andopenmeteo_get_climatetake a single cadence bucket, so they carry the notice without a cadence guard - Coordinate snapping transparency — responses echo the snapped
latitude/longitude(Open-Meteo quantizes to the nearest model grid point) so agents can reason about grid alignment data_source: "CAMS"label on air quality results distinguishes modeled data from measured station readings
Getting started
Public Hosted Instance
A public instance is available at https://open-meteo.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"open-meteo-mcp-server": {
"type": "streamable-http",
"url": "https://open-meteo.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"open-meteo-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/open-meteo-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"open-meteo-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/open-meteo-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"open-meteo-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/open-meteo-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.3.0 or higher (or Node.js v24+).
- No API key required. Non-commercial use is free and keyless.
- Commercial use requires Open-Meteo's paid API tier.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/open-meteo-mcp-server.git
- Navigate into the directory:
cd open-meteo-mcp-server
- Install dependencies:
bun install
Configuration
All configuration is validated at startup via Zod schemas. No API key is required for non-commercial use — all variables are optional.
| Variable | Description | Default | |:---|:---|:---| | MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio | | MCP_HTTP_PORT | HTTP server port | 3010 | | MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path | /mcp | | MCP_PUBLIC_URL | Public origin for TLS-terminating reverse-proxy deployments | — | | MCP_AUTH_MODE | Auth mode: none, jwt, or oauth | none | | MCP_LOG_LEVEL | Log level (debug, info, warning, error) | info | | MCP_GC_PRESSURE_INTERVAL_MS | Opt-in forced-GC interval (ms, Bun only). Set to 60000 if heap growth is observed under sustained HTTP traffic. | 0 | | LOGS_DIR | Directory for log files (Node.js only) | <project-root>/logs | | STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1 | in-memory | | CANVAS_PROVIDER_TYPE | Canvas engine for openmeteo_get_forecast / openmeteo_get_historical / openmeteo_get_marine / openmeteo_get_air_quality / openmeteo_get_ensemble / openmeteo_get_flood / openmeteo_get_climate spillover: duckdb or none. At none those tools still bound an over-budget response to a preview and set truncated: true — there is just no canvas holding the rows they omit | none | | OPEN_METEO_API_BASE_URL | Override for the main forecast + elevation API | https://api.open-meteo.com | | OPEN_METEO_ARCHIVE_BASE_URL | Override for the ERA5 historical archive API | https://archive-api.open-meteo.com | | OPEN_METEO_MARINE_BASE_URL | Override for the marine forecast API | https://marine-api.open-meteo.com | | OPEN_METEO_AIR_QUALITY_BASE_URL | Override for the CAMS air quality API | https://air-quality-api.open-meteo.com | | OPEN_METEO_GEOCODING_BASE_URL | Override for the geocoding API | https://geocoding-api.open-meteo.com | | OPEN_METEO_ENSEMBLE_BASE_URL | Override for the ensemble forecast API | https://ensemble-api.open-meteo.com | | OPEN_METEO_FLOOD_BASE_URL | Override for the GloFAS flood API | https://flood-api.open-meteo.com | | OPEN_METEO_CLIMATE_BASE_URL | Override for the CMIP6 climate projections API | https://climate-api.open-meteo.com | | OTEL_ENABLED | Enable OpenTelemetry tracing and metrics | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
- Build and run the production version:
# One-time build
bun run rebuild
# Run the built server
bun run start:http
# or
bun run start:stdio
- Run checks and tests:
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
Docker
docker build -t open-meteo-mcp-server .
docker run --rm -p 3010:3010 open-meteo-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/open-meteo-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose | |:---|:---| | src/index.ts | createApp() entry point — registers tools, initializes the Open-Meteo service | | src/config | Server-specific environment variable parsing and validation with Zod | | src/mcp-server/tools/definitions | Tool definitions (*.tool.ts) — one file per tool; includes dataframe-describe.tool.ts and dataframe-query.tool.ts | | src/services/open-meteo | Open-Meteo HTTP client wrapping all nine endpoints with retry, error classification, and columnar reshape | | src/services/canvas-accessor.ts | DataCanvas accessor for openmeteo_get_forecast / openmeteo_get_historical / openmeteo_get_marine / openmeteo_get_air_quality / openmeteo_get_ensemble / openmeteo_get_flood / openmeteo_get_climate spillover | | tests/ | Unit and integration tests mirroring src/ |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools in the
tools[]array insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.
---
Weather data by Open-Meteo.com — licensed CC BY 4.0.











