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).
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:
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.
create_todo
, list_todos
, complete_todo
, search_todos
stats://todos
and todo://{id}
(URI template)suggest_next_action
pip install fastmcp
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)
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.
Edit the MCP config file (via Settings → Developer → Edit config).
~/Library/Application Support/Claude/claude_desktop_config.json
%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
Recent VS Code builds provide MCP support.
{
"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
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
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
/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
transport="http"
.5) Observability & health
/health
for probes.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.
py
or the full path to python.exe
if python
isn’t on PATH.{ "items": [...] }
) to keep client handling simple.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.
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
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!