MCP Server Building Guide -- Python Edition

What Is MCP?

MCP (Model Context Protocol) is an open-source standard protocol announced by Anthropic in November 2024. It defines a specification for connecting AI applications to external systems (databases, APIs, file systems, etc.).

Think of it as a USB-C port for AI. Just as USB-C unified charging, data, and video output into a single standard, MCP standardizes how AI calls external tools. It was donated to the Linux Foundation in December 2025, and both OpenAI and Google DeepMind have adopted it.

Architecture

MCP consists of 3 roles.

ComponentRoleExample
HostAI application (manages multiple Clients)Claude Desktop, VS Code, Claude Code
ClientMaintains a 1:1 connection with each ServerAuto-created within the Host
ServerProvides Tools/Resources/PromptsThe part developers implement

The 3 primitives a Server provides:

PrimitiveDescriptionAnalogy
ToolExecutable function that AI can callREST POST endpoint
ResourceProvides context data to AIREST GET endpoint
PromptTemplate that structures LLM interactionsPre-written task instructions

Installation

# Using uv (recommended)
uv add "mcp[cli]"

# Using pip
pip install "mcp[cli]"

# Verify installation
python -c "import mcp; print(mcp.__version__)"
# 1.27.0

Implementing Tools

A Tool is a function that AI calls directly. Define it with a single decorator.

from mcp.server.fastmcp import FastMCP

# Create MCP server instance
mcp = FastMCP("MyTools")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Adds two numbers"""
    return a + b


@mcp.tool()
def search_files(directory: str, extension: str = ".py") -> list[str]:
    """Searches for files with a specific extension in a directory

    Args:
        directory: Directory path to search
        extension: File extension (default: .py)
    """
    from pathlib import Path
    return [str(f) for f in Path(directory).rglob(f"*{extension}")]


@mcp.tool()
async def fetch_url(url: str) -> str:
    """Fetches the content of a URL"""
    import httpx
    async with httpx.AsyncClient() as client:
        resp = await client.get(url, timeout=30.0)
        return resp.text[:5000]  # Max 5000 characters


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP schemas are automatically generated from Python type hints and docstrings. Write detailed parameter descriptions in the Args: section, as they are passed to the AI.

Implementing Resources

Resources provide context data to AI. Unlike Tools, they are read-only with no side effects.

from mcp.server.fastmcp import FastMCP
import json

mcp = FastMCP("MyResources")


# Static resource -- fixed URI
@mcp.resource("config://app-version")
def get_version() -> str:
    """Returns app version information"""
    return "v2.1.0"


# Dynamic resource -- uses URI parameters
@mcp.resource("db://users/{user_id}")
def get_user(user_id: str) -> str:
    """Returns user information as JSON"""
    users = {
        "1": {"name": "Alice", "email": "alice@example.com"},
        "2": {"name": "Bob", "email": "bob@example.com"},
    }
    user = users.get(user_id, {"error": "User not found"})
    return json.dumps(user, ensure_ascii=False)


# File-based resource
@mcp.resource("file://logs/latest")
def get_latest_log() -> str:
    """Returns the latest log file contents"""
    from pathlib import Path
    log_file = Path("logs/app.log")
    if not log_file.exists():
        return "Log file not found"
    return log_file.read_text(encoding="utf-8")[-2000:]  # Last 2000 characters


if __name__ == "__main__":
    mcp.run(transport="stdio")

Implementing Prompts

Prompts are pre-defined templates for AI interactions. They enable reuse of recurring task instructions.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("MyPrompts")


@mcp.prompt()
def code_review(language: str, code: str) -> str:
    """Generates a code review prompt"""
    return f"""Please review the following {language} code.

## Review Criteria
1. Potential bugs
2. Performance improvement opportunities
3. Readability and naming

## Code
```{language}
{code}
```"""


@mcp.prompt()
def sql_query(table_name: str, columns: str = "*") -> str:
    """SQL query generation prompt"""
    return f"""Write an optimized SQL query to retrieve the {columns} columns 
from the '{table_name}' table. Consider index usage and performance."""


if __name__ == "__main__":
    mcp.run(transport="stdio")

Practical Example: Note Management Server

A hands-on example combining Tool + Resource.

from mcp.server.fastmcp import FastMCP
import json
from datetime import datetime

mcp = FastMCP("NoteServer")

# In-memory storage
notes: dict[str, dict] = {}


@mcp.tool()
def create_note(title: str, content: str) -> str:
    """Creates a new note

    Args:
        title: Note title
        content: Note content
    """
    note_id = str(len(notes) + 1)
    notes[note_id] = {
        "title": title,
        "content": content,
        "created_at": datetime.now().isoformat(),
    }
    return f"Note #{note_id} '{title}' created"


@mcp.tool()
def delete_note(note_id: str) -> str:
    """Deletes a note"""
    if note_id not in notes:
        return f"Note #{note_id} not found"
    title = notes.pop(note_id)["title"]
    return f"Note #{note_id} '{title}' deleted"


@mcp.resource("notes://list")
def list_notes() -> str:
    """Returns all notes"""
    if not notes:
        return "No notes found"
    return json.dumps(notes, ensure_ascii=False, indent=2)


@mcp.resource("notes://{note_id}")
def get_note(note_id: str) -> str:
    """Retrieves a specific note"""
    note = notes.get(note_id)
    if not note:
        return f"Note #{note_id} not found"
    return json.dumps(note, ensure_ascii=False, indent=2)


if __name__ == "__main__":
    mcp.run(transport="stdio")

Connecting to Claude Desktop

Configuration for registering an MCP server with Claude Desktop.

Config file location:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "note-server": {
      "command": "uv",
      "args": [
        "--directory", "/path/to/note-server",
        "run", "server.py"
      ]
    }
  }
}

Connecting from Claude Code:

# Register STDIO server
claude mcp add note-server -- uv --directory /path/to/note-server run server.py

# Verify registration
claude mcp list

# Check status in session
/mcp

Important Notes

ItemDescription
No print()In STDIO transport, print() breaks JSON-RPC messages. Use the logging module
Error handlingWhen a Tool throws an exception, the error message is sent to AI. Write clear error messages
Type hints requiredMCP schema generation fails without type hints
Write docstringsFunction descriptions are passed to AI. More detail helps AI choose the right Tool
Async supportDefine with async def for async execution. Recommended for I/O operations

Summary

PrimitiveDecoratorUse Case
Tool@mcp.tool()Functions AI calls (create, update, delete, API calls)
Resource@mcp.resource("uri://pattern")Providing data to AI (queries, reads)
Prompt@mcp.prompt()Templatizing recurring tasks

The advantage of Python FastMCP is that a single decorator does it all. As long as you write proper type hints and docstrings, MCP schema generation, parameter validation, and error handling are all handled automatically.

Was this article helpful?