Table of Contents

How to Use MCP (Model Context Protocol)

A practical guide to the Model Context Protocol from zero to working server. MCP is the open standard that lets AI models connect to external tools, databases, and services — a universal protocol supported by Claude, ChatGPT, Cursor, VS Code Copilot, and more.

What is MCP?

MCP (Model Context Protocol) is an open standard created by Anthropic that standardizes how AI models communicate with external tools and data sources. Think of it as USB-C for AI — one protocol that works across clients and servers.

An MCP server exposes three types of capabilities:

Capability What It Does Example
Tools Functions the AI can call Query a database, call an API, run a calculation
Resources Data the AI can read Config files, logs, system info, database records
Prompts Reusable interaction templates Code review template, analysis workflow

Architecture

graph LR subgraph MCP Clients A[Claude Desktop] B[Claude Code CLI] C[Cursor IDE] D[Custom App] end subgraph Transport E[stdio] F[HTTP + SSE] G[Streamable HTTP] end subgraph MCP Servers H[Database Server] I[File System Server] J[API Server] K[Your Custom Server] end A --> E --> H B --> E --> I C --> E --> J D --> F --> K D --> G --> K

How it works:

  1. Client (Claude, Cursor, etc.) discovers available servers
  2. Transport carries JSON-RPC 2.0 messages between client and server
  3. Server exposes tools, resources, and prompts that the client can use

Transport Mechanisms

Transport How It Works Best For
stdio Client spawns server as subprocess, communicates via stdin/stdout Local integrations, Claude Desktop, Claude Code
HTTP + SSE Server runs as HTTP service, uses Server-Sent Events for streaming Remote servers, cloud deployments
Streamable HTTP Modern HTTP-based transport with bidirectional streaming New deployments, replaces SSE

Step 1: Install the MCP Python SDK

# Recommended: use uv (fast Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
 
# Create a new project
mkdir my-mcp-server && cd my-mcp-server
uv init --python 3.11
uv add "mcp[cli]"
 
# Alternative: pip
pip install "mcp[cli]"

Step 2: Create a Basic MCP Server

The FastMCP framework handles all protocol boilerplate. You just define tools, resources, and prompts with decorators.

Minimal Server

# server.py
from mcp.server.fastmcp import FastMCP
 
app = FastMCP(name="my-server")
 
