Cybersecurity

What We'll Build

Python Docker LLMs GPT MCP AI Agents REST API Hashing
1,323 words Includes Code
🎯 Key Takeaway: Building an MCP server in Python takes just 15 lines of code. The MCP Python SDK handles protocol, validation, and serializationβ€” you just write Python functions with type hints and docstrings.
Python MCP server tutorial showing code preview and MCP architecture
Build an MCP server: Python functions become tools and resources for AI agents.

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

MCP components showing Model, Client, and multiple Servers with Tools and Resources
MCP architecture: Model β†’ Client β†’ Servers β†’ Tools + Resources.
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]"
πŸ’‘ Note: The 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:

  1. See all available tools and resources
  2. Call tools with test parameters
  3. 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 dev before connecting to agents
  • Add your server to MCP client config to use with AI agents

Further Reading

Related BestWordz Tools

πŸ’¬ 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.

Open Tool β†’