Build an MCP Server in Python with FastMCP

Build a real MCP server and client in Python using FastMCP. Implement a Todo Manager with tools, resources, and prompts, test it programmatically, then wire it into Claude Desktop, VS Code, and Cursor. Includes simple production deployment tips.
  · 8 min read · Updated oct 2025 · Machine Learning · General Python Tutorials · Application Programming Interfaces

Get a head start on your coding projects with our Python Code Generator. Perfect for those times when you need a quick solution. Don't wait, try it today!

In this hands‑on tutorial, you’ll build a practical Model Context Protocol (MCP) server and client in Python using FastMCP. We’ll implement a concrete Todo Manager with tools, resources, and prompts; test it programmatically; then show how to connect it to popular MCP clients like Claude Desktop, VS Code, and Cursor. CodingFleet also supports MCP integrations (link at the end).

What is MCP (Model Context Protocol)?

MCP is an open protocol that standardizes how Large Language Model (LLM) applications connect to tools and data sources. You can think of it as “USB‑C for AI tools”: a unified way to expose capabilities regardless of the host app.

An MCP server exposes three core component types:

  • Tools: Actions the client (or LLM) can execute. Think POST/PUT side‑effects. Examples: create a task, call an API, transform data.
  • Resources: Read‑only data sources. Think GET endpoints. Examples: configuration, files, database records, metrics.
  • Prompts: Reusable message templates to structure LLM interactions.

Clients (like Claude Desktop, VS Code, Cursor) connect to MCP servers via transports such as stdio (local), HTTP (web), or SSE. FastMCP offers both server and client libraries, so you can develop, test, and deploy MCP services quickly in Python.

What you’ll build

  • A FastMCP server exposing:
    • Tools: create_todo, list_todos, complete_todo, search_todos
    • Resources: stats://todos and todo://{id} (URI template)
    • Prompt: suggest_next_action
  • A Python client to ping the server and exercise all features
  • Integration steps for Claude Desktop, VS Code, and Cursor

Prerequisites

  • Python 3.10+
  • pip install fastmcp
  • Basic terminal usage
  • Optional: Claude Desktop, VS Code, or Cursor if you want to connect the server to a GUI client

Part 1 — Build the MCP Server (Todo Manager)

Create a file todo_server.py:

# todo_server.py
from typing import Literal
from itertools import count
from datetime import datetime, timezone
from fastmcp import FastMCP

# In-memory storage for demo purposes
TODOS: list[dict] = []
_id = count(start=1)

mcp = FastMCP(name="Todo Manager")

@mcp.tool
def create_todo(
    title: str,
    description: str = "",
    priority: Literal["low", "medium", "high"] = "medium",
) -> dict:
    """Create a todo (id, title, status, priority, timestamps)."""
    todo = {
        "id": next(_id),
        "title": title,
        "description": description,
        "priority": priority,
        "status": "open",
        "created_at": datetime.now(timezone.utc).isoformat(),
        "completed_at": None,
    }
    TODOS.append(todo)
    return todo

@mcp.tool
def list_todos(status: Literal["open", "done", "all"] = "open") -> dict:
    """List todos by status ('open' | 'done' | 'all')."""
    if status == "all":
        items = TODOS
    elif status == "open":
        items = [t for t in TODOS if t["status"] == "open"]
    else:
        items = [t for t in TODOS if t["status"] == "done"]
    # Wrap arrays in a dict for clean JSON serialization in clients
    return {"items": items}

@mcp.tool
def complete_todo(todo_id: int) -> dict:
    """Mark a todo as done."""
    for t in TODOS:
        if t["id"] == todo_id:
            t["status"] = "done"
            t["completed_at"] = datetime.now(timezone.utc).isoformat()
            return t
    raise ValueError(f"Todo {todo_id} not found")

@mcp.tool
def search_todos(query: str) -> dict:
    """Case-insensitive search in title/description."""
    q = query.lower().strip()
    items = [t for t in TODOS if q in t["title"].lower() or q in t["description"].lower()]
    return {"items": items}

# Read-only resources
@mcp.resource("stats://todos")
def todo_stats() -> dict:
    """Aggregated stats: total, open, done."""
    total = len(TODOS)
    open_count = sum(1 for t in TODOS if t["status"] == "open")
    done_count = total - open_count
    return {"total": total, "open": open_count, "done": done_count}

@mcp.resource("todo://{id}")
def get_todo(id: int) -> dict:
    """Fetch a single todo by id."""
    for t in TODOS:
        if t["id"] == id:
            return t
    raise ValueError(f"Todo {id} not found")

# A reusable prompt
@mcp.prompt
def suggest_next_action(pending: int, project: str | None = None) -> str:
    """Render a small instruction for an LLM to propose next action."""
    base = f"You have {pending} pending TODOs. "
    if project:
        base += f"They relate to the project '{project}'. "
    base += "Suggest the most impactful next action in one short sentence."
    return base

if __name__ == "__main__":
    # Default transport is stdio; you can also use transport="http", host=..., port=...
    mcp.run()

Run your server:

python todo_server.py   # stdio transport (default)

Optionally, run over HTTP (for web/remote):

# Replace the last line with:
mcp.run(transport="http", host="127.0.0.1", port=8000)

Part 2 — Test the server with a programmatic client

Create todo_client_test.py:

# todo_client_test.py
import asyncio
from fastmcp import Client

async def main():
    # Option A: Connect to local Python script (stdio)
    client = Client("todo_server.py")

    # Option B: In-memory (for tests)
    # from todo_server import mcp
    # client = Client(mcp)

    async with client:
        await client.ping()
        print("[OK] Connected")

        # Create a few todos
        t1 = await client.call_tool("create_todo", {"title": "Write README", "priority": "high"})
        t2 = await client.call_tool("create_todo", {"title": "Refactor utils", "description": "Split helpers into modules"})
        t3 = await client.call_tool("create_todo", {"title": "Add tests", "priority": "low"})
        print("Created IDs:", t1.data["id"], t2.data["id"], t3.data["id"])

        # List open
        open_list = await client.call_tool("list_todos", {"status": "open"})
        print("Open IDs:", [t["id"] for t in open_list.data["items"]])

        # Complete one
        updated = await client.call_tool("complete_todo", {"todo_id": t2.data["id"]})
        print("Completed:", updated.data["id"], "status:", updated.data["status"])

        # Search
        found = await client.call_tool("search_todos", {"query": "readme"})
        print("Search 'readme':", [t["id"] for t in found.data["items"]])

        # Resources
        stats = await client.read_resource("stats://todos")
        print("Stats:", getattr(stats[0], "text", None) or stats[0])

        todo2 = await client.read_resource(f"todo://{t2.data['id']}")
        print("todo://{id}:", getattr(todo2[0], "text", None) or todo2[0])

        # Prompt
        prompt_msgs = await client.get_prompt("suggest_next_action", {"pending": 2, "project": "MCP tutorial"})
        msgs_pretty = [
            {"role": m.role, "content": getattr(m, "content", None) or getattr(m, "text", None)}
            for m in getattr(prompt_msgs, "messages", [])
        ]
        print("Prompt messages:", msgs_pretty)

if __name__ == "__main__":
    asyncio.run(main())

Run the client:

pip install fastmcp
python todo_client_test.py

You should see ping success, created IDs, open list, completion confirmation, search results, stats JSON, the todo:// resource JSON, and a rendered prompt message.


Below are quick‑start snippets to wire your server into common MCP clients. Restart the app after you change the configuration.

Claude Desktop

Edit the MCP config file (via Settings → Developer → Edit config).

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add your server:

{
  "mcpServers": {
    "todo-manager": {
      "command": "python",
      "args": ["todo_server.py"],
      "cwd": "/absolute/path/to/your/project",
      "env": {}
    }
  }
}

After saving, fully quit and restart Claude Desktop. You should see an MCP indicator in the chat UI; click it to browse tools/resources.

Additional options: Claude Code CLI also supports adding/importing servers. See: https://docs.claude.com/en/docs/claude-code/mcp

VS Code

Recent VS Code builds provide MCP support.

  • Browse and install MCP servers from the curated list.
  • Add your custom server via settings or workspace JSON (depending on build):
{
  "mcpServers": {
    "todo-manager": {
      "command": "python",
      "args": ["todo_server.py"],
      "cwd": "/absolute/path/to/your/project"
    }
  }
}

See: https://code.visualstudio.com/docs/copilot/customization/mcp-servers 

Cursor

Global config: ~/.cursor/mcp.json

Project config: .cursor/mcp.json

Add your server:

{
  "mcpServers": {
    "todo-manager": {
      "command": "python",
      "args": ["todo_server.py"],
      "cwd": "/absolute/path/to/your/project",
      "env": {}
    }
  }
}

Then open Cursor → Settings → MCP → ensure the server is enabled. Docs: https://cursor.com/docs/context/mcp

CodingFleet also supports MCP integrations; see the docs: https://codingfleet.com/doc/mcp-integrations 


Going to production (simple path)

Start simple, then harden as needed.

1) Switch to HTTP transport

Replace mcp.run() with:

mcp.run(transport="http", host="0.0.0.0", port=8000)

Now your MCP endpoint is typically at http://<host>:8000/mcp.

2) Put an HTTPS reverse proxy in front

  • Run Nginx, Caddy, or a cloud load balancer terminating TLS.
  • Proxy /mcp to your server. Example Caddyfile snippet:
mcp.example.com {
  reverse_proxy /mcp 127.0.0.1:8000
}

3) Add authentication (recommended)

FastMCP supports OAuth and other providers out of the box. You can pass an auth provider when creating your server (see FastMCP docs), and clients can connect with auth="oauth".

4) Containerize & deploy

  • Create a Dockerfile, expose port 8000, run your server with transport="http".
  • Deploy to your favorite PaaS or Kubernetes.

5) Observability & health

  • Use FastMCP custom routes to add /health for probes.
  • Set FASTMCP_LOG_LEVEL=INFO or DEBUG during troubleshooting.

Tip: FastMCP Cloud can host your MCP server with HTTPS and auth for you. See docs on gofastmcp.com.


Troubleshooting

  • On Windows, use py or the full path to python.exe if python isn’t on PATH.
  • Some clients prefer stdio for local development; HTTP is better for remote.
  • If your tool returns a top‑level list, wrap it in a dict (e.g., { "items": [...] }) to keep client handling simple.
  • Restart clients after configuration changes.

Conclusion

You built a real MCP server and client with FastMCP, implemented a Todo Manager with meaningful tools, resources, and prompts, validated it with a Python client, and connected it to multiple MCP‑aware apps. From here, you can add persistence (SQLite/Postgres), more resources (analytics, reporting), authentication, and deploy behind HTTPS for team use.


References

Loved the article? You'll love our Code Converter even more! It's your secret weapon for effortless coding. Give it a whirl!

View Full Code Fix My Code
Sharing is caring!




Comment panel

    Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!