Управление пулом подключений
McpService — это основной сервис модуля MCP, управляющий жизненным циклом подключений всех серверов MCP. Он поддерживает пул клиентских подключений с использованием стратегии ленивой загрузки (подключения устанавливаются только при первом использовании), а также предоставляет проверки работоспособности и возможности автоматического переподключения.
Путь к файлу: packages/desktop/app/main/services/capabilities/tools/mcp-users/McpService.ts
Структура класса
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);
}
Внутреннее состояние
| Свойство | Тип | Описание |
|---|---|---|
clients | Map<string, Client> | Сопоставление ID сервера → экземпляр MCP Client |
connectionStates | Map<string, ConnectionState> | Сопоставление ID сервера → состояние подключения |
toolsCache | Map<string, { tools, timestamp }> | Кэш списка инструментов по ID сервера (с временной меткой) |
Состояния подключения
stateDiagram-v2
[*] --> disconnected
disconnected --> connecting: initClient()
connecting --> connected: Connection successful
connecting --> error: Connection failed
connected --> disconnected: disconnect()
error --> connecting: reconnect()
connected --> connecting: reconnect()
| Состояние | Описание |
|---|---|
disconnected | Не подключено или уже отключено |
connecting | Выполняется установка подключения |
connected | Подключение активно |
error | Подключение завершилось ошибкой |
Жизненный цикл подключения
initClient — инициализация клиента
initClient(server: McpServerRecord): Promise<Client>
Основной поток выполнения метода подключения:
- Проверить повторное использование — если клиент с таким ID уже существует в Map
clients, сначала выполнить проверку работоспособности - Проверка работоспособности — вызвать
client.listTools(), чтобы убедиться, что подключение действительно - Обработка неработоспособного состояния — если проверка работоспособности не прошла, отключиться и создать подключение заново
- Создать транспорт — создать соответствующий Transport на основе
server.type - Создать клиент — создать экземпляр Client из MCP SDK с настроенными возможностями
- Установить подключение — вызвать
client.connect(transport) - Закэшировать клиент — сохранить в Map
clients - Сгенерировать событие — сгенерировать событие
server:connected
const client = new Client(
{ name: 'elftia', version: '1.0.0' },
{
capabilities: {
roots: { listChanged: true },
sampling: {}
}
}
);
await client.connect(transport);
disconnect — отключение
disconnect(serverId: string): Promise<void>
- Получить экземпляр Client
- Вызвать
client.close()(с обработкой ошибок) - Удалить из Map
clients - Установить состояние
disconnected
reconnect — переподключение
reconnect(serverId: string): Promise<void>
Сначала вызвать disconnect(), затем, если сервер имеет значение isActive, вызвать initClient().
cleanup — очистка всего
cleanup(): Promise<void>
Отключить все подключения и очистить три Map: clients, connectionStates и toolsCache. Используется при выходе из приложения.
Реализация транспортного слоя
StdioClientTransport
Используется для взаимодействия с локальным процессом, обменивается данными с дочерними процессами через стандартный ввод/вывод (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
}
});
Характеристики:
- Автоматически запускает дочерний процесс
- Переменные окружения полностью наследуются от текущего процесса и дополняются пользовательской конфигурацией с переопределением
- Подключение автоматически закрывается при завершении процесса
SSEClientTransport
Долгоживущий транспорт на основе HTTP Server-Sent Events, использующий net.fetch из Electron вместо нативного fetch из Node.js.
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 || {}
}
});
Почему используется net.fetch:
- Модуль
netв Electron следует сетевому стеку Chromium - Улучшенная поддержка SSL/TLS и управление сертификатами
- Автоматическое использование системных настроек прокси
StreamableHTTPClientTransport
Streamable HTTP-транспорт на основе спецификации MCP 2025-03-26. Конфигурация похожа на 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 || {}
}
});
Хранение конфигурации
Конфигурация сервера MCP хранится в Electron Store:
const configStore = new Store<{
mcpServers: McpServerRecord[];
}>({
name: 'mcp-config',
defaults: { mcpServers: [] }
});
Расположение файла: {userData}/mcp-config.json
Полный набор полей McpServerRecord
| Поле | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
id | string | Автоматически генерируется | Уникальный идентификатор, формат mcp_{timestamp}_{random} |
name | string | - | Имя сервера (должно быть уникальным) |
type | McpServerTransport | - | stdio / sse / http |
scope | McpServerScope | 'user' | user (глобальный) / local (на уровне проекта) |
config | McpServerConfig | - | Конфигурация подключения (command/args/env/url/headers) |
isActive | boolean | true | Включен ли сервер |
isTrusted | boolean | false | Доверенный ли сервер |
disabledTools | string[] | [] | Список имен отключенных инструментов |
disabledAutoApproveTools | string[] | [] | Список имен инструментов, для которых запрещено автоматическое подтверждение |
installSource | string | 'manual' | Источник установки: builtin / manual / protocol / unknown |
projectPath | string? | - | Связанный путь проекта (используется, когда scope является local) |
createdAt | number | Date.now() | Временная метка создания |
updatedAt | number? | - | Временная метка последнего обновления |
Основные методы
list — список серверов
async list(): Promise<McpServerRecord[]>
Читает список серверов напрямую из configStore, без участия подключения.
add — добавление сервера
async add(input: McpServerInput): Promise<McpActionResult>
- Проверить уникальность имени
- Создать
McpServerRecordи сгенерировать уникальный ID - Сохранить в configStore
- Если
isActive, попытаться установить подключение (ошибка подключения не блокирует добавление) - Сгенерировать событие
servers:changed
addJson — пакетное добавление из JSON
async addJson(input: McpServerJsonInput): Promise<McpActionResult>
Разобрать строку JSON, пройти по объекту { [name]: config }, вызвать add() для каждой записи. Вернуть агрегированный результат.
update — обновление сервера
async update(id: string, updates: Partial<McpServerRecord>): Promise<McpActionResult>
- Найти сервер по ID
- Объединить обновления (оставить ID неизменным)
- Сохранить и установить
updatedAt - Если изменились
configилиtype, запуститьreconnect() - Сгенерировать событие
servers:changed
listServerTools — получение инструментов сервера
async listServerTools(serverId: string): Promise<MCPTool[]>
- Проверить, есть ли попадание в кэш и не истек ли срок его действия
- При попадании вернуть напрямую
- При промахе выполнить
initClient()→client.listTools() - Преобразовать в формат
MCPTool[], отфильтроватьdisabledTools - Записать в кэш
Правило генерации ID инструмента: mcp__${cleanServerName}__${cleanToolName} (неалфавитно-цифровые символы заменяются на _)
callTool — вызов инструмента
async callTool(serverId: string, toolName: string, args: any, callId: string): Promise<MCPCallToolResponse>
- Обеспечить подключение с помощью
initClient() client.callTool({ name, arguments })- Записать длительность вызова
- Сгенерировать событие
tool:called - Вернуть
{ isError, content }или сообщение об ошибке
test — проверка подключения
async test(input: McpServerRemoveInput): Promise<McpTestResult>
Попытаться подключиться и получить список инструментов, вернуть успех/неудачу и список имен инструментов.
discover — обнаружение возможностей
async discover(input: McpServerRemoveInput): Promise<McpDiscoverResult>
После подключения вызвать соответственно listTools(), listPrompts() и listResources() (два последних необязательны, допускается частичный отказ), вернуть полный перечень возможностей.
События
McpService расширяет EventEmitter и генерирует следующие события:
| Событие | Аргументы | Триггер |
|---|---|---|
server:connected | serverId | Сервер успешно подключен |
connection:state | serverId, state | Состояние подключения изменилось |
servers:changed | Нет | Список серверов изменился (добавление/удаление/обновление) |
tool:called | { serverId, toolName, duration, success } | Вызов инструмента завершен |
Точки расширения
- Добавить новый тип транспорта — добавить новую ветку case в оператор switch метода
createTransport() - Настроить проверку работоспособности — изменить метод
isClientHealthy() - Стратегия кэширования — изменить
CACHE_TTLили реализовать более сложную стратегию инвалидации кэша - События подключения — отслеживать, подписываясь на события EventEmitter
Связанные файлы
- Адаптеры формата инструментов — преобразование формата инструментов
- Архитектура Worker — Worker для моста CLI
- Обзор модуля — общая архитектура