Cover image for Your First MCP Server with FastMCP and uv

At a glance

Reading time

~200 words/min

Published

8 hours ago

Jun 11, 2026

Views

5

All-time total

Your First MCP Server with FastMCP and uv

Part 1 gave you the protocol; this part gives you a server. By the end you will have a working MCP server written with the official Python SDK, running locally over stdio, visible in the MCP Inspector, and connected to Claude Code and Claude Desktop, where the model can actually call your code mid-conversation. The example we start here, a small team notes server, is the one we grow through the whole series: resources and prompts in Part 3, hardened tools in Part 4, the web in Part 5, auth in Part 6, tests in Part 7, and a deploy in Part 8.

What you will build in Part 2

  • A uv-managed project with the official MCP Python SDK installed
  • A FastMCP server exposing real tools over stdio
  • An understanding of how type hints and docstrings become tool schemas
  • The server connected to Claude Code and Claude Desktop
  • A persistence layer so your notes survive restarts
i

Info

Naming things

FastMCP here means the high-level API inside the official mcp package, imported from mcp.server.fastmcp. There is also a separate community package named fastmcp that extends the same ideas. This series uses the official SDK so everything works with stock tooling.

1. Project setup with uv

We manage the project with uv, the same tool our Python series standardized on, because it gives us a lockfile, fast installs, and a clean way for hosts to launch the server later. The cli extra matters: it brings the mcp command with the dev server and Inspector integration we will rely on constantly.

# Install uv if you do not have it (macOS / Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create the project
uv init mcp-notes --python 3.14
cd mcp-notes

# The official MCP SDK, with the CLI tooling
uv add "mcp[cli]"

# Sanity check
uv run mcp version

That is the entire toolchain. No Node, no Docker yet, no API keys. One folder, one dependency, and a lockfile you should commit. If uv is new to you, the one habit to carry over from here: never run bare python in this project, always uv run, so you are always inside the locked environment.

2. The smallest server that does something real

Create server.py at the project root. FastMCP, the high-level API of the official SDK, turns plain Python functions into protocol-compliant tools. You write a function with type hints and a docstring, decorate it, and the SDK handles everything Part 1 showed you: the handshake, tools/list, tools/call, schema generation, and error wrapping.

"""A team notes MCP server. Run with: uv run mcp dev server.py"""
import json
from pathlib import Path

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes")

STORE = Path(__file__).parent / "notes.json"


def _load() -> dict[str, str]:
    return json.loads(STORE.read_text()) if STORE.exists() else {}


def _save(notes: dict[str, str]) -> None:
    STORE.write_text(json.dumps(notes, indent=2))


@mcp.tool()
def add_note(title: str, body: str) -> str:
    """Save a note under a unique title. Fails politely if the title exists."""
    notes = _load()
    if title in notes:
        return f"A note titled {title!r} already exists. Pick another title."
    notes[title] = body
    _save(notes)
    return f"Saved note {title!r} ({len(body)} characters)."


@mcp.tool()
def search_notes(query: str, limit: int = 5) -> str:
    """Search saved notes by keyword across titles and bodies."""
    q = query.lower()
    notes = _load()
    hits = [t for t, b in notes.items() if q in t.lower() or q in b.lower()]
    if not hits:
        return f"No notes match {query!r}."
    lines = [f"- {t}: {notes[t][:80]}" for t in hits[:limit]]
    return f"{len(hits)} match(es) for {query!r}:\n" + "\n".join(lines)


if __name__ == "__main__":
    mcp.run()  # stdio transport by default

Forty lines, and it is a complete MCP server. mcp.run() starts the stdio transport from Part 1: the host will launch this file as a child process and speak JSON-RPC over its stdin and stdout. Notice there is not a single line of protocol code. You did not write the initialize handler, the tool list, or the JSON Schema; the SDK derived all of it from your function signatures. Notice also what the tools return on failure: a sentence, not an exception. The model reads these strings, so a clear sentence beats a stack trace. Part 4 turns that instinct into a discipline.

3. How your type hints become schemas

The magic in those decorators deserves demystifying, because once you see it you can predict exactly what any host will show the model. FastMCP inspects each function: the docstring becomes the tool description, every parameter's type hint maps to a JSON Schema type, and parameters without defaults become required. The playground below is a miniature, honest reimplementation of that inspection, pure Python, running in your browser. Change the function and rerun to watch the schema follow.

Python playground

Run it and compare the output with the tools/list response you stepped through in Part 1; it is the same shape. This is why hints and docstrings are not optional politeness in MCP code. They are the interface. A vague docstring is a vague tool description, and the model chooses tools by reading descriptions. Try the live designer below to feel how each field lands in both artifacts at once.

Tool schema designer

Design a tool, watch the schema and the Python write themselves

JSON Schema (what the client sees)


FastMCP Python (what you write)


Checkpoint

In FastMCP, where does the description the model reads for each tool come from?

4. Run it and poke it with the Inspector

You could wire the server straight into Claude, but a tighter loop exists. The dev command starts your server and attaches the MCP Inspector, a browser tool that acts as a fully scripted client: it runs the handshake, lists your tools, and lets you call them with arguments you type in.

uv run mcp dev server.py
# MCP Inspector is up at http://localhost:6274 ...