@app.tool()
def add(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b
 
if __name__ == "__main__":
    app.run()
# Run it
uv run python server.py
# Or: python3 server.py

Step 3: Define Tools

Tools are functions the AI can call. The SDK reads type hints and docstrings to generate the JSON schema that clients see.

from mcp.server.fastmcp import FastMCP
import httpx
 
app = FastMCP(name="utility-server")
 
@app.tool()
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression safely.
 
    Args:
        expression: A math expression like '2 + 3 * 4'
    """
    try:
        # Safe eval for math only
        allowed = set("0123456789+-*/.() ")
        if not all(c in allowed for c in expression):
            return "Error: Only math expressions allowed"
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"Error: {e}"
 
@app.tool()
async def fetch_url(url: str) -> str:
    """Fetch the content of a URL.
 
    Args:
        url: The URL to fetch (must start with https://)
    """
    if not url.startswith("https://"):
        return "Error: Only HTTPS URLs are allowed"
    async with httpx.AsyncClient() as client:
        response = await client.get(url, follow_redirects=True)
        return response.text[:5000]  # Limit response size
 
@app.tool()
def search_notes(query: str) -> list[str]:
    """Search through saved notes by keyword.
 
    Args:
        query: Search term to look for in notes
    """
    # Example: search a local file or database
    notes = [
        "Meeting with team on Monday at 10am",
        "Project deadline is March 30th",
        "Remember to update the API docs",
    ]
    return [n for n in notes if query.lower() in n.lower()]

Step 4: Define Resources

Resources provide data that the AI can read on demand.

import json
import platform
from datetime import datetime
 
@app.resource("config://app")
def get_app_config() -> str:
    """Return the application configuration."""
    config = {
        "version": "1.0.0",
        "environment": "development",
        "features": ["search", "analytics"]
    }
    return json.dumps(config, indent=2)
 
@app.resource("system://info")
def get_system_info() -> str:
    """Return current system information."""
    return json.dumps({
        "platform": platform.system(),
        "python_version": platform.python_version(),
        "timestamp": datetime.now().isoformat()
    }, indent=2)
 
# Dynamic resources with URI templates
@app.resource("notes://{note_id}")
def get_note(note_id: str) -> str:
    """Get a specific note by ID."""
    notes_db = {
        "1": "Meeting notes from Monday standup",
        "2": "Architecture decision: use PostgreSQL",
        "3": "TODO: Implement caching layer"
    }
    return notes_db.get(note_id, f"Note {note_id} not found")

Step 5: Define Prompts

Prompts are reusable templates that clients can offer to users.

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
 
@app.prompt()
def code_review(code: str, language: str = "python") -> str:
    """Generate a thorough code review.
 
    Args:
        code: The code to review
        language: Programming language
    """
    return f"""Please review the following {language} code.
Check for:
1. Bugs and logic errors
2. Security vulnerabilities
3. Performance issues
4. Code style and readability
5. Missing error handling
 
Code:
```{language}
{code}
```
 
Provide specific, actionable feedback."""
 
@app.prompt()
def debug_error(error_message: str, stack_trace: str = "") -> str:
    """Help debug an error.
 
    Args:
        error_message: The error message
        stack_trace: Optional stack trace
    """
    context = f"Error: {error_message}"
    if stack_trace:
        context += f"\n\nStack trace:\n{stack_trace}"
    return f"""Help me debug this error. Explain what likely caused it and suggest fixes.
 
{context}"""

Step 6: Complete Working Server

Here is a full server combining tools, resources, and prompts:

# server.py — Complete MCP Server
from mcp.server.fastmcp import FastMCP
import json
import platform
import httpx
from datetime import datetime
 
app = FastMCP(
    name="demo-server",
    version="1.0.0"
)
 
# ---- TOOLS ----
 
@app.tool()
def calculate(expression: str) -> str:
    """Evaluate a math expression. Example: '2 + 3 * 4'"""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "Error: Only math expressions allowed"
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"
 
@app.tool()
async def fetch_url(url: str) -> str:
    """Fetch content from a URL. Only HTTPS allowed."""
    if not url.startswith("https://"):
        return "Error: HTTPS required"
    async with httpx.AsyncClient() as client:
        resp = await client.get(url, follow_redirects=True, timeout=10)
        return resp.text[:5000]
 
@app.tool()
def search_notes(query: str) -> list[str]:
    """Search notes by keyword."""
    notes = [
        "Team standup moved to 10am",
        "Deploy v2.0 by Friday",
        "Review PR #42 for auth changes",
    ]
    return [n for n in notes if query.lower() in n.lower()]
 
# ---- RESOURCES ----
 
@app.resource("config://app")
def get_config() -> str:
    """Application configuration."""
    return json.dumps({"version": "1.0.0", "env": "dev"}, indent=2)
 
@app.resource("system://info")
def get_system_info() -> str:
    """Current system information."""
    return json.dumps({
        "platform": platform.system(),
        "python": platform.python_version(),
        "time": datetime.now().isoformat()
    }, indent=2)
 
# ---- PROMPTS ----
 
@app.prompt()
def code_review(code: str, language: str = "python") -> str:
    """Review code for bugs, security, and style."""
    return f"Review this {language} code for bugs, security issues, and style:\n```{language}\n{code}\n```"
 
# ---- RUN ----
 
if __name__ == "__main__":
    app.run()

Step 7: Connect to Claude Desktop

Configure Claude Desktop to discover your MCP server.

claude_desktop_config.json

Location:

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

Or with pip/python directly:

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

After saving, restart Claude Desktop. Your tools will appear in the tools menu.

Step 8: Connect to Claude Code (CLI)

# Add server to Claude Code
claude mcp add demo-server -- uv run --directory /path/to/my-mcp-server python server.py
 
# Or add to project config (.claude/settings.json)
claude mcp add --scope project demo-server -- python3 /path/to/server.py
 
# List configured servers
claude mcp list
 
# Test the server
claude mcp serve demo-server

Step 9: Build an MCP Client

For custom applications, build your own MCP client to connect to any MCP server.

# client.py — MCP Client Example
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio
 
async def main():
    # Connect to an MCP server via stdio
    server_params = StdioServerParameters(
        command="python3",
        args=["server.py"]
    )
 
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # Initialize the connection
            await session.initialize()
 
            # List available tools
            tools = await session.list_tools()
            print("Available tools:")
            for tool in tools.tools:
                print(f"  - {tool.name}: {tool.description}")
 
            # Call a tool
            result = await session.call_tool(
                "calculate",
                arguments={"expression": "42 * 17 + 3"}
            )
            print(f"\nCalculation result: {result.content[0].text}")
 
            # List resources
            resources = await session.list_resources()
            print("\nAvailable resources:")
            for resource in resources.resources:
                print(f"  - {resource.uri}: {resource.name}")
 
            # Read a resource
            content = await session.read_resource("config://app")
            print(f"\nConfig: {content.contents[0].text}")
 
            # List prompts
            prompts = await session.list_prompts()
            print("\nAvailable prompts:")
            for prompt in prompts.prompts:
                print(f"  - {prompt.name}: {prompt.description}")
 
if __name__ == "__main__":
    asyncio.run(main())

Testing with MCP Inspector

The MCP Inspector is a visual tool for testing servers interactively.

# Install and run
npx @modelcontextprotocol/inspector uv run python server.py
 
# Opens a web UI where you can:
# - See all tools, resources, and prompts
# - Call tools with custom arguments
# - Read resources
# - View raw JSON-RPC messages

Remote Server (HTTP + SSE)

For remote/cloud deployment, use the SSE transport instead of stdio.

# server_remote.py
from mcp.server.fastmcp import FastMCP
 
app = FastMCP(
    name="remote-server",
    host="0.0.0.0",
    port=8080
)
 
@app.tool()
def hello(name: str) -> str:
    """Say hello."""
    return f"Hello, {name}!"
 
if __name__ == "__main__":
    app.run(transport="sse")  # or transport="streamable-http"

Connect from Claude Desktop:

{
  "mcpServers": {
    "remote-server": {
      "url": "http://localhost:8080/sse"
    }
  }
}

Client-Server Flow

sequenceDiagram participant Client as MCP Client (Claude) participant Server as MCP Server Client->>Server: initialize (capabilities, protocol version) Server-->>Client: initialize response (server capabilities) Client->>Server: tools/list Server-->>Client: [calculate, fetch_url, search_notes] Client->>Server: resources/list Server-->>Client: [config:app, system:info] Note over Client: User asks "What is 42 * 17?" Client->>Server: tools/call (calculate, {"expression": "42 * 17"}) Server-->>Client: "714" Note over Client: Claude includes result in response

Best Practices for Tool Design

See Also

mcp model-context-protocol tools protocol claude how-to