MCP Weather Server
# MCP Weather Server & Travel Agent
A learning project that builds a local **Model Context Protocol (MCP)** server backed by
the free [OpenWeatherMap API](https://openweathermap.org/api), a diagnostic MCP client,
and an AI-powered travel-planning agent that combines MCP tool execution with
OpenAI function calling.
---
## Business Context
The project answers a practical question:
*How can an LLM fetch live, external data — and act on it — through a standardised protocol?*
MCP lets any AI host (Cursor, Claude Desktop, custom agents) discover and invoke
server-provided **Tools**, read **Resources**, and retrieve reusable **Prompt templates**
without hard-coding integrations. This project wires that idea end-to-end using
real-time weather data.
### What you can do with it
| Capability | Example |
|---|---|
| Ask Cursor for live weather | "What's the weather in Tokyo?" — Cursor calls `get_weather` via MCP |
| Generate a travel plan | `uv run travel_agent.py "Paris" 5` — GPT reasons over real forecasts |
| Explore all three MCP primitives | `uv run client.py` — enumerates and exercises tools, resources, prompts |
---
## Project Structure
```
MCP2/
├── server.py # MCP server — exposes tools, resources, prompts
├── client.py # Diagnostic MCP client — exercises every primitive
├── travel_agent.py # AI travel agent — MCP + OpenAI function calling
├── main.py # Scaffold entry point (placeholder)
├── .env # API keys (gitignored)
├── .python-version # Pins Python 3.14
├── pyproject.toml # Project metadata & dependencies (managed by uv)
├── uv.lock # Locked dependency graph
├── .gitignore
└── .cursor/
└── mcp.json # Cursor IDE MCP server config
```
---
## Architecture & End-to-End Flows
### Component Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ MCP HOST / CLIENT │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Cursor │ │ client.py │ │ travel_agent.py │ │
│ │ IDE │ │ (diagnostic) │ │ (AI agent) │ │
│ └────┬─────┘ └──────┬───────┘ └───────────┬────────────┘ │
│ │ │ │ │
│ │ stdio │ stdio │ stdio │
│ └────────┬───────┘────────────────────────┘ │
│ │ │
├────────────────┼────────────────────────────────────────────────┤
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ server.py (FastMCP) │ │
│ │ │ │
│ │ TOOLS RESOURCES PROMPTS │ │
│ │ ───── ───────── ─────── │ │
│ │ get_weather weather://cities weather_report │ │
│ │ get_forecast weather://help travel_advisory │ │
│ │ │ │
│ │ fetch_weather(endpoint, params) │ │
│ └──────────────────────┬────────────────────────────────┘ │
│ │ httpx (async) │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ OpenWeatherMap REST API │ │
│ │ /weather /forecast │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
### Flow 1 — Cursor IDE (interactive usage)
```
User types in Cursor:
"What's the weather in Bangalore?"
1. Cursor reads .cursor/mcp.json
2. Spawns `uv run server.py` as a child process (stdio transport)
3. MCP handshake: Cursor sends `initialize`, server responds with capabilities
4. Cursor discovers tools via `tools/list` → learns about get_weather, get_forecast
5. The LLM decides get_weather(city="Bangalore") is needed
6. Cursor sends `tools/call` → server receives request
7. server.py → fetch_weather("weather", {"q": "Bangalore"})
→ httpx GET https://api.openweathermap.org/data/2.5/weather?q=Bangalore&appid=…&units=metric
8. OpenWeatherMap returns JSON → server formats a string → returns via MCP
9. Cursor displays the result to the user
```
### Flow 2 — Diagnostic Client (`uv run client.py`)
The client runs a single, linear script that exercises every MCP primitive:
```
1. Launch server.py as subprocess via stdio_client(StdioServerParameters)
2. ClientSession handshake → session.initialize()
┌──────────────────────────────────────────────────┐
│ TOOLS │
│ a. list_tools() → enumerate all tools │
│ b. call_tool("get_weather", {city: "Toronto"}) │
│ c. call_tool("get_forecast", {city, days: 3}) │
├──────────────────────────────────────────────────┤
│ RESOURCES │
│ a. list_resources() → enumerate all resources │
│ b. read_resource("weather://cities") │
│ c. read_resource("weather://help") │
├──────────────────────────────────────────────────┤
│ PROMPTS │
│ a. list_prompts() → enumerate all prompts │
│ b. get_prompt("weather_report", {city: "Tokyo"}) │
│ c. get_prompt("travel_advisory", {city, days}) │
└──────────────────────────────────────────────────┘
3. Results printed to terminal; process exits
```
### Flow 3 — Travel Planner Agent (`uv run travel_agent.py "Tokyo" 5`)
This is the most complex flow. It implements an **agentic loop** where GPT
decides which MCP tools to call and when to stop.
```
1. Parse CLI args (city, days)
2. Connect to Weather MCP server via stdio (same as client.py)
3. MCP handshake → session.initialize()
4. Tool discovery:
a. session.list_tools() → get MCP tool definitions
b. mcp_tools_to_openai_functions() → translate MCP inputSchema
into OpenAI function-calling format
5. Resource prefetch:
a. session.list_resources()
b. Read all resources (cities list, help text) → inject as system context
6. Build initial message list:
┌────────────────────────────────────────────────────────┐
│ system: SYSTEM_PROMPT (travel planner personality) │
│ system: resource context (cities list, help text) │
│ user: "Plan a 5-day trip to Tokyo. Use the │
│ weather tools to check conditions..." │
└────────────────────────────────────────────────────────┘
7. Agent reasoning loop (max 4 iterations):
┌───────────────────────────────────────────────────────────┐
│ LOOP START │
│ │
│ a. Send messages + tool definitions to OpenAI API │
│ → openai_client.chat.completions.create( │
│ model, messages, tools, tool_choice="auto") │
│ │
│ b. IF response contains tool_calls: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ For each tool_call: │ │
│ │ - Parse function name + arguments │ │
│ │ - Execute via MCP: │ │
│ │ session.call_tool(name, args) │ │
│ │ - Extract text from MCP result │ │
│ │ - Append tool result to messages │ │
│ └─────────────────────────────────────────────────┘ │
│ → Continue loop (LLM sees tool results next round) │
│ │
│ c. ELSE IF response contains text content: │
│ → This is the FINAL answer. Print travel plan. BREAK │
│ │
│ d. ELSE (empty response): │
│ → Retry │
│ │
│ LOOP END │
└───────────────────────────────────────────────────────────┘
8. Print travel plan with iteration count
```
**Typical execution:** 3 LLM calls — (1) LLM requests `get_weather`, (2) LLM
requests `get_forecast`, (3) LLM produces the final travel plan using both results.
---
## MCP Primitives Reference
### Tools (callable functions)
| Tool | Parameters | Returns |
|---|---|---|
| `get_weather` | `city: str` | Current temp, feels-like, humidity, wind, description |
| `get_forecast` | `city: str`, `days: int` (1-5, default 3) | One line per day with temp and description |
Both call `fetch_weather()` internally, which appends the API key and `units=metric`,
then makes an async `httpx.GET` to the OpenWeatherMap endpoint. Errors are caught
and returned as user-friendly strings (not exceptions).
### Resources (read-only context)
| URI | Description |
|---|---|
| `weather://cities` | JSON array of 10 example city names |
| `weather://help` | Human-readable help text listing all capabilities |
Resources are synchronous and return static/computed strings.
### Prompts (reusable templates)
| Prompt | Parameters | What it generates |
|---|---|---|
| `weather_report` | `city: str` | Instruction for the LLM to fetch weather + forecast and summarise |
| `travel_advisory` | `city: str`, `days: int` | Instruction for the LLM to build a packing/activity/warning list |
Prompts return instruction strings; they do **not** call tools themselves.
> **Note:** MCP prompt arguments are `dict[str, str]` on the wire. When calling
> `get_prompt(...)` from the Python SDK, pass all values as strings
> (e.g. `{"days": "5"}` not `{"days": 5}`).
---
## Tech Stack
| Layer | Technology |
|---|---|
| Language | Python 3.14 |
| Package manager | [uv](https://docs.astral.sh/uv/) |
| MCP SDK | `mcp[cli] >= 1.26.0` (FastMCP) |
| HTTP client | `httpx >= 0.28.1` (async) |
| LLM (travel agent) | OpenAI `gpt-4o-mini` via `openai >= 2.30.0` |
| Config loading | `python-dotenv >= 1.2.2` |
| External API | OpenWeatherMap 2.5 (free tier) |
---
## Prerequisites
- **Python >= 3.14**
- **[uv](https://docs.astral.sh/uv/)** installed and on your PATH
- **OpenWeatherMap API key** — free at <https://openweathermap.org/appid>
- **OpenAI API key** (only needed for `travel_agent.py`)
---
## Setup
1. **Install dependencies:**
```bash
uv sync
```
2. **Create `.env`** in the project root:
```env
OPENWEATHER_API_KEY=your_openweather_key
OPENWEATHER_BASE_URL=https://api.openweathermap.org/data/2.5
OPENAI_API_KEY=sk-... # only needed for travel_agent.py
```
> `.env` is gitignored. Never commit API keys.
---
## Running
### MCP Server (standalone / via Cursor)
```bash
uv run server.py
```
The server starts on **stdio** and waits for MCP messages. You don't interact with
it directly in the terminal — it's designed to be driven by an MCP host (Cursor, the
client, or the travel agent).
### Diagnostic Client
```bash
uv run client.py
```
Runs through tools, resources, and prompts sequentially and prints results.
### Travel Planner Agent
```bash
uv run travel_agent.py "Tokyo" 5
uv run travel_agent.py "Paris" 3
uv run travel_agent.py "Toronto" # defaults to 3 days
```
Connects to the MCP server, lets GPT reason with real weather data, and outputs
a day-by-day travel plan.
### MCP Inspector (interactive debugging)
```bash
npx @modelcontextprotocol/inspector uv run server.py
```
Opens a web UI to manually invoke tools, read resources, and test prompts.
---
## Cursor Integration
Project-level config lives at `.cursor/mcp.json`:
```json
{
"mcpServers": {
"weather": {
"type": "stdio",
"command": "uv",
"args": ["run", "${workspaceFolder}/server.py"],
"envFile": "${workspaceFolder}/.env"
}
}
}
```
- `${workspaceFolder}` is resolved by Cursor to the directory containing `.cursor/mcp.json`.
- `envFile` injects `.env` variables into the spawned server process.
- After editing this file, reload MCP servers or restart Cursor.
Once loaded, Cursor's agent can call `get_weather` and `get_forecast` directly
when you ask weather-related questions in chat.
---
## Key Technical Details
- **Transport:** All three clients (Cursor, `client.py`, `travel_agent.py`) connect
over **stdio**. The server is launched as a subprocess; MCP messages flow over
stdin/stdout as JSON-RPC.
- **Async throughout:** `server.py` uses `async def` for tool handlers and `httpx.AsyncClient`
for non-blocking HTTP. `client.py` and `travel_agent.py` run inside `asyncio.run()`.
- **MCP → OpenAI schema translation:** `travel_agent.py` converts MCP `inputSchema`
(JSON Schema) to OpenAI's `tools[].function.parameters` format. The schemas are
nearly identical by design.
- **Agent loop safety:** The travel agent caps the reasoning loop at 4 iterations
to prevent runaway API calls. Typical runs complete in 2–3 iterations.
- **Error handling:** Tool handlers catch `httpx.HTTPStatusError` and generic exceptions,
returning error strings instead of raising. This keeps the MCP session alive
even if the upstream API fails.
- **Forecast de-duplication:** OpenWeatherMap's `/forecast` returns 3-hour intervals.
`get_forecast` deduplicates by date, picking the first entry per calendar day.
---
## Gotchas
- `get_prompt(...)` arguments must be **strings** in the Python MCP SDK
(e.g. `{"days": "5"}` not `5`).
- The free OpenWeatherMap tier has rate limits (~60 calls/min).
The travel agent makes 2 API calls per run.
- `travel_agent.py` requires `OPENAI_API_KEY` in `.env`. The server and
diagnostic client do not.
---
## License
No license specified.
TDQS
Scored across 2 tools
The two tools have distinct purposes: get_weather for current conditions and get_forecast for multi-day predictions. However, the names could cause minor confusion as 'weather' and 'forecast' are semantically related, but the descriptions clearly differentiate them.
Both tools follow a consistent verb_noun pattern with 'get_' prefix and snake_case naming. This makes them predictable and easy to understand at a glance.
With only two tools, the server feels under-scoped for a weather domain. It lacks operations like historical data, alerts, or location-based searches, which are common in weather APIs and would enhance agent capabilities.
The toolset is severely incomplete for a weather server. It covers current and forecast data but misses essential operations like historical weather, severe alerts, air quality, or multi-location queries, leaving significant gaps for agent workflows.