Skip to main content

When to Use This Approach

Choose Python custom containers when you:
  • Prefer to use Python for your MCP server
  • Need full control over your Docker environment
  • Want to use FastMCP with custom middleware
  • Have specific Python dependencies or requirements

Show me the repo

View the fully runnable GitHub repo for this example

What We’re Building

We’ll build a simple MCP server with a count_characters tool that:
  • Counts occurrences of a specific character in text
  • Demonstrates per-request configuration access (server token, user preferences)
  • Extracts and validates Smithery session configuration - see session config docs for more details
  • Shows proper CORS handling and middleware setup
  • Supports both HTTP and STDIO transport

Code Migration

Already have HTTP support? If your MCP server already supports HTTP transport, you can skip ahead to Step 5: Update smithery.yaml to configure your deployment settings.

Let’s say you start with this…

Here’s a typical STDIO-based MCP server that you might be starting with:
# src/main.py
import os
from mcp.server.fastmcp import FastMCP
from typing import Optional

# Create STDIO server
mcp = FastMCP("Character Counter")

# Get configuration from environment variables
server_token = os.getenv("SERVER_TOKEN")
case_sensitive = os.getenv("CASE_SENSITIVE", "false").lower() == "true"

def validate_server_access(server_token: Optional[str]) -> bool:
    """Validate server token - accepts any string including empty ones for demo."""
    # In a real app, you'd validate against your server's auth system
    # For demo purposes, we accept any non-empty token
    return server_token is not None and len(server_token.strip()) > 0 if server_token else True

@mcp.tool()
def count_characters(text: str, character: str) -> str:
    """Count occurrences of a specific character in text"""
    # Validate server access (your custom validation logic)
    if not validate_server_access(server_token):
        raise ValueError("Server access validation failed. Please provide a valid serverToken.")
    
    # Apply user preferences from config
    search_text = text if case_sensitive else text.lower()
    search_char = character if case_sensitive else character.lower()
    
    # Count occurrences
    count = search_text.count(search_char)
    
    return f'The character "{character}" appears {count} times in the text.'

# Run with STDIO transport
if __name__ == "__main__":
    mcp.run()

Step 1: Update Imports and Setup

Now let’s migrate this to work as an HTTP container. Set up your FastMCP server with the necessary imports:
# src/main.py
import os
import uvicorn
from mcp.server.fastmcp import FastMCP
from starlette.middleware.cors import CORSMiddleware
from typing import Optional
from src.middleware import SmitheryConfigMiddleware

# Initialize MCP server
mcp = FastMCP(name="Character Counter")

Step 2: Configuration Handling (Optional)

Skip this entire step if no configuration needed: If your MCP server doesn’t need any configuration (API keys, settings, etc.), you can skip this entire step including the middleware setup. Your server will work perfectly fine without any configuration handling.
If your server needs configuration (like API keys, user preferences, etc.), prefer using the Smithery Python SDK helpers rather than hand-rolled parsing.

Step 2a: Add Configuration Functions (SDK)

Use these helpers and accessors with the SDK’s parsing utilities:
# src/main.py (continued - only add if you need configuration)

def handle_config(config: dict):
    """Handle configuration from Smithery - for backwards compatibility with stdio mode."""
    global _server_token
    if server_token := config.get('serverToken'):
        _server_token = server_token
    # You can handle other session config fields here

# Store server token only for stdio mode (backwards compatibility)
_server_token: Optional[str] = None

def get_request_config() -> dict:
    """Get full config from current request context."""
    try:
        # Access the current request context from FastMCP
        import contextvars
        
        # Try to get from request context if available
        request = contextvars.copy_context().get('request')
        if hasattr(request, 'scope') and request.scope:
            return request.scope.get('smithery_config', {})
    except:
        pass

def get_config_value(key: str, default=None):
    """Get a specific config value from current request."""
    config = get_request_config()
    return config.get(key, default)

def validate_server_access(server_token: Optional[str]) -> bool:
    """Validate server token - accepts any string including empty ones for demo."""
    # In a real app, you'd validate against your server's auth system
    # For demo purposes, we accept any non-empty token
    return server_token is not None and len(server_token.strip()) > 0 if server_token else True

Step 2b: Create Middleware Setup (SDK)

Create middleware that uses parse_config_from_asgi_scope from the SDK:
# src/middleware.py
from smithery.utils.config import parse_config_from_asgi_scope

class SmitheryConfigMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope.get('type') == 'http':
            try:
                scope['smithery_config'] = parse_config_from_asgi_scope(scope)
            except Exception as e:
                print(f"SmitheryConfigMiddleware: Error parsing config: {e}")
                scope['smithery_config'] = {}
        await self.app(scope, receive, send)
This middleware:
  • Extracts configuration using the SDK’s dot-notation parser
  • Parses values (numbers/booleans) from JSON where applicable
  • Stores the config in the request scope for per-request access
  • Provides error handling and logging for debugging

Step 3: Register Tools

Define your MCP tool:
# src/main.py
# MCP Tool - demonstrates per-request config access
@mcp.tool()
def count_characters(text: str, character: str) -> str:
    """Count occurrences of a specific character in text"""
    # Example: Get various config values that users can pass to your server session
    server_token = get_config_value("serverToken")
    case_sensitive = get_config_value("caseSensitive", False)
    
    # Validate server access (your custom validation logic)
    if not validate_server_access(server_token):
        raise ValueError("Server access validation failed. Please provide a valid serverToken.")
    
    # Apply user preferences from config
    search_text = text if case_sensitive else text.lower()
    search_char = character if case_sensitive else character.lower()
    
    # Count occurrences
    count = search_text.count(search_char)
    
    return f'The character "{character}" appears {count} times in the text.'

Step 4: Main Function and Server Startup

Handle both HTTP and STDIO transport modes:
# src/main.py
def main():
    transport_mode = os.getenv("TRANSPORT", "stdio")
    
    if transport_mode == "http":
        # HTTP mode with config extraction from URL parameters
        print("Character Counter MCP Server starting in HTTP mode...")
        
        # 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,
        )

        # Apply custom middleware for config extraction (per-request API key handling)
        app = SmitheryConfigMiddleware(app)

        # Use Smithery-required PORT environment variable
        port = int(os.environ.get("PORT", 8081))
        print(f"Listening on port {port}")

        uvicorn.run(app, host="0.0.0.0", port=port, log_level="debug")
    
    else:
        # Optional: add stdio transport for backwards compatibility
        # You can publish this to uv for users to run locally
        print("Character Counter MCP Server starting in stdio mode...")
        
        server_token = os.getenv("SERVER_TOKEN")
        # Set the server token for stdio mode (can be None)
        handle_config({"serverToken": server_token})
        
        # Run with stdio transport (default)
        mcp.run()

if __name__ == "__main__":
    main()
How it works: When you deploy with custom containers, your FastMCP server handles HTTP requests directly through the streamable HTTP app. The main() function only runs STDIO mode when you execute the file directly (like python src/main.py), giving you STDIO support for local development and backward compatibility.

Configuration Changes

Step 5: Update smithery.yaml

# smithery.yaml
startCommand:
  type: stdio
  commandFunction: |
    (config) => ({
      command: 'python',
      args: ['server.py'],
      env: {
        SERVER_TOKEN: config.serverToken,
        CASE_SENSITIVE: config.caseSensitive
      }
    })
  configSchema:
    type: object
    required: [serverToken]
    properties:
      serverToken:
        type: string
        description: Your server token
      caseSensitive:
        type: boolean
        description: Whether character matching should be case sensitive
        default: false
  exampleConfig:
    serverToken: "token-123"
    caseSensitive: false

Step 6: Update 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
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 []

# STDIO servers don't expose ports
CMD ["python", "src/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

  1. Run directly with Python:
    # Install dependencies
    uv sync
    
    # Run in HTTP mode
    TRANSPORT=http uv run python src/main.py
    
  2. Or with Docker:
    docker build -t character-counter .
    docker run -p 8081:8081 -e PORT=8081 character-counter
    
Local vs Deployed Ports: For local testing, you can use any port. However, when deployed to Smithery, your server must listen on the PORT environment variable, which Smithery will set to 8081.
  1. Test interactively: Once your server is running in HTTP mode, you can test it interactively using the Smithery playground:
    npx @smithery/cli playground --port 8081
    
    Smithery Interactive Playground
    Config Handling Limitation: The Smithery playground doesn’t currently support config handling for custom containers (Python/TypeScript). Your deployed server will support configuration, but local testing with the playground will use default/empty config values. We’re actively working on adding this support.

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 migrated a Python MCP server from STDIO to HTTP transport using custom Docker containers and FastMCP. We implemented a character counter tool with proper CORS configuration, used SmitheryConfigMiddleware for configuration handling, and supported both HTTP and STDIO transport modes. This approach gives us full control over our Python environment and middleware while maintaining backward compatibility.
Need help? Join our Discord or email support@smithery.ai for assistance.
I