Skip to main content

Connection Pool Management

McpService is the core service of the MCP module, managing the connection lifecycle of all MCP servers. It maintains a client connection pool using a lazy-loading strategy (connections are established only on first use) and provides health checks and automatic reconnection capabilities.

File path: packages/desktop/app/main/services/capabilities/tools/mcp-users/McpService.ts

Class Structure

class McpService extends EventEmitter {
private clients: Map<string, Client>;
private connectionStates: Map<string, ConnectionState>;
private toolsCache: Map<string, { tools: MCPTool[]; timestamp: number }>;
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes

constructor(private logger: LoggerService);
}

Internal State

PropertyTypeDescription
clientsMap<string, Client>Server ID → MCP Client instance mapping
connectionStatesMap<string, ConnectionState>Server ID → connection state mapping
toolsCacheMap<string, { tools, timestamp }>Server ID → tools list cache (with timestamp)

Connection States

stateDiagram-v2
[*] --> disconnected
disconnected --> connecting: initClient()
connecting --> connected: Connection successful
connecting --> error: Connection failed
connected --> disconnected: disconnect()
error --> connecting: reconnect()
connected --> connecting: reconnect()
StateDescription
disconnectedNot connected or already disconnected
connectingEstablishing connection
connectedConnection active
errorConnection failed

Connection Lifecycle

initClient — Initialize Client

initClient(server: McpServerRecord): Promise<Client>

Core connection method execution flow:

  1. Check reuse — If a client with that ID already exists in the clients Map, perform health check first
  2. Health check — Call client.listTools() to verify connection is valid
  3. Unhealthy handling — If health check fails, disconnect and rebuild
  4. Create transport — Create corresponding Transport based on server.type
  5. Create client — Create MCP SDK Client instance with configured capabilities
  6. Establish connection — Call client.connect(transport)
  7. Cache client — Store in clients Map
  8. Emit event — Emit server:connected event
const client = new Client(
{ name: 'elftia', version: '1.0.0' },
{
capabilities: {
roots: { listChanged: true },
sampling: {}
}
}
);
await client.connect(transport);

disconnect — Disconnect

disconnect(serverId: string): Promise<void>
  1. Get Client instance
  2. Call client.close() (with error handling)
  3. Remove from clients Map
  4. Set state to disconnected

reconnect — Reconnect

reconnect(serverId: string): Promise<void>

Call disconnect() first, then if the server is isActive, call initClient().

cleanup — Cleanup All

cleanup(): Promise<void>

Disconnect all connections and clear the three Maps: clients, connectionStates, and toolsCache. Used on application exit.

Transport Layer Implementation

StdioClientTransport

Used for local process communication, communicates with child processes via standard input/output (stdin/stdout).

new StdioClientTransport({
command: server.config.command,
args: server.config.args || [],
env: {
...process.env, // Inherit system environment
...server.config.env // Override with custom variables
}
});

Characteristics:

  • Auto-starts child process
  • Environment variables fully inherit current process, overlaid with custom configuration
  • Connection auto-closes when process exits

SSEClientTransport

Long-lived transport based on HTTP Server-Sent Events, using Electron's net.fetch instead of Node.js native fetch.

new SSEClientTransport(new URL(server.config.url), {
fetch: (url, init) => {
return net.fetch(
typeof url === 'string' ? url : url.toString(),
init
);
},
requestInit: {
headers: server.config.headers || {}
}
});

Why use net.fetch:

  • Electron's net module follows Chromium's network stack
  • Better SSL/TLS support and certificate management
  • Automatic use of system proxy settings

StreamableHTTPClientTransport

Streamable HTTP transport based on MCP 2025-03-26 specification. Configuration is similar to SSE:

new StreamableHTTPClientTransport(new URL(server.config.url), {
fetch: (url, init) => {
return net.fetch(
typeof url === 'string' ? url : url.toString(),
init
);
},
requestInit: {
headers: server.config.headers || {}
}
});

Configuration Storage

MCP server configuration is stored in Electron Store:

const configStore = new Store<{
mcpServers: McpServerRecord[];
}>({
name: 'mcp-config',
defaults: { mcpServers: [] }
});

File location: {userData}/mcp-config.json

McpServerRecord Complete Fields

FieldTypeDefaultDescription
idstringAuto-generatedUnique identifier, format mcp_{timestamp}_{random}
namestring-Server name (must be unique)
typeMcpServerTransport-stdio / sse / http
scopeMcpServerScope'user'user (global) / local (project-level)
configMcpServerConfig-Connection config (command/args/env/url/headers)
isActivebooleantrueWhether enabled
isTrustedbooleanfalseWhether trusted
disabledToolsstring[][]List of disabled tool names
disabledAutoApproveToolsstring[][]List of tool names that disallow auto-approval
installSourcestring'manual'Install source: builtin / manual / protocol / unknown
projectPathstring?-Associated project path (used when scope is local)
createdAtnumberDate.now()Creation timestamp
updatedAtnumber?-Last update timestamp

Core Methods

list — List Servers

async list(): Promise<McpServerRecord[]>

Read server list directly from configStore, no connection involved.

add — Add Server

async add(input: McpServerInput): Promise<McpActionResult>
  1. Check name uniqueness
  2. Create McpServerRecord and generate unique ID
  3. Save to configStore
  4. If isActive, try to establish connection (connection failure doesn't block adding)
  5. Emit servers:changed event

addJson — Batch Add from JSON

async addJson(input: McpServerJsonInput): Promise<McpActionResult>

Parse JSON string, iterate over { [name]: config } object, call add() for each entry. Return aggregated result.

update — Update Server

async update(id: string, updates: Partial<McpServerRecord>): Promise<McpActionResult>
  1. Find server by ID
  2. Merge updates (keep ID unchanged)
  3. Save and set updatedAt
  4. If config or type changed, trigger reconnect()
  5. Emit servers:changed event

listServerTools — Get Server Tools

async listServerTools(serverId: string): Promise<MCPTool[]>
  1. Check if cache hit and not expired
  2. If hit, return directly
  3. If miss, initClient()client.listTools()
  4. Convert to MCPTool[] format, filter disabledTools
  5. Write to cache

Tool ID generation rule: mcp__${cleanServerName}__${cleanToolName} (non-alphanumeric characters replaced with _)

callTool — Call Tool

async callTool(serverId: string, toolName: string, args: any, callId: string): Promise<MCPCallToolResponse>
  1. Ensure connection with initClient()
  2. client.callTool({ name, arguments })
  3. Record call duration
  4. Emit tool:called event
  5. Return { isError, content } or error message

test — Test Connection

async test(input: McpServerRemoveInput): Promise<McpTestResult>

Try to connect and list tools, return success/failure and tool name list.

discover — Discover Capabilities

async discover(input: McpServerRemoveInput): Promise<McpDiscoverResult>

After connecting, call listTools(), listPrompts(), and listResources() respectively (latter two are optional, allow partial failure), return complete capability inventory.

Events

McpService extends EventEmitter and emits the following events:

EventArgumentsTrigger
server:connectedserverIdServer connected successfully
connection:stateserverId, stateConnection state changed
servers:changedNoneServer list changed (add/delete/update)
tool:called{ serverId, toolName, duration, success }Tool call completed

Extension Points

  • Add new transport type — Add new case branch in switch statement of createTransport()
  • Customize health check — Modify isClientHealthy() method
  • Cache strategy — Modify CACHE_TTL or implement more complex cache invalidation strategy
  • Connection events — Monitor by listening to EventEmitter events