MCP踩坑记录

MCP踩坑记录

1 背景

基于 MCP 标准协议实现了知识检索 Server,解决了 stdio 传输下的 GIL 死锁、日志污染协议流、同步阻塞操作三个生产级问题,支持 GitHub Copilot / Claude Desktop 即插即用调用私有文档库。

2 stdio 传输的 GIL 死锁问题

这是 MCP Server 中最容易被忽视的生产问题:

python 复制代码
# server.py --- _preload_heavy_imports()
def _preload_heavy_imports():
    """在 anyio 启动前预加载重型模块,避免 stdio 读取线程和 import 锁竞争死锁。
    
    原理:MCP SDK 用 anyio 的 stdio 传输,内部有一个后台线程在读 stdin。
    如果某个工具被执行时才 import chromadb(它又 import onnxruntime),
    这个 import 可能卡住 2-5 秒。与此同时 stdin-reader 线程也在等------
    两个线程都在等对方释放 GIL,就死锁了。
    
    解决:在主线程、任何 async 线程启动前,先把所有重型模块 import 完。
    """
    import chromadb           # 启动慢,但 import 一次就够了
    import src.libs.splitter  # 触发 langchain 导入
    import src.core.query_engine  # 触发所有检索器导入

为什么其他模块没有这个问题? ------普通 HTTP Server 每个请求是一个新线程/进程,import 阻塞只影响那个请求。但 MCP 的 stdio 传输是单进程 + 后台 reader 线程 + JSON-RPC over stdin/stdout,reader 线程一直在等输入,worker 线程一直在等 import 锁,形成死锁。

3 日志污染协议流

python 复制代码
# server.py --- _redirect_all_loggers_to_stderr()
def _redirect_all_loggers_to_stderr():
    """MCP 协议规定 stdout 只能发送 JSON-RPC 消息。
    如果任何 logger 往 stdout 写日志,会把日志混进 JSON 流里,
    客户端解析 JSON 失败就会断开连接。
    
    解决:把所有 logger 的 StreamHandler 重定向到 stderr。
    """
    root = logging.getLogger()
    stderr_handler = logging.StreamHandler(sys.stderr)
    # 移除所有 stdout handler
    for handler in root.handlers[:]:
        if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler):
            root.removeHandler(handler)
    root.addHandler(stderr_handler)

踩坑经历 :我第一次跑 MCP Server 时就遇到了 Invalid JSON 错误,排查了 2 小时才发现是 httpx 库的 logger 往 stdout 打了请求日志。

4 异步桥接模式

python 复制代码
# query_knowledge_hub.py --- execute()
async def execute(self, query, top_k=5, collection=None):
    # 所有阻塞操作都桥接到线程池,不阻塞事件循环
    await asyncio.to_thread(self._ensure_initialized, collection)
    results = await asyncio.to_thread(self._perform_search, query, top_k, trace)
    if self.config.enable_rerank and results:
        results = await asyncio.to_thread(self._apply_rerank, query, results, top_k, trace)
    response = self._response_builder.build(results, query, collection)
    return response

为什么用 asyncio.to_thread 而不是 async def? ------因为 hybrid_search.search() 内部调用了 ChromaDB 的同步 API 和 httpx 的同步请求,这些库没有 async 版本。asyncio.to_thread 把它们放到线程池中执行,不阻塞 MCP 的事件循环。

5 即插即用的过程

json 复制代码
// Claude Desktop 配置 (claude_desktop_config.json)
{
  "mcpServers": {
    "knowledge-base": {
      "command": "python",
      "args": ["-m", "src.mcp_server"],
      "env": {
        "SETTINGS_PATH": "/path/to/settings.yaml"
      }
    }
  }
}

用户在 Claude Desktop 里加这个配置后,Claude 就能直接调用 query_knowledge_hub 工具搜索私有文档库。工具的描述(TOOL_DESCRIPTION)会自动展示给 Claude 的 system prompt,Claude 就知道什么时候该调用这个工具。

相关推荐
浮链序13 小时前
Shopify 把 10 亿商品开放给任意 agent 了:我用零依赖 Python 撸了个带预算闸门的代购网关
后端·代码规范
这个DBA有点耶1 天前
ELT 工具选型完整指南:从 ETL 差异分析到工程落地
数据库·架构·代码规范
这个DBA有点耶2 天前
分布式事务深度:TCC空回滚、悬挂与Saga补偿风暴的根因与解法
数据库·mysql·代码规范
这个DBA有点耶4 天前
为什么UPDATE比SELECT危险得多?锁范围等于扫描行数
数据库·架构·代码规范
沈麽鬼4 天前
纸上代码
代码规范
深圳老胡5 天前
STM32CubeMX 生成 CMake 工程,用 VSCode 编译与调试的完整流程
笔记·stm32·单片机·代码规范
星星泡饭吃6 天前
【信息安全】越权防护设计评审指南
代码规范
GlobalSign数字证书6 天前
申请 EV 代码签名证书需要准备哪些材料?
代码规范
深圳老胡6 天前
STM32F4 OTA 升级双 App 方案设计与实现
笔记·stm32·单片机·代码规范
深圳老胡6 天前
STM32F4 OTA升级:单App与双App方案优缺点对比
笔记·stm32·单片机·代码规范