跳转到内容

MCP Client 集成模式

MCP Client 集成模式

学习目标

理解 Khoj 如何通过 MCPClient 类实现 MCP 协议的 stdio 和 SSE 双传输模式,以及连接生命周期管理。

前置知识

本章涉及 MCP 协议与工具调用的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 Khoj 的 MCP 客户端实现。

项目实践

MCPClient 架构

Khoj 的 MCPClientsrc/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_transport
self.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 服务器。

参考来源