Building an MCP Server — Python FastMCP
MCP (Model Context Protocol) lets Claude call custom tools you define. FastMCP is the simplest Python way to build one.
Minimal server
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
import json
mcp = FastMCP("my_mcp")
class AddInput(BaseModel):
a: float = Field(..., description="First number")
b: float = Field(..., description="Second number")
@mcp.tool(
name="add_numbers",
annotations={
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False
}
)
async def add_numbers(params: AddInput) -> str:
"""Add two numbers together."""
return json.dumps({"result": params.a + params.b})
if __name__ == "__main__":
mcp.run() # stdio transport by defaultInstall
pip install "mcp[cli]" httpx pydanticTest with Inspector
npx @modelcontextprotocol/inspector python my_server.pyOpens browser UI — call tools manually and inspect responses.
Connect to Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json (Mac):
{
"mcpServers": {
"my_mcp": {
"command": "python",
"args": ["/absolute/path/to/my_server.py"]
}
}
}Restart Claude Desktop after editing.
Transport options
| Transport | When to use |
|---|---|
mcp.run() | Local / Claude Desktop (stdio) |
mcp.run(transport="streamable_http", port=8000) | Remote / shared server |
Tool annotation guide
| Annotation | Meaning |
|---|---|
readOnlyHint: true | Tool doesn’t modify anything |
destructiveHint: true | Tool deletes/overwrites data |
idempotentHint: true | Safe to call multiple times |
openWorldHint: true | Tool talks to external services |
Updates — 2026-05-13
Email MCP (SMTP via Gmail)
Full working example — MCP server that exposes send_email as a Claude-callable tool:
# email_mcp.py
from mcp.server.fastmcp import FastMCP
import smtplib
from email.mime.text import MIMEText
mcp = FastMCP("email-server")
@mcp.tool()
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email via Gmail SMTP."""
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = "you@gmail.com"
msg["To"] = to
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login("you@gmail.com", "APP_PASSWORD_HERE")
smtp.send_message(msg)
return f"Email sent to {to}"
if __name__ == "__main__":
mcp.run()Gmail setup: Use an App Password (not your account password). Google Account → Security → 2-Step Verification → App passwords.
Register in claude_desktop_config.json:
{
"mcpServers": {
"email": {
"command": "python",
"args": ["/path/to/email_mcp.py"]
}
}
}Key advantage over skills/scripts: Claude can call send_email as a native tool call mid-task (e.g., at the end of the daily PKM run) without needing a separate scripted trigger.