calculator
README.md
# Learn MCP by Building an MCP Server
> **The hands-on guide to the Model Context Protocol.** Build a calculator
> server, test it the way real AI apps do, upgrade it into a live weather
> server, and finally build **your own AI host** β a chat UI where an LLM calls
> your tools for real.
[](https://www.python.org)
[](https://modelcontextprotocol.io)
[](LICENSE)
[]()
[](https://tharunramavath.github.io/learn-mcp-by-building/)
> π **Full docs:** [tharunramavath.github.io/learn-mcp-by-building](https://tharunramavath.github.io/learn-mcp-by-building/)
**MCP (Model Context Protocol)** is the open standard that lets AI apps (Claude,
ChatGPT, VS Code, Cursor) talk to your tools and data β **"USB-C for AI"**.
This repo is a from-zero, **build-it-yourself** tutorial: every concept becomes
a file you write, run, and break on purpose. No AI/ML background needed.
---
## Who this is for
- Developers who want to understand MCP by **building**, not just reading.
- Python users comfortable with functions, async, and JSON.
- People who learn best when they can **see the protocol wire traffic** fly by.
You do **not** need any AI/ML background, an API key, or an LLM for Steps 1β6.
---
## What you'll build
| Step | You build | You learn |
|------|-----------|-----------|
| 0 | Your environment | The Python MCP SDK |
| 1 | `server_basic.py` β an offline calculator server | The 3 primitives: **tools, resources, prompts** |
| 2 | `test_client_basic.py` β a tiny client | The whole JSON-RPC conversation |
| 3 | MCP Inspector | Visual tool-calling + raw protocol traffic |
| 4 | A real AI app (Claude / VS Code / Cursor) | What a **host** and client configs look like |
| 5 | `server_weather.py` β a live weather server | Async tools, external APIs, geocoding |
| 6 | β | The two layers & stateless discovery |
| 7 | `app.py` β your own AI host (chat UI) | Function calling driving real MCP tools |
---
## Quickstart (60 seconds)
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python server_basic.py
```
That server **does nothing visible β by design**. It waits for a client. Open a
**second** terminal and talk to it:
```powershell
python test_client_basic.py
```
If you see discovery, tools, resources, prompts, and `add(2, 3) β 5.0` β you
just ran a full MCP conversation. Now go do the steps below.
---
## Table of contents
- [Read these docs first](#read-these-docs-first)
- [Project layout](#project-layout)
- [Step 0 β Setup](#step-0--setup-one-time)
- [Step 1 β Your first server](#step-1--your-first-server-server_basicpy)
- [Step 2 β Test it with a tiny client](#step-2--test-it-with-a-tiny-client-test_client_basicpy)
- [Step 3 β Explore with the MCP Inspector](#step-3--explore-with-the-mcp-inspector-visual-ui)
- [Step 4 β Attach to a real AI app](#step-4--attach-to-a-real-ai-app)
- [Step 5 β The real-world upgrade](#step-5--the-real-world-upgrade-server_weatherpy)
- [Step 6 β Under the hood](#step-6--under-the-hood-recommended-reading)
- [Step 7 β Become the host](#step-7--become-the-host-chat-with-your-servers-streamlit-ui)
- [Challenges](#challenges-do-these-yourself)
- [FAQ](#faq)
- [Where to go next](#where-to-go-next)
- [Troubleshooting](#troubleshooting)
---
## Read these docs first
> - `docs/00-learning-path.md` β **start here**: the recommended reading/doing order
> - `docs/01-concepts.md` β the theory: what MCP is, architecture, primitives
> - `docs/02-glossary.md` β a cheat-sheet of every term
> - `docs/03-inspector-v2-guide.md` β how the new Inspector v2 UI maps to the classic (v1) layout
> - `docs/04-mcp-vs-alternatives.md` β how MCP compares to plain function calling, provider tools, and REST APIs
> - `docs/05-faq.md` β a deeper FAQ than the short one at the bottom of this README
> π‘ Prefer reading in the browser? The same docs are hosted at
> **[tharunramavath.github.io/learn-mcp-by-building](https://tharunramavath.github.io/learn-mcp-by-building/)**.
---
## Project layout
```
mcp-tutorial/
βββ server_basic.py STEP 1 - your first (offline) server
βββ test_client_basic.py STEP 2 - a tiny client that tests it
βββ server_weather.py STEP 5 - the real-world weather server
βββ test_client_weather.py STEP 5 - a client for the weather server
βββ mcp_host.py STEP 7 - the MCP host/client wrapper (sync bridge)
βββ chat_engine.py STEP 7 - the LLM agent loop (Groq function calling)
βββ app.py STEP 7 - the Streamlit chat UI
βββ run-chat.cmd STEP 7 - double-click to launch the chat UI
βββ claude_desktop_config.json STEP 4 - config for Claude Desktop (edit the path)
βββ .vscode/mcp.json STEP 4 - config for VS Code
βββ inspector.json STEP 3 - Inspector catalog (launch config for both servers)
βββ run-inspector.cmd STEP 3 - double-click to launch the Inspector
βββ requirements.txt
βββ docs/
βββ 00-learning-path.md START HERE - the sequential reading order
βββ 01-concepts.md
βββ 02-glossary.md
βββ 03-inspector-v2-guide.md
βββ 04-mcp-vs-alternatives.md
βββ 05-faq.md
```
---
## Step 0 β Setup (one time)
```powershell
# From inside the mcp-tutorial/ folder:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
```
That installs the official Python MCP SDK (`mcp`) plus `httpx2` (the HTTP client
the weather server uses). Verify:
```powershell
python -c "from mcp.server import MCPServer; print('MCP SDK OK')"
```
### Prefer `uv`? (faster, modern alternative)
If you have [uv](https://docs.astral.sh/uv/) installed, you can skip the venv
setup entirely β the project has a `pyproject.toml`:
```powershell
uv sync # creates .venv and installs all deps from pyproject.toml
uv run server_basic.py # run the server
uv run test_client_basic.py # run the test client
```
> **Windows note:** use `uv` for *running* scripts, but launch the Inspector with
> `npx @modelcontextprotocol/inspector --catalog inspector.json` instead of
> `uv run mcp dev` β see Step 3. If a GUI app says `'uv' is not recognized`,
> add uv's folder to your PATH permanently:
> `[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path","User") + ";$env:USERPROFILE\.local\bin", "User")`
> then fully restart the app.
---
## Step 1 β Your first server: `server_basic.py`
Open `server_basic.py` and read it top to bottom. It is ~90 lines and shows all
three primitives at once.
```python
from mcp.server import MCPServer
mcp = MCPServer("calculator") # 1 line to create a server
```
### The 3 tools (what the AI can call)
```python
@mcp.tool()
def add(a: float, b: float) -> float:
"""Add two numbers together.
Args:
a: The first number.
b: The second number.
"""
return a + b
```
Three magic things happen here:
1. **`@mcp.tool()`** registers the function as an MCP tool.
2. The **type hints** (`a: float, b: float`) become the tool's input schema.
3. The **docstring** becomes the tool's description β this is what the AI model
reads to decide when to call it. Write it like the model will read it.
### The 1 resource (read-only data)
```python
@mcp.resource("config://app", mime_type="application/json")
def get_app_config() -> dict:
return APP_CONFIG
```
Resources have a **URI** (`config://app`). The app loads them to give the model
context β like opening a file.
### The 1 prompt (a template)
```python
@mcp.prompt()
def math_help(problem: str) -> list[dict]:
return [{"role": "user", "content": f"You are a math tutor. Problem: {problem}"}]
```
Prompts are templates the **user** invokes explicitly (like a slash command).
### Run it
```powershell
python server_basic.py
```
It prints a log line and then just **sits there**. That is normal β a stdio
server waits for a client to spawn it and talk to it.
> **Why no output?** Over the `stdio` transport, your server's stdout *is* the
> protocol. If you `print()` anything, you corrupt the JSON-RPC messages and the
> server silently breaks. That's why we use `logging` (which goes to stderr).
> This is **the golden rule of stdio MCP servers** β see `docs/01-concepts.md`.
---
## Step 2 β Test it with a tiny client: `test_client_basic.py`
You don't need an AI app yet. This script is a minimal stand-in client that
connects to your server over stdio and drives the whole protocol.
```powershell
python test_client_basic.py
```
You'll see, in order:
1. **Discovery** β the client asks what the server supports (`server/discover`).
2. **List tools** β notice each tool's auto-generated `inputSchema`.
3. **List resources** β the `config://app` URI.
4. **List prompts** β `math_help`.
5. **Call tools** β `add(2, 3)` returns `5.0`.
6. **Read the resource** β your config JSON comes back.
7. **Get the prompt** β the template comes back.
That's the entire MCP conversation, end to end. The client code:
```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async with stdio_client(server_params) as (read, write): # spawn server
async with ClientSession(read, write) as session: # open conversation
await session.initialize() # handshake
await session.list_tools()
await session.call_tool("add", {"a": 2, "b": 3})
```
---
## Step 3 β Explore with the MCP Inspector (visual UI)
The official **MCP Inspector** is a web UI for poking at servers and watching
the raw JSON-RPC messages.
This project ships an `inspector.json` catalog that tells the Inspector how to
launch both servers using **paths relative to this folder** (no `uv`, no PATH
lookup, no machine-specific paths).
**Easiest way β double-click `run-inspector.cmd`**, or run it from the terminal:
```powershell
.\run-inspector.cmd
```
Or invoke npx directly:
```powershell
npx @modelcontextprotocol/inspector --catalog "<your-path>\mcp-tutorial\inspector.json"
```
The web UI opens at `http://localhost:6274` (the browser auto-launches; the URL
may carry an `MCP_INSPECTOR_API_TOKEN=` query param β keep it if you
bookmark the page). Pick a server (`calculator` or `weather`) from the catalog
and click **Connect**. Click around:
- **Tools / Resources / Prompts** tabs β browse what your server exposes.
- **Call a tool** β try `add` with `{"a": 10, "b": 5}`.
- **Protocol** tab β watch the actual JSON-RPC requests and responses fly by.
This is where you *see* what Step 2 did programmatically.
> If the UI looks different from YouTube tutorials, that's because this project
> runs the new **Inspector v2**. See `docs/03-inspector-v2-guide.md` for a
> feature-by-feature map (transport type, command/args, pings, sampling, roots,
> protocol routes, etc.) to the v2 tabs.
> **Windows note:** avoid `mcp dev` / `uv run mcp dev` β it forces the Inspector
> to spawn your server via `uv`, which often fails on Windows (the Inspector's
> spawned process can't see `uv` on PATH). The `--catalog` approach above avoids
> that entirely by using relative paths to the venv Python. Also launch the
> Inspector from **PowerShell or cmd** (double-click `run-inspector.cmd`) rather
> than from Git Bash β the MSYS environment mis-resolves `node` for child
> processes and the Inspector dies with `spawn node ENOENT`. Only one Inspector
> can run at a time β if you see "PORT IS IN USE", close the existing
> tab/instance instead of starting another. First run downloads the Inspector
> via `npx` and takes a minute.
---
## Step 4 β Attach to a real AI app
Now the payoff: plug your server into an AI assistant you already use.
### Claude Desktop
Copy `claude_desktop_config.json` to your real config file:
`C:\Users\<you>\AppData\Roaming\Claude\claude_desktop_config.json`, then fully
restart Claude Desktop. Your `calculator` server will appear. Ask:
> *"Use the calculator server to multiply 17 by 23."*
The config tells Claude Desktop: *launch this server by running this Python
script*, then talk to it over stdio.
### VS Code
VS Code reads project-scoped MCP servers from `.vscode/mcp.json` (already
included). Open this folder in VS Code, then:
1. Open the command palette β **"MCP: List Servers"** / **"MCP: Add Server"**.
2. Or open **Copilot Chat** and use the MCP tools that appear.
### Cursor
Settings β **MCP** β **Add new global MCP server**, and paste the same JSON
shape from `claude_desktop_config.json`.
> **Paths in the config files:** `inspector.json` and `.vscode/mcp.json` use
> **relative paths** (`.venv\Scripts\python.exe`), so they work as long as you
> open the project folder. `claude_desktop_config.json` is a copy-paste example
> and uses a `<YOUR-PATH>` placeholder β replace it with the absolute path to
> your checkout before copying (Claude Desktop doesn't resolve relative paths).
> On Windows use double backslashes `\\` in JSON.
---
## Step 5 β The real-world upgrade: `server_weather.py`
Now you take what you learned and build something genuinely useful: a weather
server for **India** backed by the free [Open-Meteo API](https://open-meteo.com)
(no API key required).
Read `server_weather.py`. The MCP part is identical to Step 1 β what's new:
- **Async tools** β `async def` because we do I/O.
- **External API calls** β `httpx2` fetches live data; `make_request` wraps
it with error handling so a network failure returns a friendly message
instead of crashing.
- **City name geocoding** β users just type `Mumbai`, not coordinates.
`geocode_city` resolves the name to a latitude/longitude internally via
Open-Meteo's geocoding API. A classic real-world API pattern.
- **Heuristic alerts** β `get_alerts` derives advisories from current and
near-term conditions (thunderstorm, heavy rain, fog, extreme heat), since
India has no free keyless official alert feed.
Test it (needs internet):
```powershell
python test_client_weather.py
```
Then explore it in the Inspector (the `weather` server is already in the catalog):
```powershell
npx @modelcontextprotocol/inspector --catalog "<your-path>\mcp-tutorial\inspector.json"
```
Then plug it into VS Code / Claude Desktop using the `weather` entry already in
`.vscode/mcp.json` / `claude_desktop_config.json`, and ask:
> *"What's the weather like in Mumbai?"*
---
## Step 6 β Under the hood (recommended reading)
Read `docs/01-concepts.md` fully. The two ideas that make everything click:
1. **Two layers.** *Data layer* = the JSON-RPC 2.0 messages (what).
*Transport layer* = how messages travel (stdio locally, Streamable HTTP
remotely). Same messages, different plumbing.
2. **Stateless + discovery.** Every request carries the protocol version and
capabilities; the client discovers what the server supports before using it.
Then try the challenges below.
---
## Step 7 β Become the host: chat with your servers (Streamlit UI)
So far your servers just sit and wait for a client. Now you build the other half
of MCP β the **host** (the AI app). This is a real chat UI in your browser that
spawns your servers, discovers their tools, and lets an LLM call them through
**function calling**.
Three files, each a distinct layer:
- **`mcp_host.py`** β a reusable MCP *client/host*. It spawns a server over
stdio and exposes plain synchronous methods (`list_tools`, `call_tool`,
`read_resource`, `get_prompt`, ...). MCP's Python SDK is async but Streamlit
is sync, so it runs a real asyncio event loop on a background thread and
bridges the two.
- **`chat_engine.py`** β the **agent loop**. It sends your message plus the
discovered tool schemas to [Groq](https://console.groq.com/). If the model
decides it needs a tool, the engine executes it on the server and feeds the
result back, repeating until the model answers in plain text. That loop is
*function calling* driving real MCP tools.
- **`app.py`** β the Streamlit UI. Pick a server, paste a Groq API key,
connect, and chat. Every reply can show an **agent trace** of the real MCP
tool calls that produced it.
Run it β double-click **`run-chat.cmd`**, or:
```powershell
python -m streamlit run app.py
```
The UI opens at `http://localhost:8501`. You need a free Groq API key
(https://console.groq.com/keys). Then:
1. Paste the key in the sidebar.
2. Pick the **calculator** server β **Connect**. The sidebar lists its
discovered 3 tools, 1 resource, and 1 prompt.
3. Ask *"What is 17 * 23?"*. The **agent trace** under the reply shows the LLM
called your real `multiply` tool and used its answer.
4. Switch to the **weather** server and ask *"What's the weather in Mumbai?"* β
it geocodes the city and fetches live data, all via MCP.
> **Why this is the big finale:** every AI app is a host. Claude Desktop, VS
> Code, and Cursor do exactly what `app.py` does β spawn servers, discover
> tools, and call them on the model's behalf. You just built one from scratch.
---
## Challenges (do these yourself)
1. **Add a `divide` tool** to `server_basic.py`. What happens when `b == 0`?
2. **Add a resource template** β a resource with a URI like
`weather://city/{city}` instead of a fixed URI.
3. **Make `server_basic.py` a remote server** β change the last line to
`mcp.run(transport="streamable-http")` and see what URL it prints. (You'll
need to configure a client for HTTP transport to talk to it.)
4. **Write your own tiny client** that takes a math question in English,
extracts numbers, and calls your calculator tools.
5. **Add a tool to `server_basic.py`** and re-connect the chat UI β the host
should discover it automatically. What happens if the LLM divides by zero?
6. **Swap the Groq model** in the chat sidebar. Which models call tools more
reliably? (Watch the agent trace to see.)
---
## FAQ
**Do I need an API key or an AI model to follow along?**
No. Steps 1β6 need only Python. The only key in the whole project is the free
Groq key used in Step 7 (the host). You can go through the entire protocol
without ever touching an LLM.
**Why does my server "do nothing" when I run it?**
It's working. A stdio server doesn't run on its own β it waits for a client to
spawn it and speak JSON-RPC over stdin/stdout. See Step 2 / Step 3.
**What's the difference between a tool, a resource, and a prompt?**
Short version: *tools do something* (the model calls them), *resources are data
to read* (the app loads them), *prompts are recipes* (the user invokes them).
Full details in `docs/01-concepts.md`.
**Isn't this just "function calling"?**
Function calling is how an LLM decides to use a tool β MCP is the standard for
*transporting* tools and data between apps and models. They're complementary,
not the same thing. The whole comparison is in `docs/04-mcp-vs-alternatives.md`.
**stdio vs Streamable HTTP β which do I use?**
Same JSON-RPC messages, different plumbing. stdio for local servers you run
yourself; Streamable HTTP for remote/deployed servers. See `docs/01-concepts.md`.
**Can I plug my server into ChatGPT?**
Claude Desktop, VS Code, and Cursor are the easy wins in Step 4. Support varies
by app β any app that speaks MCP can use your server; that's the whole point.
**Do I have to learn JSON-RPC?**
No. The SDK generates and parses it for you β but Step 3 shows you the raw
traffic so you understand what's happening underneath.
More questions answered in `docs/05-faq.md`.
---
## Where to go next
- Official docs: <https://modelcontextprotocol.io>
- MCP specification: <https://modelcontextprotocol.io/specification/latest>
- Reference servers: <https://github.com/modelcontextprotocol/servers>
---
## Troubleshooting
| Symptom | Fix |
|---------|-----|
| `ModuleNotFoundError: No module named 'mcp'` | Activate the venv first, or run with `python -m` inside it. |
| LSP/editor red squiggles on `MCPServer` | Your editor is using system Python. Select the interpreter at `.venv\Scripts\python.exe`. |
| Server starts then exits instantly | Something printed to stdout. Search for `print(` in the server. |
| Weather returns "Unable to fetch" | No internet, or you hit the Open-Meteo rate limit. Wait a minute and retry. |
| Client hangs | The server isn't on your `PATH` in the config β use relative paths to the venv python (as shipped) or absolute ones. |
| `UnicodeEncodeError` on Windows | Avoid non-ASCII characters in tool output, or run with `PYTHONIOENCODING=utf-8`. |
| `streamlit: command not found` | The venv isn't active β run `.\.venv\Scripts\python.exe -m streamlit run app.py`. |
| Chat shows `Groq call failed: ... 401` | The API key is missing or wrong β get one at https://console.groq.com/keys. |
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues