Skip to main content
Glama
matjmiles

mcp-weather-tutorial

by matjmiles

MCP Weather Tutorial

A working MCP server and two clients, built against Open-Meteo (free, no API key, no signup).

Heads up: most MCP tutorials online are out of date

They open with:

from mcp.server.fastmcp import FastMCP   # ModuleNotFoundError on mcp 2.0

FastMCP was removed in mcp 2.0. It is now MCPServer, from mcp.server. Response fields also moved to snake_case (input_schema, not inputSchema). If a tutorial mentions FastMCP, it is describing 1.x.

Verified against mcp 2.0.0 and claude-agent-sdk 0.2.95.

Files

File

What it does

Needs

server.py

MCP server exposing geocode_city and get_forecast

nothing

client_basic.py

Raw protocol client -- no LLM, nothing hidden

nothing

client_agent.py

Claude picks and chains the tools itself

Claude Code login

Run it

uv run client_basic.py                        # defaults to Denver
uv run client_basic.py Lisbon
uv run client_basic.py San Francisco          # spaces are fine, no quotes
uv run client_basic.py Portland --pick 1      # 2nd match, not the 1st
uv run client_basic.py Tokyo --days 7
uv run client_basic.py --lat 27.99 --lon 86.93   # skip geocoding entirely
uv run client_basic.py Lisbon --json          # also dump the raw payload

uv run client_agent.py                        # just ask, in English
uv run client_agent.py "should I bring an umbrella in Lisbon tomorrow?"
uv run client_agent.py "compare Tokyo and Seoul this weekend"

--pick exists because city names are ambiguous: "Denver" matches five places and "Portland" two well-known ones. Open-Meteo ranks by population, so the top hit is a guess. client_basic.py lists every match and marks the one it used; client_agent.py lets Claude choose and say why.

uv run server.py on its own will look like it hangs. It hasn't -- it is waiting for a client on stdin.

Debugging

Press F5 in VS Code and pick a configuration. Start with "1. Step into server tools (in-process)".

The thing that catches people out: normally client_basic.py launches server.py as a subprocess, and a debugger does not follow across a process boundary. Breakpoints in server.py are silently never hit, which looks like the tool is not being called at all.

--in-process fixes this. Client() accepts an MCPServer object directly and runs it in the same process, no pipes involved:

uv run client_basic.py Denver --in-process     # identical output, one process

Now a breakpoint inside geocode_city hits, and Step Into (F11) on call_tool(...) walks from the client, through the protocol layer, into the tool body.

Good breakpoints to start from:

File

Line

What you see

client_basic.py

the list_tools() call

the schemas generated from your type hints

client_basic.py

the first call_tool(...)

arguments going out as a plain dict

server.py

first line of geocode_city

your tool receiving them

server.py

the return in get_forecast

the value before MCP serializes it

The configs set "justMyCode": false, so you can also step down into the mcp library itself and watch the JSON-RPC request get built. Set it to true to stay in your own files.

Debugging the real subprocess

When you specifically want the true stdio path (config 3 debugs the client only), have the server wait for a debugger. Add this at the top of server.py, run the client normally, then launch config 4. Attach to server subprocess:

import debugpy
debugpy.listen(5678)
debugpy.wait_for_client()   # server pauses here until you attach

Remove it when you are done -- it makes the server hang for anything that is not a debugger. For everyday work --in-process is easier.

Why two tools instead of one

get_forecast takes coordinates, not city names, because Open-Meteo's forecast endpoint does. Answering "weather in Denver" therefore takes two calls:

geocode_city("Denver")  ->  lat 39.73915, lon -104.9847
get_forecast(39.73915, -104.9847)  ->  daily highs and lows

client_basic.py does that hand-off by hand so you can see it. client_agent.py never mentions Denver or latitude -- Claude works it out:

[tool] mcp__weather__geocode_city(name='Denver')
[tool] mcp__weather__get_forecast(latitude=39.73915, longitude=-104.9847, days=6)

That is the point of the whole exercise.

Two things that will bite you

Return annotations need a schema. A tool annotated -> dict fails with InvalidSignature: return type <class 'dict'> is not serializable for structured output. Use a TypedDict (see City, Forecast in server.py).

List returns get wrapped, object returns do not. A tool returning a list arrives as {"result": [...]}, because JSON Schema needs an object at the top level. A tool returning a TypedDict arrives as-is:

geo.structured_content["result"]   # list-returning tool
forecast.structured_content        # object-returning tool -- no "result" key

Connecting it to Claude Code

claude mcp add weather -- uv run server.py

Then ask Claude Code about the weather directly. claude mcp remove weather undoes it.

License

MIT -- see LICENSE.

Design notes

See docs/specs/2026-07-28-mcp-weather-tutorial-design.md.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/matjmiles/mcp-weather-tutorial'

If you have feedback or need assistance with the MCP directory API, please join our Discord server