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.
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.tsuv + FastMCP
# Create project
uv init mcp-server
cd mcp-server
# Install dependencies
uv add fastmcp uvicorn
# Recommended additional packages
uv add httpx structlog pydanticPython 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-sdkAuthentication 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 userStructured 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)
)
raiseExample 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 1Multi‑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: bridgeNginx 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:8000Inspector 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"] == 30References
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/inspectorSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tech Ocean
Focused on AI programming, sharing ready-to-use development efficiency solutions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
