MCP Server 开发实战:从零构建一个生产级 MCP 服务
快速上手:5 分钟写一个 MCP Server
# server.py
from mcp.server import Server, Tool
from mcp.server.stdio import stdio_server
server = Server("weather-server")
@server.tool()
async def get_weather(city: str, unit: str = "celsius") -> str:
"""获取指定城市的天气信息。
Args:
city: 城市名称,如 "北京"、"上海"
unit: 温度单位,celsius 或 fahrenheit
"""
# 实际调用天气API...
return f"{city}: 晴,28°C,湿度 45%"
@server.tool()
async def get_forecast(city: str, days: int = 3) -> dict:
"""获取未来几天天气预报"""
return {"city": city, "forecast": [...]}
async def main():
async with stdio_server() as (read, write):
await server.run(read, write)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
在 Claude Desktop 中配置
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["path/to/server.py"],
"env": { "API_KEY": "xxx" }
}
}
}
MCP Server 的生产级考量
1. 错误处理与重试
外部 API 不可靠是常态。每个工具调用都应有超时、重试、降级策略:
@server.tool()
async def get_weather(city: str):
for attempt in range(3):
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"https://api.weather.com/{city}")
return resp.json()
except httpx.TimeoutException:
if attempt == 2:
return {"error": "天气服务暂时不可用,请稍后重试"}
await asyncio.sleep(2 ** attempt)
2. 认证与权限
生产环境的 MCP Server 需要处理认证:
- API Key 注入:通过环境变量传入,不在代码中硬编码
- 工具级权限:危险操作(写操作)可设置 require_confirmation
- 用户隔离:多租户场景下按 user_id 隔离数据和配额
3. 资源与工具的正确设计
| 类型 | 何时用 | 示例 |
|---|---|---|
| Tool | 有副作用的操作、需要参数的计算 | 发送邮件、创建订单、查询数据库 |
| Resource | 只读数据、可以被 LLM 直接消费的内容 | 文件内容、知识库文档、配置项 |
| Prompt | 可复用的提示模板 | 代码审查模板、SQL优化模板 |
4. 日志与可观测性
stdio 传输模式下,stderr 是唯一的日志通道(stdout 被协议占用):
import logging
logging.basicConfig(
level=logging.INFO,
stream=sys.stderr, # 注意:输出到 stderr
format="%(asctime)s [%(levelname)s] %(message)s"
)
MCP Server 的 SSE 模式
stdio 适合本地,SSE 适合远程服务和多客户端:
# SSE Server
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route
transport = SseServerTransport("/messages")
async def handle_sse(request):
async with transport.connect_sse(request.scope, request.receive, request._send) as streams:
await server.run(streams[0], streams[1])
app = Starlette(routes=[
Route("/sse", endpoint=handle_sse),
Route("/messages", endpoint=transport.handle_post_message, methods=["POST"]),
])
MCP 与其他协议的对比
| 协议 | 定位 | 适用场景 |
|---|---|---|
| MCP | LLM ↔ 工具/数据标准化 | Agent 工具集成 |
| A2A (Agent-to-Agent) | Agent ↔ Agent 通信 | 多Agent协作 |
| OpenAPI/Swagger | HTTP REST API 描述 | 传统微服务 |
| gRPC | 高性能 RPC | 内部服务通信 |
常见坑与对策
- 坑:stdio 缓冲区阻塞 — 大量输出时 stdout 缓冲区满导致 Server 卡死。对策:及时 flush;分批返回大数据
- 坑:SSE 连接断开无感知 — SSE 是单向的,Server 无法主动知道 Client 离线。对策:加心跳机制
- 坑:工具定义 Schema 过于宽松 — LLM 容易"钻空子"传入意料外的参数。对策:尽量用 enum、pattern 等约束
- 坑:Server 进程泄露 — 子进程 MCP Server 未正确清理。对策:Host 退出时给子进程发 SIGTERM,设置进程组