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://todosandtodo://{id}(URI template) - Prompt:
suggest_next_action
- Tools:
- 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.
Part 3 — Connect to popular MCP clients
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
/mcpto 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
/healthfor probes. - Set
FASTMCP_LOG_LEVEL=INFOorDEBUGduring troubleshooting.
Tip: FastMCP Cloud can host your MCP server with HTTPS and auth for you. See docs on gofastmcp.com.
Troubleshooting
- On Windows, use
pyor the full path topython.exeifpythonisn’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
- FastMCP Client: https://gofastmcp.com/clients/client
- FastMCP Server: https://gofastmcp.com/servers/server
- FastMCP Resources & Templates: https://gofastmcp.com/servers/resources
- FastMCP GitHub repo: https://github.com/jlowin/fastmcp
- Claude Desktop (local MCP): https://modelcontextprotocol.io/docs/develop/connect-local-servers
- Claude Code MCP: https://docs.claude.com/en/docs/claude-code/mcp
- VS Code MCP (user): https://code.visualstudio.com/docs/copilot/customization/mcp-servers
- VS Code MCP (dev): https://code.visualstudio.com/api/extension-guides/ai/mcp
- Cursor MCP: https://cursor.com/docs/context/mcp
- CodingFleet MCP Integrations: https://codingfleet.com/doc/mcp-integrations


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!