Skip to main content
Glama
openmeteo-mcp
by DrokAI

openmeteo-mcp

A reference-quality demo MCP server: six narrow, well-typed weather tools over the free, keyless Open-Meteo API — written in TypeScript on the official MCP TypeScript SDK.

This repo exists to demonstrate what senior MCP server craft looks like — zod input schemas with range constraints, deliberate response sizing, per-endpoint 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 ~840 lines, zero build step).

Runs natively on Bun (first-class) and Node ≥ 22.18 (native TS type stripping) — no compile step, no dist/ to keep in sync.

Why Open-Meteo?

  • Zero credentials. No API key, no signup, no env vars — anyone can run this 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: Weather 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. zod ranges, 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 objects 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 (MCP error -32602).

Quickstart

Runs in under a minute. With Bun installed:

# zero-install smoke — clones, installs, and starts the stdio server
bunx github:DrokAI/mcp-demo-weather

or the classic way (also works with npm install on Node ≥ 22.18):

git clone https://github.com/DrokAI/mcp-demo-weather
cd mcp-demo-weather
bun install            # or: npm install

bun run test           # hermetic suite (71 tests, no network)
bun run verify         # LIVE check: spawns the server over stdio,
                       # handshake, tools/list, 2 real calls, 1 clean error
bun run start          # serve MCP over stdio
bun run typecheck      # tsc --noEmit (TypeScript 7)

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "open-meteo": {
      "command": "bun",
      "args": ["/absolute/path/to/mcp-demo-weather/bin/openmeteo-mcp.js"]
    }
  }
}

On Node ≥ 22.18, swap "command": "bun" for "command": "node" (same args). Zero-clone alternative: "command": "bunx", "args": ["github:DrokAI/mcp-demo-weather"].

Cursor

~/.cursor/mcp.json (same shape):

{
  "mcpServers": {
    "open-meteo": {
      "command": "bun",
      "args": ["/absolute/path/to/mcp-demo-weather/bin/openmeteo-mcp.js"]
    }
  }
}

Architecture

src/
├── server.ts    # McpServer (official SDK) + the 6 tools: zod-typed inputs,
│                # response shaping/truncation, APIError → isError mapping
├── client.ts    # fetch wrapper: one fixed base URL per Open-Meteo service,
│                # coordinate validation, per-endpoint TTL cache wiring,
│                # error normalization (timeout / HTTP / reason passthrough)
├── cache.ts     # TTLCache: bounded (256), lazy expiry,
│                # expire-first-then-FIFO eviction (~90 lines, no deps)
├── wmo.ts       # WMO 4677 weather-code → plain-language table
├── version.ts   # single version constant
└── index.ts     # entry: buildServer() + StdioServerTransport
bin/openmeteo-mcp.js   # shebang launcher (bun or node ≥ 22.18)

Request flow: tools/call → zod schema validation (rejects before any network) → OpenMeteoClient (cache hit? return) → fetch GET to a fixed allowlisted host → response normalized → zipped, truncated, annotated rows → compact JSON back to the client. Logs go to stderr only (stdout carries the protocol).

tests/                      # hermetic: a fake fetch injects every upstream
                            # response; server+client connect over an
                            # in-memory transport pair (71 tests)
scripts/verify-stdio.ts     # LIVE smoke: raw initialize + SDK client,
                            # tools/list, real tool calls, clean error path
.github/workflows/ci.yml    # tsc --noEmit + vitest on Bun and Node 22

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 (numbers/enums/date patterns). A tool call cannot redirect the server to another host (redirect: "error" — redirects are refused, not followed).

  • 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 AbortSignal.timeout, no redirects, no retries with unclear 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 this commit:

$ bun run test
 Test Files  3 passed (3)
      Tests  71 passed (71)

$ bun run verify
== initialize ==
serverInfo: open-meteo v0.2.0 | protocol: 2025-11-25
== tools/list ==
6 tools: geocode_location, get_air_quality, get_current_weather, get_daily_forecast, get_history_weather, get_hourly_forecast
== tools/call: geocode_location ==
2 results: Reykjavik, Iceland (64.13548, -21.89541) tz=Atlantic/Reykjavik
== tools/call: get_current_weather ==
Atlantic/Reykjavik: -0.3°C, Moderate snow fall
== schema violation ==
rejected as expected: MCP error -32602: Input validation error: Invalid arguments for tool get_current_weather: Too big: expected number to be <=90 at latitude

VERIFY OK: 6 steps — raw initialize (protocol version), tools/list (6 tools), 2 live tool calls, 1 clean schema-violation error

MCP Inspector smoke also passes:

$ npx @modelcontextprotocol/inspector --cli node bin/openmeteo-mcp.js --method tools/list
# full tool schemas + annotations (readOnlyHint: true, ...)
$ npx @modelcontextprotocol/inspector --cli node bin/openmeteo-mcp.js \
    --method tools/call --tool-name geocode_location --tool-arg name=Osaka
# 5 compact place rows

CI runs typecheck + the same hermetic suite on every push/PR, on Bun and Node 22.

License

MIT © 2026 Drok AI. Weather data by Open-Meteo (free for non-commercial use; see their terms).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers