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

Welcome! Meet our Python Code Assistant, your new coding buddy. Why wait? Start exploring now!

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

Just finished the article? Why not take your Python skills a notch higher with our Python Code Assistant? Check it out!

View Full Code Assist My Coding
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!