What We'll Build
What We'll Build
In this tutorial, you'll build a safe, local MCP server with three tools:
- Calculator β Basic math operations
- Documentation Search β Search local markdown files
- Safe File Reader β Read files from a restricted directory
All tools are safe by designβno unrestricted filesystem access, no network calls, no dangerous operations.
MCP Components Explained
| Component | What It Is | Example |
|---|---|---|
| Server | Exposes tools and resources | Your MCP server |
| Tools | Functions the agent can call | calculator, search_docs |
| Resources | Data the agent can read | project_files, topics |
| Client | Connects to MCP servers | Claude, Cursor, custom app |
| Model | LLM that uses the tools | Claude, GPT-4, Gemini |
Prerequisites
- Python 3.10 or higher
- Basic Python knowledge
- A terminal/command line
Step 1: Setup
Create a project directory and install the MCP SDK:
# Create project
mkdir my-mcp-server
cd my-mcp-server
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install MCP SDK
pip install "mcp[cli]"
mcp[cli] package includes the SDK plus helpful CLI tools like mcp dev for testing.
Step 2: Create the Server
Create server.py:
"""Safe MCP Server with Calculator, Docs Search, and File Reader."""
import os
from pathlib import Path
from mcp.server import MCPServer
# Create the server
mcp = MCPServer("Safe Local Tools")
# ββ Configuration ββββββββββββββββββββββββββββββββββββββββββ
# Restrict file access to this directory only
SAFE_DIR = Path("./safe_files").resolve()
DOCS_DIR = Path("./docs").resolve()
# Create directories if they don't exist
SAFE_DIR.mkdir(exist_ok=True)
DOCS_DIR.mkdir(exist_ok=True)
# ββ Helper: Safe path resolution βββββββββββββββββββββββββββ
def safe_path(user_path: str, base_dir: Path) -> Path | None:
"""Resolve a path safely within the base directory."""
try:
resolved = (base_dir / user_path).resolve()
# Ensure the resolved path is within the base directory
if resolved.is_relative_to(base_dir):
return resolved
return None
except (ValueError, OSError):
return None
Step 3: Add the Calculator Tool
# ββ Tool 1: Calculator ββββββββββββββββββββββββββββββββββββββ
@mcp.tool()
def calculator(a: float, b: float, operation: str) -> str:
"""
Perform basic math operations.
Args:
a: First number
b: Second number
operation: One of: add, subtract, multiply, divide
Returns:
The result as a string
"""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b != 0 else "Error: Division by zero",
}
if operation not in operations:
return f"Error: Unknown operation '{operation}'. Use: add, subtract, multiply, divide"
result = operations[operation]
return f"{a} {operation} {b} = {result}"
Step 4: Add the Documentation Search Tool
# ββ Tool 2: Documentation Search βββββββββββββββββββββββββββββ
@mcp.tool()
def search_docs(query: str) -> str:
"""
Search local markdown documentation files.
Args:
query: Search term to find in documentation
Returns:
Matching lines from documentation files
"""
results = []
query_lower = query.lower()
# Search only .md files in the docs directory
for md_file in DOCS_DIR.rglob("*.md"):
try:
content = md_file.read_text(encoding="utf-8")
for i, line in enumerate(content.splitlines(), 1):
if query_lower in line.lower():
relative_path = md_file.relative_to(DOCS_DIR)
results.append(f"{relative_path}:{i}: {line.strip()}")
except (OSError, UnicodeDecodeError):
continue
if not results:
return f"No results found for '{query}'"
# Limit to 10 results
return "\n".join(results[:10])
Step 5: Add the Safe File Reader
# ββ Tool 3: Safe File Reader βββββββββββββββββββββββββββββββββ
@mcp.tool()
def read_file(file_path: str) -> str:
"""
Read a file from the safe directory.
Only files within the ./safe_files/ directory can be read.
This prevents access to sensitive system files.
Args:
file_path: Relative path within the safe directory
Returns:
File contents or error message
"""
resolved = safe_path(file_path, SAFE_DIR)
if resolved is None:
return "Error: Path outside allowed directory"
if not resolved.exists():
return f"Error: File '{file_path}' not found"
if not resolved.is_file():
return f"Error: '{file_path}' is not a file"
# Check file size (max 1MB)
if resolved.stat().st_size > 1_000_000:
return "Error: File too large (max 1MB)"
try:
content = resolved.read_text(encoding="utf-8")
return content
except UnicodeDecodeError:
return "Error: File is not a text file"
Step 6: Add Resources
# ββ Resources ββββββββββββββββββββββββββββββββββββββββββββββββ
@mcp.resource("safe_files://list")
def list_safe_files() -> str:
"""List all files in the safe directory."""
files = []
for item in SAFE_DIR.rglob("*"):
if item.is_file():
relative = item.relative_to(SAFE_DIR)
files.append(str(relative))
return "\n".join(files) if files else "No files in safe directory"
@mcp.resource("docs://topics")
def list_doc_topics() -> str:
"""List all documentation topics."""
topics = []
for md_file in DOCS_DIR.rglob("*.md"):
relative = md_file.relative_to(DOCS_DIR)
topics.append(str(relative.with_suffix("")))
return "\n".join(topics) if topics else "No documentation topics"
Step 7: Run the Server
# ββ Run ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
print("Starting Safe MCP Server...")
print(f"Safe directory: {SAFE_DIR}")
print(f"Docs directory: {DOCS_DIR}")
mcp.run()
Step 8: Test the Server
Run the server with the MCP Inspector:
# Start the server in development mode
mcp dev server.py
This opens the MCP Inspector where you can:
- See all available tools and resources
- Call tools with test parameters
- View responses
Step 9: Create Test Files
Create some test content:
# Create test files
echo "# Python Guide\n\nPython is a versatile programming language." > docs/python.md
echo "# API Reference\n\nREST APIs use HTTP methods." > docs/api.md
echo "Hello, this is a test file." > safe_files/test.txt
Step 10: Connect to an AI Agent
Add your server to your MCP client configuration:
// Claude Desktop config (claude_desktop_config.json)
{
"mcpServers": {
"safe-local-tools": {
"command": "python",
"args": ["/path/to/server.py"]
}
}
}
Complete Server Code
"""Complete Safe MCP Server."""
import os
from pathlib import Path
from mcp.server import MCPServer
mcp = MCPServer("Safe Local Tools")
SAFE_DIR = Path("./safe_files").resolve()
DOCS_DIR = Path("./docs").resolve()
SAFE_DIR.mkdir(exist_ok=True)
DOCS_DIR.mkdir(exist_ok=True)
def safe_path(user_path: str, base_dir: Path) -> Path | None:
"""Resolve a path safely within the base directory."""
try:
resolved = (base_dir / user_path).resolve()
return resolved if resolved.is_relative_to(base_dir) else None
except (ValueError, OSError):
return None
@mcp.tool()
def calculator(a: float, b: float, operation: str) -> str:
"""Perform basic math: add, subtract, multiply, divide."""
ops = {"add": a + b, "subtract": a - b, "multiply": a * b,
"divide": a / b if b != 0 else "Error: Division by zero"}
if operation not in ops:
return f"Error: Use add, subtract, multiply, divide"
return f"{a} {operation} {b} = {ops[operation]}"
@mcp.tool()
def search_docs(query: str) -> str:
"""Search local markdown docs for a query."""
results = []
for f in DOCS_DIR.rglob("*.md"):
try:
for i, line in enumerate(f.read_text().splitlines(), 1):
if query.lower() in line.lower():
results.append(f"{f.name}:{i}: {line.strip()}")
except (OSError, UnicodeDecodeError):
continue
return "\n".join(results[:10]) or f"No results for '{query}'"
@mcp.tool()
def read_file(file_path: str) -> str:
"""Read a file from the safe directory only."""
resolved = safe_path(file_path, SAFE_DIR)
if resolved is None:
return "Error: Path outside allowed directory"
if not resolved.exists():
return f"Error: '{file_path}' not found"
if resolved.stat().st_size > 1_000_000:
return "Error: File too large (max 1MB)"
return resolved.read_text(encoding="utf-8")
@mcp.resource("safe_files://list")
def list_safe_files() -> str:
"""List files in safe directory."""
files = [str(f.relative_to(SAFE_DIR)) for f in SAFE_DIR.rglob("*") if f.is_file()]
return "\n".join(files) or "No files"
@mcp.resource("docs://topics")
def list_doc_topics() -> str:
"""List documentation topics."""
topics = [str(f.relative_to(DOCS_DIR).with_suffix("")) for f in DOCS_DIR.rglob("*.md")]
return "\n".join(topics) or "No topics"
if __name__ == "__main__":
print(f"Safe directory: {SAFE_DIR}")
print(f"Docs directory: {DOCS_DIR}")
mcp.run()
Security Features
| Feature | Implementation |
|---|---|
| Path restriction | Only reads from ./safe_files/ directory |
| Path traversal prevention | Validates resolved path stays within base |
| File size limit | Max 1MB per file |
| No network access | All operations are local |
| No write operations | Read-only file access |
| Unicode safety | Handles encoding errors gracefully |
Key Takeaways
- MCP servers expose tools (functions) and resources (data)
- The Python SDK handles protocol, validation, and serialization
- Just write Python functions with type hints and docstrings
- Always implement safety restrictions for file access
- Test with
mcp devbefore connecting to agents - Add your server to MCP client config to use with AI agents
Further Reading
- MCP Servers Explained β BestWordz
- MCP vs APIs β BestWordz
- Context Engineering Explained β BestWordz
- Python Docker Workspace β BestWordz
- Official MCP Server Tutorial β External
Related BestWordz Tools
- π JSON Formatter β Format MCP payloads
- π Regex Tester β Test search patterns
- π Hash Generator β Generate file checksums
π¬ Join the conversation on BestWordz Community β Share your MCP server implementations and get help.
Try the JSON Formatter
Put what you've learned into practice with this free BestWordz tool.
π¬ Discuss this topic
Have questions or insights about What We'll Build? Join the BestWordz Community.
π Related Articles
The Problem: AI Without Context
Key Takeaway --> π― RAG retrieves relevant knowledge from your documents. MCP connects AI agβ¦
CybersecurityFirst, What Is an API?
Key Takeaway --> π― APIs connect applications to services. MCP connects AI agents to tools aβ¦
CybersecurityThe 10-Stage CS Learning Roadmap
A computer science education in 2026 requires more than traditional coursework. Today's students neβ¦
CybersecurityIntroduction
Computer programming is undergoing its most significant transformation since the invention of high-β¦
CybersecurityWhy Build MCP Servers?
Key Takeaway --> π― The best way to learn MCP is by building. These 10 projects progress froβ¦
CybersecurityThe Privacy Problem with Cloud AI
Key Takeaway --> π― You can build a fully private AI agent that runs entirely on your local β¦
π§ Related Tools
Regex Tester
Test regular expressions live: matches with positions, capture groups, and flag validation.
Try it now βPEM Decoder
Decode PEM-encoded certificates, keys, and CSRs.
Try it now βJSON Formatter
Pretty-print or minify any JSON document instantly, with clear line/column error reporting.
Try it now βJWT Header Decoder
Decode the header segment of a JSON Web Token.
Try it now βπ¬ Discuss on BestWordz Community
Join the conversation about Python, Docker, LLMs on the BestWordz Community forum.
Visit Forum β