✻Founding-member pricing: every lesson for $19/mo. The first 3 lessons are free. Start lesson 1 →
Unit 6 · Model Context Protocol (MCP) · Lesson 28 of 40

Build Your Own MCP Server in Python

Build a working MCP server with the official Python SDK: define tools with decorators, add resources, test with the MCP Inspector and connect it to Claude Code.

ACADEMY14 min+78 XP4-question quiz

🎯 By the end of this lesson you'll be able to

  • Set up a Python MCP server project
  • Define tools with decorators and type hints
  • Add static and templated resources
  • Test with the Inspector and connect to Claude Code

Why build one?

Your company has things no public server covers: an internal API, a proprietary database or a home-grown deploy system. A small MCP server makes them available to Claude Code (and any other MCP client) in about 30 lines.

Set up

bash
uv init docs-server && cd docs-server
uv add "mcp[cli]"

Define tools with decorators

The Python SDK's FastMCP class turns type-hinted functions into MCP tools. The docstring and type hints generate the JSON schema for you, so you never hand-write schemas.

server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("docs")

DOCS = {
    "deploy": "Deploys run from main via `make deploy`. Staging deploys on every merge.",
    "oncall": "On-call rotates Mondays. Page via #oncall-urgent.",
}

@mcp.tool()
def list_docs() -> list[str]:
    """List the IDs of all internal docs."""
    return sorted(DOCS)

@mcp.tool()
def read_doc(doc_id: str) -> str:
    """Read an internal doc by its ID."""
    if doc_id not in DOCS:
        raise ValueError(f"Unknown doc '{doc_id}'. Call list_docs first.")
    return DOCS[doc_id]

@mcp.tool()
def edit_doc(doc_id: str, new_content: str) -> str:
    """Replace the content of an existing doc."""
    if doc_id not in DOCS:
        raise ValueError(f"Unknown doc '{doc_id}'.")
    DOCS[doc_id] = new_content
    return f"Updated {doc_id}"

if __name__ == "__main__":
    mcp.run()  # stdio transport by default
✓
Write errors for the model

Error messages are read by Claude. "Unknown doc 'deploy-v2'. Call list_docs first." tells it exactly how to recover, while a bare KeyError doesn't.

Add resources

Resources expose data the app or user can attach. They can be static or templated:

python
@mcp.resource("docs://index")
def docs_index() -> str:
    """All doc IDs, one per line."""
    return "\n".join(sorted(DOCS))

@mcp.resource("docs://{doc_id}")
def doc_resource(doc_id: str) -> str:
    """A single doc as a resource."""
    return DOCS[doc_id]

Add a prompt

python
@mcp.prompt()
def summarize_doc(doc_id: str) -> str:
    """Summarize a doc for a new teammate."""
    return f"Read the doc '{doc_id}' and summarize it in 5 bullets for someone in their first week."

Test with the Inspector

Before wiring it into Claude, test it in the browser-based MCP Inspector:

bash
uv run mcp dev server.py

You can list tools, call them with test inputs, read resources and see the exact JSON messages. That makes it much easier to debug a server before an AI is involved.

Connect it to Claude Code

bash
claude mcp add --transport stdio docs -- uv run --directory /path/to/docs-server server.py
▶Watch it runYour server in action
Your server in action
⇅Ship an MCP serverdrag or use arrows
  1. ⋮⋮Create the project and add the mcp[cli] package
  2. ⋮⋮Define tools with @mcp.tool() and clear docstrings
  3. ⋮⋮Add resources and prompts if they're useful
  4. ⋮⋮Test every tool in the MCP Inspector
  5. ⋮⋮Register it with claude mcp add and try it in a session

Check your understanding

?Lesson quiz10 XP per correct answer · +25 for a perfect score
QUESTION 1 OF 4

What generates a FastMCP tool's input schema?

Why: The SDK derives the schema from type hints and the docstring.
QUESTION 2 OF 4

Why write descriptive error messages in tools?

Why: Errors are context for the model, so make them actionable.
QUESTION 3 OF 4

What does uv run mcp dev server.py do?

Why: The Inspector lets you call tools and read resources directly.
QUESTION 4 OF 4

Which decorator defines a templated resource?

Why: Resources take a URI, and {placeholders} make them templated.
0 / 4 correct
Academy lesson

Unlock lesson 28 and the next 12

You've finished the free lessons. Join the Academy for $19/month to unlock every lesson, quiz, terminal simulation and puzzle, plus the final exam and your certificate of completion.

  • All 40 lessons across 9 units
  • 165+ quiz questions
  • Final exam + certificate
  • New lessons as Claude Code evolves
  • XP, streaks and badges
  • Cancel anytime

Already a member? Log in to continue.

Key takeaways

  • FastMCP turns type-hinted Python functions into MCP tools.
  • Write docstrings and errors for the model. They're its instructions.
  • Resources expose data; prompts expose reusable templates.
  • Test in the Inspector, then add it with claude mcp add.

Nice work. Lesson 28 done?

Mark it complete to earn +78 XP, keep your streak alive and unlock badges.

Written by Curtis Cornell from Anthropic's public Claude Code documentation. Last reviewed November 14, 2026. Claude Code changes fast; if something here differs from your version, the official docs win.

Start lesson 1 free →