前两篇文章聊了 MCP 协议本身和生态盘点,很多读者私信问同一个问题:“协议我懂了,Demo 也跑通了,但怎么把一个 MCP Server 真正部署到生产环境?”
这是一个非常关键的问题。MCP 协议只定义了"模型怎么调用工具",但生产环境要考虑的东西远不止协议本身:
谁能调用你的工具?——鉴权
调用频率怎么控制?——限流
返回的简历里带着手机号怎么办?——数据脱敏
出问题了怎么排查?——审计日志
多个租户的数据会不会串?——会话隔离
服务怎么上线、扩容、滚动更新?——容器化 + K8s
今年我们在招聘平台落地了企业级 MCP 服务(简历检索、人才画像、职位查询等),把这些能力全部沉淀成了一个开源模板 fastmcp-enterprise 。这篇文章就把整个落地过程讲透,所有代码都能直接跑。
一、先看整体架构#
企业级 MCP Server 不是"一个 FastMCP 实例",而是一组横切能力 + 业务 Tools 的组合。fastmcp-enterprise 的架构长这样:
┌
│
│
└
┌
│
│
│
│
│
│
│
│
│
│
│
└
┌
│
└
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
┌
│
└
┌
│
│
└
┌
│
└
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
鉴
─
─
─
─
─
─
─
─
─
─
─
─
权
─
─
简
─
─
─
─
─
─
─
─
─
─
─
─
历
─
─
─
─
─
D
─
─
─
─
─
─
─
检
─
─
─
─
─
e
─
─
C
─
─
─
─
─
索
─
─
─
─
─
p
─
─
l
─
─
─
─
─
─
─
─
─
─
l
─
─
a
─
─
─
─
─
│
─
─
─
─
─
o
─
─
u
─
─
─
│
─
─
─
─
─
─
─
y
─
─
d
─
─
┐
┘
─
人
─
─
─
─
─
m
─
─
e
─
─
│
─
才
─
─
─
─
─
e
─
─
─
─
┌
└
─
画
─
─
─
─
─
n
─
─
C
─
─
F
─
限
─
─
像
─
─
─
─
─
t
─
─
o
─
─
a
─
流
─
─
─
─
─
─
─
─
─
d
─
─
s
─
─
─
业
│
─
─
响
─
─
─
│
─
─
e
─
─
t
─
─
─
务
─
─
应
─
─
─
─
─
─
─
M
─
─
─
健
─
─
脱
─
─
─
S
─
─
客
/
─
─
C
─
─
─
T
康
─
─
敏
─
─
─
e
─
─
户
─
─
P
─
─
─
o
检
─
─
中
─
─
─
r
─
─
端
C
┬
│
▼
─
│
─
─
o
查
─
─
间
─
┬
│
▼
v
─
─
(
u
─
─
S
─
─
─
l
─
─
件
─
─
─
i
─
─
L
r
─
M
─
e
┐
│
┘
─
s
│
─
─
─
─
D
─
c
─
─
L
s
─
C
─
r
─
─
─
(
─
─
o
─
e
─
─
M
o
─
P
─
v
┌
审
└
─
层
你
─
─
P
─
─
c
─
─
─
r
─
─
e
─
计
─
─
的
─
─
I
─
─
k
─
│
─
─
A
─
协
─
r
─
日
─
─
业
─
─
I
─
─
e
─
─
─
g
/
─
议
─
─
志
─
─
务
─
─
)
─
─
r
─
I
─
─
e
─
─
(
─
─
─
工
─
─
─
─
─
n
─
─
n
自
─
(
─
F
─
│
─
─
具
─
─
─
─
/
─
g
─
─
t
研
─
H
─
a
─
─
─
─
─
─
─
─
r
─
─
)
─
T
─
s
─
│
─
─
─
─
─
─
K
─
e
─
─
A
─
T
─
t
─
─
─
─
─
─
─
8
─
s
─
─
g
─
P
─
M
─
会
─
─
─
─
─
─
s
─
s
─
─
e
─
─
C
┐
话
┘
─
─
─
─
─
─
─
─
n
─
/
─
P
隔
─
│
─
─
─
─
─
│
─
─
t
─
─
┌
离
└
─
─
─
─
─
─
─
─
─
S
─
4
─
─
─
─
─
─
─
─
C
─
─
─
T
─
.
─
─
─
│
─
─
─
─
─
I
─
─
─
D
─
x
─
─
─
─
─
─
─
─
/
─
─
─
I
─
)
─
─
─
─
─
─
─
─
C
─
─
─
O
─
─
─
─
─
─
─
─
─
D
─
─
─
)
─
─
│
─
─
─
─
│
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
│
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
│
─
─
─
─
─
─
─
─
─
─
│
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
─
┐
┘
┐
┘
─
─
─
─
│
─
─
─
─
│
─
─
─
─
─
─
┐
┘
─
─
─
┐
│
┘
┐
│
│
│
│
┘
┐
┘
│
│
│
│
关键点:横切能力通过中间件注入,业务 Tools 保持纯粹 。这样新接一个业务工具,不需要改任何鉴权/限流/脱敏代码。
二、FastMCP 4.x 的中间件机制#
FastMCP 4.x 引入了钩子式中间件(Middleware),这是构建企业级能力的关键。它不再是"装饰器套装饰器",而是提供了一套生命周期钩子:
on_initialize:初始化阶段
on_request:每个请求进入时
on_message:消息层面
on_call_tool:工具被调用时(重点!)
on_read_resource / on_get_prompt:资源和提示词
来看一个最简单的审计中间件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from fastmcp.server.middleware import Middleware, MiddlewareContext
class AuditMiddleware (Middleware):
"""记录每次工具调用的审计日志。"""
async def on_call_tool (self, ctx: MiddlewareContext):
start = time. time()
result = await super(). on_call_tool(ctx) # 调用下一个中间件/业务逻辑
duration_ms = (time. time() - start) * 1000
# 记录:谁、什么时候、调了什么工具、耗时多久
self. logger. info(
f "[audit] client= { ctx. client_id} tool= { ctx. message. params. name} "
f "duration= { duration_ms: .1f } ms"
)
return result
中间件是洋葱模型 :super().on_call_tool(ctx) 往下走,返回时再处理后置逻辑。鉴权、限流、审计、会话隔离全部按这个模式写,然后 mcp.add_middleware(...) 一层层挂上去。
三、企业级鉴权:Bearer Token#
生产环境第一件事就是:不是谁都能调你的工具 。FastMCP 4.x 通过 AuthProvider 提供标准化鉴权,我们实现了一个 BearerTokenAuthProvider:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from fastmcp.server.auth import AccessToken
from mcp.server.auth.provider import AuthProvider
class BearerTokenAuthProvider (AuthProvider):
"""基于静态 Token 列表的鉴权(可替换为 JWT / OAuth2)。"""
def __init__(self, tokens: list[str], scopes: list[str] | None = None ):
self. _tokens = set(tokens)
self. required_scopes = scopes or ["read" , "write" ]
async def verify_token (self, token: str) -> AccessToken | None :
if token in self. _tokens:
return AccessToken(
token= token,
client_id= self. _derive_client_id(token), # 稳定标识
scopes= self. required_scopes,
)
return None # 无效 token → 401
注意一个坑:AccessToken 在 FastMCP 4.x 里必须提供 client_id ,否则会抛 Pydantic 校验错误。我们从 token 哈希派生一个稳定 client_id,既满足协议又不暴露原始 token。
四、限流:滑动窗口#
生产环境要防滥用。我们实现了一个进程内的滑动窗口限流器,按客户端维度限流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
import threading
from collections import deque
class SlidingWindowRateLimiter :
"""滑动窗口限流器:固定窗口 + 细粒度滑动,避免边界突刺。"""
def __init__(self, max_requests: int, window_seconds: int = 60 ):
self. max_requests = max_requests
self. window_seconds = window_seconds
self. _hits: dict[str, deque] = {} # client_id -> 时间戳队列
self. _lock = threading. Lock()
def allow (self, client_id: str) -> bool:
now = time. time()
with self. _lock:
q = self. _hits. setdefault(client_id, deque())
while q and q[0 ] < now - self. window_seconds:
q. popleft()
if len(q) >= self. max_requests:
return False
q. append(now)
return True
为什么不用固定窗口? 固定窗口有一个经典问题:如果限制 60 次/分钟,用户在 59:59 和 00:01 各发 60 次,实际 2 秒内打了 120 次。滑动窗口让"限流"更平滑。生产环境多实例部署时,把 deque 换成 Redis ZSET 即可(模板预留了 REDIS_ENABLED 开关)。
五、数据脱敏:PII 保护#
招聘平台最敏感的是简历里的手机号、身份证、邮箱 。LLM 工具调用的响应要过一层脱敏中间件:
1
2
3
4
5
6
7
8
9
10
11
12
import re
_PHONE = re. compile(r "(?<!\d)1[3-9]\d {9} (?!\d)" )
_IDCARD = re. compile(r "(?<!\d)\d {17} [\dXx](?!\d)" )
_EMAIL = re. compile(r "([\w.+-]+)@([\w-]+\.)+[\w-]+" )
def desensitize_value (value: str) -> str:
"""对字符串做 PII 脱敏。"""
value = _PHONE. sub(lambda m: m. group(0 )[:3 ] + "****" + m. group(0 )[- 4 :], value)
value = _IDCARD. sub(lambda m: m. group(0 )[:6 ] + "********" + m. group(0 )[- 4 :], value)
value = _EMAIL. sub(lambda m: m. group(1 )[:2 ] + "***@" + m. group(2 ), value)
return value
脱敏中间件放在中间件链的最外层 ,确保无论业务返回什么,最终给到 LLM 的都是脱敏后的数据:
1
2
3
4
5
6
7
8
9
10
class DesensitizeMiddleware (Middleware):
"""响应脱敏中间件:保护手机号/身份证/邮箱等 PII。"""
async def on_call_tool (self, ctx: MiddlewareContext):
result = await super(). on_call_tool(ctx)
# 对 text 输出逐条脱敏
for content in result. content:
if getattr(content, "type" , "" ) == "text" :
content. text = desensitize_value(content. text)
return result
效果(真实测试):
原
脱
值
敏
:
:
1
1
3
3
9
9
1
*
2
*
3
*
4
*
4
4
3
3
2
2
1
1
l
l
i
i
n
*
z
*
h
*
u
@
a
e
n
x
g
a
@
m
e
p
x
l
a
e
m
.
p
c
l
o
e
m
.
c
o
m
3
3
3
3
0
0
1
1
0
0
6
6
1
*
9
*
9
*
0
*
0
*
1
*
0
*
1
*
1
1
2
2
3
3
4
4
六、会话隔离:多租户不串数据#
企业 MCP 服务往往是多租户的——不同客户看到的是自己的简历库。我们在中间件里注入 client_id 到请求上下文,业务工具通过 ctx.request_state 读取当前租户:
1
2
3
4
5
6
7
8
class SessionIsolationMiddleware (Middleware):
"""会话隔离:把 client_id 注入请求上下文,业务侧按租户过滤。"""
async def on_request (self, ctx: MiddlewareContext):
request_state = ctx. request_state
if request_state:
request_state. client_id = ctx. client_id # 来自鉴权
return await super(). on_request(ctx)
业务工具侧:
1
2
3
async def resume_search (query: ResumeQuery, ctx: Context) -> dict:
tenant = ctx. request_state. client_id if ctx. request_state else "default"
# 只查当前租户的简历数据 ...
一个容易踩的坑 :在 FastMCP 4.x 里,函数参数里注入 Context 必须显式标注类型 ctx: Context ,否则 FastMCP 会把它当成业务参数,报 Missing required argument。
七、业务工具:保持纯粹#
有了横切能力,业务工具就只需要关心业务本身。以简历检索为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
class ResumeQuery (BaseModel):
keyword: str = Field(description= "搜索关键词" )
min_years: int = Field(default= 3 , description= "最低工作年限" )
limit: int = Field(default= 10 , description= "返回条数" )
@mcp.tool ()
async def resume_search (query: ResumeQuery) -> dict:
"""按关键词和年限检索简历(企业级 MCP 示例工具)。"""
candidates = demo_db. filter(keyword= query. keyword, min_years= query. min_years)
return {
"total" : len(candidates),
"items" : [c. model_dump() for c in candidates[: query. limit]],
}
八、部署:Docker + K8s#
Dockerfile(非 root 运行)#
1
2
3
4
5
6
7
8
9
10
11
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src ./src
RUN pip install -e .
# 非 root 用户,生产安全基线
RUN useradd -m appuser && chown -R appuser /app
USER appuser
EXPOSE 8080
CMD ["python" , "-m" , "enterprise_mcp.server" ]
K8s Deployment#
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
apiVersion : apps/v1
kind : Deployment
metadata :
name : enterprise-mcp
spec :
replicas : 2
selector :
matchLabels :
app : enterprise-mcp
template :
metadata :
labels :
app : enterprise-mcp
spec :
containers :
- name : server
image : your-registry/enterprise-mcp:latest
ports :
- containerPort : 8080
envFrom :
- secretRef :
name : mcp-secrets # Token 走 Secret 注入
resources :
requests : {cpu : "100m" , memory : "128Mi" }
limits : {cpu : "500m" , memory : "512Mi" }
readinessProbe :
httpGet : {path: /mcp, port : 8080 }
CI 自动化#
GitHub Actions 里跑测试,自动构建镜像:
1
2
3
4
5
6
7
8
9
10
11
name : CI
on : [push, pull_request]
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : actions/setup-python@v5
with : {python-version : "3.12" }
- run : pip install -e ".[test]"
- run : pytest tests/ -v
九、本地验证全流程#
模板自带 13 个单元 + 集成测试:
1
2
pytest tests/ -v
# 13 passed
启动 HTTP 服务,用官方 MCP 客户端连接:
1
AUTH_ENABLED = true API_TOKENS = sk-test-001 python -m enterprise_mcp.server
1
2
3
4
5
6
7
8
from fastmcp import Client
async with Client("http://localhost:8080/mcp" , auth= "sk-test-001" ) as client:
tools = await client. list_tools()
result = await client. call_tool(
"resume_search" ,
{"query" : {"keyword" : "MCP" , "min_years" : 5 }},
)
不带 token → 401 拒绝 ✅
带有效 token → 正常调用 ✅
响应脱敏 → 手机号/邮箱被打码 ✅
十、模板结构速览#
f
├
│
│
│
│
│
├
│
│
├
└
a
─
─
─
─
s
─
─
─
─
t
m
s
├
├
├
├
├
d
├
└
t
e
c
r
─
─
─
─
─
e
─
─
e
x
p
c
─
─
─
─
─
p
─
─
s
a
-
/
l
t
m
e
e
s
c
a
m
t
o
d
k
s
p
n
n
e
o
u
i
o
y
8
/
l
t
t
r
n
t
d
o
c
s
e
e
e
v
f
h
d
l
k
/
s
r
r
e
i
/
l
s
e
/
p
p
r
g
e
/
r
r
r
.
/
w
/
i
i
p
s
a
s
s
y
e
r
e
e
t
e
/
_
t
/
m
i
c
n
p
g
/
s
.
p
y
#
#
#
#
#
#
#
#
#
入
配
鉴
审
示
D
D
单
客
口
置
权
计
例
o
e
元
户
:
管
(
业
c
p
端
组
理
T
务
k
l
+
调
装
(
工
e
o
用
全
e
k
脱
具
r
y
集
示
部
n
e
敏
(
f
m
成
例
能
v
n
简
i
e
测
力
/
历
l
n
试
校
检
e
t
验
会
索
.
话
等
+
+
e
+
隔
)
n
离
d
S
v
A
o
e
)
u
/
c
r
t
k
v
h
限
e
i
P
流
r
c
r
-
e
o
c
v
o
+
i
m
d
p
I
e
o
n
r
s
g
)
e
r
e
s
s
十一、项目地址#
完整可运行代码已开源(MIT 协议,欢迎 Star / PR):
从"能跑"到"能上线",MCP Server 还有不少路要走。核心思路是:横切能力中间件化,业务工具纯粹化 。鉴权、限流、脱敏、审计、会话隔离这些企业级能力,用 FastMCP 4.x 的钩子中间件可以优雅地组合起来,再加上 Docker/K8s/CI,一个生产级的 MCP 服务就成型了。
如果你正在做企业级 MCP 落地,欢迎在评论区交流踩过的坑。下一篇我会写如何用 Redis 把限流和会话做到多实例共享 ,以及 JWT 鉴权在 MCP 里的实践 ,欢迎关注。
延伸阅读
💡 相关推荐 :把 MCP Server 部署到生产环境需要稳定的云服务器,可看看腾讯云近期活动:腾讯云活动
微信公众号「福清而不淡」
后端架构 · AI 工程 · 云原生的一线实践,扫码关注,不错过更新。
本文已同步发布到公众号,欢迎留言交流。