mcp-test
# mcp-test — learning the Model Context Protocol
A minimal [MCP](https://modelcontextprotocol.io) server in Python: one tool that
returns the current temperature for a city. Plus a client that calls it, so you can
see both halves of the protocol as plain Python.
No API key required — it uses [Open-Meteo](https://open-meteo.com/), which is free
and needs no signup.
```
server.py the MCP server — one tool, get_temperature
test_client.py a real MCP client that launches the server over stdio
```
## Run it
Requires [uv](https://docs.astral.sh/uv/) and Python 3.12+.
```bash
uv run test_client.py
```
That launches `server.py` as a subprocess, connects over stdio, and walks through
four things: tool discovery, a successful call, a recoverable error, and schema
validation rejecting a bad argument.
## ⚠️ This uses SDK v2, not v1
The MCP Python SDK is at **v2.x**, a major rework. Most tutorials online are v1 and
will not run against it:
| v1 (most tutorials) | v2 (this repo) |
| --- | --- |
| `from mcp.server.fastmcp import FastMCP` | `from mcp.server import MCPServer` |
| `ClientSession` + `stdio_client` | `from mcp import Client` (one class) |
| `tool.inputSchema` | `tool.input_schema` (snake_case) |
## What the code demonstrates
**The three primitives**, split by who decides to use them: **tools** (the model
decides), **resources** (the application decides), **prompts** (the user decides).
This server exposes a tool, because "get the temperature" is a model-driven action.
**Your function is the contract.** From
`async def get_temperature(city: str) -> Temperature`, the SDK derives the tool name,
the description (from the docstring), the input schema (from the type hints), and the
output schema (from the return annotation). No JSON Schema is written by hand — and
the type hints are *enforced*, so a client sending `city=42` is rejected before the
function runs.
**Two results, two audiences.** Every call returns `content` (text the model reads)
and `structured_content` (typed data the application reads). Returning a Pydantic
model rather than a string is what makes the second one useful.
**Errors: `raise`, never `return`.** A `ToolError` comes back with `is_error=True` and
your message where the model can read it, so the model can retry. A returned error
string has `is_error=False` — the model would read it as the answer.
**stdio means stdout is the wire.** The client launches the server as a subprocess and
speaks JSON-RPC over its stdin/stdout, so a stray `print()` in a server corrupts the
protocol stream. Use `logging`, which writes to stderr.
## Try it in the MCP Inspector
A browser UI over your server, showing the raw protocol messages (needs `npx`):
```bash
uv run mcp dev server.py
```
## Connect it to Claude Code
```bash
claude mcp add weather -- uv run --directory "$PWD" server.py
```
TDQS
Scored across 1 tool
Only one tool exists, so there is no possibility of overlapping purposes or misselection. The single get_temperature action is completely unambiguous.
The one tool uses a clear get_ + noun naming convention. There are no competing conventions or mixed styles to create inconsistency.
A single tool is on the 'too few' end of the scale, making the server feel like a minimal stub rather than a useful service. Unless the intended purpose is literally only a current temperature lookup, this is insufficient.
For a temperature/weather-related domain, the surface lacks related operations such as unit selection, forecast, geocoding, or validation. The single lookup creates a dead end and leaves obvious gaps for broader temperature use cases.