MCP Tutorial: Build an AI Tool Server in Python

Disclosure: As an Amazon Associate, I earn from qualifying purchases. Some links in this post are affiliate links — they cost you nothing extra.

Every AI model today faces the same problem: it’s trapped inside a box. It can think, but it can’t do anything in the real world — no file access, no database queries, no API calls — unless you wire up each integration by hand.

Model Context Protocol (MCP) fixes this. It’s an open standard that gives AI models a universal way to connect to tools and data. Think of it as USB-C for AI: one protocol, any tool, any model.

In this tutorial, you’ll build a working MCP server in Python from scratch.

What is MCP?

MCP is an open protocol created by Anthropic in November 2024, now governed by the Linux Foundation’s Agentic AI Foundation with backing from OpenAI, Google, Microsoft, AWS, and Cloudflare.

The numbers speak for themselves:

  • 97 million monthly SDK downloads
  • 21,600+ GitHub stars on the Python SDK
  • Supported by Claude, ChatGPT, VS Code, Cursor, JetBrains, and more
  • $6.3M raised by Manufact (Feb 2026) just for MCP tooling

If you want to understand how to architect AI systems that actually work in production, Designing Machine Learning Systems by Chip Huyen covers the full lifecycle—including how to integrate tools and services like MCP into real-world ML pipelines.

Architecture: Hosts, Clients, Servers

┌─────────────────────────────────────────┐
│  MCP Host (Claude Desktop, VS Code)     │
│                                         │
│  ┌──────────┐  ┌──────────┐             │
│  │ Client 1 │  │ Client 2 │   ...       │
│  └────┬─────┘  └────┬─────┘             │
└───────┼──────────────┼──────────────────┘
        │              │
   ┌────▼────┐    ┌────▼────┐
   │ Server A│    │ Server B│
   │(Database)│   │(GitHub) │
   └─────────┘    └─────────┘
  • Host: The AI application (Claude Desktop, VS Code, Cursor)
  • Client: Maintains a 1:1 connection to a server
  • Server: Exposes tools, resources, and prompts to the AI

Three Primitives

Primitive Who Controls It Example
Tools The AI model decides when to call query_database(), create_issue()
Resources The application loads as context db://schema, file://config.yaml
Prompts The user selects explicitly “Code review template”, “SQL analysis”

Transport

MCP supports two transport modes:

  • stdio: Server runs as a local subprocess. Zero network overhead. Best for local tools.
  • Streamable HTTP: Server runs remotely over HTTP + SSE. Supports OAuth 2.1 auth.

Setting Up

# Install with uv (recommended)
uv init mcp-demo && cd mcp-demo
uv add "mcp[cli]"

# Or with pip
pip install "mcp[cli]"

Requires Python 3.10+.

Enjoying this article? Get more like it delivered to your inbox. Subscribe to the newsletter

Building Your First MCP Server

Create server.py:

from mcp.server.fastmcp import FastMCP

# Initialize server
mcp = FastMCP("demo-server")


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


@mcp.tool()
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


@mcp.resource("config://app")
def get_config() -> str:
    """Return application configuration."""
    return "debug=true\nlog_level=INFO\nmax_retries=3"


@mcp.prompt()
def explain_code(code: str, language: str = "python") -> str:
    """Generate a prompt to explain code."""
    return f"Explain this {language} code step by step:\n\n```{language}\n{code}\n```"


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

That’s it. The @mcp.tool() decorator auto-generates the tool schema from your type hints and docstring. No JSON schema boilerplate needed.

Test it:

# Launch the MCP Inspector for interactive testing
npx -y @modelcontextprotocol/inspector uv run server.py

For building robust production servers like your MCP implementation, Python Cookbook by David Beazley is an excellent reference for handling concurrency, networking, and real-world server patterns.

Real-World Example: SQLite Database Server

Here’s a practical MCP server that lets AI models query a SQLite database safely:

from mcp.server.fastmcp import FastMCP
import sqlite3

mcp = FastMCP("sqlite-explorer")

DB_PATH = "example.db"


@mcp.resource("db://schema")
def get_schema() -> str:
    """Return the complete database schema."""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.execute(
        "SELECT sql FROM sqlite_master WHERE type='table'"
    )
    schemas = [row[0] for row in cursor.fetchall() if row[0]]
    conn.close()
    return "\n\n".join(schemas)


