
Global weather via Open-Meteo: forecast, ERA5 archive, marine, air quality, geocoding, elevation.
Geocode places, fetch global weather forecasts, historical climate, marine conditions, air quality, and terrain elevation via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://open-meteo.caseyjhand.com/mcp
Global weather from Open-Meteo: forecasts, the historical archive, marine conditions, air quality, probabilistic ensembles, river discharge, and CMIP6 climate projections. Geocode a place name, pull hourly and daily variables for its coordinates, and run SQL over large results staged to DataCanvas. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
openmeteo_search_locations | Resolve a place name to ranked coordinates with country, region, timezone, and population |
openmeteo_get_forecast | Current conditions and hourly/daily forecast up to 16 days, with up to 92 past days |
openmeteo_get_historical | Hourly/daily history from the reanalysis archive, 1940 to present |
openmeteo_get_marine | Wave, swell, and sea-surface conditions: up to 8 forecast days or an archive range |
openmeteo_get_air_quality | Modeled CAMS pollutants, pollen, and AQI: current, up to 7 forecast days, or an archive range |
openmeteo_get_elevation | Copernicus DEM terrain elevation for up to 100 coordinate pairs |
openmeteo_get_ensemble | Per-member ensemble forecast (up to 64 members, 16 days) for exceedance and uncertainty |
openmeteo_get_flood | GloFAS river discharge forecast (up to 210 days) or reanalysis from 1984 |
openmeteo_get_climate | Bias-corrected daily CMIP6 projections, 1950–2050, across up to 7 models |
openmeteo_dataframe_describe | List the tables and columns staged on a DataCanvas |
openmeteo_dataframe_query | Run a read-only SQL SELECT against staged tables |
openmeteo_dataframe_drop | Remove one staged table or view from a DataCanvas (opt-in) |
openmeteo_search_locations toolname ("Paris", not "Paris, France"), with optional country (ISO 3166-1 alpha-2) and language (default en); count 1–10, default 5timezone, country / country_code, admin1 / admin2, population, and feature_code; no match fails as no_resultsnotice flags a top match with null or sub-100,000 population, which is what a historic exonym ("Bangalore", "Calcutta") often resolves toopenmeteo_get_forecast toolcurrent_variables, hourly_variables, or daily_variables (up to 50 each); forecast_days 1–16 (default 7), past_days 0–92 (default 0)hourly / daily records with hourly_units / daily_units; current_variables adds a current object from 15-minute data (interval 900) plus current_unitspast_days over openmeteo_get_historical for the last ~5 days, where the archive's ERA5 components lagopenmeteo_get_historical toolstart_date and end_date required (YYYY-MM-DD, from 1940-01-01), plus at least one of hourly_variables / daily_variables, named as on the forecast toolmodels reads Best Match (IFS HRES + ERA5 + ERA5-Land, source varies by date); up to 8 models pin a source. The ERA5 family runs ~5 days behind, ecmwf_ifs has no delay, and cerra covers Europe onlymodels and the returned date_range; with 2+ models each column carries a model-name suffixopenmeteo_get_marine toolforecast_days 1–8 (upstream default 7) with past_days 0–92, or a start_date / end_date archive range back to at least 2022. Mixing them fails as forecast_window_conflict, a lone date as date_range_incompletehourly_variables / daily_variables (wave height, period, and direction, swell, sea-surface temperature); sheltered or inland points return near-zero waves, and ocean_current_velocity is null off the open oceanopenmeteo_get_air_quality toolforecast_days 1–7 (upstream default 5) with past_days 0–92, or a start_date / end_date archive range from August 2022 (us_aqi starts a day later). Same forecast_window_conflict and date_range_incomplete rejections as the marine toolcurrent_variables (a current object, interval 3600) or hourly_variables: PM2.5, PM10, NO2, SO2, O3, CO, dust, pollen, European and US AQIdata_source: "CAMS" marks every response as modeled grid data, not station measurementsopenmeteo_get_elevation toollatitudes[] / longitudes[] arrays of up to 100 pairs; unequal lengths fail as coordinate_count_mismatchelevations[] in input order, each { latitude, longitude, elevation_m } from the Copernicus DEM (~90 m)openmeteo_get_ensemble toolhourly_variables / daily_variables; forecast_days 1–16 (default 7), past_days 0–92. models takes one ensemble name (e.g. ecmwf_ifs025_ensemble, 51 members; gem_global_ensemble, 21), or is omitted for the API default blendtemperature_2m_member01, …) across up to 64 members; member_count counts the perturbed members, not the control runopenmeteo_get_flood tooldaily_variables required (up to 20): river_discharge (ensemble mean), river_discharge_mean / _min / _max / _median / _p25 / _p75, all in m³/sforecast_days 1–210, or a start_date / end_date reanalysis range from 1984-01-01. Mixing them fails as forecast_days_conflict, a lone date as date_range_incompleteopenmeteo_get_climate toolstart_date / end_date within 1950-01-01 to 2050-12-31 and daily_variables required (the API is daily-only); up to 7 CMIP6 models, e.g. CMCC_CM2_VHR4, MRI_AGCM3_2_Sopenmeteo_dataframe_describe toolcanvas_id a spilled weather tool returned; lists each table's name, kind, row_count, and columns (name, type, nullability), plus the canvas expires_atcanvas_not_enabled when canvas is off, or canvas_not_found when the ID is unknown or past its 24-hour sliding TTLopenmeteo_dataframe_query toolcanvas_id plus a read-only SELECT against the staged table_name; rows is capped at 100 inline and row_count gives the full total, so page with LIMIT / OFFSETcanvas_not_enabled, canvas_not_found, missing_table, or system_catalog_access (system catalogs are blocked)openmeteo_dataframe_drop toolOPENMETEO_DATAFRAME_DROP_ENABLED=truecanvas_id plus the exact table_name from openmeteo_dataframe_describe; removes that one table or view and returns dropped and the remaining_tables. A name that is not staged returns dropped: false, so a repeated drop is safecanvas_not_enabled, canvas_not_found, or invalid_table_name (not a plain SQL identifier, or a reserved keyword)Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Open-Meteo-specific:
*_units maps, on the same variable names across forecast and archiveCANVAS_PROVIDER_TYPE=duckdb an over-budget result is staged as a DuckDB table and the response carries canvas_id, table_name, and truncated: true; with the default none it returns a bounded preview and truncated: truemodels on the historical, ensemble, and climate tools is not an allowlist, so a model Open-Meteo adds later works without a server updateAgent-friendly output:
openmeteo_search_locations returns the IANA timezone alongside coordinates, ready for any weather tool's timezone input (default auto)variable_wrong_cadence before the upstream call, naming the field it belongs in; an over-wide request fails as request_too_large, naming the inputs to narrow; rate-limit rejections are not retriednotice per response for what the data alone won't show: variable names the endpoint doesn't serve, requested windows outside a dataset's coverage, and where spilled rows wentA 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
Every variable is optional.
| Variable | Description | Default |
|---|---|---|
OPEN_METEO_API_BASE_URL | Forecast and elevation API base URL. | https://api.open-meteo.com |
OPEN_METEO_ARCHIVE_BASE_URL | Historical archive API base URL. | https://archive-api.open-meteo.com |
OPEN_METEO_MARINE_BASE_URL | Marine API base URL. | https://marine-api.open-meteo.com |
OPEN_METEO_AIR_QUALITY_BASE_URL | CAMS air quality API base URL. | https://air-quality-api.open-meteo.com |
OPEN_METEO_GEOCODING_BASE_URL | Geocoding API base URL. | https://geocoding-api.open-meteo.com |
OPEN_METEO_ENSEMBLE_BASE_URL | Ensemble API base URL. | https://ensemble-api.open-meteo.com |
OPEN_METEO_FLOOD_BASE_URL | GloFAS flood API base URL. | https://flood-api.open-meteo.com |
OPEN_METEO_CLIMATE_BASE_URL | CMIP6 climate API base URL. | https://climate-api.open-meteo.com |
CANVAS_PROVIDER_TYPE | duckdb stages over-budget weather results on a DataCanvas and enables the dataframe tools; none returns a bounded preview. | none |
OPENMETEO_DATAFRAME_DROP_ENABLED | true makes openmeteo_dataframe_drop callable; otherwise it is listed as disabled. | false |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, notice, warning, error). | info |
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 |
OTEL_ENABLED | Enable OpenTelemetry. | 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 and DataCanvas. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts), one file per tool. |
src/mcp-server/tools | Shared tool helpers — model catalog, cadence validation, columnar reshape, spillover, response notices. |
src/services/open-meteo | HTTP client for the Open-Meteo APIs — retry, error classification, coverage-gap rejection. |
src/services/canvas-accessor.ts | DataCanvas accessor shared by the spill-capable tools. |
tests/ | Unit, integration, fuzz, and smoke tests. |
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.tsWeather data by Open-Meteo.com, licensed CC BY 4.0.
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.