Global weather via Open-Meteo: forecast, ERA5 archive, marine, air quality, geocoding, elevation.
Geocode places, fetch global weather forecasts, ERA5 historical climate, marine conditions, air quality, and terrain elevation via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://open-meteo.caseyjhand.com/mcp
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_locationsResolve 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.
country filter (ISO 3166-1 alpha-2, e.g. US) or by raising count (default 5, up to 10) and reading the admin1/country fields on each result — those are output fields for choosing among matches, not search inputsopenmeteo_search_locations result directly to weather tools as the timezone parameterno_results error (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 insteadopenmeteo_get_forecastWeather forecast for a coordinate pair with hourly and/or daily variable selection.
forecast_days 1–16, default 7)past_days (0–92) covers recent history via the forecast model — use instead of openmeteo_get_historical for dates within the last ~5 days to avoid ERA5 lagtemperature_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_2mtemperature_2m_max, temperature_2m_min, precipitation_sum, wind_speed_10m_max, sunrise, sunset, uv_index_max, precipitation_hours, weather_codehourly_variables or daily_variables is requiredcloud_cover in daily_variables → cloud_cover_max/_mean/_min). Names in neither set are passed upstream unchangedhourly_units / daily_units mappast_days plus many hourly variables) spills to DataCanvas when CANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_historicalHistorical weather from the ERA5 reanalysis archive, covering 1940 to approximately 5 days ago.
start_date and end_date (YYYY-MM-DD); ERA5 has a variable ~1–5 day lagopenmeteo_get_forecast — past and forecast data are directly comparable on one schemahourly_variables or daily_variables is requiredCANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true whenever a result is too large to return inline, which a wide multi-variable pull can be at any row countopenmeteo_dataframe_describe with the canvas_id to list tables, then openmeteo_dataframe_query to run SQL SELECT against the staged dataopenmeteo_get_marineMarine wave and ocean conditions for coastal and open-ocean coordinates.
forecast_days 1–8, upstream default 7) with optional past_days (0–92)start_date and end_date — real wave values go back to at least 2022forecast_days/past_days, and needs both ends — a lone start_date or end_date is rejectedwave_height, wave_direction, wave_period, wind_wave_height, wind_wave_direction, wind_wave_period, swell_wave_height, swell_wave_direction, swell_wave_periodwave_height_max, wave_direction_dominant, wave_period_maxhourly_variables or daily_variables is requiredocean_current_velocity is null for non-open-ocean coordinatesCANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_air_qualityModeled CAMS air quality, forecast and archive.
forecast_days 1–7, upstream default 5) with optional past_days (0–92)start_date and end_date — real CAMS values go back to at least 2022-10-01; earlier dates return rows of nullsforecast_days/past_days, and needs both ends — a lone start_date or end_date is rejectedpm2_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_pollenhourly_variables is requiredopenaq-mcp-serverdata_source: "CAMS" to distinguish modeled from measured dataCANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_elevationTerrain elevation from the Copernicus Digital Elevation Model (~90m resolution).
latitudes[] and longitudes[] arrays; both must have equal length (up to 100 pairs){ latitude, longitude, elevation_m }openmeteo_get_ensembleProbabilistic ensemble weather forecast exposing all individual model member trajectories.
forecast_days 1–16, default 7) with optional past_days (0–92)temperature_2m_member01, temperature_2m_member02, … Use the spread across members to compute exceedance probabilities, interquantile ranges, and decision thresholdsecmwf_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)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 location from the meteoswiss_* pair, an HTTP 200 carrying nan coordinates from the rest — and both surface as a non-retryable input error naming the coverage gap, so switch to a global model rather than retryingmodels to 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 workingmodel (system used) and member_count (perturbed members, excluding the control run)hourly_variables or daily_variables is requiredtemperature_2m_max and temperature_2m_min as 3-hourly aggregations as well as daily, so those are accepted in either fieldCANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_floodGloFAS (Global Flood Awareness System) river discharge forecast and reanalysis via the Open-Meteo Flood API.
forecast_days for the future outlook, or start_date and end_date together for historical analysis. The two are mutually exclusive, and a date range needs both ends — a lone start_date or end_date is rejectedriver_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³/sCANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_climateLong-range climate projections from bias-corrected daily CMIP6 models — the future-projection counterpart to openmeteo_get_historical.
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 listtemperature_2m_max_CMCC_CM2_VHR4); a single or omitted model returns plain variable namestemperature_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_meanCMCC_CM2_VHR4 has no shortwave_radiation_sum)CANVAS_PROVIDER_TYPE=duckdb — output includes canvas_id and truncated: true; query with openmeteo_dataframe_queryBuilt on @cyanheads/mcp-ts-core:
none, jwt, oauthin-memory, filesystem, Supabase, Cloudflare KV/R2/D1Open-Meteo–specific:
openmeteo_search_locations resolves place names so agents don't need a separate geocoder*_units mapopenmeteo_get_forecast, openmeteo_get_historical, openmeteo_get_marine, openmeteo_get_air_quality, openmeteo_get_ensemble, openmeteo_get_flood, and openmeteo_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. With CANVAS_PROVIDER_TYPE=none (the default) the same size check still applies — those tools return a bounded preview with truncated: true and no canvas_id, never an unbounded payload claiming to be completeAgent-friendly output:
openmeteo_search_locations returns the IANA timezone alongside coordinates — pass it directly as timezone to any weather toolopenmeteo_get_forecast, openmeteo_get_historical, openmeteo_get_marine, and openmeteo_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"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, and openmeteo_get_climate take a single cadence bucket, so they carry the notice without a cadence guardlatitude/longitude (Open-Meteo quantizes to the nearest model grid point) so agents can reason about grid alignmentdata_source: "CAMS" label on air quality results distinguishes modeled data from measured station readingsA 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"
}
}
}
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
git clone https://github.com/cyanheads/open-meteo-mcp-server.git
cd open-meteo-mcp-server
bun install
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.
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 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.
| 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/ |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagetools[] array in src/index.tsIssues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
Weather data by Open-Meteo.com — licensed CC BY 4.0.