Back to topics
Chapter 01 — Foundations

What is MCP?

Model Context Protocol is a universal connector that lets AI assistants talk to tools, databases, and services.

Simple Analogy

Think of MCP as a USB-C port for AI.

The Analogy Without USB-C: Every device needs a different cable.
With USB-C: One cable works for everything.
MCP is the same: Claude doesn't need custom code for Qdrant, PostgreSQL, APIs, etc. One MCP system connects to anything.

Real-World Examples

Database Access

MCP → PostgreSQL → Get customer data

Vector Search

MCP → Qdrant → Semantic search documents

Image Analysis

MCP → Computer Vision → Analyze images

API Calls

MCP → REST API → Fetch data

What You Get with MCP

  • Universal interface — Same format for all tools
  • No vendor lock-in — Works with Claude, ChatGPT, Gemini
  • Framework agnostic — LangGraph, AutoGen, CrewAI all supported
  • Easy to maintain — One MCP = multiple applications
  • Scalable — Add tools without rewriting code
Key Insight MCP is about standardization. Instead of writing custom code for each tool, you write one MCP that works everywhere.
Chapter 02 — Foundations

The Problem MCP Solves

Why custom integration code doesn't scale — and how MCP fixes it.

Before MCP: The Mess

Imagine building an AI app that needs to talk to 5 different systems:

without mcp
Your App
    ├─ Custom code for PostgreSQL (50 lines)
    ├─ Custom code for Qdrant (50 lines)
    ├─ Custom code for Pinecone (50 lines)
    ├─ Custom code for REST API (50 lines)
    └─ Custom code for Redis (50 lines)

Total: 250 lines of custom integration code

Add a 6th tool? Write 50 more lines.
Change tools? Rewrite everything.
Use with LangGraph? Add 50 more lines per framework.

Result: 350+ lines of custom code just to connect tools.

After MCP: The Solution

with mcp
Your App
    ├─ PostgreSQL MCP (100 lines, write once)
    ├─ Qdrant MCP (100 lines, write once)
    ├─ Pinecone MCP (100 lines, write once)
    ├─ REST API MCP (100 lines, write once)
    └─ Redis MCP (100 lines, write once)

Add a 6th tool? Write 100 lines for new MCP.
Change tools? Just swap MCPs, no app code changes.
Use with LangGraph? Works automatically.
Use with AutoGen? Works automatically.

Result: Write once per tool, works everywhere.

The Key Difference

AspectWithout MCPWith MCP
Code per toolCustom for each frameworkWrite once, use everywhere
Framework changesRewrite all integrationsNo changes needed
Adding toolsCustom code per frameworkJust add MCP
TestingTest per frameworkTest once
MaintenanceHigh (multiple copies)Low (single source)
One-Sentence Summary Without MCP: custom code for every tool × every framework = exponential complexity.
With MCP: one MCP per tool, works with every framework.
Chapter 03 — Foundations

MCP Architecture

The three parts of MCP and how they communicate.

How MCP Works (3 Parts)

1MCP Client
The part that asks for help. Usually Claude Desktop, Claude API, or an AI agent (LangGraph, AutoGen).
2MCP Server
The part that does the work. You build this. It connects to your database, API, or service.
3Connection
The protocol that connects client to server. Usually JSON-RPC over stdio or HTTP.

The Flow

request / response
User: "Find customers named John"
    ↓
Claude (Client): "I need to call search_database tool"
    ↓
MCP Client → MCP Server (JSON-RPC request)
    {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "search_database",
            "arguments": {"name": "John"}
        }
    }
    ↓
MCP Server: Connects to PostgreSQL
    ↓
PostgreSQL: Returns [John Doe, john@example.com, ...]
    ↓
MCP Server → MCP Client (JSON response)
    {
        "result": {
            "content": [{
                "type": "text",
                "text": "Found: John Doe, john@example.com"
            }]
        }
    }
    ↓
Claude: "I got the results. Here's the answer..."

Server vs Client

Responsibilities You Build (MCP Server): Python/JS app → listens for requests → calls your database/API → returns results
Claude Provides (MCP Client): Claude Desktop / Claude API / LangGraph integrations → automatically sends requests

Communication Protocol

  • Transport: Stdio (process pipes) or HTTP/WebSocket
  • Format: JSON-RPC 2.0 (standardized)
  • Tools: Functions your server exposes
  • Resources: Data your server provides
Why JSON-RPC? It's language-independent, standardized, and every tool understands it. Write your MCP in Python, use it with JavaScript — doesn't matter.
Chapter 04 — Foundations

