Skip to main content

Introduction

Build an MCP server using Docker containers for maximum flexibility. While this example uses Python, the Docker container principles and patterns shown here can be adapted to any programming language - Go, Rust, Java, C#, or any other language of your choice.

Show me the repo

Show me the repo

View the fully runnable GitHub repo for this example
Want to follow along? Clone the example repository:
git clone https://github.com/smithery-ai/smithery-cookbook.git
cd smithery-cookbook/servers/python/quickstart

What We’re Building

We’ll build a simple MCP server with a say_hello tool that:
  • Greets users by name
  • Shows proper CORS handling
  • Supports HTTP transport

Prerequisites

  • Python 3.11+ and uv
  • Docker installed locally
  • A Smithery API key for development features
Port Requirement: Your server must listen on the configurable PORT environment variable. Smithery will set this to 8081 when deployed, but you can use any port for local development.

Step 1: Basic Server Setup

Start with a simple FastMCP server with basic imports:
# main.py
import os
import uvicorn
from mcp.server.fastmcp import FastMCP
from starlette.middleware.cors import CORSMiddleware

# Initialize MCP server
mcp = FastMCP(name="Say Hello")

Step 2: Create a Basic Tool

Define a simple MCP tool:
# main.py
@mcp.tool()
def say_hello(name: str) -> str:
    """Greet a user by name."""
    return f"Hello, {name}!"

Step 3: Complete Server Startup

Set up the HTTP server with CORS:
# main.py
def main():
    print("Say Hello MCP Server starting...")
    
    # Setup Starlette app with CORS for cross-origin requests
    app = mcp.streamable_http_app()
    
    # IMPORTANT: add CORS middleware for browser based clients
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["GET", "POST", "OPTIONS"],
        allow_headers=["*"],
        expose_headers=["mcp-session-id", "mcp-protocol-version"],
        max_age=86400,
    )

    # Get port from environment variable (Smithery sets this to 8081)
    port = int(os.environ.get("PORT", 8080))
    print(f"Listening on port {port}")

    uvicorn.run(app, host="0.0.0.0", port=port, log_level="debug")

if __name__ == "__main__":
    main()

Step 4: Update smithery.yaml

# smithery.yaml
runtime: "container"
build:
  dockerfile: "Dockerfile"           # Path to your Dockerfile
  dockerBuildPath: "."               # Docker build context
startCommand:
  type: "http"

Step 5: Create Dockerfile

You can create your own Dockerfile or use this recommended template:
# Dockerfile
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.12-alpine

# Install the project into `/app`
WORKDIR /app

# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy

# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --locked --no-install-project --no-dev

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-dev

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Reset the entrypoint, don't invoke `uv`
ENTRYPOINT []

# Run the application directly using the venv Python
CMD ["python", "main.py"]
uv Docker Best Practices: For more examples and best practices on using uv with Docker, including multistage builds and development workflows, check out the uv Docker example repository.

Local Testing

Run directly with Python:
# Install dependencies
uv sync

# Run the server
uv run python main.py
Or with Docker:
docker build -t character-counter .
docker run -p 8080:8080 -e PORT=8080 character-counter
Local vs Deployed Ports: For local testing, you can use any port (like 8080 above). However, when deployed to Smithery, your server must listen on the PORT environment variable, which Smithery will set to 8081.
Test interactively: Once your server is running in HTTP mode, you can test it interactively using the Smithery playground:
# Using npx (no installation needed)
npx -y @smithery/cli playground --port 8080
Or install globally:
# Install globally
npm install -g @smithery/cli

# Then run playground
smithery playground --port 8080
Smithery Playground Interface

Deployment

  1. Push Code: Push your updated code (including Dockerfile and smithery.yaml) to GitHub
  2. Deploy: Go to smithery.ai/new and connect your GitHub repository
  3. Verify: Test your deployed server through the Smithery interface
Smithery will automatically build your container and host your MCP server.

Summary

This guide showed how to build a Python MCP server using custom Docker containers and FastMCP. We implemented a simple say_hello tool with proper CORS configuration and HTTP transport. This approach gives you full control over your Python environment and can be extended to work with any other programming language by adapting the Dockerfile and server implementation. Need help? Join our Discord or email support@smithery.ai for assistance.
I