McpHub: gestión del ciclo de vida de servidores MCP
Responsabilidades
McpHub es el núcleo central por el que Cline se conecta a servidores MCP externos. Se encarga de leer todos los servidores configurados en cline_mcp_settings.json, establecer para cada uno una conexión independiente de client + transport, y luego cachear en memoria las herramientas (tool), recursos (resource) y prompts (prompt) que exponen, de modo que el agente principal pueda invocarlos. Toda la cadena MCP —desde cambios de configuración, file watching, conexión/reconexión, descubrimiento de capacidades, hasta la llamada tools/call— converge en esta única clase; el Task principal no necesita tocar el SDK de MCP directamente.
Su posición se sitúa entre «el agent loop principal» y «el protocolo MCP concreto». Cuando el Task quiere invocar una herramienta MCP, lo hace a través de McpHub.callTool (callTool:1233); cuando la UI quiere consultar la lista actual de servidores, llama a getServers() y obtiene una instantánea ordenada (getServers:112). En cada instancia de Extension solo hay un McpHub, pero el arreglo connections: McpConnection[] contiene un transport independiente por entrada: si uno cae, no afecta a los demás.
Motivación de diseño
- Un client por servidor: distintos servidores MCP tienen distintas capabilities, configuraciones y tratamientos de errores; un client por servidor simplifica la reconexión y el aislamiento de ámbito (
per-server client:363). - Unificación de tres transport:
stdiolanza procesos locales,sseusa Server-Sent Events ystreamableHttpemplea el nuevo flujo HTTP; se reparten en elswitchdeconnectToServer(transport switch:382). - Doble canal: file watcher + actualización interna:
watchMcpSettingsFiledetecta ediciones externas del archivo de configuración, mientrasisUpdatingClineSettingsmarca cuando la escritura proviene del propio módulo y se salta el disparo (isUpdatingClineSettings:76), evitando bucles. - OAuth con carga perezosa: solo
sse/streamableHttppiden un authProvider aMcpOAuthManager;stdiono lo necesita (authProvider setup:377). - Políticas empresariales previas a la conexión: los despliegues empresariales pueden deshabilitar el marketplace o fijar listas blancas, interceptando conexiones stdio antes de que ocurran (
enterprise allowlist:310). - UID corto para nombres de herramienta: cada servidor recibe un uid corto formado por
c+ nanoid(5), evitando que los nombres de herramientas MCP se conviertan en cadenas largas (getMcpServerKey:130).
Archivos clave
McpHub class:51— clase singleton; mantiene connections, el file watcher, el OAuth manager y el callback de notificación.constructor:108— al iniciar lanzawatchMcpSettingsFile+initializeMcpServers.watchMcpSettingsFile:213— usa chokidar para vigilar cambios en el archivo de configuración, calcula un diff de altas/bajas/modificaciones y dispara reconexiones.connectToServer:286— método de conexión central: valida, elige transport, crea el client y descubre las capacidades.transport switch:382— tres ramas: stdio / sse / streamableHttp.stdio stderr pipe:415— los servidores stdio canalizan stderr como flujo de logs info/error.fetchToolsList:677— envíatools/listy marca cada tool conautoApprove.fetchResourcesList:713— envíaresources/list; los servidores deshabilitados devuelven vacío.notification handler:611— registra el callbacknotifications/messagey entrega los logs del servidor al Task actual o los deja en stash.deleteConnection:784— cierra transport + client y filtra la conexión fuera del arreglo.restartConnection:1047— reconecta un servidor con un retardo artificial de 500 ms para que la UI muestre el estado «connecting».callTool:1233— envíatools/callaplicando el timeout por configuración del servidor y registrando telemetry en todo el recorrido.
Flujo de datos
connectToServer entra y hace tres cosas: valida políticas empresariales, construye un placeholder disabled o un client real, y elige transport. Tras elegir transport crea el McpConnection, lo empuja al arreglo y ejecuta client.connect(transport) para el handshake real. Solo después de un handshake exitoso registra los callbacks de notificación y obtiene las cuatro listas de capacidades:
// 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)Esto se encuentra cerca de post-connect setup:584. Una vez establecida la conexión, las llamadas externas a herramientas pasan por callTool: busca la conexión, calcula el timeout según config.timeout, envía tools/call (tools/call request:1270). La lectura de recursos es análoga, vía resources/read (resources/read:1183).
Límites y fallos
- OAuth ausente = degradación: si
client.connectlanzaUnauthorizedError, no lo reporta como error; marca la conexión comooauthRequired: truey espera a que el usuario autorice desde el frontend (UnauthorizedError branch:556). - Servidor deshabilitado como placeholder: la configuración deshabilitada sigue entrando en el arreglo connections para que la UI la muestre, pero client/transport quedan a
null(disabled placeholder:339). - Clasificación de stderr en stdio: si stderr contiene la palabra
errorse registra como error; en caso contrario se trata como log info (stderr classification:420). - streamableHttp trata 404 como 405: muchos servidores devuelven 404 cuando deberían devolver 405 al no soportar SSE; la capa fetch normaliza 404 a 405 para que el SDK lo acepte (
404 to 405:499). - Refresco dinámico de token SSE: la rama sse usa un fetch personalizado que obtiene
authProvider.tokens()en cada llamada; de lo contrario, tras el vencimiento del token la reconexión seguiría devolviendo 401 (dynamic token fetch:459). - Reconexión streamableHttp delegada al handler:
StreamableHttpReconnectHandlerrecibe el closureconnectToServery decide por sí mismo el ritmo de reconexión (reconnect handler:519). - Notificaciones en stash: cuando no hay un Task activo, las notificaciones entrantes del servidor se guardan en
pendingNotificationshasta que se levante el siguiente Task (pendingNotifications:631).
Resumen
McpHub encapsula todos los detalles del protocolo MCP y expone al agente principal tres entradas: callTool/readResource/getServers. Para ver cómo la capa de herramientas lo envuelve como use_mcp_tool, consulta cap-tools/use-mcp-tool; para ver cómo un subagente aísla su propia ventana de contexto, consulta subagente (subagent); para ver cómo el Task principal inyecta la lista de servidores MCP en el system prompt, consulta agent-loop/task-class.