mcp-weather-tutorial
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.0FastMCP 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 |
| MCP server exposing | nothing |
| Raw protocol client -- no LLM, nothing hidden | nothing |
| 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 processNow 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 |
| the | the schemas generated from your type hints |
| the first | arguments going out as a plain dict |
| first line of | your tool receiving them |
| the | 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 attachRemove 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 lowsclient_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" keyConnecting it to Claude Code
claude mcp add weather -- uv run server.pyThen ask Claude Code about the weather directly. claude mcp remove weather
undoes it.
License
MIT -- see LICENSE.
Design notes
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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