# open-meteo-mcp-server [Health: Active]

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/cyanheads/open-meteo-mcp-server  
**GitHub Stars:** 5  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/open-meteo-mcp-server

## Description
Global weather via Open-Meteo: forecast, ERA5 archive, marine, air quality, geocoding, elevation.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `bunx` (confidence: high):

```json
"mcpServers": {
  "open-meteo-mcp-server": {
    "command": "bunx",
    "args": ["@cyanheads/open-meteo-mcp-server@latest"]
  }
}
```

## Documentation & README

<div align="center">
  <h1>@cyanheads/open-meteo-mcp-server</h1>
  <p><b>Geocode places, fetch global weather forecasts, 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">

[![Version](https://img.shields.io/badge/Version-0.3.9-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/open-meteo-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/open-meteo-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/open-meteo-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)

</div>

<div align="center">

[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/open-meteo-mcp-server/releases/latest/download/open-meteo-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=open-meteo-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvb3Blbi1tZXRlby1tY3Atc2VydmVyIl19) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22open-meteo-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Fopen-meteo-mcp-server%22%5D%7D)

[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)

</div>

<div align="center">

**Public Hosted Server:** [https://open-meteo.caseyjhand.com/mcp](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: 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`

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 `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 inputs
- Pass the timezone from an `openmeteo_search_locations` result directly to weather tools as the `timezone` parameter
- Fails with a `no_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 instead
- When the top match has null or sub-100,000 population, the response carries an advisory `notice` naming that place, its country, and its feature code. Historic and colonial exonyms ("Bangalore", "Calcutta", "Peking") resolve to unrelated small features rather than the modern city, and the index returns no error for it — the results themselves are returned unchanged, so verify the coordinates or retry with the place's current official name

---

### `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)
- `current_variables` returns conditions at this instant from Open-Meteo's 15-minute current-conditions data — a `current` object (the variables plus `time` and `interval`, the update cadence in seconds) and a matching `current_units` map. It satisfies the variable requirement on its own, so a "what's it doing right now?" call needs no hourly series; both keys are absent when it isn't requested
- `past_days` (0–92) covers recent history via the forecast model — use instead of `openmeteo_get_historical` for dates within the last ~5 days, where the archive's ERA5 components 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 `current_variables`, `hourly_variables`, or `daily_variables` is 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_cover` in `daily_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_units` map
- A wide window (a large `past_days` plus many hourly variables) spills to DataCanvas when `CANVAS_PROVIDER_TYPE=duckdb` — output includes `canvas_id` and `truncated: true`; query with `openmeteo_dataframe_query`

---

### `openmeteo_get_historical`

Historical weather from the Open-Meteo reanalysis archive, covering 1940 to the present.

- Requires `start_date` and `end_date` (YYYY-MM-DD)
- Omitting `models` reads Open-Meteo's Best Match, which blends IFS HRES, ERA5, and ERA5-Land — the source varies by date, so no single update lag describes the response. Pass `models` to pin one: `era5`, `era5_land`, and `era5_ensemble` update daily with about a 5-day delay, `ecmwf_ifs` has none, and `cerra` covers Europe only (requested elsewhere it returns a coverage-gap error naming the model). Not an allowlist — an unlisted name is sent upstream unchanged, and the response echoes the selection on `models`
- With 2+ models each variable column is suffixed with the model name
- Same variable vocabulary as `openmeteo_get_forecast` — past and forecast data are directly comparable on one schema
- At least one of `hourly_variables` or `daily_variables` is 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 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 count
- Spill → query workflow: call `openmeteo_dataframe_describe` with the `canvas_id` to list tables, then `openmeteo_dataframe_query` to 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 optional `past_days` (0–92)
- Or an archive range via `start_date` and `end_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 lone `start_date` or `end_date` is 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_variables` or `daily_variables` is 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_velocity` is null for non-open-ocean coordinates
- Wide windows spill to DataCanvas when `CANVAS_PROVIDER_TYPE=duckdb` — output includes `canvas_id` and `truncated: true`; query with `openmeteo_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 optional `past_days` (0–92)
- Or an archive range via `start_date` and `end_date` — the CAMS global archive begins in August 2022; earlier dates return rows of nulls, and `us_aqi` starts a day later than the pollutant series (`european_aqi` starts with it)
- One window per call: a date range is mutually exclusive with `forecast_days`/`past_days`, and needs both ends — a lone `start_date` or `end_date` is 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`
- `current_variables` returns pollutant and index values at this instant — a `current` object (the variables plus `time` and `interval`, the update cadence in seconds, 3600 on this endpoint) and a matching `current_units` map. It satisfies the variable requirement on its own, so an "AQI right now" call needs no hourly series; both keys are absent when it isn't requested
- At least one variable from `current_variables` or `hourly_variables` is 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 includes `canvas_id` and `truncated: true`; query with `openmeteo_dataframe_query`

---

### `openmeteo_get_elevation`

Terrain elevation from the Copernicus Digital Elevation Model (~90m resolution).

- Accepts parallel `latitudes[]` and `longitudes[]` 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 optional `past_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 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 retrying
- Omit `models` 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 working
- Response includes `model` (system used) and `member_count` (perturbed members, excluding the control run)
- At least one of `hourly_variables` or `daily_variables` is 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_max` and `temperature_2m_min` as 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 includes `canvas_id` and `truncated: true`; query with `openmeteo_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; discharge comes from the largest modeled river within 5 km of the point, which is not always the closest one. Near confluences and parallel channels this can select an unintended reach; Open-Meteo's own suggestion is to vary the coordinate by about 0.1° and compare the values when a result looks unrepresentative
- Forecast horizon up to 210 days; reanalysis history from 1984-01-01 to present
- One mode per call: `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 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 includes `canvas_id` and `truncated: true`; query with `openmeteo_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_VHR4` has no `shortwave_radiation_sum`)
- Multi-decade daily pulls across several models spill to DataCanvas when `CANVAS_PROVIDER_TYPE=duckdb` — output includes `canvas_id` and `truncated: true`; query with `openmeteo_dataframe_query`
- Configurable temperature, wind speed, and precipitation units

## Features

Built on [`@cyanheads/mcp-ts-core`](https://github.com/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_locations` resolves place names so agents don't need a separate geocoder
- Historical archive from 1940 to present with same variable schema as the forecast API — direct past/forecast comparisons on one schema, and a `models` selector for pinning the reanalysis source
- Automatic columnar-to-record reshape: Open-Meteo returns parallel time/variable arrays; handlers convert to per-timestamp records with a `*_units` map
- DataCanvas spillover for `openmeteo_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 complete, and the disclosure explaining the absent `canvas_id` and how to reach the omitted rows travels in `notice` as well as in the rendered text. A truncated response omits the cadence key that was never requested, exactly as an untruncated one does
- 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](https://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_locations` returns the IANA timezone alongside coordinates — pass it directly as `timezone` to any weather tool
- Recovery hints on all error contracts — invalid variable names surface correction guidance with common variable examples
- Upstream rejections are told apart rather than collapsed: a request Open-Meteo refuses as too wide returns `request_too_large`, naming the levers that shrink it (fewer variables, fewer models, a narrower window) instead of a spelling check, and an HTTP 429 keeps its rate-limit code, relays Open-Meteo's own quota wording, and is not retried — the window reopens on a clock, not on a retry
- Cadence-aware variable validation on `openmeteo_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
- 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`, and `openmeteo_get_climate` take 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:

```json
{
  "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.

```json
{
  "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):

```json
{
  "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:

```json
{
  "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:

```sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
```

### Prerequisites

- [Bun v1.3.0](https://bun.sh/) 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](https://open-meteo.com/en/pricing).

### Installation

1. **Clone the repository:**

```sh
git clone https://github.com/cyanheads/open-meteo-mcp-server.git
```

2. **Navigate into the directory:**

```sh
cd open-meteo-mcp-server
```

3. **Install dependencies:**

```sh
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_HOST` | HTTP server host | `localhost` |
| `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`. `auto` resolves to `stateful`; this server holds no per-session state and ships `stateless` as its explicit default. | `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`](https://github.com/cyanheads/open-meteo-mcp-server/blob/HEAD/.env.example) for the full list of optional overrides.

## Running the server

### Local development

- **Build and run the production version:**

  ```sh
  # One-time build
  bun run rebuild

  # Run the built server
  bun run start:http
  # or
  bun run start:stdio
  ```

- **Run checks and tests:**

  ```sh
  bun run devcheck  # Lint, format, typecheck, security
  bun run test      # Vitest test suite
  ```

### Docker

```sh
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`](https://github.com/cyanheads/open-meteo-mcp-server/blob/HEAD/CLAUDE.md) for development guidelines and architectural rules. The short version:

- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
- Register new tools in the `tools[]` array in `src/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:

```sh
bun run devcheck
bun run test
```

## License

Apache-2.0 — see [LICENSE](https://github.com/cyanheads/open-meteo-mcp-server/blob/HEAD/LICENSE) for details.

---

> Weather data by [Open-Meteo.com](https://open-meteo.com/) — licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).

