McpHub:MCP 服务器生命周期管理
职责
McpHub 是 Cline 接入外部 MCP 服务器的中央枢纽。它负责读取 cline_mcp_settings.json 里配置的所有服务器,为每个服务器建立一条独立的 client + transport 连接,然后把暴露出来的工具 (tool)、资源 (resource)、提示 (prompt) 缓存到内存里供主 agent 调用。整条 MCP 工具链——从配置变更、文件监听、连接/重连、能力发现、到 tools/call 调用——都收敛到这一个类,主 Task 不需要直接碰 MCP SDK。
它的位置在「主 agent loop」和「具体 MCP 协议」之间。Task 想调一个 MCP 工具时,通过 McpHub.callTool (callTool:1233) 走出去;UI 想看当前服务器列表,通过 getServers() 拿到一个排序后的快照 (getServers:112)。一个 Extension 实例里只有一个 McpHub,但 connections: McpConnection[] 数组里每条都是独立 transport,挂了不影响别的。
设计动机
- 每服务器一个 client:不同 MCP 服务器有不同 capabilities、不同配置、不同错误处理,独立 client 让重连和作用域隔离都简单 (
per-server client:363)。 - 统一三种 transport:
stdio跑本地进程、sse走 Server-Sent Events、streamableHttp用新版 HTTP 流,在connectToServer的switch里分发 (transport switch:382)。 - **文件监听 + 内部更新双通道
watchMcpSettingsFile监听外部编辑器改配置,isUpdatingClineSettings标记内部自己写时跳过触发 (isUpdatingClineSettings:76),避免回环。 - OAuth 按需懒加载:只有
sse/streamableHttp才找McpOAuthManager要 authProvider,stdio不走 (authProvider setup:377)。 - 企业策略前置于连接:企业部署可以禁 marketplace、设白名单,在 stdio 连接前就拦截掉 (
enterprise allowlist:310)。 - UID 缩短工具名:每个服务器分配一个
c+ nanoid(5) 的短 uid,避免 MCP 工具名拼成长串 (getMcpServerKey:130)。
关键文件
McpHub class:51— 单例类,持有 connections、文件监听器、OAuth manager 和通知回调。constructor:108— 启动时立刻watchMcpSettingsFile+initializeMcpServers。watchMcpSettingsFile:213— 用 chokidar 监听配置文件改动,diff 出增/删/改触发重连。connectToServer:286— 核心连接方法,校验、选 transport、建 client、发现能力。transport switch:382— stdio / sse / streamableHttp 三分支。stdio stderr pipe:415— stdio 服务器把 stderr 拉出来当 info/error 日志流。fetchToolsList:677— 发tools/list,并给每个 tool 标autoApprove标记。fetchResourcesList:713— 发resources/list,disabled 服务器直接返空。notification handler:611— 注册notifications/message回调,把服务器日志塞给当前 Task 或暂存。deleteConnection:784— 关 transport + client,从 connections 里过滤掉。restartConnection:1047— 重新连一个服务器,带 500ms 人为延迟让 UI 看到「connecting」状态。callTool:1233— 发tools/call,按服务器配置取超时,全程埋 telemetry。
数据流
connectToServer 进来先做三件事:校验企业策略、构造 disabled 占位或真实 client、选 transport。transport 选完建 McpConnection 推进数组,然后 client.connect(transport) 真正握手。握手成功后才注册通知回调并拉四份能力清单:
// apps/vscode/src/services/mcp/McpHub.ts
connection.server.status = "connected"
connection.server.error = ""
// Register notification handler for real-time messages
try {
// ...
connection.client.setNotificationHandler(NotificationMessageSchema as any, async (notification: any) => {
// ...
if (this.notificationCallback) {
this.notificationCallback(name, level, message)
} else {
this.pendingNotifications.push({ serverName: name, level, message, timestamp: Date.now() })
}
})
} catch (error) {
Logger.error(`[MCP Debug] Error setting notification handlers for ${name}:`, error)
}
// Initial fetch of tools, resources, and prompts
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
connection.server.prompts = await this.fetchPromptsList(name)这段在 post-connect setup:584 附近。连接建好后,外部工具调用都走 callTool——查连接、按 config.timeout 算超时、发 tools/call (tools/call request:1270)。资源读类似,走 resources/read (resources/read:1183)。
边界与失败
- OAuth 缺失即降级:
client.connect抛UnauthorizedError时,不报错而是把 connection 标成oauthRequired: true,等用户在前端授权 (UnauthorizedError branch:556)。 - disabled 服务器占位但不连接:disabled 配置仍进 connections 数组以便 UI 显示,但 client/transport 都是
null(disabled placeholder:339)。 - stdio stderr 分类:stderr 里出现
error字样才记 error,否则当 info log (stderr classification:420)。 - streamableHttp 404 当 405:很多服务器把 SSE 不支持时该返 405 错返 404,这里在 fetch 层把 404 归一成 405 让 SDK 接受 (
404 to 405:499)。 - SSE token 动态刷新:sse 分支用自定义 fetch 每次现取
authProvider.tokens(),否则 token 过期后重连会一直 401 (dynamic token fetch:459)。 - streamableHttp 重连交给 handler:
StreamableHttpReconnectHandler拿到connectToServer闭包,自己决定重连节奏 (reconnect handler:519)。 - 通知暂存:没有活动 Task 时,服务器来的 notification 先存
pendingNotifications,等下一个 Task 起来再取 (pendingNotifications:631)。
小结
McpHub 把 MCP 协议细节全收进自己,对主 agent 暴露 callTool/readResource/getServers 三个口子。想看它如何被工具层包装成 use_mcp_tool 可以读 cap-tools/use-mcp-tool;想看 subagent 如何隔离自己的上下文窗口可以读 subagent;想看主 Task 怎么把 MCP 服务器列表塞进系统提示可以读 agent-loop/task-class。