Skip to main content
Glama
weather-mcp
by Z4D4LY
README.md
# weather-mcp

**An MCP server that gives any AI client live weather, sea, and news data for any place on
Earth — free, with no API keys and no signup.**

[Model Context Protocol](https://modelcontextprotocol.io/) server (stdio transport) exposing
five tools. Ask an AI assistant about a city and it calls these tools to fetch real,
up-to-the-minute data instead of guessing.

- šŸ†“ **Zero running cost** — Open-Meteo and RSS need no keys or accounts.
- šŸŒ **Any location** — geocoding by name, worldwide.
- šŸŒ”ļø **Metric or imperial** — one `units` parameter across every tool.
- 🧭 **Weather, sea, and news** — current conditions, multi-day forecast, wave height/period,
  and headlines.
- šŸ **Minimal** — one dependency (`mcp`), pure standard library for everything else.

---

## Table of contents

- [Why this exists](#why-this-exists)
- [Tools](#tools)
- [Quick start](#quick-start)
- [Connect an AI client](#connect-an-ai-client)
- [Example prompts and results](#example-prompts-and-results)
- [Units](#units)
- [Data sources, cost, and privacy](#data-sources-cost-and-privacy)
- [The news caveat](#the-news-caveat)
- [Development](#development)
- [Project layout](#project-layout)
- [License](#license)

---

## Why this exists

Without MCP, every AI app needs custom glue code for every data source. With MCP, a data
source is written **once** as a server, and every compatible client can use it. This project
is a small, complete, dependency-light example of that pattern: five tools, three free data
sources, no keys, no bill.

The same skeleton applies to any data you want an assistant to reach — point a tool at *your*
notes file, database, or API and nothing else changes.

---

## Tools

| Tool | What it returns |
| --- | --- |
| `geocode(city)` | Latitude, longitude, country, timezone for a place name |
| `get_weather(city, units)` | Current temperature, feels-like, humidity, wind + gusts, UV index + risk, conditions |
| `get_forecast(city, days, units)` | Per-day min/max temp, mean humidity, max wind + gusts, max UV + risk, sunrise/sunset, rain chance |
| `get_marine(city, units)` | Wave height, period (s), direction (degrees + compass) — coastal places only |
| `get_news(city, limit, query)` | Recent headlines: title, source, published date, link |

Every tool returns JSON. If something goes wrong (unknown place, no network, inland marine),
it returns a friendly `{"error": "..."}` object instead of crashing.

---

## Quick start

Requires **Python 3.10+** and network access.

```bash
git clone https://github.com/z4d4ly/weather-mcp.git
cd weather-mcp

python3 -m venv .venv
./.venv/bin/pip install -e .      # or: ./.venv/bin/pip install -r requirements.txt
```

Run the server (it speaks MCP over stdio, so it waits for a client):

```bash
./.venv/bin/python server.py
# or, once installed:  ./.venv/bin/weather-mcp
```

Verify it works end to end against the live APIs:

```bash
./.venv/bin/python smoke_test.py
```

---

## Connect an AI client

Point the client at the Python interpreter and `server.py`. Use **absolute paths on your own
machine** — replace `<path-to-weather-mcp>` below.

### OpenCode (V2)

Add it with the CLI:

```bash
opencode mcp add weather --global -- \
  <path-to-weather-mcp>/.venv/bin/python <path-to-weather-mcp>/server.py
```

Or in `~/.config/opencode/opencode.jsonc`:

```jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "servers": {
      "weather": {
        "type": "local",
        "command": [
          "<path-to-weather-mcp>/.venv/bin/python",
          "<path-to-weather-mcp>/server.py"
        ]
      }
    }
  }
}
```

### Claude Desktop

In `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "weather": {
      "command": "<path-to-weather-mcp>/.venv/bin/python",
      "args": ["<path-to-weather-mcp>/server.py"]
    }
  }
}
```

### Cursor

In `.cursor/mcp.json` (or the global equivalent):

```json
{
  "mcpServers": {
    "weather": {
      "command": "<path-to-weather-mcp>/.venv/bin/python",
      "args": ["<path-to-weather-mcp>/server.py"]
    }
  }
}
```

### Any other client

It is a standard stdio MCP server: launch it as `python server.py` and speak MCP over
stdin/stdout.

---

## Example prompts and results

Real outputs, trimmed for length.

> **"What's the weather in Casablanca right now?"**

```json
{
  "city": "Casablanca", "country": "Morocco",
  "temperature": 22.0, "feels_like": 22.3,
  "humidity_pct": 54, "wind": 2.3, "wind_gusts": 12.2,
  "uv_index": 3.2, "uv_risk": "moderate",
  "conditions": "mainly clear",
  "units": { "temperature": "°C", "wind": "km/h" },
  "observed_at": "2026-11-07T10:15"
}
```

> **"Give me a 3-day forecast for Marrakesh in Fahrenheit."**

```json
{
  "city": "Marrakesh", "country": "Morocco",
  "units": { "temperature": "°C", "wind": "km/h" },
  "forecast": [
    { "date": "2026-11-07", "temp_min": 15.0, "temp_max": 27.6,
      "humidity_mean_pct": 62, "wind_max": 6.7, "wind_gusts_max": 19.4,
      "uv_index_max": 5.15, "uv_risk": "moderate",
      "sunrise": "2026-11-07T06:53", "sunset": "2026-11-07T17:38",
      "rain_chance_pct": 0 },
    { "date": "2026-11-08", "temp_min": 16.3, "temp_max": 28.1,
      "humidity_mean_pct": 59, "wind_max": 10.5, "wind_gusts_max": 27.7,
      "uv_index_max": 4.6, "uv_risk": "moderate",
      "sunrise": "2026-11-08T06:54", "sunset": "2026-11-08T17:37",
      "rain_chance_pct": 40 }
  ]
}
```

> **"Are the waves any good in Casablanca?"**

```json
{
  "city": "Casablanca", "country": "Morocco",
  "wave_height": 0.64, "wave_period_s": 10.8,
  "wave_direction_deg": 311, "wave_direction_compass": "NW",
  "units": { "wave_height": "m" },
  "observed_at": "2026-11-07T11:15"
}
```

> **"Waves in Marrakesh?"** (inland — handled gracefully)

```json
{
  "city": "Marrakesh", "country": "Morocco",
  "error": "No sea near Marrakesh — the marine model has no data here."
}
```

> **"Latest news about Rabat."**

```json
{
  "city": "Rabat", "country": "Morocco",
  "query": "\"Rabat\" Morocco",
  "count": 2,
  "articles": [
    { "title": "Morocco's Rabat festival showcases work of six women directors",
      "source": "Muslim Network TV",
      "published": "Sat, 07 Nov 2026 00:47:35 GMT",
      "link": "https://news.google.com/rss/articles/..." },
    { "title": "Spain Visa: BLS Rabat Permits Filing Without an Appointment ...",
      "source": "Morocco World News",
      "published": "Fri, 06 Nov 2026 20:20:58 GMT",
      "link": "https://news.google.com/rss/articles/..." }
  ]
}
```

---

## Units

`get_weather`, `get_forecast`, and `get_marine` accept `units`:

| `units` | Temperature | Wind | Wave height |
| --- | --- | --- | --- |
| `"metric"` *(default)* | °C | km/h | m |
| `"imperial"` | °F | mph | ft |

Unknown values fall back to metric.

---

## Data sources, cost, and privacy

| Data | Source | Key required? |
| --- | --- | --- |
| Geocoding | Open-Meteo Geocoding API | No |
| Weather & forecast | Open-Meteo Forecast API | No |
| Marine / waves | Open-Meteo Marine API | No |
| News | Google News RSS | No |

- **Cost:** nothing. There is no paid tier, no API key, and no quota to configure.
- **Privacy:** the only data leaving your machine is the place name you search for. No
  accounts, no telemetry, no analytics.
- **Rate limits:** Open-Meteo is free for non-commercial use with fair-use limits; be a good
  citizen and avoid hammering it in tight loops.

---

## The news caveat

`get_news` reads **Google News RSS**. Google's feed states it is for **personal,
non-commercial use**. That makes it fine for local demos and personal tools, but **do not
ship a public or commercial product on it** without switching to a source whose terms allow
it (for example, an outlet's own RSS feed).

The news code is deliberately isolated in one function (`get_news`) so it can be swapped
without touching anything else.

By default the tool searches the city *within its country* (e.g. `"Casablanca" Morocco`) and
keeps only headlines that mention the city. This filters out unrelated hits such as the
"Casablanca Records" label or the 1942 film. Pass `query` to override the search entirely.

---

## Development

**Unit tests** (offline, milliseconds):

```bash
./.venv/bin/python -m unittest discover -s tests -v
```

**Live smoke test** (network, exercises every tool + error paths, exits non-zero on failure):

```bash
./.venv/bin/python smoke_test.py
```

The unit tests cover the pure helpers (unit conversion, compass mapping, UV bands, input
clamping). The smoke test drives the server over stdio exactly as a real client would.

---

## Project layout

```
weather-mcp/
ā”œā”€ā”€ server.py            # the MCP server: helpers, HTTP, and the five tools
ā”œā”€ā”€ smoke_test.py        # live end-to-end test over stdio
ā”œā”€ā”€ tests/
│   └── test_server.py   # offline unit tests (stdlib unittest)
ā”œā”€ā”€ pyproject.toml       # packaging metadata + console script
ā”œā”€ā”€ requirements.txt     # one-line dependency list
ā”œā”€ā”€ .gitignore
ā”œā”€ā”€ LICENSE              # MIT
└── README.md
```

---

## Notes

- Requires **`mcp` v2**. In v2 the server class was renamed `FastMCP` → `MCPServer`
  (`from mcp.server import MCPServer`). Many older tutorials still show `FastMCP` and will
  not run against v2.
- Open-Meteo's geocoder uses the English spelling **"Marrakesh"**, not the French
  **"Marrakech"**.

---

## License

[MIT](LICENSE).