@mcp.resource("db://tables")
def list_tables() -> str:
    """List all tables in the database."""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table'"
    )
    tables = [row[0] for row in cursor.fetchall()]
    conn.close()
    return "\n".join(tables)


@mcp.tool()
def query(sql: str) -> str:
    """Execute a read-only SQL query.

    Args:
        sql: A SELECT query to run against the database.
             Only SELECT statements are allowed for safety.
    """
    sql_stripped = sql.strip().upper()
    if not sql_stripped.startswith("SELECT"):
        return "Error: Only SELECT queries allowed."

    dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE"]
    if any(kw in sql_stripped for kw in dangerous):
        return "Error: Write operations not permitted."

    conn = sqlite3.connect(DB_PATH)
    try:
        cursor = conn.execute(sql)
        columns = [desc[0] for desc in cursor.description]
        rows = cursor.fetchall()
        if not rows:
            return "No results found."
        result = [dict(zip(columns, row)) for row in rows]
        return str(result[:50])  # Limit output size
    except Exception as e:
        return f"SQL Error: {e}"
    finally:
        conn.close()


@mcp.tool()
def table_stats(table_name: str) -> str:
    """Get row count and column info for a table.

    Args:
        table_name: Name of the table to analyze.
    """
    # Prevent SQL injection
    if not table_name.isalnum():
        return "Error: Invalid table name."

    conn = sqlite3.connect(DB_PATH)
    try:
        count = conn.execute(
            f"SELECT COUNT(*) FROM {table_name}"
        ).fetchone()[0]
        columns = conn.execute(
            f"PRAGMA table_info({table_name})"
        ).fetchall()
        col_info = [
            f"  {c[1]} ({c[2]})" + (" PRIMARY KEY" if c[5] else "")
            for c in columns
        ]
        return f"Table: {table_name}\nRows: {count}\nColumns:\n" + "\n".join(col_info)
    except Exception as e:
        return f"Error: {e}"
    finally:
        conn.close()


@mcp.prompt()
def analyze_table(table_name: str) -> str:
    """Create an analysis prompt for a database table."""
    return f"Analyze the '{table_name}' table:\n1. Check the schema using db://schema\n2. Run a query to see sample data\n3. Get table_stats for row counts\n4. Report any data quality issues"

Using With Claude Desktop

Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "sqlite-explorer": {
      "command": "uv",
      "args": ["--directory", "/path/to/your/project", "run", "server.py"]
    }
  }
}

Now Claude can explore your database conversationally:

“What tables are in the database?”
“Show me the top 10 users by signup date”
“Are there any NULL values in the orders table?”

Advanced: Lifecycle Management

For servers that need database connections or API clients, use the lifespan pattern:

from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from dataclasses import dataclass
import aiohttp
from mcp.server.fastmcp import FastMCP, Context


@dataclass
class AppContext:
    session: aiohttp.ClientSession


@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
    """Manage HTTP session lifecycle."""
    session = aiohttp.ClientSession()
    try:
        yield AppContext(session=session)
    finally:
        await session.close()


mcp = FastMCP("api-server", lifespan=app_lifespan)


@mcp.tool()
async def fetch_url(url: str, ctx: Context) -> str:
    """Fetch content from a URL.

    Args:
        url: The URL to fetch (must be HTTPS).
    """
    if not url.startswith("https://"):
        return "Error: Only HTTPS URLs allowed."
    session = ctx.request_context.lifespan_context.session
    async with session.get(url) as resp:
        text = await resp.text()
        return text[:5000]  # Truncate large responses

MCP vs Function Calling vs LangChain

Function Calling LangChain Tools MCP
Layer API feature App framework Wire protocol
Portability One provider One framework Any host
Discovery Static (prompt-time) Static Dynamic (tools/list)
State Stateless Framework-managed Session-based
Reusability Re-implement per provider Re-implement per chain Write once, use everywhere

Function calling is a feature. LangChain is a framework. MCP is a protocol. They complement each other — you can use MCP servers inside LangChain via langchain-mcp-adapters.

Security Best Practices

