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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues