openmeteo-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@openmeteo-mcp what's the current weather in San Francisco?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
openmeteo-mcp
A reference-quality demo MCP server: six narrow, well-typed weather tools over the free, keyless Open-Meteo API.
This repo exists to demonstrate what senior MCP server craft looks like —
typed input schemas with range constraints, deliberate response sizing,
TTL caching matched to upstream update cadence, error messages written for
an LLM caller, hermetic tests, and CI. It is intentionally small enough to
read in one sitting (src/ is ~690 lines).
Built on the official MCP Python SDK
(v2.x, MCPServer), served over stdio.
Why Open-Meteo?
Zero credentials. No API key, no signup, no env vars — anyone can clone this repo and have a working MCP server in under a minute. For a public demo, an auth-free upstream removes the single biggest source of setup friction and secret-handling risk.
Real utility, four distinct services. Geocoding, forecast, air quality, and a historical archive (ERA5, back to 1940) under one provider. That variety is exactly what rewards narrow-tool decomposition instead of one god-tool with mode flags.
Well-behaved API surface. Stable JSON, per-field units, and a machine-readable
reasonon errors — which this server surfaces verbatim so callers get actionable messages instead of opaque HTTP codes.Generous free tier (10,000 calls/day) — and this server stays far below it thanks to TTL caching.
Related MCP server: open-meteo-mcp-server
Tools
Tool | Purpose | Key inputs (validated) | Hard caps | Cache TTL |
| Place name → coordinates, country, timezone, population |
| 10 results | 24 h |
| Current conditions + plain-language sky description |
| — | 10 min |
| Daily rows: highs/lows, precipitation, wind, UV, sunrise/sunset | coords, | 10 days | 15 min |
| Hourly rows: temp, apparent temp, precip probability/amount, wind | coords, | 72 h | 15 min |
| US EPA AQI + PM2.5, PM10, ozone, NO₂ | coords, | 48 h | 30 min |
| Archived daily weather (≈1940 → ~5 days ago) | coords, | 366 days/call | 24 h |
Every tool is annotated readOnlyHint = true, destructiveHint = false,
idempotentHint = true, openWorldHint = true.
Design principles
Narrow tools, not a god-tool. One tool per question a user actually asks. Clients see small schemas; the LLM can't pick a wrong "mode".
Validation at the schema. Ranges (
ge/le), string lengths, regex date patterns, and enum units are declared in the tool signature, so invalid arguments are rejected before any network call — and the JSON Schema is visible to clients intools/list.Response size discipline. Columnar API arrays are zipped into compact row dicts with units attached, lists are capped (and defensively re-sliced even if upstream ignores the cap), geocoding drops junk fields like
postcodes, and WMO weather codes are expanded inline toweather_descriptionso no second lookup is needed.TTL cache tuned to data cadence. Current observations change every ~15 min → 10 min TTL; forecasts → 15 min; place metadata and archive data are effectively immutable → 24 h. Bounded at 256 entries with expire-first/FIFO eviction.
Errors written for an LLM. Upstream
reasontext is surfaced verbatim; timeouts/network failures explain what happened and suggest a retry; validation failures name the field and the constraint.
Quickstart
git clone <this-repo> && cd mcp-demo-weather
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
# hermetic test suite (56 tests, no network)
.venv/bin/python -m pytest
# real end-to-end check: spawns the server over stdio, performs the MCP
# handshake, lists tools, and makes live calls to Open-Meteo
.venv/bin/python scripts/verify_stdio.pyClaude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"open-meteo": {
"command": "/absolute/path/to/mcp-demo-weather/.venv/bin/python",
"args": ["-m", "openmeteo_mcp.server"]
}
}
}Cursor
~/.cursor/mcp.json (same shape):
{
"mcpServers": {
"open-meteo": {
"command": "/absolute/path/to/mcp-demo-weather/.venv/bin/python",
"args": ["-m", "openmeteo_mcp.server"]
}
}
}Once published, uvx --from git+https://github.com/<you>/mcp-demo-weather openmeteo-mcp
also works as the command (the console script runs the same stdio server).
Architecture
src/openmeteo_mcp/
├── server.py # MCPServer instance + 6 tools: schema-typed inputs,
│ # response shaping/truncation, APIError → ToolError mapping
├── client.py # async httpx wrapper: one fixed base URL per Open-Meteo
│ # service, coordinate validation, per-endpoint TTL cache
│ # wiring, error normalization
├── cache.py # TTLCache: bounded, thread-safe, lazy expiry,
│ # expire-first-then-FIFO eviction (~90 lines, no deps)
└── wmo.py # WMO 4677 weather-code → plain-language tableRequest flow: tools/call → pydantic schema validation → OpenMeteoClient
(cache hit? return) → httpx GET to a fixed allowlisted host → response
validated/normalized → zipped, truncated, annotated rows → structured JSON
back to the client. Logs go to stderr only (stdout carries the protocol).
tests/ # hermetic: respx mocks every upstream response
scripts/verify_stdio.py # live smoke: real handshake + real tool calls
.github/workflows/ci.yml # pytest on Python 3.11/3.12/3.13Security posture
Read-only. Every tool is a lookup; nothing writes, executes, or retains user data. Annotations declare this machine-readably.
No credentials, no env, no config. There is nothing to leak; the server works with a completely empty environment.
No URL injection surface. Base URLs are module constants; caller input only ever becomes typed query parameters (floats/ints/enums/date patterns). A tool call cannot redirect the server to another host.
No tool-poisoning surface. Tool names, descriptions, and annotations are static code — never derived from upstream responses or user input.
Bounded in and out. Input ranges at the schema; output caps (72 h / 10 days / 366-day archive spans) and defensive re-slicing keep responses context-window sized.
Timeboxed network. 10 s httpx timeout, no redirects, single retry-free attempt with clear errors — no hanging tool calls.
stdio discipline. Protocol on stdout, logs on stderr; no files, subprocesses, or shell anywhere in the codebase.
Verification
Both of these were run against the initial commit:
$ .venv/bin/python -m pytest -q
56 passed in 0.33s
$ .venv/bin/python scripts/verify_stdio.py
== initialize ==
serverInfo: open-meteo v0.1.0 | protocol: 2025-11-25
== tools/list ==
6 tools: geocode_location, get_air_quality, get_current_weather,
get_daily_forecast, get_historical_weather, get_hourly_forecast
== tools/call: geocode_location {"name": "Reykjavik"} ==
{"count": 1, "results": [{"name": "Reykjavik", "country": "Iceland",
"latitude": 64.13548, "longitude": -21.89541, "timezone": "Atlantic/Reykjavik", ...}]}
== tools/call: get_current_weather (live coordinates) ==
{"timezone": "Atlantic/Reykjavik", "current": {"temperature_2m": -0.7,
"weather_code": 0, "weather_description": "Clear sky", "is_day": true, ...}}
== tools/call: schema violation ==
isError=True
Error executing tool get_current_weather: ... Input should be less than or equal to 90 ...
VERIFY OK: handshake, tools/list, 2 live tool calls, 1 clean error pathCI runs the same suite on every push/PR across Python 3.11–3.13.
License
MIT © 2026 Dylan Sproule. Weather data by Open-Meteo (free for non-commercial use; see their terms).
Available Tools
6 toolsgeocode_locationARead-onlyIdempotent
Resolve a place name to coordinates, country, timezone and population.
Call this first when you only have a place name, then pass the returned latitude/longitude into any weather tool.
Inputs: name (required), max_results. Returns: object
get_air_qualityBRead-onlyIdempotent
Hourly air quality (up to 48 hours): US EPA AQI plus PM2.5, PM10, ozone and NO2 concentrations.
Inputs: latitude (required), longitude (required), hours. Returns: object
get_current_weatherARead-onlyIdempotent
Current weather at a coordinate: temperature, apparent temperature, humidity, precipitation, wind, and a plain-language sky description.
Inputs: latitude (required), longitude (required). Returns: object
get_daily_forecastARead-onlyIdempotent
Day-by-day forecast (up to 10 days): highs/lows, precipitation amount and probability, max wind, UV index, and sunrise/sunset, in local time.
Inputs: latitude (required), longitude (required), days, temperature_unit. Returns: object
get_historical_weatherARead-onlyIdempotent
Historical daily weather from the Open-Meteo archive (about 1950 to roughly five days ago): highs/lows, precipitation, snowfall and sky conditions. Ranges are capped at 366 days per call.
Inputs: end_date (required), latitude (required), longitude (required), start_date (required). Returns: object
get_hourly_forecastARead-onlyIdempotent
Hour-by-hour forecast (up to 72 hours): temperature, apparent temperature, precipitation probability and amount, wind, and sky description.
Inputs: latitude (required), longitude (required), hours. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool targets a distinct weather data type or workflow step: geocoding, current conditions, daily forecast, hourly forecast, air quality, and historical weather. There is no meaningful overlap, and the descriptions clearly indicate when to use each tool.
All tools use snake_case and most follow a clear get_<resource> pattern. geocode_location is a minor deviation from the get_ prefix, but it remains readable and predictable in context.
Six tools are well-scoped for a weather API server, covering the main forecast and historical data needs without redundancy. Each tool has a clear purpose and earns its place.
The surface covers geocoding plus current, daily, hourly, historical, and air-quality data, which are the core operations for an Open-Meteo-style server. No obvious lifecycle gaps or dead ends are apparent for this domain.
Related MCP Connectors
Global weather via Open-Meteo: forecast, historical, marine, air quality, geocoding, elevation.
Current weather and forecast lookup as an agent tool.
Current weather and forecasts for any coordinates, backed b… — paid per call (x402/credits), 1 tools
US weather & geo for AI agents: forecasts, alerts, earthquakes, elevation, geocoding. No keys.
Related MCP Servers
- AlicenseBqualityBmaintenanceProvides comprehensive access to Open-Meteo APIs for weather forecasts, historical data, air quality, and marine conditions. It enables LLMs to query specialized meteorological models, perform geocoding, and access advanced climate or flood projections.171,384 npm73MIT
- AlicenseAqualityDmaintenanceProvides comprehensive access to Open-Meteo weather APIs, including forecasts, historical data, air quality, marine weather, and geocoding, enabling LLMs to retrieve weather information and location data.171,384 npmMIT
- FlicenseNot gradedqualityCmaintenanceProvides real-time weather data via 12 tools (current weather, forecasts, air quality, umbrella advice, etc.) using Open-Meteo and FastMCP, enabling LLMs to answer weather-related queries.1-
- AlicenseAqualityFmaintenanceProvides comprehensive weather data (forecasts, historical, air quality, marine) from Open-Meteo through 12 tools, with support for single and batch location queries.121Apache 2.0