Building MCP Servers with Python

Learn how to create powerful MCP servers using the Python SDK with tools, resources, and best practices.

Python is one of the most popular languages for building MCP servers thanks to its rich ecosystem and the excellent mcp package. This guide walks you through creating production-ready MCP servers in Python.


Getting Started

Installation

pip install mcp

Or with uv (recommended for modern Python projects):

uv add mcp

Minimal Server

The simplest MCP server in Python is just a few lines:

from mcp.server import Server

server = Server("hello-server")

if __name__ == "__main__":
    server.run()

This starts a server that connects over stdio but doesn’t expose any tools or resources yet.


Adding Tools

Tools are async functions decorated with @server.tool():

from mcp.server import Server

server = Server("math-server")

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

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

if __name__ == "__main__":
    server.run()

The SDK automatically generates the JSON Schema for your tool parameters from Python type hints. The docstring becomes the tool description.

Tools with Complex Inputs

from typing import Literal
from pydantic import BaseModel

class UserQuery(BaseModel):
    name: str
    age: int
    role: Literal["admin", "user", "viewer"]

@server.tool()
async def create_user(query: UserQuery) -> dict:
    """Create a new user in the system"""
    # Your business logic here
    return {"id": 123, **query.model_dump()}

Adding Resources

Resources expose data as accessible URIs:

from mcp.server import Server

server = Server("docs-server")

@server.resource("docs://{path}")
async def get_doc(path: str) -> str:
    """Serve documentation files"""
    docs = {
        "readme": "# Welcome\n\nThis is the documentation server.",
        "api": "# API Reference\n\n## Tools\n- get_doc: Read documentation",
    }
    return docs.get(path, "Document not found")

@server.list_resources()
async def list_docs() -> list[dict]:
    """List all available documentation"""
    return [
        {"uri": "docs://readme", "name": "README", "mimeType": "text/markdown"},
        {"uri": "docs://api", "name": "API Reference", "mimeType": "text/markdown"},
    ]

Adding Prompts

MCP supports prompt templates that AI clients can use:

@server.prompt()
async def code_review(language: str) -> list[dict]:
    """Generate a code review prompt template"""
    return [
        {
            "role": "user",
            "content": {
                "type": "text",
                "text": f"Please review this {language} code following best practices..."
            }
        }
    ]

Working with Environment Variables

import os
from mcp.server import Server

server = Server("api-server")

API_KEY = os.environ.get("API_KEY")
if not API_KEY:
    raise RuntimeError("API_KEY environment variable is required")

DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///local.db")

@server.tool()
async def fetch_data(endpoint: str) -> dict:
    """Fetch data from an external API"""
    import httpx
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"https://api.example.com/{endpoint}",
            headers={"Authorization": f"Bearer {API_KEY}"}
        )
        resp.raise_for_status()
        return resp.json()

Error Handling

Always return meaningful error messages:

@server.tool()
async def divide(a: float, b: float) -> float:
    """Divide a by b"""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

For more complex error handling, you can return structured error responses:

from mcp.types import TextContent, ToolError

@server.tool()
async def safe_operation() -> dict:
    try:
        # Risky operation
        result = risky_call()
        return result
    except PermissionError:
        raise ToolError("You don't have permission to perform this operation")
    except TimeoutError:
        raise ToolError("The operation timed out. Try again later.")

Testing Your Server

Using MCP Inspector

npx @modelcontextprotocol/inspector python server.py

Unit Testing

import pytest
from your_server import server

@pytest.mark.asyncio
async def test_add_tool():
    result = await server.call_tool("add", {"a": 2, "b": 3})
    assert result[0].text == "5"

@pytest.mark.asyncio
async def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        await server.call_tool("divide", {"a": 1, "b": 0})

Complete Production Example

Here’s a fully-featured MCP server:

import os
import httpx
from mcp.server import Server
from mcp.types import ToolError

server = Server("production-server")
API_BASE = os.environ.get("API_BASE", "https://api.example.com/v1")

@server.tool()
async def search(query: str, limit: int = 10) -> list[dict]:
    """Search the knowledge base"""
    if not query.strip():
        raise ToolError("Search query cannot be empty")
    if limit < 1 or limit > 100:
        raise ToolError("Limit must be between 1 and 100")
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"{API_BASE}/search",
            params={"q": query, "limit": limit}
        )
        resp.raise_for_status()
        return resp.json()

@server.resource("status://health")
async def health_check() -> str:
    """Health check endpoint"""
    return "OK"

if __name__ == "__main__":
    server.run()

Deployment

Standalone

python server.py

Docker

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "server.py"]

Claude Desktop Config

{
  "mcpServers": {
    "python-server": {
      "command": "python",
      "args": ["path/to/server.py"],
      "env": {
        "API_KEY": "sk-...",
        "API_BASE": "https://api.example.com/v1"
      }
    }
  }
}

Next Steps