Key Concepts

Tools, resources, discovery, tool-calling flow, input validation, and error handling.

1. Tools

Functions that your MCP server exposes. Claude can call them.

tool definition
Tool: query_database
Description: "Search the database for customers"
Parameters:
  - name: "John" (required)
  - limit: 10 (optional)

Result: List of customers matching "John"

2. Resources

Static data your server provides (files, documents, knowledge bases). URI-based identifiers — Claude can read them without calling functions.

3. Server Discovery

When Claude connects to your MCP server, it asks: "What tools and resources do you have?"

discovery
Claude: "What can you do?"

MCP Server responds:
{
    "tools": [
        {"name": "query_database", "description": "..."},
        {"name": "insert_record", "description": "..."},
        {"name": "delete_record", "description": "..."}
    ],
    "resources": [
        {"uri": "file:///data/customers.csv", "description": "..."}
    ]
}

Claude: "Great! I can call these 3 tools and access this file."

4. Tool Calling Flow

1Discovery
Claude asks MCP server: "What tools do you have?"
2Request
Claude decides to call a tool with parameters
3Execution
MCP server executes the tool (calls database, API, etc.)
4Response
MCP server returns results to Claude
5Reasoning
Claude uses results to answer the user's question

5. Input Validation

schema
{
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Customer name to search for"
        },
        "limit": {
            "type": "integer",
            "description": "Max results (default: 10)"
        }
    },
    "required": ["name"]
}

Valid call:   search_database(name="John", limit=5) ✓
Invalid call: search_database()                     ✗ (missing required 'name')

6. Error Handling

responses
Success:
{ "content": [{"type":"text","text":"Found 3 customers"}], "isError": false }

Error:
{ "content": [{"type":"text","text":"Database connection failed"}], "isError": true }
Key Point Every tool response has 2 fields: content (what happened) and isError (success or failure). Claude handles both cases.
Chapter 05 — Building

Setup & Installation

Everything you need to start building MCPs.

Prerequisites

  • Python 3.8+ (for Python MCPs)
  • pip or npm (package manager)
  • Claude Desktop (optional, for testing) or Claude API

Step 1: Install MCP SDK

python
pip install mcp
javascript
npm install @modelcontextprotocol/sdk

Step 2: Project Structure

tree
my-mcp-project/
├── my_mcp_server.py        (your MCP server)
├── requirements.txt         (dependencies)
├── config.json             (Claude Desktop config)
└── README.md               (documentation)

Step 3: Configure Claude Desktop (Optional)

json
{
  "mcpServers": {
    "my-database": {
      "command": "python",
      "args": ["/path/to/my_mcp_server.py"]
    }
  }
}
Config File Location Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Linux: ~/.config/Claude/claude_desktop_config.json
Chapter 06 — Building

Boilerplate Code

Ready-to-use templates for building MCPs — basic, database, and API.

Python MCP Boilerplate

python — full template
import asyncio
import json
from mcp.server import Server
from mcp.types import Tool, TextContent, ToolResult

server = Server("my-server")

@server.tool()
async def my_first_tool(param1: str, param2: int = 10) -> ToolResult:
    """This is what your tool does."""
    try:
        result = f"Processed: {param1} with {param2}"
        return ToolResult(
            content=[TextContent(text=result)],
            is_error=False
        )
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Error: {str(e)}")],
            is_error=True
        )

server.register_tool(
    Tool(
        name="my_first_tool",
        description="Describe what this tool does",
        inputSchema={
            "type": "object",
            "properties": {
                "param1": {"type": "string", "description": "What is param1?"},
                "param2": {"type": "integer", "description": "Optional (default: 10)"}
            },
            "required": ["param1"]
        }
    )
)

if __name__ == "__main__":
    async def main():
        async with server:
            print("MCP Server is running...")
            await asyncio.sleep(float('inf'))
    asyncio.run(main())

With Database Connection (PostgreSQL)

python — database mcp
import asyncio
import psycopg2
from psycopg2.extras import RealDictCursor
from mcp.server import Server
from mcp.types import Tool, TextContent, ToolResult

server = Server("database-mcp")

def get_db_connection():
    return psycopg2.connect(
        host="localhost", database="mydb",
        user="postgres", password="password"
    )

