MCP 服务器生产部署指南——从开发到上线的完整实战

MCP 服务器生产部署指南——从开发到上线的完整实战

整理日期: 2026-07-20


简介

在前两篇 MCP 服务器开发教程中(入门篇 + 进阶篇),你已经学会了如何使用 FastMCP 构建功能完整的 MCP 服务器。但开发完成只是第一步——将 MCP 服务器部署到生产环境,让它稳定、安全、高性能地对外服务,才是真正的挑战。

本教程聚焦于 MCP 服务器的生产部署,覆盖从单进程服务到多租户集群的完整部署体系。你将掌握:

  • 生产架构设计 —— HTTPS 端点、反向代理、负载均衡
  • 安全鉴权 —— API Key、JWT、OAuth2 三种方案
  • 进程守护 —— systemd 服务配置与自动重启
  • 日志与监控 —— 结构化日志、Prometheus 指标暴露
  • Docker 部署 —— 多阶段构建、健康检查、自动重启策略
  • 多租户隔离 —— 进程级与命名空间级隔离方案
  • 性能调优 —— 连接池、请求限流、超时控制

适合谁阅读

  • 已经完成 MCP 自定义服务器开发的开发者(入门篇 + 进阶篇)
  • 需要将 MCP 服务器部署到生产环境的技术人员
  • 对 MCP 服务器架构设计和运维有需求的 DevOps 工程师

前置要求

要求 说明
已完成 MCP 入门篇开发 掌握 FastMCP 基础用法和数据模型
已完成 MCP 进阶篇开发 了解错误处理、性能优化基础
Linux 服务器 Ubuntu 22.04+ 或 CentOS 8+
Python >= 3.11 MCP SDK 需要 async/await 支持
Docker(可选) 容器化部署方案
Nginx / Caddy(可选) 反向代理方案
域名 + SSL 证书(可选) 生产 HTTPS 端点

推荐阅读顺序

1
入门篇 → 进阶篇 → 本教程(生产部署)

一、生产架构总览

一个生产级 MCP 服务器的典型架构如下:

1
2
3
4
5
6
7
8
9
10
11
┌─────────────┐     ┌──────────┐     ┌──────────────┐     ┌────────────────┐
│ AI 客户端 │────▶│ HTTPS │────▶│ 反向代理 │────▶│ MCP Server │
│ (Claude Code │ │ 443 │ │ Nginx/Caddy │ │ (FastMCP) │
│ / Hermes │ │ │ │ + 负载均衡 │ │ + 鉴权/限流 │
│ / Cursor) │ │ │ │ │ │ │
└─────────────┘ └──────────┘ └──────────────┘ └────────────────┘

┌─────────▼────────┐
│ 后端服务 │
│ (API / DB / 内部) │
└──────────────────┘

架构关键决策:

  1. 传输方式:生产环境强烈推荐 HTTP(S) SSE 模式而非 stdio。stdio 模式要求客户端与服务器同机部署,适合开发调试;HTTP 模式允许远程访问、水平扩展和细粒度流量管理。
  2. 反向代理:Nginx 或 Caddy 处理 TLS 终止、请求路由、速率限制和访问日志。
  3. 进程管理:systemd(裸机)或 Docker(容器化)保证服务自动恢复。
  4. 鉴权层:在反向代理层或应用层实现,确保只有授权客户端可以调用 MCP 工具。

二、HTTP 传输模式配置

2.1 启用 HTTP SSE 传输

FastMCP 支持通过 uvicorn 以 HTTP 模式运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Production MCP Server")

@mcp.tool()
def greet(name: str) -> str:
"""向用户打招呼"""
return f"你好,{name}!欢迎使用生产级 MCP 服务器。"

@mcp.tool()
def add(a: int, b: int) -> int:
"""计算两个数字之和"""
return a + b

# 启动入口
if __name__ == "__main__":
mcp.run(transport="http")

启动命令:

1
2
3
4
5
# 开发模式
python server.py

# 生产模式(指定主机和端口)
python -c "from server import mcp; mcp.run(transport='http', host='0.0.0.0', port=8000)"

或者使用 uvicorn 直接启动:

1
2
# 等效于上面
uvicorn server:mcp.sse_app --host 0.0.0.0 --port 8000 --workers 4

注意mcp.sse_app 是 FastMCP 暴露的 Starlette ASGI 应用,可以直接挂载到 uvicorn、gunicorn 或其他 ASGI 服务器。

2.2 验证 HTTP 端点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 测试 SSE 端点是否正常
curl -N http://localhost:8000/mcp

# 预期输出(SSE 连接建立)
# event: endpoint
# data: /mcp/message
#
# event: heartbeat
# data: ...

# JSON-RPC 测试
curl -X POST http://localhost:8000/mcp/message \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}' \
-w "\nHTTP Status: %{http_code}\n"

