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
| Property | Type | Description |
|---|---|---|
clients | Map<string, Client> | Server ID → MCP Client instance mapping |
connectionStates | Map<string, ConnectionState> | Server ID → connection state mapping |
toolsCache | Map<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()
| State | Description |
|---|---|
disconnected | Not connected or already disconnected |
connecting | Establishing connection |
connected | Connection active |
error | Connection failed |
Connection Lifecycle
initClient — Initialize Client
initClient(server: McpServerRecord): Promise<Client>
Core connection method execution flow:
- Check reuse — If a client with that ID already exists in the
clientsMap, perform health check first - Health check — Call
client.listTools()to verify connection is valid - Unhealthy handling — If health check fails, disconnect and rebuild
- Create transport — Create corresponding Transport based on
server.type - Create client — Create MCP SDK Client instance with configured capabilities
- Establish connection — Call
client.connect(transport) - Cache client — Store in
clientsMap - Emit event — Emit
server:connectedevent
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>
- Get Client instance
- Call
client.close()(with error handling) - Remove from
clientsMap - 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
netmodule 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
| Field | Type | Default | Description |
|---|---|---|---|
id | string | Auto-generated | Unique identifier, format mcp_{timestamp}_{random} |
name | string | - | Server name (must be unique) |
type | McpServerTransport | - | stdio / sse / http |
scope | McpServerScope | 'user' | user (global) / local (project-level) |
config | McpServerConfig | - | Connection config (command/args/env/url/headers) |
isActive | boolean | true | Whether enabled |
isTrusted | boolean | false | Whether trusted |
disabledTools | string[] | [] | List of disabled tool names |
disabledAutoApproveTools | string[] | [] | List of tool names that disallow auto-approval |
installSource | string | 'manual' | Install source: builtin / manual / protocol / unknown |
projectPath | string? | - | Associated project path (used when scope is local) |
createdAt | number | Date.now() | Creation timestamp |
updatedAt | number? | - | 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>
- Check name uniqueness
- Create
McpServerRecordand generate unique ID - Save to configStore
- If
isActive, try to establish connection (connection failure doesn't block adding) - Emit
servers:changedevent
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>
- Find server by ID
- Merge updates (keep ID unchanged)
- Save and set
updatedAt - If
configortypechanged, triggerreconnect() - Emit
servers:changedevent
listServerTools — Get Server Tools
async listServerTools(serverId: string): Promise<MCPTool[]>
- Check if cache hit and not expired
- If hit, return directly
- If miss,
initClient()→client.listTools() - Convert to
MCPTool[]format, filterdisabledTools - 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>
- Ensure connection with
initClient() client.callTool({ name, arguments })- Record call duration
- Emit
tool:calledevent - 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:
| Event | Arguments | Trigger |
|---|---|---|
server:connected | serverId | Server connected successfully |
connection:state | serverId, state | Connection state changed |
servers:changed | None | Server 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_TTLor implement more complex cache invalidation strategy - Connection events — Monitor by listening to EventEmitter events
Related Files
- Tool Format Adapters — Tool format conversion
- Worker Architecture — Worker for CLI bridging
- Module Overview — Overall architecture