@server.tool()
async def query_database(sql: str, limit: int = 10) -> ToolResult:
    """Execute a SELECT query on the database"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(cursor_factory=RealDictCursor)
        cursor.execute(f"{sql} LIMIT {limit}")
        rows = cursor.fetchall()
        cursor.close(); conn.close()

        if not rows:
            return ToolResult(content=[TextContent(text="No results found")])

        result_text = "\n".join([str(dict(row)) for row in rows])
        return ToolResult(content=[TextContent(text=result_text)])
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Query failed: {str(e)}")],
            is_error=True
        )

With REST API Integration

python — api mcp
import asyncio, httpx
from mcp.server import Server
from mcp.types import Tool, TextContent, ToolResult

server = Server("api-mcp")

@server.tool()
async def call_external_api(endpoint: str, method: str = "GET",
                            data: dict = None) -> ToolResult:
    """Call an external REST API"""
    try:
        async with httpx.AsyncClient() as client:
            url = f"https://api.example.com/{endpoint}"
            if method == "GET":
                response = await client.get(url)
            elif method == "POST":
                response = await client.post(url, json=data)
            else:
                return ToolResult(
                    content=[TextContent(text=f"Unsupported method: {method}")],
                    is_error=True
                )
            return ToolResult(content=[TextContent(text=str(response.json()))])
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"API call failed: {str(e)}")],
            is_error=True
        )
Pro Tip Start with the basic boilerplate, then add your specific tools. Follow the same pattern for each tool you add.
Chapter 07 — Building

First MCP: Step-by-Step

Build a complete temperature sensor MCP from scratch.

python — temperature_mcp.py
import asyncio, random
from mcp.server import Server
from mcp.types import Tool, TextContent, ToolResult

server = Server("temperature-sensor")

def read_temperature():
    return round(random.uniform(15, 35), 1)

def read_humidity():
    return round(random.uniform(30, 80), 1)

@server.tool()
async def get_temperature() -> ToolResult:
    """Read current temperature from sensor"""
    try:
        temp = read_temperature()
        return ToolResult(content=[
            TextContent(text=f"Current temperature: {temp}°C")
        ])
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Error: {str(e)}")],
            is_error=True
        )

@server.tool()
async def check_temperature_alert(threshold: float = 30.0) -> ToolResult:
    """Check if temperature exceeds a threshold"""
    try:
        temp = read_temperature()
        humidity = read_humidity()
        if temp > threshold:
            msg = f"ALERT! {temp}°C exceeds {threshold}°C"
        else:
            msg = f"OK. {temp}°C is below {threshold}°C"
        msg += f"\nHumidity: {humidity}%"
        return ToolResult(content=[TextContent(text=msg)])
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Error: {str(e)}")],
            is_error=True
        )

server.register_tool(Tool(
    name="get_temperature",
    description="Read current temperature from sensor",
    inputSchema={"type": "object", "properties": {}, "required": []}
))
server.register_tool(Tool(
    name="check_temperature_alert",
    description="Check if temperature exceeds threshold",
    inputSchema={
        "type": "object",
        "properties": {
            "threshold": {"type": "number", "description": "Celsius threshold"}
        },
        "required": []
    }
))

if __name__ == "__main__":
    async def main():
        async with server:
            print("Temperature Sensor MCP running...")
            await asyncio.sleep(float('inf'))
    asyncio.run(main())
Test It Run python temperature_mcp.py — then configure Claude Desktop to point to this file and ask: "What's the current temperature?"
Chapter 08 — Building

Creating Tools

Every tool has three parts: function, description, and input schema.

Tool Template

python — pattern
@server.tool()
async def my_tool(param1: str, param2: int = 10) -> ToolResult:
    """One-line description of what this tool does."""
    try:
        result = do_something(param1, param2)
        return ToolResult(
            content=[TextContent(text=str(result))],
            is_error=False
        )
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Error: {str(e)}")],
            is_error=True
        )

Input Schema Reference

TypeJSON SchemaExample
String"type": "string"name, email, description
Number"type": "number"age, price, temperature
Integer"type": "integer"count, limit, id
Boolean"type": "boolean"active, verified
Array"type": "array"tags, emails, files

Common Patterns

Database Query

Connect to DB, execute query, return rows. Always close connections.

API Call

Use httpx.AsyncClient, call endpoint, return JSON response.

File Operation

Read/write files, validate paths, return content.

Data Processing

Transform input data, apply logic, return result.

Tool Design Rule One tool = one function. Don't make mega-tools that do multiple things. Keep them focused and testable.
Chapter 09 — Integration

Using MCPs with Claude

Configure Claude Desktop and the Claude API to use your MCP tools.

Claude Desktop (Local)

1Configure
Edit ~/.claude_desktop_config.json to add your MCP server.
2Restart
Close and reopen Claude Desktop for changes to take effect.
3Use
Just ask normally. Claude will automatically discover and use the tools.
json — config
{
  "mcpServers": {
    "my-database": {
      "command": "python",
      "args": ["/full/path/to/my_mcp_server.py"]
    },
    "my-api": {
      "command": "python",
      "args": ["/full/path/to/api_mcp.py"]
    }
  }
}

Claude API (Programmatic)

python
from anthropic import Anthropic

client = Anthropic()
conversation_history = []

def chat_with_mcp(user_message):
    conversation_history.append({"role": "user", "content": user_message})
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        messages=conversation_history
    )
    assistant_message = response.content[0].text
    conversation_history.append({"role": "assistant", "content": assistant_message})
    return assistant_message

result = chat_with_mcp("Find customers in the database")
Verify Connection In Claude Desktop, type: "What tools are available?" — Claude should list your MCP tools. If missing, check your config file and restart.
Chapter 10 — Integration

Using MCPs with AI Agents

Integrate MCP tools with LangGraph and AutoGen agent frameworks.

LangGraph Integration

python — langgraph + mcp
from langgraph.graph import StateGraph, START, END
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
import subprocess, json

@tool
def database_tool(query: str) -> str:
    """Search the database using MCP"""
    request = {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "query_database",
            "arguments": {"query": query}
        }
    }
    result = subprocess.run(
        ["python", "my_mcp_server.py"],
        input=json.dumps(request),
        capture_output=True, text=True
    )
    return result.stdout

llm = ChatAnthropic(model="claude-sonnet-4-20250514")
tools = [database_tool]
llm_with_tools = llm.bind_tools(tools)

def agent_step(state):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": state["messages"] + [response]}

graph = StateGraph(AgentState)
graph.add_node("agent", agent_step)
graph.add_edge(START, "agent")
graph.add_edge("agent", END)
agent = graph.compile()

result = agent.invoke({
    "messages": [HumanMessage(content="Find customers named John")]
})

AutoGen Integration

python — autogen + mcp
from autogen import AssistantAgent, UserProxyAgent
import subprocess, json

def query_database(query: str) -> str:
    """Call MCP database tool"""
    request = {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {"name": "query_database", "arguments": {"query": query}}
    }
    result = subprocess.run(
        ["python", "my_mcp_server.py"],
        input=json.dumps(request),
        capture_output=True, text=True
    )
    return result.stdout

assistant = AssistantAgent(
    name="Database Assistant",
    system_message="You are a helpful assistant with database access.",
    llm_config={"config_list": [{"model": "gpt-4", "api_key": "YOUR_KEY"}]}
)
user_proxy = UserProxyAgent(name="User", human_input_mode="ALWAYS")
assistant.register_function(function_map={"query_database": query_database})
user_proxy.initiate_chat(assistant, message="Find all customers named John")
Key Pattern Wrap MCP calls in @tool functions, then bind to your agent. The agent automatically calls tools when needed.
Chapter 11 — Integration

Using Multiple MCPs

Configure and organize multiple MCP servers working together.

json — multi-mcp config
{
  "mcpServers": {
    "database": {"command": "python", "args": ["/path/to/database_mcp.py"]},
    "vectordb": {"command": "python", "args": ["/path/to/vector_mcp.py"]},
    "api":      {"command": "python", "args": ["/path/to/api_mcp.py"]},
    "files":    {"command": "python", "args": ["/path/to/files_mcp.py"]}
  }
}
Multi-MCP Workflow Example User: "Find documents about cloud computing and list interested customers"
Claude uses: (1) Vector DB MCP → search docs, (2) Database MCP → find customers, (3) API MCP → get real-time data, (4) Files MCP → read policies

Tool Naming Convention

naming pattern
db_query()       db_insert()       db_update()
vector_search()  vector_embed()
api_fetch()      api_post()
file_read()      file_write()

Prefixes help Claude know which MCP to call.
Avoid naming conflicts Make sure tool names are unique across all MCPs. If two MCPs both have a "search" tool, Claude won't know which one to use.
Chapter 12 — Production

Components Checklist

Ensure all these components are in place before deploying.

Development

Integration

Testing & Security

Before Production Check off at least 12 of these 15 items. The remaining 3 are for advanced use cases.
Chapter 13 — Production

Deployment

Local, Docker, systemd, and cloud deployment options.

Option 1: Direct Python (Local)

json
{ "mcpServers": { "my-mcp": { "command": "python", "args": ["/path/to/server.py"] } } }

Option 2: Docker (Production)

dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY my_mcp_server.py .
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["python", "my_mcp_server.py"]
shell
docker build -t my-mcp:latest .
docker run -p 8000:8000 my-mcp:latest

Option 3: Systemd Service (Linux)

service file
[Unit]
Description=My MCP Server
After=network.target

[Service]
Type=simple
User=mcpuser
WorkingDirectory=/opt/my-mcp
ExecStart=/usr/bin/python3 /opt/my-mcp/my_mcp_server.py
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Health Checks & Monitoring

python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@server.tool()
async def health_check() -> ToolResult:
    """Check if MCP server is healthy"""
    try:
        conn = get_db_connection()
        conn.close()
        return ToolResult(content=[TextContent(text="Server healthy")])
    except Exception as e:
        return ToolResult(
            content=[TextContent(text=f"Server error: {str(e)}")],
            is_error=True
        )
Production Checklist Logging configured, health checks working, environment variables set, error handling tested, rate limiting enabled.
Chapter 14 — Production

Best Practices

Tool design, error handling, security, performance, and testing guidelines.

Tool Design

Do

One tool = one function. Clear names. Validate inputs. Return meaningful results.

Don't

Mega-tools. Cryptic abbreviations. Skip validation. Return raw error messages.

Error Handling Pattern

python
@server.tool()
async def my_tool(param: str) -> ToolResult:
    try:
        if not param:
            return ToolResult(
                content=[TextContent(text="Parameter cannot be empty")],
                is_error=True)
        result = do_work(param)
        return ToolResult(content=[TextContent(text=result)])
    except ValueError as e:
        logger.error(f"Invalid value: {str(e)}")
        return ToolResult(
            content=[TextContent(text=f"Invalid input: {str(e)}")],
            is_error=True)
    except Exception as e:
        logger.error(f"Unexpected error: {str(e)}", exc_info=True)
        return ToolResult(
            content=[TextContent(text="An error occurred")],
            is_error=True)

Security

  • Validate inputs — Check types, ranges, formats
  • Use SQL parameterization — Prevent SQL injection
  • Don't log secrets — Never log API keys or passwords
  • Rate limit — Prevent abuse of tools
  • Use HTTPS — For remote MCPs

Performance

  • Cache results — Avoid repeated database queries
  • Add timeouts — Tools shouldn't hang
  • Paginate results — Don't return millions of rows
  • Monitor latency — Track tool execution time

Testing

python — pytest
import pytest

@pytest.mark.asyncio
async def test_query_database_success():
    result = await query_database("SELECT * FROM customers")
    assert not result.is_error
    assert "customer" in result.content[0].text

@pytest.mark.asyncio
async def test_query_database_invalid_sql():
    result = await query_database("DROP TABLE customers")
    assert result.is_error
Golden Rules (1) Keep tools focused (2) Always handle errors (3) Document what you're doing (4) Test thoroughly (5) Monitor in production (6) Version your MCP (7) Never expose secrets
Chapter 15 — Production

Troubleshooting

Common issues and how to fix them.

MCP not showing in Claude Desktop

Fix (1) Check config file syntax (valid JSON?) (2) Verify file path is absolute (3) Restart Claude Desktop completely (4) Check server runs manually (5) Look at Claude logs for errors

Tool call fails silently

Fix (1) Add logging to see what's happening (2) Test tool directly: python my_mcp_server.py (3) Check database/API connection (4) Verify parameters are passed correctly

"Tool not found" error

Fix (1) Check tool name matches exactly (case-sensitive) (2) Verify tool is registered: server.register_tool() (3) Make sure @server.tool() decorator is present (4) Check input schema is valid JSON

Database connection fails

Fix (1) Is database running? (2) Verify credentials (3) Check firewall (4) Try connecting directly with psql (5) Check environment variables

Debug Checklist

  • Is the MCP server running? (check terminal)
  • Is Claude Desktop restarted? (fully exit and reopen)
  • Is the config file valid JSON?
  • Is the file path absolute? (not ~/path)
  • Are tool names registered correctly?
  • Do input schemas match what Claude sends?
  • Are try/except blocks catching errors?
  • Is logging enabled?

Enable Verbose Logging

python
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('mcp.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)
Pro Debugging Tip Run your MCP in a terminal and watch the logs in real-time as you use tools in Claude.