Building Your First MCP Server from Scratch: The Complete Python & Claude Guide

Table of Contents
For years, connecting Large Language Models to internal databases, local filesystems, and developer tools required writing bespoke function-calling schemas for each LLM provider. Every time you switched models or updated an agent framework, you had to re-implement tool definitions, authorization boundaries, and serialization logic.
The Model Context Protocol (MCP), introduced by Anthropic and adopted broadly across Claude Desktop, Cursor, and modern AI developer environments, solves this fragmentation. MCP is the universal, open standard—essentially the "USB-C of AI integrations"—that enables LLMs to discover, inspect, and invoke local and remote tools through a standardized JSON-RPC interface.
In this guide, we break down the architecture of MCP, build a production-grade Python MCP server from scratch using FastMCP, implement secure tools and dynamic resources, and wire it directly into Claude Desktop.
The MCP Architecture: How Hosts, Clients, and Servers Connect
To understand how MCP functions, examine the three core participants in the protocol:
┌────────────────────────────────────────────────────────┐
│ MCP Host (Claude Desktop / Cursor IDE) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ MCP Client (Protocol Adapter & Security Layer) │ │
│ └───────────────────────┬────────────────────────┘ │
└───────────────────────────┼────────────────────────────┘
│ (JSON-RPC 2.0 over Stdio or SSE)
▼
┌────────────────────────────────────────────────────────┐
│ MCP Server (Your Python / TypeScript Application) │
│ │
│ ├── Tools: Callable functions (read, write, API) │
│ ├── Resources: Read-only context (logs, DB rows) │
│ └── Prompts: Pre-engineered prompt templates │
└────────────────────────────────────────────────────────┘
- MCP Host: The application running the AI model interface (e.g., Claude Desktop, Zed editor).
- MCP Client: The internal protocol driver inside the host that negotiates capabilities, enforces security permissions, and forwards tool calls.
- MCP Server: A standalone lightweight process (or remote service) that advertises its available capabilities and executes requested operations.
Transports: Stdio vs Server-Sent Events (SSE)
- Stdio Transport: The host spawns your Python server as a child process and communicates via standard input/output (
stdin/stdout). This is the standard, zero-overhead mode for local developer machines. - SSE Transport: The host connects to an external HTTP server over Server-Sent Events for streaming updates. This mode is used for cloud-hosted microservices and enterprise SaaS connectors.
1. Setting Up Your Python Environment with uv
We'll use uv, the blazing-fast Python package manager, to initialize our project and install the official Python MCP SDK:
# Create project directory
mkdir local-dev-mcp && cd local-dev-mcp
# Initialize Python workspace
uv init
uv add mcp pydantic
2. Building the Server with FastMCP
The Python SDK provides FastMCP, a high-level framework modeled after FastAPI. It automatically extracts parameter schemas, type hints, and documentation strings into valid JSON-RPC tool schemas.
Create server.py:
# server.py: Production Local Developer MCP Server
import os
from pathlib import Path
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
# Initialize the MCP Server with identity and version
mcp = FastMCP("Developer-Workspace-Tools", version="1.0.0")
# Define a safe base directory to prevent directory traversal attacks
SAFE_WORKSPACE = Path(os.getenv("MCP_WORKSPACE_DIR", "./workspace")).resolve()
SAFE_WORKSPACE.mkdir(parents=True, exist_ok=True)
def resolve_safe_path(relative_path: str) -> Path:
"""Guarantees file operations remain strictly inside the authorized workspace."""
resolved = (SAFE_WORKSPACE / relative_path).resolve()
if not str(resolved).startswith(str(SAFE_WORKSPACE)):
raise PermissionError(f"Access denied: '{relative_path}' attempts path traversal.")
return resolved
# ====================================================================
# Primitive 1: Tools (Callable Actions)
# ====================================================================
@mcp.tool()
def save_code_snippet(filename: str, code: str, language: str = "python") -> str:
"""
Saves a code snippet to the authorized local workspace.
Args:
filename: Name of the file including extension (e.g., 'auth_utils.py')
code: Full code content to write
language: Programming language identifier for metadata
"""
target_path = resolve_safe_path(filename)
target_path.parent.mkdir(parents=True, exist_ok=True)
with open(target_path, "w", encoding="utf-8") as f:
f.write(code)
return f"Successfully wrote {len(code)} bytes to {filename} (Language: {language})"
@mcp.tool()
def list_workspace_files() -> list[dict]:
"""Lists all files in the developer workspace with file size and modified timestamps."""
files = []
for p in SAFE_WORKSPACE.rglob("*"):
if p.is_file():
stat = p.stat()
files.append({
"relative_path": str(p.relative_to(SAFE_WORKSPACE)),
"size_bytes": stat.st_size,
"modified_timestamp": stat.st_mtime,
})
return files
# ====================================================================
# Primitive 2: Resources (Context Streams)
# ====================================================================
@mcp.resource("workspace://logs/{log_name}")
def get_system_log(log_name: str) -> str:
"""Reads a designated log file from the workspace for debugging context."""
log_path = resolve_safe_path(f"logs/{log_name}.log")
if not log_path.exists():
return f"Log file '{log_name}' not found."
with open(log_path, "r", encoding="utf-8") as f:
# Return the last 50 lines to conserve context window
lines = f.readlines()
return "".join(lines[-50:])
if __name__ == "__main__":
# Runs the Stdio transport event loop
mcp.run()
3. Configuring Claude Desktop
To allow Claude Desktop to discover and invoke your new MCP server, configure its JSON configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add your server definition under mcpServers:
{
"mcpServers": {
"developer-workspace": {
"command": "uv",
"args": [
"run",
"--directory",
"/absolute/path/to/local-dev-mcp",
"server.py"
],
"env": {
"MCP_WORKSPACE_DIR": "/absolute/path/to/workspace"
}
}
}
}
Restart Claude Desktop. You will notice a new 🔌 hammer/tool icon appear in the bottom-right corner of the chat input box. Clicking it displays your registered tools: save_code_snippet and list_workspace_files.
4. Testing and Debugging with MCP Inspector
Before connecting to Claude Desktop, you can interactively test and debug your MCP server using the official browser-based MCP Inspector:
# Launch the interactive MCP debugger
npx @modelcontextprotocol/inspector uv run server.py
The inspector launches a local web UI where you can:
- Inspect auto-generated JSON schemas for all tools and resources.
- Execute tools with custom JSON arguments and view raw JSON-RPC responses.
- Verify error handling and path traversal rejections.
Security Best Practices for Production MCP Servers
- Path Traversal Protection: Always resolve file paths against an explicit base directory using
pathlib.Path.resolve(). Never trust relative path strings passed directly by an LLM. - Read-Only Enclaves: If a tool only needs to read telemetry or database records, connect with a database role that has strict
SELECT-only privileges. - Stdio Cleanliness: Never use
print()statements for debugging in an MCP server. Because standard output is reserved strictly for JSON-RPC messages, arbitrary text output will corrupt the wire protocol. Use standard Pythonloggingconfigured to write tosys.stderr.
Frequently Asked Questions
Can an MCP server call external cloud APIs?
Yes. An MCP server is simply standard code. You can use httpx or requests inside your tool implementations to call GitHub APIs, Slack webhooks, Jira tickets, or internal Kubernetes clusters.
How does Claude decide when to call an MCP tool?
Claude inspects the function signature and docstrings advertised by your MCP server. Writing descriptive docstrings (explaining what the tool does, what parameters mean, and when it should be called) is the single most important factor for reliable tool calling.
What is the difference between an MCP Tool and an MCP Resource?
- Tools are actions with side effects (writing files, executing commands, querying external APIs) that require explicit user approval in the host UI.
- Resources are passive, read-only data streams (like attaching a file or feeding database logs into context) that the host can read without side-effect risks.
You Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Cloudflare and Anthropic Introduces Claude Managed Agents on Cloudflare
I spent months wrestling with agent deployment. Here's why the Cloudflare-Anthropic integration actually solves the problems everyone else ignores.
Read more
How I Actually Use ChatGPT, Claude, and Gemini for Development Work
A developer's honest take on ChatGPT, Claude, and Gemini: where each one helps, where each one gets in the way, and how to pick the right tool for the task.
Read more
Getting the Most Out of Claude's Free Plan (Including MCP & Claude Desktop)
What Claude's free tier actually gives you: Sonnet model, 200K context, Projects, and local MCP tools via Claude Desktop, and how to stretch the message limit further than you'd expect.
Read more