Step‑by‑Step Guide to Building a Production‑Ready MCP Server

This article walks through turning a local MCP demo into a production‑grade server, covering project layout, uv + FastMCP setup, authentication options, error‑code handling, structured logging, Docker containerization with health checks and load balancing, and comprehensive testing using Inspector and pytest.

Tech Ocean
Tech Ocean
Tech Ocean
Step‑by‑Step Guide to Building a Production‑Ready MCP Server

Project Structure

Official Layout

mcp-server-project/
├── src/
│   └── index.ts          # entry file
├── package.json
├── tsconfig.json
├── uv.lock               # Python dependencies managed by uv
├── Dockerfile
├── README.md
└── tests/
    └── server.test.ts

uv + FastMCP

# Create project
uv init mcp-server
cd mcp-server

# Install dependencies
uv add fastmcp uvicorn

# Recommended additional packages
uv add httpx structlog pydantic

Python entry

from fastmcp import FastMCP

mcp = FastMCP(
    name="production-server",
    dependencies=["httpx", "pydantic"]
)

@mcp.tool()
async def query_database(sql: str) -> list[dict]:
    """Execute read‑only query"""
    ...

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8000)

TypeScript SDK

# Install MCP TypeScript SDK
npm install @modelcontextprotocol/server
# See official example at https://github.com/modelcontextprotocol/typescript-sdk

Authentication and Authorization

MCP supports two remote authentication methods:

API Key – internal services, single‑node deployment (low complexity)

OAuth 2.0 – multi‑tenant, open platform (high complexity)

OAuth Providers

GitHub OAuth (suitable for internal tools)

from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
import os

auth = GitHubProvider(
    client_id=os.environ.get("GITHUB_CLIENT_ID"),
    client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
    base_url=os.environ.get("BASE_URL", "https://your-server.com")
)

mcp = FastMCP(name="internal-server", auth=auth)

Auth0 (suitable for multi‑tenant platforms)

from fastmcp import FastMCP
from fastmcp.server.auth.providers.auth0 import Auth0Provider
import os

auth = Auth0Provider(
    config_url="https://your-app.auth0.com/.well-known/openid-configuration",
    client_id=os.environ["AUTH0_CLIENT_ID"],
    audience="https://your-api"
)

mcp = FastMCP(name="public-server", auth=auth)
Note: FastMCP 0.5.x/1.x APIs have significant changes; consult the official Auth documentation for the latest configuration.

Path‑level Access Control

@mcp.tool(path_filter=["/api/v1/*", "/api/v2/read/*"])
async def sensitive_operation(path: str) -> dict:
    """Only usable on whitelisted paths"""
    ...

@mcp.middleware
async def path_guard(request, next_handler):
    allowed = ["/api/v1/users", "/api/v1/orders"]
    if request.path not in allowed:
        raise PermissionError("路径不在白名单内")
    return await next_handler(request)

Error Handling and Structured Logging

MCP Error Code System

-32700

– Parse error – check request JSON format -32600 – Invalid request – validate request parameters -32601 – Method not found – verify method name -32603 – Internal error – inspect server logs -32000 – Custom error – define per business logic

Custom Error Example

from fastmcp.exceptions import MCPError

class UserNotFoundError(MCPError):
    code = -32001
    message = "用户不存在"

@mcp.tool()
async def get_user(user_id: str) -> dict:
    if not user := await db.users.find(user_id):
        raise UserNotFoundError(f"user_id={user_id}")
    return user

Structured Logging

import structlog

logger = structlog.get_logger()

@mcp.tool()
async def batch_process(items: list[str]) -> dict:
    logger.info("batch_process_started", item_count=len(items))
    try:
        results = await processor.run(items)
        logger.info(
            "batch_process_completed",
            total=len(items),
            success=len(results)
        )
        return {"success": results}
    except Exception as e:
        logger.error(
            "batch_process_failed",
            error=str(e),
            item_count=len(items)
        )
        raise

Example log entries (JSON):

{"event": "batch_process_started", "item_count": 100, "timestamp": "2026-05-09T10:00:00Z"}
{"event": "batch_process_completed", "total": 100, "success": 98, "timestamp": "2026-05-09T10:00:05Z"}

