Skip to main content
Glama
paodanchacon

hello-mcp

by paodanchacon

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.


Related MCP server: mcpscope

2. Prerequisites

  • 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

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:

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)

# 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

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:

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

# 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.

@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

  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

    {
      "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

  1. Install and sign in with your Google account:

    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:

    {
      "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:

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:

    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

# 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

Available Tools

3 tools
addA

Add two numbers together.

ParametersJSON Schema
NameRequiredDescriptionDefault
aYes
bYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The behavior is transparent for a pure arithmetic operation: it takes two numbers and returns their sum. It does not describe edge cases or return details, but the operation is simple and output schema is available.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, concise sentence with no unnecessary words. It is well-structured and immediately understandable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple arithmetic tool, the description is complete enough for correct use. It lacks explicit edge-case or return-type details, but the operation's simplicity and available output schema reduce the need for further explanation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description says 'two numbers,' which collectively defines both parameters as addends. It does not individually describe a or b, but for addition the individual roles are clear and order is irrelevant.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's exact purpose: adding two numbers together. It is a specific verb and resource, and it naturally distinguishes from sibling tools like subtract and multiply.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description makes it clear that this tool is for addition, and sibling names imply when to use other operations. However, it does not explicitly state when not to use this tool or provide alternative selection guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

current_timeA

Return the current date and time for an IANA timezone (e.g. 'America/Lima', 'Asia/Tokyo'). Defaults to UTC.

ParametersJSON Schema
NameRequiredDescriptionDefault
timezoneNoUTC

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description is the sole source. It does not disclose return format, precision, or error handling for invalid timezones, which is a gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single sentence, front-loaded with key information, no unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With only one optional parameter and an output schema, the description is nearly complete. Lacks output format details, but output schema exists to cover that.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, but description adds value by specifying 'IANA timezone' with examples and default UTC, clarifying the parameter's format beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool returns current date and time for a specified IANA timezone, with examples. It is distinct from sibling tools 'add' and 'say_hello'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The context of use is clear: to get current time in a timezone. No explicit when-not-to-use or alternatives, but siblings are unrelated, so it's sufficient.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

say_helloA

Return a friendly greeting for the given name.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNoworld

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A3.5/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so the description carries the full burden. It correctly states that the tool returns a greeting, which is a safe read operation. However, it does not disclose any further behavioral traits such as idempotency or lack of side effects.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence with no wasted words. It efficiently conveys the tool's purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one optional parameter, no nested objects) and the existence of an output schema (not shown but indicated), the description is adequate. It doesn't explain the return format, but that is covered by the output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, and the description adds only minimal meaning by referencing 'the given name' without explaining the parameter's role, constraints, or expected formats beyond what the schema already provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Return a friendly greeting for the given name' uses a specific verb ('Return') and resource ('greeting'), clearly distinguishing it from sibling tools 'add' and 'current_time'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool versus alternatives; the description simply states what it does without context or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.1.0
    • First observedadd
    • First observedcurrent_time
    • First observedsay_hello

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

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol server that provides basic tools for arithmetic operations (addition) and dynamic greeting resources, demonstrating MCP integration patterns for other projects and clients.
    9 npm
    ISC
  • F
    license
    A
    quality
    D
    maintenance
    A simple local MCP server that provides greeting and integer addition tools.
    2
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    A simple MCP server that exposes a calculator tool (addition) and a dynamic greeting resource.
    16
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Basic MCP server demonstrating tools for arithmetic operations (add, subtract) and a personalized greeting resource.
    1
    -