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.

Related MCP server: open-meteo-mcp

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.

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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