Open-Meteo MCP server
README.md
# Open-Meteo MCP server
[](https://github.com/amirhoseinTaghizadeh/open-meteo-mcp/actions/workflows/ci.yml)
An [MCP](https://modelcontextprotocol.io) server in TypeScript that gives an LLM three weather tools backed by the free [Open-Meteo](https://open-meteo.com) 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 |
| ------------------- | --------------------------------------------------------------------------- |
| `search_places` | Find a place by name, get coordinates and timezone. |
| `get_forecast` | Current conditions and a 1 to 16 day forecast for a coordinate. |
| `compare_locations` | Look up 2 to 5 places at once, average the forecasts, say which is warmest. |
## Run it
With Docker:
```bash
docker compose up --build
```
Or with Node 24:
```bash
npm ci
npm run build
npm start
```
Then 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:
```bash
npm run dev # server on :3000
npm run dev:web # vite on :5173, proxies /mcp
```
### From Claude Desktop or MCP Inspector
These start the server over stdio. Build first, then point them at `server/dist/stdio.js`:
```json
{
"mcpServers": {
"open-meteo": {
"command": "node",
"args": ["/absolute/path/to/open-meteo-mcp/server/dist/stdio.js"]
}
}
}
```
```bash
npx @modelcontextprotocol/inspector node server/dist/stdio.js
```
## Tools
### `search_places`
| Input | Type | Rules |
| ------- | ------- | ----------------------------- |
| `query` | string | required, 2 to 100 characters |
| `count` | 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 |
| ----------- | ------- | ----------------------------------- |
| `latitude` | number | required, -90 to 90 |
| `longitude` | number | required, -180 to 180 |
| `days` | integer | 1 to 16, default 3 |
| `units` | enum | `celsius` (default) or `fahrenheit` |
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 |
| -------- | -------- | ------------------------------------- |
| `places` | string[] | required, 2 to 5 names, no duplicates |
| `days` | integer | 1 to 7, default 3 |
| `units` | enum | `celsius` (default) or `fahrenheit` |
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.ts` builds the request, caches responses for a minute, times out after 5s, validates the JSON with Zod and turns every failure into one `OpenMeteoError` with a readable message.
- `tools/*.ts` are plain objects: Zod input and output schemas, an `execute`, and a `summarize` for the text block. All validation rules live in the schemas, so clients can see them in `tools/list`.
- `mcp.ts` registers the tools and decides what the client sees: a known failure becomes `isError` with a sentence, a bug is logged to stderr and answered with a generic message.
- `http-server.ts` is a plain `node:http` server: `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:
1. Host and Origin check (the SDK's DNS-rebinding guard, HTTP only).
2. Only `POST` on `/mcp`, and a bearer token if `MCP_BEARER_TOKEN` is set.
3. Input validation against the tool's schema, before the handler runs.
4. A rate limit for the whole process (`TOOL_CALLS_PER_MINUTE`). Over the limit, the LLM gets a sentence saying how long to wait.
5. The tool itself. Upstream failures become readable `isError` results.
6. Output validation against `outputSchema`.
7. One audit line per call on stderr:
```json
{
"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 |
| ----------------------- | --------------------------------- | --------------------------------------------------- |
| `PORT` | `3000` | Listen port |
| `HOST` | `127.0.0.1` (`0.0.0.0` in Docker) | Bind address |
| `ALLOWED_HOSTS` | `localhost,127.0.0.1` | Accepted `Host` and `Origin` values |
| `STATIC_DIR` | `web/dist` | Built UI directory |
| `TOOL_CALLS_PER_MINUTE` | `60` | Rate limit across the process, `0` disables |
| `MCP_BEARER_TOKEN` | unset | When set, `/mcp` requires `Authorization: Bearer …` |
## Tests
```bash
npm test # 47 tests, no network
npm run lint
npm run typecheck
```
Open-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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues