Skip to content

McpHub: MCP server lifecycle management

源码版本v4.0.10

Responsibilities

McpHub is Cline's central hub for integrating with external MCP servers. It reads every server configured in cline_mcp_settings.json, establishes an independent client + transport connection per server, and caches the exposed tools, resources, and prompts in memory for the main agent to call. The entire MCP pipeline — from config changes, file watching, connect/reconnect, capability discovery, to tools/call invocation — converges in this one class; the main Task never touches the MCP SDK directly.

It sits between "the main agent loop" and "the specific MCP protocol". When a Task wants to call an MCP tool, it goes out through McpHub.callTool (callTool:1233); when the UI wants to see the current server list, it gets a sorted snapshot via getServers() (getServers:112). There is only one McpHub per Extension instance, but each entry in the connections: McpConnection[] array is an independent transport, so one crashing does not affect the others.

Design motivation

  • One client per server: different MCP servers have different capabilities, configurations, and error handling; independent clients make reconnect and scope isolation simple (per-server client:363).
  • Three unified transports: stdio runs a local process, sse uses Server-Sent Events, and streamableHttp uses the new HTTP streaming. They are dispatched in a switch inside connectToServer (transport switch:382).
  • Dual channels for file watching vs internal updates: watchMcpSettingsFile watches for external editor changes to the config, while isUpdatingClineSettings marks when writes come from inside Cline itself and skips the trigger (isUpdatingClineSettings:76), avoiding loops.
  • OAuth loaded lazily: only sse / streamableHttp ask McpOAuthManager for an authProvider; stdio does not (authProvider setup:377).
  • Enterprise policy precedes connection: enterprise deployments can disable the marketplace and set allow-lists; blocks happen before a stdio connection is even attempted (enterprise allowlist:310).
  • UID shortens tool names: each server is assigned a short uid of c + nanoid(5), to keep MCP tool names from becoming long concatenated strings (getMcpServerKey:130).

Key files

  • McpHub class:51 — Singleton class; holds connections, file watcher, OAuth manager, and notification callbacks.
  • constructor:108 — Immediately calls watchMcpSettingsFile + initializeMcpServers at startup.
  • watchMcpSettingsFile:213 — Uses chokidar to watch config file changes; diffs additions/deletions/modifications and triggers reconnects.
  • connectToServer:286 — Core connection method; validates, picks transport, builds the client, and discovers capabilities.
  • transport switch:382 — Three branches: stdio / sse / streamableHttp.
  • stdio stderr pipe:415 — stdio servers pipe stderr out as info/error log streams.
  • fetchToolsList:677 — Sends tools/list and tags each tool with an autoApprove flag.
  • fetchResourcesList:713 — Sends resources/list; disabled servers return empty.
  • notification handler:611 — Registers the notifications/message callback; routes server logs to the current Task or stages them.
  • deleteConnection:784 — Closes transport + client and filters the entry out of connections.
  • restartConnection:1047 — Reconnects a server, with an artificial 500ms delay so the UI can show the "connecting" state.
  • callTool:1233 — Sends tools/call; picks the timeout from server config, with telemetry throughout.

Data flow

connectToServer does three things up front: validate enterprise policy, construct either a disabled placeholder or a real client, and pick the transport. Once the transport is selected, it builds an McpConnection, pushes it into the array, and then client.connect(transport) does the actual handshake. Only after a successful handshake does it register notification callbacks and fetch the four capability lists:

typescript
// 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)

This sits around post-connect setup:584. Once the connection is up, external tool calls all go through callTool — look up the connection, compute the timeout from config.timeout, and send tools/call (tools/call request:1270). Resource reads are similar, via resources/read (resources/read:1183).

Boundaries and failure modes

  • Missing OAuth means graceful degradation: when client.connect throws UnauthorizedError, it does not surface as an error; instead the connection is marked oauthRequired: true and waits for the user to authorize on the front-end (UnauthorizedError branch:556).
  • Disabled servers are placeholders but not connected: a disabled config still enters the connections array so the UI can display it, but its client/transport are both null (disabled placeholder:339).
  • stdio stderr classification: stderr lines containing the word error are recorded as errors; everything else is treated as info log (stderr classification:420).
  • streamableHttp treats 404 as 405: many servers that should return 405 when SSE is unsupported instead return 404; the fetch layer normalizes 404 into 405 so the SDK accepts it (404 to 405:499).
  • SSE token refresh on demand: the sse branch uses a custom fetch that calls authProvider.tokens() every time; otherwise an expired token would keep returning 401 on reconnect (dynamic token fetch:459).
  • streamableHttp reconnects are delegated: StreamableHttpReconnectHandler takes a connectToServer closure and decides its own reconnect cadence (reconnect handler:519).
  • Notifications staged: when no Task is active, server notifications are first stored in pendingNotifications and drained when the next Task starts (pendingNotifications:631).

Summary

McpHub folds all MCP protocol details into itself and exposes three entry points to the main agent: callTool / readResource / getServers. To see how it is wrapped as use_mcp_tool by the tool layer, read cap-tools/use-mcp-tool; to see how a subagent isolates its own context window, read subagent; to see how the main Task injects the MCP server list into the system prompt, read agent-loop/task-class.

See official docs: Cline 文档 · README