Global weather via Open-Meteo: forecast, historical, 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, historical archive, marine conditions, air quality, probabilistic ensembles, river discharge, and CMIP6 climate projections. Geocode place names, pull hourly and daily variables, 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 coordinate matches with country, region, elevation, timezone, and population |
openmeteo_get_forecast | Weather forecast for coordinates: current conditions and/or hourly and daily variables for up to 16 days, with optional recent past data; wide windows spill to DataCanvas |
openmeteo_get_historical | Historical weather from the Open-Meteo reanalysis archive (1940–present); Best Match by default, or pin a models selection; 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; current conditions, 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, resolving to the largest river within 5 km; 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 toolcountry filter (ISO 3166-1 alpha-2) or by raising count (default 5, up to 10) and reading admin1/country on each resulttimezone parameterno_results error (not an empty array) when nothing matchesnotice naming that place, its country, and its feature code — historic and colonial exonyms ("Bangalore", "Calcutta") can resolve to an unrelated small feature rather than the modern cityopenmeteo_get_forecast toolforecast_days, default 7) plus optional past_days (0–92) for recent history — prefer past_days over openmeteo_get_historical for the last ~5 days, where the archive's ERA5 components lagcurrent_variables, hourly_variables, or daily_variables is required; current_variables alone satisfies it and returns a current object plus current_units from Open-Meteo's 15-minute current-conditions datahourly_units/daily_units mapspast_days plus many hourly variables) spills to DataCanvas when CANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; an over-wide request Open-Meteo refuses outright fails as request_too_large, naming the levers to narrow itopenmeteo_get_historical toolstart_date and end_date (YYYY-MM-DD); archive covers 1940-01-01 to presentmodels reads Best Match (blends IFS HRES, ERA5, and ERA5-Land — source varies by date); pass models to pin one: era5/era5_land/era5_ensemble update daily with about a 5-day delay, ecmwf_ifs has none, cerra covers Europe only (elsewhere it fails as a coverage-gap error). Not an allowlist — an unlisted name still goes upstreamopenmeteo_get_forecast for direct past/forecast comparison; at least one of hourly_variables or daily_variables is required, and the two are separate sets — a wrong-cadence name is rejected before the requestCANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; query via openmeteo_dataframe_describe then openmeteo_dataframe_queryopenmeteo_get_marine toolforecast_days, upstream default 7) with optional past_days (0–92), or an archive range via start_date/end_date (real wave values go back to at least 2022)forecast_days/past_days, and needs both ends (a lone start_date or end_date is rejected)hourly_variables or daily_variables is required; the two are separate sets — a wrong-cadence name is rejected before the requestocean_current_velocity is null for non-open-ocean coordinatesCANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_air_quality toolforecast_days, upstream default 5) with optional past_days (0–92), or an archive range via start_date/end_date — the CAMS global archive begins August 2022; earlier dates return nulls, and us_aqi starts a day later than the pollutant seriesforecast_days/past_days, and needs both endscurrent_variables or hourly_variables is required; current_variables alone answers "right now" (a current object plus current_units, interval 3600s on this endpoint)openaq-mcp-server for measured readings; output carries data_source: "CAMS" to distinguish the twoCANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_elevation toollatitudes[]/longitudes[] arrays of equal length, up to 100 pairs per call{ latitude, longitude, elevation_m } from the Copernicus DEM (~90m resolution)openmeteo_get_ensemble toolforecast_days, default 7) with optional past_days (0–92); at least one of hourly_variables or daily_variables is required, and the two are separate sets (though this endpoint's own catalog publishes temperature_2m_max/_min under both)temperature_2m_member01, temperature_2m_member02, …) across up to 64 members — use the spread for exceedance probabilities and uncertainty rangesmodels selects one global or regional ensemble (member counts vary, e.g. ecmwf_ifs025_ensemble 51, gem_global_ensemble 21); omit for the API default blend. Not an allowlist — an unlisted name still goes upstreamCANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_flood toolforecast_days); reanalysis history from 1984-01-01 to present via start_date/end_dateforecast_days is mutually exclusive with a start_date/end_date range, and the range needs both endsriver_discharge (ensemble mean), river_discharge_mean/_min/_max/_median, river_discharge_p25/_p75 — all in m³/s; returns null for coordinates outside GloFAS coverageCANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_get_climate toolopenmeteo_get_historicalCMCC_CM2_VHR4, MRI_AGCM3_2_S); not an allowlist — an unlisted name still goes upstream, and a multi-model rejection names only the offending modelCANVAS_PROVIDER_TYPE=duckdb — output carries canvas_id and truncated: true; query with openmeteo_dataframe_queryopenmeteo_dataframe_describe toolopenmeteo_get_forecast, _historical, _marine, _air_quality, _ensemble, _flood, _climate) — call this first, since table and column names are generated per requestcanvas_not_enabled when CANVAS_PROVIDER_TYPE is not duckdb, or canvas_not_found when canvas_id is unknown or past its 24-hour sliding TTLexpires_atopenmeteo_dataframe_query toolSELECT against tables staged by the seven spillover tools — pass the canvas_id and reference the exact table_name they return, or discover both via openmeteo_dataframe_describecanvas_not_enabled when CANVAS_PROVIDER_TYPE is not duckdb, canvas_not_found when canvas_id is unknown or past its 24-hour sliding TTL, or missing_table when the SQL references a table not staged on the canvasinformation_schema, sqlite_master, pg_catalog, duckdb_*() functions) are blocked so callers cannot enumerate other staged canvases — fails as system_catalog_accessrow_count reports the full total — page further results with LIMIT/OFFSET in the SQLBuilt 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:
openmeteo_search_locations resolves place names so agents don't need a separate geocodermodels selector for pinning the reanalysis source*_units mapCANVAS_PROVIDER_TYPE=none (the default) those tools return a bounded preview with truncated: true insteadAgent-friendly output:
openmeteo_search_locations returns the IANA timezone alongside coordinates, ready to pass straight to any weather tool's timezone parameterrequest_too_large naming the levers to shrink it, distinct from a rate-limit rejection, which is not retriednotice rather than passing as an unremarked data gapA 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_HOST | HTTP server host | 127.0.0.1 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path | /mcp |
MCP_HTTP_MAX_BODY_BYTES | Maximum HTTP request body bytes; 0 disables the limit. | 1048576 |
MCP_PUBLIC_URL | Public origin for TLS-terminating reverse-proxy deployments | — |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. The server declares stateless in code, so it applies when this is unset; set a value to override it. auto resolves to stateful. | stateless |
MCP_HTTP_RESUMABILITY | Replay missed SSE events for stateful HTTP sessions. No effect on stateless mode or protocol revision 2026-07-28. | true |
MCP_HTTP_RESUMABILITY_MAX_EVENTS | Events retained per stateful session for replay; oldest evicted first. | 512 |
MCP_HTTP_RESUMABILITY_TTL_MS | How long retained events remain replayable (ms). | 300000 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (debug, info, notice, warning, error) | info |
MCP_LOG_RATE_LIMIT_THRESHOLD | Maximum repeated emissions per level and message in each log window; 0 disables suppression. | 10 |
MCP_LOG_RATE_LIMIT_WINDOW_MS | Repeated-log suppression window (ms). | 60000 |
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 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.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.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/open-meteo-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-open-meteo-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/open-meteo-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/open-meteo-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.