Production Deployment

Dockerfile

FROM python:3.12-slim

WORKDIR /app

# Install dependencies with uv
COPY uv.lock pyproject.toml ./
RUN pip install uv && uv sync --frozen

COPY src ./src

EXPOSE 8000

CMD ["uvicorn", "src.server:app", "--host", "0.0.0.0", "--port", "8000"]

Health Checks

from fastapi import FastAPI, HTTPException

app = FastMCP(fastapi_app=FastAPI())

@app.get("/health")
async def health_check():
    return {"status": "healthy", "mcp_version": "1.0.0", "uptime": get_uptime_seconds()}

@app.get("/ready")
async def readiness_check():
    if not await db.ping():
        raise HTTPException(status_code=503)
    return {"ready": True}

Docker HEALTHCHECK configuration:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

Multi‑Instance Deployment

# docker-compose.yml
services:
  mcp-server:
    image: mcp-server:latest
    deploy:
      replicas: 3
    environment:
      - MCP_API_KEY=${MCP_API_KEY}
      - LOG_LEVEL=info
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      - mcp-network

  nginx:
    image: nginx:alpine
    ports:
      - "8000:8000"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - mcp-server
    networks:
      - mcp-network

networks:
  mcp-network:
    driver: bridge

Nginx upstream configuration:

upstream mcp_servers {
    least_conn;
    server mcp-server-1:8000;
    server mcp-server-2:8000;
    server mcp-server-3:8000;
}

server {
    listen 8000;
    location / {
        proxy_pass http://mcp_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Testing the MCP Server

MCP Inspector

# Global install
npm install -g @modelcontextprotocol/inspector

# Start Inspector on default port
mcp-inspector --port=3100

# Or point to a specific server
mcp-inspector --server=http://localhost:8000

Inspector UI capabilities:

View the list of available tools

Manually invoke tools for testing

Inspect request/response details

Debug authentication flows

Unit Tests

import pytest
from fastmcp import FastMCP

@pytest.fixture
def mcp_server():
    """Create a test Server instance"""
    server = FastMCP(name="test-server")

    @server.tool()
    async def add(a: int, b: int) -> int:
        return a + b

    return server

@pytest.mark.asyncio
async def test_add_tool(mcp_server):
    """Test the addition tool"""
    result = await mcp_server.call_tool("add", {"a": 2, "b": 3})
    assert result == 5

@pytest.mark.asyncio
async def test_auth_failure(mcp_server):
    """Test authentication failure scenario"""
    with pytest.raises(AuthError):
        await mcp_server.call_tool(
            "add",
            {"a": 1, "b": 2},
            headers={"X-API-Key": "wrong-key"}
        )

Integration Tests

import httpx
import pytest

@pytest.fixture
async def server_url():
    """Start a test server and yield its URL"""
    server = TestServer()
    await server.start()
    yield server.url
    await server.stop()

@pytest.mark.asyncio
async def test_end_to_end(server_url):
    """End‑to‑end test"""
    async with httpx.AsyncClient() as client:
        # Health check
        resp = await client.get(f"{server_url}/health")
        assert resp.status_code == 200

        # Call an MCP tool
        resp = await client.post(
            f"{server_url}/mcp/v1/call",
            headers={"X-API-Key": "test-key"},
            json={
                "method": "tools/call",
                "params": {
                    "name": "add",
                    "arguments": {"a": 10, "b": 20}
                }
            }
        )
        assert resp.json()["result"] == 30

References

MCP 官方文档:https://modelcontextprotocol.io/
MCP TypeScript SDK:https://github.com/modelcontextprotocol/typescript-sdk
MCP Python SDK:https://github.com/modelcontextprotocol/python-sdk
MCP Inspector:https://github.com/modelcontextprotocol/inspector
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DockerTypeScriptPythonMCPTestingAuthenticationerror handlingFastMCP
Tech Ocean
Written by

Tech Ocean

Focused on AI programming, sharing ready-to-use development efficiency solutions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.