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 就知道什么时候该调用这个工具。

相关推荐
十二同学啊2 天前
Go 工程化详解:项目结构、代码规范、测试体系与 CI/CD 实践
ci/cd·golang·代码规范
这个DBA有点耶3 天前
为什么删掉索引后,查询瞬间变快了?探讨MySQL的随机I/O与顺序I/O博弈
数据库·mysql·代码规范
加宝的起点雷达3 天前
40 行原子写入:为什么临时文件必须写在同一个目录
人工智能·代码规范
电子科技圈6 天前
2026 RISC-V中国峰会观察:编译调试工具链的“在场节奏”,释放了什么信号?
大数据·人工智能·设计模式·软件工程·软件构建·代码规范·risc-v
ZhengEnCi8 天前
如何审核 AI 生成的代码:一份可落地的 6 层检查清单
ai编程·代码规范
挖掘狂人8 天前
AI 代码上线前,真正的瓶颈不在 "写",而在 "验"
ai编程·测试·代码规范
深圳老胡9 天前
STM32C5A3ZG 网络调试总结:从启动失败到连续 Ping 稳定
笔记·stm32·单片机·代码规范
big男孩10 天前
autocode 使用指南
代码规范
这个DBA有点耶10 天前
备份成功≠数据可恢复:静默损坏会让物理备份把坏页一起带走
数据库·mysql·代码规范
深圳老胡10 天前
STM32F103 硬件 SPI 与软件 IO 模拟 SPI 最高速度差异详解
笔记·stm32·单片机·代码规范·1024程序员节