MCP Client 集成模式
MCP Client 集成模式
学习目标
理解 Khoj 如何通过 MCPClient 类实现 MCP 协议的 stdio 和 SSE 双传输模式,以及连接生命周期管理。
前置知识
本章涉及 MCP 协议与工具调用的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 Khoj 的 MCP 客户端实现。
项目实践
MCPClient 架构
Khoj 的 MCPClient(src/khoj/processor/tools/mcp.py,约 125 行)是一个轻量级 MCP 客户端封装,核心职责是连接 MCP 服务器、发现工具列表、执行工具调用。
传输模式自动检测
connect() 方法根据 path 的格式自动选择传输方式:
path 以 http:// 或 https:// 开头 → SSE 模式:sse_client(url) → ClientSession → initialize()
path 是本地文件路径或 npm 包名 → stdio 模式: .py 文件 → python script.py .js 文件 → node script.js npm 包 → npx package-name → stdio_client(ServerParameters) → ClientSession → initialize()这种设计让用户只需指定一个路径,系统自动判断使用哪种传输方式——零配置。
AsyncExitStack 生命周期管理
stdio 模式的连接使用 AsyncExitStack 管理多个异步上下文:
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))self.stdio, self.writer = stdio_transportself.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.writer))AsyncExitStack 确保在 close() 时所有资源按相反顺序正确释放——先关闭 session,再关闭 stdio 管道。这避免了资源泄漏,尤其是在多 MCP 服务器同时连接时。
多模态结果处理
run_tool() 根据返回内容类型做不同处理:
| 内容类型 | 处理方式 |
|---|---|
TextContent | 直接返回文本列表 |
ImageContent | 转为 {data: base64, format: mimeType} 字典 |
AudioContent | 转为 {data: base64, format: mimeType} 字典 |
这使得上层代码可以统一处理多模态工具结果,无需关心底层 MCP 协议的细节。
数据库集成
MCP 服务器配置存储在 McpServer 模型中:
McpServer: - name: 服务器名称 - path: 连接路径(本地路径或 URL) - api_key: 可选的 API 密钥 - user: 所属用户每个用户可以配置多个 MCP 服务器,这些服务器的工具会动态发现并加入到对话工具集中。
问题与规避
| 问题 | 影响 | 规避策略 |
|---|---|---|
| stdio 模式 npx 首次执行慢 | 首次工具调用延迟高 | npx 会自动下载包,后续调用使用缓存 |
| SSE 长连接断线 | 工具调用失败 | 需要在调用层实现重连(当前代码未实现) |
| 多个 MCP 服务器并发 | 资源竞争 | 每个 MCPClient 实例独立管理自己的连接 |
| Python stdio 服务器的依赖 | 需要确保 Python 环境和依赖已安装 | 部署文档中需要说明 |
设计取舍
为什么选择轻量级封装而非完整的 MCP 客户端库?
Khoj 的 MCPClient 只有 125 行,是对 mcp Python SDK 的极简封装。原因是 MCP 协议本身已经通过 SDK 提供了高层抽象,Khoj 只需要处理连接管理和结果转换。这种策略减少了维护负担,但也意味着一些高级功能(如订阅、通知、采样)暂未支持。
为什么 path 同时支持本地路径和 URL?
MCP 协议本身定义了 stdio 和 SSE 两种传输。Khoj 将两者统一到一个 path 参数下,用户无需关心底层细节。这简化了配置,但也意味着用户无法显式指定传输方式——如果本地文件需要通过 SSE 提供,就需要搭建额外的 HTTP 服务器。