hello-mcp
by paodanchacon
README.md
# Hello World MCP Server π
A minimal **MCP (Model Context Protocol)** server in Python, built to learn the
basics. It exposes the three MCP primitives:
- **Tools** β `say_hello`, `add`, `current_time` (functions the AI calls)
- **Resources** β `info://server`, `greeting://{name}` (data the AI reads)
- **Prompt** β `daily_briefing` (a reusable template the user invokes)
This guide lets you replicate the whole thing from scratch.
---
## 1. What is MCP? (the 30-second version)
MCP is an open standard that lets AI apps connect to external tools and data in
a consistent way β like a **USB-C port for AI**.
```
You (plain English) βββΊ Claude Code βββΊ [JSON-RPC over stdio] βββΊ your server
(the host) (the tools)
```
- **Host** β the AI app you talk to (Claude Code).
- **Server** β the program *you* write that exposes capabilities.
- **Tool** β a function the AI can call (e.g. `say_hello`).
- **stdio** β the transport: Claude Code launches your server as a subprocess
and talks to it via standard input/output using JSON-RPC messages.
> β οΈ **Key idea:** you never type into the server directly. You talk to Claude
> Code in plain English, and *it* decides to call your tools.
---
## 2. Prerequisites
- **[uv](https://docs.astral.sh/uv/)** β Python package/project manager.
(Handles Python versions for you β even if your system Python is old.)
- The MCP Python SDK needs **Python 3.10+**; `uv` installs a compatible one
automatically.
---
## 3. Set up the project
```bash
mkdir hello-mcp && cd hello-mcp
# Create a uv project pinned to Python 3.11
uv init --name hello-mcp --python 3.11 .
# Install the MCP SDK (with CLI extras like the Inspector)
uv add "mcp[cli]"
# uv creates a placeholder main.py β remove it, we use server.py
rm main.py
```
---
## 4. Write the server
Create **`server.py`**:
```python
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from mcp.server.fastmcp import FastMCP
# The name shows up in the host when it lists servers.
mcp = FastMCP("hello-mcp")
@mcp.tool()
def say_hello(name: str = "world") -> str:
"""Return a friendly greeting for the given name."""
return f"Hello, {name}! π This came from your MCP server."
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@mcp.tool()
def current_time(timezone: str = "UTC") -> str:
"""Return the current date and time for an IANA timezone
(e.g. 'America/Lima', 'Asia/Tokyo'). Defaults to UTC."""
try:
tz = ZoneInfo(timezone)
except ZoneInfoNotFoundError:
return f"Unknown timezone: {timezone!r}. Use an IANA name like 'America/Lima'."
now = datetime.now(tz)
return now.strftime("%Y-%m-%d %H:%M:%S %Z")
if __name__ == "__main__":
# Talk to the host over stdio (server runs as a local subprocess).
mcp.run(transport="stdio")
```
### What each part does
| Part | Why it matters |
|------|----------------|
| `FastMCP("hello-mcp")` | Creates the server. The name identifies it in the host. |
| `@mcp.tool()` | Registers a function as a callable tool. |
| **Type hints** (`name: str`, `a: int`) | Become the tool's input schema β the AI knows what arguments to send. |
| **Docstring** (`"""..."""`) | Tells the AI *when and how* to use the tool. Examples help a lot β e.g. mentioning `'Asia/Tokyo'` lets Claude map "Tokyo" β the right value. |
| **Default values** (`= "world"`) | Make arguments optional. |
| **`try/except`** in `current_time` | Returns a friendly message instead of crashing on bad input. |
| `mcp.run(transport="stdio")` | Starts the server speaking JSON-RPC over stdio. |
---
## 5. Verify it works (before connecting)
```bash
# Does it import cleanly?
uv run python -c "import server; print('OK')"
# Test a tool function directly
uv run python -c "import server; print(server.current_time('Asia/Tokyo'))"
```
---
## 6. Register it with Claude Code
```bash
claude mcp add hello-mcp -- uv --directory /ABSOLUTE/PATH/TO/hello-mcp run server.py
```
- Use the **absolute path** to your project so it works from anywhere.
- Check it's healthy:
```bash
claude mcp list
# β hello-mcp: ... - β Connected
```
---
## 7. Use it
1. **Reload the VSCode window** (`Cmd+Shift+P` β "Reload Window") so Claude Code
picks up the server.
2. Ask Claude Code in plain English:
- *"Use the say_hello tool to greet Dani."*
- *"Add 2 and 3."*
- *"What time is it in Madrid?"*
Claude discovers the tools, calls them, and uses the results in its answer.
---
## 8. The dev loop (whenever you change the server)
```
edit server.py β reload the VSCode window β the new/updated tool appears
```
The server is loaded at session start, so changes need a reload (or restart) to
take effect.
---
## 9. Add a resource
A **resource** is *readable data* identified by a URI β think of it as a GET
endpoint, while a tool is a POST. The key difference is **who drives it**:
| | Tool | Resource |
|--|------|----------|
| Initiated by | the **AI** decides to call it | the **user/host** attaches it to the chat |
| Analogy | POST endpoint (do something) | GET endpoint (read something) |
| Example | `add(2, 3)` | `greeting://Dani` |
```python
# Static resource β fixed URI, fixed-ish content.
@mcp.resource("info://server")
def server_info() -> str:
"""Static facts about this server."""
return "hello-mcp v0.1.0 ..."
# Resource TEMPLATE β {name} in the URI becomes a function argument.
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""A personalized greeting."""
return f"Β‘Hola, {name}!"
```
The URI scheme (`info://`, `greeting://`) is yours to invent β it just needs to
be a valid URI. In the Inspector, check the **Resources** tab to read them.
---
## 10. Add a prompt
A **prompt** is a reusable message template. Unlike tools (AI-driven) and
resources (data), prompts are **user-driven**: hosts surface them as slash
commands or menu items, the user picks one and fills the arguments, and the
returned text is sent to the AI as if the user typed it.
```python
@mcp.prompt()
def daily_briefing(name: str, timezone: str = "America/Lima") -> str:
"""Kick off a personalized briefing that exercises the server's tools."""
return (
f"Please greet {name} using the say_hello tool, then use current_time "
f"to report the time in {timezone}, and finish with one fun fact "
f"about that part of the world."
)
```
In Claude Code, MCP prompts appear as slash commands named
`/mcp__<server>__<prompt>`. In the Inspector, use the **Prompts** tab.
### The three primitives, side by side
| Primitive | Who triggers it | Shape | This server |
|-----------|----------------|-------|-------------|
| **Tool** | the AI (model) | function call with typed args | `say_hello`, `add`, `current_time` |
| **Resource** | the user / host app | read-only data at a URI | `info://server`, `greeting://{name}` |
| **Prompt** | the user | message template β conversation starter | `daily_briefing` |
---
## 10.5 Test resources & prompts in Claude Code β
(verified)
After editing `server.py`, **reload the VSCode window** first β the host
snapshots the server at session start (section 8).
### Resources
- Type `@` in the chat input β MCP resources show up in the mention picker;
attaching one sends its content as context.
- Or just ask: *"read the resource `info://server`"* / *"read
`greeting://Dani`"* β Claude has a built-in tool for `resources/read`.
### Prompts
MCP prompts become slash commands with **inline, space-separated, positional
arguments** (no form UI here β that's a Claude Desktop thing):
```
/mcp__hello-mcp__daily_briefing Dani Asia/Tokyo
β ββ timezone (optional, has a default)
ββ name (required)
```
What happens: the server renders the template β it's sent as *your* message β
Claude reads it and calls `say_hello` and `current_time` to fulfill it. One
command exercises the prompt **and** the tools.
| Gotcha | Cause & fix |
|--------|-------------|
| `Missing required argument: name` | Arguments are inline and positional β append them after the command. Args with spaces aren't supported in this UI. |
| `Unknown command: /hello-mcp:daily_briefing` | Wrong name format. The canonical form is `/mcp__hello-mcp__daily_briefing` (use autocomplete). |
---
## 11. Test in other hosts
The whole point of MCP: **the same server works in any host**. Here's how to
connect this one to three different AI apps.
### A. Claude Desktop β
(verified)
> π **Full walkthrough with every gotcha:** [docs/claude-desktop.md](docs/claude-desktop.md)
1. Install from https://claude.ai/download and sign in.
2. Edit the config file (create it if missing):
`~/Library/Application Support/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"hello-mcp": {
"command": "uv",
"args": ["--directory", "/Users/chocodani/dev/mcp", "run", "server.py"]
}
}
}
```
> If Claude Desktop can't find `uv`, use its absolute path
> (`which uv` β e.g. `/opt/homebrew/bin/uv`).
3. **Fully quit** (Cmd+Q) and reopen Claude Desktop.
4. Test each primitive:
- **Tools** β ask *"What time is it in Lima?"* (look for the π¨ / connectors icon).
- **Resources** β click **+** β search for the server name β attach `info://server`.
- **Prompt** β click **+** β choose `daily_briefing`, fill in the arguments.
### B. Gemini CLI β
(verified)
> π **Full walkthrough with every gotcha:** [docs/gemini-cli.md](docs/gemini-cli.md)
1. Install and sign in with your Google account:
```bash
npm install -g @google/gemini-cli
gemini # first run opens the login flow
```
2. Register the server in `~/.gemini/settings.json` (user-wide) β same shape
as Claude Desktop's config:
```json
{
"mcpServers": {
"hello-mcp": {
"command": "uv",
"args": ["--directory", "/Users/chocodani/dev/mcp", "run", "server.py"]
}
}
}
```
3. Run `gemini`, then check `/mcp` β it lists the server and its tools.
4. Test: ask *"add 2 and 40"*; the prompt appears as the slash command
`/daily_briefing`.
### C. ChatGPT (GPT)
ChatGPT **cannot launch local stdio servers** β it's a web app, so it needs a
**public HTTP URL**. That's why `server.py` supports a second transport:
```bash
uv run server.py --http # serves at http://localhost:8000/mcp
```
1. Start the server with `--http` (leave it running).
2. Expose it to the internet with a tunnel:
```bash
brew install ngrok # one-time; needs a free ngrok account + authtoken
ngrok http 8000 # gives you https://<random>.ngrok-free.app
```
3. In ChatGPT: **Settings β Apps & Connectors β Advanced β Developer mode**
(requires a paid plan), then **Create** a connector with URL
`https://<random>.ngrok-free.app/mcp` and no authentication.
4. In a new chat, enable the connector via the **+** menu and ask it to greet
you or add numbers.
> β οΈ While the tunnel is up, anyone with the URL can call your server β fine
> for a hello-world, but remember it for real servers (MCP uses OAuth for
> this). Stop ngrok when you're done.
> π‘ **Stdio vs HTTP** β same server, same tools; only the transport changes.
> Local hosts (Claude Code/Desktop, Gemini CLI) spawn the process and speak
> stdio; remote hosts (ChatGPT) speak HTTP to a URL.
>
> Note: host support for the primitives varies β every host supports **tools**,
> but resources/prompts UI differs (e.g. Gemini CLI exposes prompts as slash
> commands but has no resource picker).
---
## Common gotchas
| Symptom | Cause & fix |
|---------|-------------|
| `Invalid JSON: expected value...` errors | You typed English/shell commands into a manually-run `server.py`. Don't run it by hand β let Claude Code launch it. Just talk to Claude. |
| New tool doesn't show up | Reload the VSCode window after editing. |
| Tools unavailable mid-session | The server disconnected β reload the window to reconnect. |
| `claude mcp list` shows nothing | Run it in a normal terminal, not inside a running `server.py`. |
---
## Optional: visual debugging with the MCP Inspector
The **MCP Inspector** is the official dev tool for MCP servers β think *Postman
for MCP*. Instead of wiring the server into Claude Code to test it, you connect
the Inspector directly, call tools by hand, and watch the raw JSON-RPC traffic.
Great for iterating on `server.py` without touching your Claude Code config.
### Launch it
```bash
# Starts your server AND opens the Inspector web UI in your browser
uv run mcp dev server.py
```
This spawns the server over **stdio** and prints a local URL (e.g.
`http://localhost:5173`). Open it if the browser doesn't open automatically.
> Equivalent without uv: `npx @modelcontextprotocol/inspector uv --directory . run server.py`
### Connect
In the left panel, the transport is **STDIO** and the command/args are already
filled in. Click **Connect** β the status should turn green.
### Invoke the tools
1. Open the **Tools** tab β click **List Tools**. You'll see `say_hello`,
`add`, and `current_time`.
2. Click a tool, fill in the form, and hit **Run Tool**. The result appears on
the right; expand the JSON view to see the raw `tools/call` request/response.
| Tool | Input | Result |
|------|-------|--------|
| `say_hello` | `name = Dani` | `Hello, Dani! π This came from your MCP server.` |
| `say_hello` | *(blank)* | `Hello, world! π ...` (uses the default) |
| `add` | `a = 2`, `b = 40` | `42` |
| `current_time` | `timezone = America/Lima` | e.g. `2026-06-10 12:34:56 -05` |
| `current_time` | *(blank)* | current UTC time |
| `current_time` | `timezone = Mars/Phobos` | `Unknown timezone: 'Mars/Phobos'. Use an IANA name...` |
### Dev loop with the Inspector
```
edit server.py β click "Restart" (or reconnect) in the Inspector β re-run the tool
```
No VSCode reload needed β the Inspector manages the server process itself.
---
## Project structure
```
hello-mcp/
βββ server.py β your MCP server (3 tools, 2 resources, 1 prompt)
βββ docs/
β βββ claude-desktop.md β guide: connect the server to Claude Desktop
β βββ gemini-cli.md β guide: connect the server to Gemini CLI
βββ pyproject.toml β project + mcp[cli] dependency
βββ .python-version β pins Python 3.11
βββ uv.lock β locked dependency versions
βββ README.md β this guide
βββ .venv/ β virtual environment (managed by uv)
```
---
## Where to go next
- ~~Add a resource~~ β
done (section 9)
- ~~Add a prompt~~ β
done (section 10)
- ~~Test in other hosts~~ β
done (section 11)
- **Add authentication** β protect an HTTP server with OAuth.
- **Use real data** β wrap an API or database in tools/resources.
- **Async tools** β `async def` works too, for I/O-bound work.
π Official docs: https://modelcontextprotocol.io
TDQS
A3.6/5.0
Scored across 3 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: arithmetic, time retrieval, and greeting. No overlap or ambiguity.
Naming Consistency4/5
All names use lowercase and underscores, but 'current_time' and 'say_hello' follow a verb_noun pattern while 'add' is a single verb, creating minor inconsistency.
Tool Count5/5
Three tools is well-scoped for a simple utility server, not too few or too many.
Completeness4/5
The set covers basic arithmetic, time, and greeting needs, but lacks related operations like subtraction or time formatting, leaving minor gaps.
Maintenance
ActivityInactive
ResponsivenessNo issues