Open the URL, press Connect, and click through Tools. You are watching the exact sequence from Part 1's protocol walkthrough, performed live against your code: initialize, the initialized notification, tools/list, and when you press a tool's Run button, tools/call. Call add_note with a title and body, then search_notes for a word in it, and check that notes.json appeared on disk. We return to the Inspector properly in Part 7; for now it is your fastest feedback loop.

5. Connect it to Claude Code and Claude Desktop

Now the real thing. For Claude Code, one command registers the server; the part after the double dash is exactly how the host will launch your process, which is why we point uv at the project directory explicitly.

claude mcp add notes -- uv run --directory /absolute/path/to/mcp-notes python server.py

# Verify it registered and connects
claude mcp list

For Claude Desktop, add the server to the mcpServers map in claude_desktop_config.json, which lives in Application Support on macOS and AppData on Windows, then restart the app fully. The shape mirrors the command above: a binary and its arguments.

{
  "mcpServers": {
    "notes": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/mcp-notes",
               "python", "server.py"]
    }
  }
}

Warning

Absolute paths, always

Hosts launch your server from their own working directory, not your project folder. A relative path that works in your terminal will fail silently inside Claude Desktop. Use absolute paths in every host configuration, including the path to uv itself if it is not on the host's PATH.

Open a fresh conversation and ask something like: save a note titled standup that says demo the notes server on Thursday. The host will show you a permission prompt, the tool call, and the result, and then you can ask it to search your notes a message later. That round trip, model to host to client to your Python function and back, is the whole point of the protocol, and you have now built every visible piece of it except the host.

6. What just happened, exactly

It is worth replaying the sequence with Part 1's vocabulary, because it cements where your code sits. When Claude Code started, it read its config, spawned your process, and its embedded client ran initialize against you; the SDK answered. The host fetched tools/list and injected your two descriptions into the model's context. When you asked about saving a note, the model emitted a tool call; the host asked your permission, forwarded it as tools/call, your function ran, and the string you returned traveled back into the conversation as a content block. Your server never saw the rest of the chat. It saw two method calls.

Checkpoint

Your server works in the terminal but Claude Desktop shows it as failed. Which is the most likely cause, given how hosts launch servers?

! Common mistakes to avoid

  • Using print() for debugging inside a stdio server

    stdout is the protocol channel. Configure logging to stderr instead; the host surfaces it in its logs without corrupting the session.

  • Registering the server with a relative path or bare python

    Use uv run --directory with an absolute project path so the host launches the locked environment from anywhere.

  • Writing tools that raise exceptions for expected failures

    Return a clear sentence the model can act on, like the duplicate-title message in add_note. Reserve exceptions for genuine bugs; Part 4 covers the full error contract.

  • Editing claude_desktop_config.json and only closing the window

    Quit the app entirely and relaunch. Config is read at startup, and a half-restarted host is a classic source of phantom failures.

The bottom line

A working MCP server is a uv project, one dependency, and a few decorated functions. FastMCP turns signatures into schemas and docstrings into descriptions, the Inspector gives you a tight feedback loop, and two small configs put your code one permission prompt away from a live model. The server is real but naive: everything is a tool, errors are ad hoc, and anyone could call anything. The next two parts fix the first problems by adding resources and prompts and then making the tools genuinely agent-proof.

? Frequently asked questions

Why uv instead of pip and venv? +

Hosts launch your server as a subprocess, so the launch command must reproduce your environment exactly. uv run --directory does that in one line with a lockfile behind it. pip works too, but you will be hand-managing activation paths in every host config.

Can a tool be async? +

Yes. Declare it async def and FastMCP awaits it. Use async tools whenever you call databases or HTTP APIs; Part 5 leans on this when the server moves to the web.

Where should real state live instead of notes.json? +

Anything your Python can reach: SQLite, Postgres, an internal API. The JSON file keeps this part focused on the protocol. Part 8 swaps it for a proper database during deployment.

Do I need to restart the host every time I edit a tool? +

For stdio servers, the host owns the process, so a restart or a reconnect from the host side picks up changes. During development, the Inspector loop with uv run mcp dev is much faster than restarting Claude.

Up next: Part 3, resources and prompts.

Newsletter

Want more posts like this?

Get practical software notes and tutorials delivered when something new is published.

No spam. Unsubscribe anytime.

How did this land?

Comments

0
Log in or sign up to join the discussion and react to this post.

No comments yet. Be the first to share your thoughts.

Related posts

Important functionalities of Pandas in Python : Tricks and Features

Pandas is one of my favorite libraries in python. It’s very useful to visualize the data in a clean structural manner. Nowadays Pandas is widely used in Data Science, Machine Learning and other areas.

5 years ago

How to get data from twitter using Tweepy in Python?

To start working on Python you need to have Python installed on your PC. If you haven’t installed python. Go to the Python website and get it installed.

6 years ago

Predicting per capita income of the US using linear regression

Python enables us to predict and analyze any given data using Linear regression. Linear Regression is one of the basic machine learning or statistical techniques created to solve complex problems.

6 years ago

Essential Sorting Algorithms for Computer Science Students

Algorithms are commonly taught in Computer Science, Software Engineering subjects at your Bachelors or Masters. Some find it difficult to understand due to memorizing.

6 years ago

AI Agents in 2026: What Developers Actually Need to Know

A hype-free developer guide to AI agents in 2026 to the agent loop, tools, memory, MCP, evaluation, guardrails, and the cost and failure modes that separate a demo from production.

1 week ago