Skip to main content
Glama

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 reason on 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

geocode_location

Place name → coordinates, country, timezone, population

name (1–100 chars), max_results (1–10)

10 results

24 h

get_current_weather

Current conditions + plain-language sky description

latitude (−90…90), longitude (−180…180)

—

10 min

get_daily_forecast

Daily rows: highs/lows, precipitation, wind, UV, sunrise/sunset

coords, days (1–10), temperature_unit (celsius|fahrenheit)

10 days

15 min

get_hourly_forecast

Hourly rows: temp, apparent temp, precip probability/amount, wind

coords, hours (1–72)

72 h

15 min

get_air_quality

US EPA AQI + PM2.5, PM10, ozone, NO₂

coords, hours (1–48)

48 h

30 min

get_historical_weather

Archived daily weather (≈1940 → ~5 days ago)

coords, start_date/end_date (ISO YYYY-MM-DD)

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 in tools/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 to weather_description so 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 reason text 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.py

Claude 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 table

Request 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.13

Security 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 path

CI 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 tools
geocode_locationA
Read-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_qualityB
Read-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_weatherA
Read-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_forecastA
Read-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_weatherA
Read-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_forecastA
Read-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

    A3.9/5.0

    Scored across 6 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness5/5

    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

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      Provides 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.
      17
      1,384 npm
      73
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Provides 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.
      17
      1,384 npm
      MIT
    • F
      license
      Not graded
      quality
      C
      maintenance
      Provides 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
      -
    • A
      license
      A
      quality
      F
      maintenance
      Provides comprehensive weather data (forecasts, historical, air quality, marine) from Open-Meteo through 12 tools, with support for single and batch location queries.
      12
      1
      Apache 2.0