Skip to main content
Glama
deepcodes7

MCP Weather Server

by deepcodes7
README.md
# 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

B3.2/5.0

Scored across 2 tools

Disambiguation4/5

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.

Naming Consistency5/5

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.

Tool Count2/5

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.

Completeness2/5

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.

Maintenance

ActivityInactive
ResponsivenessNo issues