三、反向代理配置

3.1 Nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# /etc/nginx/sites-available/mcp-server
upstream mcp_backend {
# 负载均衡:多个 MCP 服务器实例
server 127.0.0.1:8001 weight=3;
server 127.0.0.1:8002 weight=2;
server 127.0.0.1:8003 weight=1;
keepalive 32;
}

server {
listen 443 ssl http2;
server_name mcp.yourdomain.com;

# SSL 证书(使用 certbot 免费获取)
ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

# SSE 需要长连接,禁用缓冲
proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Connection '';
chunked_transfer_encoding on;

# MCP SSE 端点
location /mcp {
proxy_pass http://mcp_backend/mcp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# SSE 需要不缓冲且持续读取
proxy_read_timeout 86400s; # 24 小时长连接
proxy_send_timeout 86400s;
}

# MCP 消息端点(POST)
location /mcp/message {
proxy_pass http://mcp_backend/mcp/message;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 限制请求体大小
client_max_body_size 1m;
proxy_read_timeout 60s;
}

# 健康检查端点
location /health {
proxy_pass http://mcp_backend/health;
access_log off;
proxy_read_timeout 5s;
}

# 监控指标端点(内网访问)
location /metrics {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://mcp_backend/metrics;
}

# 速率限制
location /mcp/message {
limit_req zone=mcp_api burst=20 nodelay;
limit_req_status 429;
# ... 同上配置
}
}

# HTTP → HTTPS 重定向
server {
listen 80;
server_name mcp.yourdomain.com;
return 301 https://$server_name$request_uri;
}

3.2 Nginx 速率限制配置

http 块中定义限流区域:

1
2
3
# /etc/nginx/nginx.conf 的 http 块内
limit_req_zone $binary_remote_addr zone=mcp_api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=mcp_auth:10m rate=5r/s;