MCP servers run with real system access. Take security seriously:

1. Input Validation

@mcp.tool()
def read_file(path: str) -> str:
    """Read a file from the allowed directory only."""
    import os
    # Prevent path traversal
    safe_path = os.path.realpath(path)
    if not safe_path.startswith("/allowed/directory/"):
        return "Error: Access denied."
    with open(safe_path) as f:
        return f.read()

2. Principle of Least Privilege

  • Read-only database access for query tools
  • Restrict file access to specific directories
  • Use scoped API tokens, not admin credentials

3. Never Trust Tool Annotations

  • MCP server descriptions are self-reported
  • Only install servers from trusted sources
  • The ClawHub malicious skills incident (Feb 2026) showed compromised MCP registries are a real threat

4. Human-in-the-Loop

  • Always confirm destructive operations (file writes, API calls)
  • Log all tool invocations for auditing

Deploying a Remote MCP Server

For team access, deploy over HTTP with authentication:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("remote-server")

# ... define tools, resources, prompts ...

if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        host="0.0.0.0",
        port=8000,
    )

Connect from Claude Code:

claude mcp add --transport http my-server http://your-server:8000/mcp

For production, add OAuth 2.1 authentication and HTTPS via a reverse proxy (nginx, Caddy).

The MCP Ecosystem in 2026

The ecosystem has exploded since Anthropic open-sourced MCP:

Timeline:

  • Nov 2024: Anthropic creates MCP
  • Mar 2025: OpenAI officially adopts it
  • Jun 2025: Structured tool outputs, elicitation, security specs
  • Dec 2025: Donated to Linux Foundation’s Agentic AI Foundation
  • Feb 2026: Manufact raises $6.3M for MCP tooling; 97M monthly SDK downloads

Popular MCP Servers:

  • Database: PostgreSQL, SQLite, MongoDB, Qdrant
  • Dev Tools: GitHub, Git, filesystem, Docker
  • Cloud: AWS, Cloudflare, Vercel
  • Monitoring: Sentry, Datadog
  • Communication: Slack, Gmail
  • Search: Brave Search, Google Search

Browse thousands of community servers at mcp.so or the MCP Registry on GitHub.

Quick Reference: FastMCP Cheat Sheet

from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("name")

# Tool (AI-controlled)
@mcp.tool()
def my_tool(arg: str) -> str:
    """Docstring becomes the tool description."""
    return "result"

# Async tool with progress
@mcp.tool()
async def slow_tool(data: str, ctx: Context) -> str:
    """Long-running operation with progress."""
    for i in range(10):
        await ctx.report_progress(i, 10)
    return "done"

# Resource (app-controlled context)
@mcp.resource("scheme://path")
def static_resource() -> str:
    return "data"

@mcp.resource("scheme://path/{param}")
def dynamic_resource(param: str) -> str:
    return f"data for {param}"

# Prompt (user-controlled template)
@mcp.prompt()
def my_prompt(topic: str) -> str:
    return f"Analyze {topic} in detail."

# Run
mcp.run(transport="stdio")           # Local
mcp.run(transport="streamable-http") # Remote

FAQ

What Python version does MCP require?

MCP’s Python SDK requires Python 3.10 or higher. The recommended setup is using uv for dependency management, which handles Python version management automatically. Install the SDK with uv add "mcp[cli]" or pip install "mcp[cli]".

Can I use MCP with ChatGPT, not just Claude?

Yes. OpenAI officially adopted MCP in March 2025, and ChatGPT Desktop supports MCP servers. The whole point of MCP being an open protocol is vendor neutrality — any MCP server you build works with Claude, ChatGPT, VS Code, Cursor, JetBrains IDEs, and any other MCP-compatible host without code changes.

Is MCP replacing function calling and LangChain?

No. MCP operates at the protocol layer (like HTTP), while function calling is an API feature and LangChain is an application framework. They are complementary. You can use MCP servers inside LangChain via the langchain-mcp-adapters package. MCP standardizes the tool connectivity; function calling and LangChain handle the orchestration logic on top.

Did you find this helpful?

Your support keeps this blog running and ad-free content coming.

☕ Buy me a coffee
TODAY 329 | TOTAL 146,861