Open-Meteo MCP server
Open-Meteo MCP server
An MCP server in TypeScript that gives an LLM three weather tools backed by the free Open-Meteo API. It comes with a small React page that talks to the server over MCP, so you can try the tools in a browser.
No API key, no config. One command runs it.
Tool | What it does |
| Find a place by name, get coordinates and timezone. |
| Current conditions and a 1 to 16 day forecast for a coordinate. |
| Look up 2 to 5 places at once, average the forecasts, say which is warmest. |
Run it
With Docker:
docker compose up --buildOr with Node 24:
npm ci
npm run build
npm startThen open http://localhost:3000. The MCP endpoint is POST /mcp (Streamable HTTP) and GET /healthz is the health check.
For development, run the server and the UI separately so both reload:
npm run dev # server on :3000
npm run dev:web # vite on :5173, proxies /mcpFrom Claude Desktop or MCP Inspector
These start the server over stdio. Build first, then point them at server/dist/stdio.js:
{
"mcpServers": {
"open-meteo": {
"command": "node",
"args": ["/absolute/path/to/open-meteo-mcp/server/dist/stdio.js"]
}
}
}npx @modelcontextprotocol/inspector node server/dist/stdio.jsTools
search_places
Input | Type | Rules |
| string | required, 2 to 100 characters |
| integer | 1 to 10, default 5 |
Returns { query, results: Place[] }. No match is not an error, just an empty list.
get_forecast
Input | Type | Rules |
| number | required, -90 to 90 |
| number | required, -180 to 180 |
| integer | 1 to 16, default 3 |
| enum |
|
Returns { location, units, current, daily[] } with readable condition labels ("Partly cloudy"). Fahrenheit also switches wind to mph and precipitation to inches.
compare_locations
Input | Type | Rules |
| string[] | required, 2 to 5 names, no duplicates |
| integer | 1 to 7, default 3 |
| enum |
|
Places are looked up concurrently. Each row has its own status (ok, not_found or failed), so one bad name doesn't fail the others. The call only errors when nothing could be answered. Ties are named in the text.
Every tool returns a short text block for people and LLMs plus structuredContent matching its outputSchema.
How it's built
stdio ──▶ server/src/stdio.ts ─┐
├─▶ mcp.ts ─▶ tools/* ─▶ openmeteo/client.ts ─▶ Open-Meteo
HTTP ──▶ server/src/http.ts ──┘
(also serves web/dist)openmeteo/client.tsbuilds the request, caches responses for a minute, times out after 5s, validates the JSON with Zod and turns every failure into oneOpenMeteoErrorwith a readable message.tools/*.tsare plain objects: Zod input and output schemas, anexecute, and asummarizefor the text block. All validation rules live in the schemas, so clients can see them intools/list.mcp.tsregisters the tools and decides what the client sees: a known failure becomesisErrorwith a sentence, a bug is logged to stderr and answered with a generic message.http-server.tsis a plainnode:httpserver:POST /mcp,/healthz, static files. No Express needed.web/is a real MCP client (@modelcontextprotocol/client). Forms are generated from the tool schemas, so adding a tool needs no UI change.
Guardrails
Every tool call goes through, in order:
Host and Origin check (the SDK's DNS-rebinding guard, HTTP only).
Only
POSTon/mcp, and a bearer token ifMCP_BEARER_TOKENis set.Input validation against the tool's schema, before the handler runs.
A rate limit for the whole process (
TOOL_CALLS_PER_MINUTE). Over the limit, the LLM gets a sentence saying how long to wait.The tool itself. Upstream failures become readable
isErrorresults.Output validation against
outputSchema.One audit line per call on stderr:
{
"event": "tool_call",
"ts": "2026-09-10T09:14:02.118Z",
"tool": "compare_locations",
"args": { "places": ["Lisbon", "Porto"], "days": 3, "units": "celsius" },
"outcome": "ok",
"durationMs": 412
}Configuration
Variable | Default | Meaning |
|
| Listen port |
|
| Bind address |
|
| Accepted |
|
| Built UI directory |
|
| Rate limit across the process, |
| unset | When set, |
Tests
npm test # 47 tests, no network
npm run lint
npm run typecheckOpen-Meteo is stubbed, the protocol tests use the SDK's in-memory transport, and the HTTP tests start the real server on a random port. CI runs all of it plus a Docker build.
What I'd do next
Move the cache and rate limit to a shared store for multi-instance deployments.
Per-client identity (OAuth) so limits and permissions can be per user, not per process.
Ship audit lines to a log store instead of stderr.