3.3 Caddy 配置(更简洁的替代方案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# /etc/caddy/Caddyfile
mcp.yourdomain.com {
reverse_proxy /mcp/* 127.0.0.1:8000 {
# SSE 需要禁用缓冲
flush_interval -1
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
}

reverse_proxy /health 127.0.0.1:8000
reverse_proxy /metrics 127.0.0.1:8000

# 速率限制
rate_limit {
zone mcp_api {
key {remote_host}
events 10
window 1s
}
}

# 自动 HTTPS(Caddy 默认自动获取 Let's Encrypt 证书)
tls your-email@example.com
}

为什么推荐 Caddy:Caddy 自动管理 SSL 证书、默认支持 HTTP/2 和 HTTP/3,配置比 Nginx 简洁 60% 以上,特别适合中小规模部署。


四、鉴权方案

MCP 协议本身不定义鉴权机制,生产部署时必须自行实现。以下提供三种方案。

4.1 API Key 鉴权(轻量级,推荐入门)

在 FastMCP 中通过 middleware 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# auth.py
import os
from functools import wraps
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

# 从环境变量读取 API Keys(用逗号分隔支持多 Key)
VALID_API_KEYS = set(
os.getenv("MCP_API_KEYS", "dev-key-123").split(",")
)

security_scheme = HTTPBearer(auto_error=False)

def verify_api_key(credentials: HTTPAuthorizationCredentials = Security(security_scheme)):
"""验证 API Key"""
if not credentials:
raise HTTPException(
status_code=401,
detail="缺少 Authorization 头,请提供 API Key"
)

token = credentials.credentials

if token not in VALID_API_KEYS:
raise HTTPException(
status_code=403,
detail="无效的 API Key"
)

return token

在 FastMCP 中集成鉴权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# server.py
from mcp.server.fastmcp import FastMCP
from auth import verify_api_key

mcp = FastMCP("Production MCP Server")

@mcp.tool()
def secure_greet(name: str) -> str:
"""需要 API Key 才能调用的工具"""
return f"你好,{name}!"

# 修改启动入口,挂载鉴权 middleware
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()

# 鉴权 middleware
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# 健康检查和指标端点不需要鉴权
if request.url.path in ["/health", "/metrics"]:
return await call_next(request)

# 验证 API Key
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=401,
content={"error": "缺少 Authorization 头"}
)

token = auth_header.replace("Bearer ", "")
if token not in VALID_API_KEYS:
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=403,
content={"error": "无效的 API Key"}
)

return await call_next(request)

app.add_middleware(AuthMiddleware)

# 挂载 MCP SSE 端点
app.mount("/", mcp.sse_app())

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

客户端连接配置:

1
2
3
4
5
# Claude Code 连接(使用远程 HTTP MCP)
claude mcp add my-server -s user \
--transport http \
-e MCP_API_KEY=sk-prod-abc123 \
-- https://mcp.yourdomain.com/mcp

4.2 JWT 鉴权(企业级)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# jwt_auth.py
import os
import time
import jwt
from typing import Optional

# 从环境变量读取 JWT Secret
JWT_SECRET = os.getenv("MCP_JWT_SECRET", "change-me-in-production")
JWT_ALGORITHM = "HS256"

def create_token(user_id: str, role: str = "user", expiry_hours: int = 24) -> str:
"""生成 JWT Token"""
payload = {
"sub": user_id,
"role": role,
"iat": int(time.time()),
"exp": int(time.time()) + expiry_hours * 3600,
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def verify_jwt(token: str) -> Optional[dict]:
"""验证 JWT Token,返回 payload"""
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None

JWT 提供了更精细的权限控制——你可以在 payload 中嵌入角色(role),然后在工具级别进行细粒度授权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# tools.py
from jwt_auth import verify_jwt
from functools import wraps

def require_role(role: str):
"""装饰器:要求特定角色才能调用"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 从上下文中获取用户信息(需要注入 context)
user_role = kwargs.get("_user_role", "anonymous")
if user_role != role and user_role != "admin":
raise PermissionError(f"需要 {role} 角色,当前为 {user_role}")
return await func(*args, **kwargs)
return wrapper
return decorator

@mcp.tool()
@require_role("admin")
def admin_only_tool() -> str:
"""仅管理员可调用"""
return "这是管理员专属工具"

4.3 OAuth2 代理模式(集成外部身份提供商)

不需要修改 MCP 服务器代码,在反向代理层解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Nginx + OAuth2 Proxy 集成
# 使用 oauth2-proxy (https://oauth2-proxy.github.io/oauth2-proxy/)

server {
listen 443 ssl;
server_name mcp.yourdomain.com;

location /mcp {
# 代理到 oauth2-proxy 监听端口
proxy_pass http://127.0.0.1:4180;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
}
}

# oauth2-proxy 配置
# docker run -p 4180:4180 \
# -e OAUTH2_PROXY_PROVIDER=github \
# -e OAUTH2_PROXY_CLIENT_ID=... \
# -e OAUTH2_PROXY_CLIENT_SECRET=... \
# -e OAUTH2_PROXY_EMAIL_DOMAINS=* \
# -e OAUTH2_PROXY_UPSTREAM=http://127.0.0.1:8000 \
# quay.io/oauth2-proxy/oauth2-proxy

鉴权方案对比

方案 复杂度 安全性 适用场景
API Key ⭐ 低 ⭐⭐⭐ 中 个人/小团队、内部服务
JWT ⭐⭐⭐ 中 ⭐⭐⭐⭐⭐ 高 企业多用户、多角色
OAuth2 代理 ⭐⭐⭐⭐⭐ 高 ⭐⭐⭐⭐⭐ 高 集成公司 SSO、GitHub/Google 登录

五、进程守护(systemd 配置)

在裸机部署中,使用 systemd 保证 MCP 服务器随系统启动、崩溃自动恢复。

5.1 创建 systemd 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# /etc/systemd/system/mcp-server.service
[Unit]
Description=MCP Production Server
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=mcpuser
Group=mcpuser
WorkingDirectory=/opt/mcp-server

# 虚拟环境中的 Python
ExecStart=/opt/mcp-server/.venv/bin/uvicorn server:mcp.sse_app \
--host 127.0.0.1 \
--port 8000 \
--workers 4 \
--limit-concurrency 100 \
--timeout-keep-alive 120

# 环境变量
Environment=MCP_API_KEYS=sk-prod-abc123,sk-prod-def456
Environment=MCP_LOG_LEVEL=INFO
Environment=PYTHONUNBUFFERED=1

# 自动重启策略
Restart=always
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3

# 资源限制
LimitNOFILE=65536
LimitNPROC=4096
MemoryMax=2G
CPUQuota=80%

# 日志
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

5.2 管理服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 重新加载 systemd 配置
sudo systemctl daemon-reload

# 启用开机自启并启动
sudo systemctl enable mcp-server
sudo systemctl start mcp-server

# 查看状态
sudo systemctl status mcp-server

# 查看实时日志
sudo journalctl -u mcp-server -f

# 重启服务
sudo systemctl restart mcp-server

# 查看最近 100 条日志
sudo journalctl -u mcp-server -n 100 --no-pager

5.3 健康检查脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# /opt/mcp-server/healthcheck.sh
# 用于 systemd HealthCheck 或监控系统

SERVER_URL="http://127.0.0.1:8000"
EXPECTED_STATUS=200

response=$(curl -s -o /dev/null -w "%{http_code}" "$SERVER_URL/health")

if [ "$response" != "$EXPECTED_STATUS" ]; then
echo "Health check failed: HTTP $response"
exit 1
fi

echo "Health check passed: HTTP $response"
exit 0

六、日志与监控

6.1 结构化日志

使用 structlog 替代标准 logging,输出 JSON 格式日志,便于日志聚合系统(ELK/Loki)解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# logger.py
import structlog
import logging
import sys

def setup_logging():
"""配置结构化日志"""
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
# JSON 输出,适合生产环境
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)

# 设置 root logger
root_logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
root_logger.addHandler(handler)
root_logger.setLevel(logging.INFO)

return structlog.get_logger()

# 使用
logger = setup_logging()
logger.info("mcp_server_started", port=8000, workers=4, transport="http")

在 FastMCP 中集成结构日志:

1
2
3
4
5
6
7
# server.py
from logger import logger

@mcp.tool()
def greet(name: str) -> str:
logger.info("greet_called", name=name, source_ip=request.client.host)
return f"你好,{name}!"

6.2 Prometheus 指标暴露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# metrics.py
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
from starlette.responses import Response
import time

# 定义指标
TOOL_CALLS_TOTAL = Counter(
"mcp_tool_calls_total",
"MCP 工具调用总数",
["tool_name", "status"] # label: 工具名 + 成功/失败
)

TOOL_CALL_DURATION = Histogram(
"mcp_tool_call_duration_seconds",
"MCP 工具调用耗时(秒)",
["tool_name"],
buckets=(0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
)

ACTIVE_CONNECTIONS = Gauge(
"mcp_active_connections",
"当前活跃的 SSE 连接数"
)

ACTIVE_TOOLS = Gauge(
"mcp_registered_tools_total",
"注册的工具总数"
)

def metrics_endpoint(request):
"""Prometheus metrics 端点"""
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

def track_tool_metrics(tool_name: str):
"""工具调用耗时追踪装饰器"""
def decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
try:
result = func(*args, **kwargs)
TOOL_CALLS_TOTAL.labels(tool_name=tool_name, status="success").inc()
return result
except Exception as e:
TOOL_CALLS_TOTAL.labels(tool_name=tool_name, status="error").inc()
raise
finally:
duration = time.time() - start
TOOL_CALL_DURATION.labels(tool_name=tool_name).observe(duration)
return wrapper
return decorator

在 FastMCP 中添加 metrics 端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
# server.py
from metrics import metrics_endpoint, track_tool_metrics, ACTIVE_TOOLS

# 在启动时登记工具数量
ACTIVE_TOOLS.set(len(mcp._tool_manager.list_tools()))

# 在 ASGI app 中挂载 metrics
app.mount("/metrics", metrics_endpoint)

@mcp.tool()
@track_tool_metrics("greet")
def greet(name: str) -> str:
return f"你好,{name}!"

6.3 Prometheus + Grafana 监控栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# docker-compose.monitoring.yml
version: '3.8'

services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"

grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana

volumes:
grafana_data:
1
2
3
4
5
6
7
8
9
10
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: 'mcp-server'
static_configs:
- targets: ['mcp-server:8000']
metrics_path: '/metrics'

Grafana 推荐面板

  • 工具调用率:每分钟调用次数(rate/irate 函数)
  • P50/P95/P99 延迟histogram_quantile 聚合
  • 错误率mcp_tool_calls_total{status="error"} 占比
  • 活跃连接数mcp_active_connections 实时曲线
  • 健康状态up 指标,配合告警规则

七、Docker 部署方案

7.1 多阶段构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Dockerfile
# ========== 构建阶段 ==========
FROM python:3.12-slim AS builder

WORKDIR /build

# 只复制依赖文件,利用 Docker 缓存
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# ========== 运行阶段 ==========
FROM python:3.12-slim

# 创建非 root 用户
RUN groupadd -r mcp && useradd -r -g mcp -d /app -s /sbin/nologin mcp

WORKDIR /app

# 只复制已安装的依赖,减少镜像体积
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

# 复制应用代码
COPY server.py .
COPY auth.py .
COPY logger.py .
COPY metrics.py .

# 健康检查
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

# 切换非 root 用户
USER mcp

EXPOSE 8000

# 使用 gunicorn + uvicorn workers 作为生产级入口
CMD ["gunicorn", "server:mcp.sse_app", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--timeout", "120", \
"--keep-alive", "120", \
"--log-level", "info"]

7.2 requirements.txt

1
2
3
4
5
6
mcp>=1.6.0
uvicorn[standard]>=0.29.0
gunicorn>=22.0.0
structlog>=24.1.0
prometheus-client>=0.20.0
pyjwt>=2.8.0

7.3 docker-compose 完整部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# docker-compose.yml
version: '3.8'

services:
mcp-server:
build:
context: .
dockerfile: Dockerfile
image: mcp-server:prod
container_name: mcp-server
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000" # 仅监听本地,由反向代理转发
environment:
- MCP_API_KEYS=${MCP_API_KEYS:-sk-dev-key}
- MCP_LOG_LEVEL=INFO
- PYTHONUNBUFFERED=1
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- mcp_network

# 可选:Nginx 反向代理(同 Docker 网络内)
nginx:
image: nginx:alpine
container_name: mcp-nginx
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- mcp-server
networks:
- mcp_network

networks:
mcp_network:
driver: bridge

7.4 镜像构建与发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 构建
docker build -t mcp-server:prod .

# 运行
docker compose up -d

# 查看日志
docker compose logs -f mcp-server

# 滚动更新(零停机)
docker compose up -d --no-deps --build mcp-server

# 查看资源使用
docker stats mcp-server

# 手动健康检查
docker compose exec mcp-server python -c "
import urllib.request
resp = urllib.request.urlopen('http://localhost:8000/health')
print(f'Health status: {resp.status}')
"

八、多租户隔离

当你的 MCP 服务器需要服务多个客户/团队时,多租户隔离是刚需。

8.1 方案一:单进程 + 租户命名空间(轻量级)

适用于租户间资源隔离需求不严格的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# tenant.py
import os
import threading
from contextvars import ContextVar

# 使用 ContextVar 实现线程/协程级租户隔离
current_tenant: ContextVar[str] = ContextVar("current_tenant", default="default")

class TenantRouter:
"""根据租户 ID 路由到不同的资源配置"""

def __init__(self):
self._tenants = {} # tenant_id -> config

def register_tenant(self, tenant_id: str, config: dict):
self._tenants[tenant_id] = config

def get_config(self, key: str, default=None):
tenant = current_tenant.get()
config = self._tenants.get(tenant, {})
return config.get(key, default)

# 全局路由
tenant_router = TenantRouter()

# 初始化租户
tenant_router.register_tenant("acme-corp", {
"database_url": "postgresql://acme:pass@db:5432/acme",
"api_key": "ak-acme-secret",
"rate_limit": 100, # 每分钟允许的请求数
})
tenant_router.register_tenant("startup-inc", {
"database_url": "postgresql://startup:pass@db:5432/startup",
"api_key": "ak-startup-secret",
"rate_limit": 20,
})

在工具中按租户隔离数据:

1
2
3
4
5
6
7
@mcp.tool()
def query_tenant_data(query: str) -> str:
"""查询当前租户的数据"""
db_url = tenant_router.get_config("database_url")
# 连接到该租户的数据库
# ... 执行查询
return f"查询完成(租户:{current_tenant.get()})"

8.2 方案二:独立进程 + Docker(强隔离)

每个租户启动独立的 MCP 服务器进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# tenant_manager.py
import subprocess
import os
import signal

class TenantProcessManager:
"""管理每个租户的独立 MCP 服务器进程"""

def __init__(self, base_port: int = 9000):
self.base_port = base_port
self._processes: dict[str, subprocess.Popen] = {}
self._ports: dict[str, int] = {}

def start_tenant(self, tenant_id: str, config: dict):
"""为租户启动一个独立的 MCP 服务器进程"""
if tenant_id in self._processes:
return self._ports[tenant_id]

port = self.base_port + len(self._processes)
env = os.environ.copy()
env.update({
"MCP_TENANT_ID": tenant_id,
"MCP_DATABASE_URL": config["database_url"],
"MCP_API_KEYS": config["api_key"],
"MCP_PORT": str(port),
})

proc = subprocess.Popen(
["uvicorn", "server:mcp.sse_app",
"--host", "0.0.0.0",
"--port", str(port),
"--workers", "2"],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)

self._processes[tenant_id] = proc
self._ports[tenant_id] = port
return port

def stop_tenant(self, tenant_id: str):
"""停止租户进程"""
if tenant_id in self._processes:
self._processes[tenant_id].terminate()
self._processes[tenant_id].wait()
del self._processes[tenant_id]
del self._ports[tenant_id]

def stop_all(self):
"""停止所有租户进程"""
for tenant_id in list(self._processes.keys()):
self.stop_tenant(tenant_id)

8.3 方案三:Kubernetes 命名空间(云原生)

每个租户作为一个独立的 Kubernetes Deployment + Service,放在各自的 Namespace 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# tenant-template.yaml
apiVersion: v1
kind: Namespace
metadata:
name: tenant-${TENANT_ID}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
namespace: tenant-${TENANT_ID}
spec:
replicas: 2
selector:
matchLabels:
app: mcp-server
tenant: ${TENANT_ID}
template:
metadata:
labels:
app: mcp-server
tenant: ${TENANT_ID}
spec:
containers:
- name: mcp-server
image: mcp-server:prod
env:
- name: MCP_TENANT_ID
value: "${TENANT_ID}"
- name: MCP_DATABASE_URL
value: "${TENANT_DB_URL}"
- name: MCP_API_KEYS
valueFrom:
secretKeyRef:
name: tenant-${TENANT_ID}-secret
key: api-key
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: "0.25"
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: mcp-server
namespace: tenant-${TENANT_ID}
spec:
selector:
app: mcp-server
tenant: ${TENANT_ID}
ports:
- port: 8000
targetPort: 8000

多租户方案对比

方案 隔离强度 资源效率 运维复杂度 适用场景
ContextVar 命名空间 ⭐⭐ 中 ⭐⭐⭐⭐⭐ 高 ⭐ 低 内部多团队共享
独立 Docker 进程 ⭐⭐⭐⭐ 强 ⭐⭐⭐ 中 ⭐⭐⭐ 中 SaaS 多租户
K8s Namespace ⭐⭐⭐⭐⭐ 最强 ⭐⭐ 较低 ⭐⭐⭐⭐⭐ 高 大型云原生部署

九、性能调优

9.1 连接池优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# connection_pool.py
import aiohttp
import asyncio
from typing import Optional

class ConnectionPool:
"""全局 HTTP 连接池"""

_instance: Optional["ConnectionPool"] = None
_session: Optional[aiohttp.ClientSession] = None

def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

async def get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
connector = aiohttp.TCPConnector(
limit=100, # 最大并发连接数
limit_per_host=20, # 每主机最大连接数
ttl_dns_cache=300, # DNS 缓存 5 分钟
enable_cleanup_closed=True,
)
timeout = aiohttp.ClientTimeout(
total=30, # 总超时
connect=5, # 连接超时
sock_read=30, # 读取超时
)
self._session = aiohttp.ClientSession(
connector=connector,
timeout=timeout,
)
return self._session

async def close(self):
if self._session and not self._session.closed:
await self._session.close()

# 应用关闭时清理
pool = ConnectionPool()
import atexit
atexit.register(lambda: asyncio.run(pool.close()))

9.2 请求限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# rate_limiter.py
import time
import asyncio
from collections import defaultdict

class TokenBucket:
"""令牌桶限流器"""

def __init__(self, rate: float, capacity: int):
"""
rate: 每秒新增令牌数
capacity: 桶容量(最大突发)
"""
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.monotonic()

def consume(self, tokens: int = 1) -> bool:
"""消费令牌,返回是否允许通过"""
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill = now

if self.tokens >= tokens:
self.tokens -= tokens
return True
return False

class RateLimiter:
"""多租户限流器"""

def __init__(self, default_rate: float = 10, default_capacity: int = 20):
self.default_rate = default_rate
self.default_capacity = default_capacity
self._buckets: dict[str, TokenBucket] = {}

def check(self, key: str, rate: Optional[float] = None, capacity: Optional[int] = None) -> bool:
"""检查是否限流"""
if key not in self._buckets:
self._buckets[key] = TokenBucket(
rate or self.default_rate,
capacity or self.default_capacity
)
return self._buckets[key].consume()

def get_wait_time(self, key: str) -> float:
"""获取需要等待的秒数"""
bucket = self._buckets.get(key)
if not bucket or bucket.tokens > 0:
return 0
return (1 - bucket.tokens / bucket.capacity) / bucket.rate

# 全局限流器
rate_limiter = RateLimiter()

在 FastMCP 中集成限流:

1
2
3
4
5
6
7
8
@mcp.tool()
def rate_limited_tool(name: str) -> str:
"""受限流保护的工具"""
tenant = current_tenant.get()
if not rate_limiter.check(tenant):
wait = rate_limiter.get_wait_time(tenant)
raise RateLimitError(retry_after=int(wait))
return f"你好,{name}!"

9.3 超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# timeout.py
import asyncio
from functools import wraps

def with_timeout(seconds: float):
"""带超时的工具装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
try:
return await asyncio.wait_for(
func(*args, **kwargs),
timeout=seconds
)
except asyncio.TimeoutError:
raise TimeoutError(f"工具执行超时({seconds}秒)")
return wrapper
return decorator

# 使用
@mcp.tool()
@with_timeout(30.0)
async def slow_data_fetch(query: str) -> str:
"""可能耗时较长的数据查询"""
await asyncio.sleep(1) # 模拟耗时操作
return f"查询结果:{query}"

9.4 性能调优清单

优化项 操作方法 预期提升
增加 Workers --workers 4-8(与 CPU 核数相关) 2-8x 吞吐量
启用 Keep-Alive proxy_set_header Connection '' 减少 TCP 握手
连接池复用 aiohttp TCPConnector 复用 减少 5-10x 连接开销
结果缓存 @functools.lru_cache / Redis 缓存 10-100x 响应速度
异步改造 async def 替代 def 1.5-3x 并发能力
请求限流 Token Bucket 算法 防止雪崩
超时控制 asyncio.wait_for 避免连接泄露
数据库连接池 psycopg2 pool / SQLAlchemy pool 5-10x 查询吞吐

十、生产部署检查清单

在将 MCP 服务器推向生产前,逐项核对此清单:

基础检查

  • HTTP SSE 传输已启用,stdio 仅用于开发调试
  • 反向代理已配置(Nginx / Caddy)
  • TLS/SSL 证书已生效,强制 HTTPS
  • 服务器绑定 127.0.0.1,不直接暴露服务端口
  • 健康检查端点 /health 正常返回 200

安全检查

  • API Key / JWT / OAuth2 鉴权已启用
  • 默认/弱密码已替换
  • .env 文件和密钥不在版本控制中
  • 非 root 用户运行服务
  • 请求体大小限制已配置

可靠性检查

  • systemd 或 Docker restart policy 已配置
  • 日志已配置为 JSON 结构化格式
  • 资源限制已设置(CPU / 内存 / 文件描述符)
  • 数据库连接池已配置
  • 超时控制已实现

监控检查

  • Prometheus metrics 端点已暴露
  • Grafana 仪表盘已配置
  • 关键指标的告警规则已设置(错误率 > 5%、延迟 > 5s)
  • 日志已接入集中日志系统(Loki / ELK)

性能检查

  • Workers 数量已根据 CPU 核数调整
  • 请求限流已配置
  • 缓存策略已实施(如有重复查询)
  • 负载测试已通过(建议 1000+ 并发)

常见问题

Q1:生产环境应该用 stdio 还是 HTTP?

推荐 HTTP SSE。stdio 模式要求 MCP 客户端与服务器在同一台机器上,通过子进程通信,适合开发和临时使用。生产环境需要远程访问、负载均衡、鉴权和监控,HTTP 模式是唯一选择。如果对延迟极其敏感,可以考虑 stdio + Unix socket,但会失去大部分运维能力。

Q2:多 workers 模式下,SSE 连接如何保持?

SSE 连接的 session 信息存储在单个 worker 的内存中。使用多 workers 时,同一个客户端的 SSE 连接和后续 POST 消息可能到达不同的 worker,导致 session 丢失。

解决方案

  1. Sticky Session:Nginx ip_hash 将同一客户端路由到同一 worker
  2. Redis Session Store:将 session 信息存储在 Redis 中,所有 worker 共享
  3. 单 Worker + 多进程:使用 --workers 1 配合 preload 模式,单进程利用 asyncio 处理高并发
1
2
3
4
5
6
# Nginx sticky session
upstream mcp_backend {
ip_hash; # 同一 IP 始终路由到同一 worker
server 127.0.0.1:8001;
server 127.0.0.1:8002;
}

Q3:MCP 服务器支持哪几种鉴权方式?推荐哪种?

MCP 协议本身不限制鉴权方式。推荐优先级:JWT > API Key > OAuth2 Proxy

  • 个人/小团队:API Key(最简单,写在客户端环境变量中)
  • 企业多用户:JWT(支持角色和过期时间,可细粒度控制权限)
  • 大型组织:OAuth2 代理(集成公司 SSO,零代码改造)

鉴权的最佳实践是在反向代理层(Nginx/Caddy)实现,而不是在应用代码中硬编码——这样切换鉴权方案不需要重启 MCP 服务器。

Q4:Docker 部署时,容器频繁重启怎么办?

排查步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1. 查看容器日志
docker logs mcp-server --tail 100

# 2. 检查健康检查配置
docker inspect mcp-server | jq '.[].State.Health'

# 3. 手动运行健康检查命令
docker exec mcp-server python -c "
import urllib.request
try:
resp = urllib.request.urlopen('http://localhost:8000/health')
print(f'OK: {resp.status}')
except Exception as e:
print(f'FAIL: {e}')
"

# 4. 常见原因
# - 启动时间不足:增加 HEALTHCHECK 的 start_period
# - 端口绑定冲突:检查端口是否被占用
# - 内存不足:检查 dmesg 是否有 OOM Killer 日志
# - 依赖服务未就绪:添加 depends_on + wait-for-it.sh

Q5:如何在不重启的情况下更新 MCP 服务器?

方案一:Docker 滚动更新(零停机)

1
2
3
4
5
# 构建新镜像
docker build -t mcp-server:new .

# 滚动更新(逐个替换容器)
docker compose up -d --no-deps --build --scale mcp-server=4 mcp-server

方案二:进程级热加载

1
2
3
# uvicorn 支持 --reload(仅开发环境)
# 生产环境使用 SIGHUP 信号优雅重启
kill -HUP $(cat /var/run/mcp-server.pid)

方案三:蓝绿部署

1
2
# docker-compose.blue.yml 和 docker-compose.green.yml
# 交替更新,切换 Nginx upstream

Q6:MCP 服务器如何做负载测试?

使用 locustwrk 对 MCP 的 HTTP 端点进行压测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# locustfile.py
from locust import HttpUser, task, between
import json

class MCPUser(HttpUser):
wait_time = between(0.5, 2)

def on_start(self):
"""每个模拟用户先建立 SSE 连接"""
self.client.headers = {
"Authorization": "Bearer sk-test-key",
"Content-Type": "application/json"
}

@task(3)
def list_tools(self):
payload = {
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": 1
}
with self.client.post(
"/mcp/message",
json=payload,
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"Status: {response.status_code}")

@task(7)
def call_tool(self):
payload = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "greet",
"arguments": {"name": "test"}
},
"id": 2
}
with self.client.post(
"/mcp/message",
json=payload,
catch_response=True
) as response:
if response.status_code != 200:
response.failure(f"Status: {response.status_code}")

运行测试:

1
2
3
4
5
6
7
8
9
10
# 安装 locust
pip install locust

# 启动压测(Web UI: http://localhost:8089)
locust -f locustfile.py --host https://mcp.yourdomain.com

# 无界面模式
locust -f locustfile.py --host https://mcp.yourdomain.com \
--headless -u 100 -r 10 --run-time 5m \
--csv mcp-benchmark

Q7:生产环境日志太大,如何管理?

Docker 日志轮转(已在 docker-compose 中配置):

1
2
3
4
5
logging:
driver: "json-file"
options:
max-size: "10m" # 每个日志文件最大 10MB
max-file: "3" # 保留最近 3 个文件

结构化日志 + 外部存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 方案一:直接写入文件 + logrotate
sudo tee /etc/logrotate.d/mcp-server <<EOF
/opt/mcp-server/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
}
EOF

# 方案二:journald 限制(使用 systemd 日志)
sudo journalctl --vacuum-size=500M # 限制日志总大小

# 方案三:接入 Loki(推荐)
# docker-compose 中添加 Loki + Promtail

Q8:客户端提示 SSE connection closed 是什么原因?

常见原因:

  1. 反向代理超时太短 —— Nginx proxy_read_timeout 至少设为 86400s(24 小时)
  2. Docker 网络断开 —— 检查 docker-compose 中的网络配置,确保容器在同一 network
  3. Worker 进程崩溃 —— 检查 journalctl -u mcp-serverdocker logs
  4. 内存不足被 OOM Kill —— dmesg | grep mcp 查看是否有 OOM 信息
  5. 客户端侧网络不稳定 —— 检查客户端是否有代理/VPN 干扰长连接

如果频繁断连,建议实现客户端的自动重连逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 客户端自动重连示例
import asyncio
import sseclient

async def connect_with_retry(url: str, max_retries: int = 5):
for attempt in range(max_retries):
try:
response = requests.get(url, stream=True)
client = sseclient.SSEClient(response)
for event in client.events():
process_event(event)
break
except (ConnectionError, requests.RequestException) as e:
wait = 2 ** attempt # 指数退避
print(f"连接断开,{wait}s 后重试({attempt+1}/{max_retries})")
await asyncio.sleep(wait)

关联阅读

本教程是 MCP 开发部署系列的一部分。推荐按以下顺序阅读:


总结

本教程从生产架构设计出发,完整覆盖了 MCP 服务器从开发环境走向生产环境的全部关键环节:

章节 核心内容 关键收获
架构设计 HTTP SSE + 反向代理 + 负载均衡 知道生产架构是什么样的
反向代理 Nginx / Caddy 配置 掌握 TLS 终止和请求路由
鉴权方案 API Key / JWT / OAuth2 能按需选择鉴权方式
进程守护 systemd 服务配置 MCP 服务器自动恢复
日志与监控 结构化日志 + Prometheus 可观测性体系
Docker 部署 多阶段构建 + 健康检查 容器化生产部署
多租户隔离 ContextVar / 独立进程 / K8s 了解三种隔离方案
性能调优 连接池 / 限流 / 超时 生产级性能配置

下一步

  • 安全审计:定期检查依赖库的 CVE 漏洞(pip audit / safety check
  • 容灾演练:模拟服务器宕机、网络分区等故障场景
  • 自动化部署:编写 Ansible Playbook 或 Terraform 模板
  • Serverless 方案:探索将 MCP 服务器部署到 AWS Lambda 或 Cloudflare Workers

本文链接: https://geniux.top/2026/07/20/MCP-服务器生产部署指南/
版权声明: 自由转载,请保留原文链接和作者信息。