Перейти к основному содержимому

Управление пулом подключений

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);
}

Внутреннее состояние

СвойствоТипОписание
clientsMap<string, Client>Сопоставление ID сервера → экземпляр MCP Client
connectionStatesMap<string, ConnectionState>Сопоставление ID сервера → состояние подключения
toolsCacheMap<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>

Основной поток выполнения метода подключения:

  1. Проверить повторное использование — если клиент с таким ID уже существует в Map clients, сначала выполнить проверку работоспособности
  2. Проверка работоспособности — вызвать client.listTools(), чтобы убедиться, что подключение действительно
  3. Обработка неработоспособного состояния — если проверка работоспособности не прошла, отключиться и создать подключение заново
  4. Создать транспорт — создать соответствующий Transport на основе server.type
  5. Создать клиент — создать экземпляр Client из MCP SDK с настроенными возможностями
  6. Установить подключение — вызвать client.connect(transport)
  7. Закэшировать клиент — сохранить в Map clients
  8. Сгенерировать событие — сгенерировать событие 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>
  1. Получить экземпляр Client
  2. Вызвать client.close() (с обработкой ошибок)
  3. Удалить из Map clients
  4. Установить состояние 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

ПолеТипЗначение по умолчаниюОписание
idstringАвтоматически генерируетсяУникальный идентификатор, формат mcp_{timestamp}_{random}
namestring-Имя сервера (должно быть уникальным)
typeMcpServerTransport-stdio / sse / http
scopeMcpServerScope'user'user (глобальный) / local (на уровне проекта)
configMcpServerConfig-Конфигурация подключения (command/args/env/url/headers)
isActivebooleantrueВключен ли сервер
isTrustedbooleanfalseДоверенный ли сервер
disabledToolsstring[][]Список имен отключенных инструментов
disabledAutoApproveToolsstring[][]Список имен инструментов, для которых запрещено автоматическое подтверждение
installSourcestring'manual'Источник установки: builtin / manual / protocol / unknown
projectPathstring?-Связанный путь проекта (используется, когда scope является local)
createdAtnumberDate.now()Временная метка создания
updatedAtnumber?-Временная метка последнего обновления

Основные методы

list — список серверов

async list(): Promise<McpServerRecord[]>

Читает список серверов напрямую из configStore, без участия подключения.

add — добавление сервера

async add(input: McpServerInput): Promise<McpActionResult>
  1. Проверить уникальность имени
  2. Создать McpServerRecord и сгенерировать уникальный ID
  3. Сохранить в configStore
  4. Если isActive, попытаться установить подключение (ошибка подключения не блокирует добавление)
  5. Сгенерировать событие servers:changed

addJson — пакетное добавление из JSON

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

Разобрать строку JSON, пройти по объекту { [name]: config }, вызвать add() для каждой записи. Вернуть агрегированный результат.

update — обновление сервера

async update(id: string, updates: Partial<McpServerRecord>): Promise<McpActionResult>
  1. Найти сервер по ID
  2. Объединить обновления (оставить ID неизменным)
  3. Сохранить и установить updatedAt
  4. Если изменились config или type, запустить reconnect()
  5. Сгенерировать событие servers:changed

listServerTools — получение инструментов сервера

async listServerTools(serverId: string): Promise<MCPTool[]>
  1. Проверить, есть ли попадание в кэш и не истек ли срок его действия
  2. При попадании вернуть напрямую
  3. При промахе выполнить initClient()client.listTools()
  4. Преобразовать в формат MCPTool[], отфильтровать disabledTools
  5. Записать в кэш

Правило генерации ID инструмента: mcp__${cleanServerName}__${cleanToolName} (неалфавитно-цифровые символы заменяются на _)

callTool — вызов инструмента

async callTool(serverId: string, toolName: string, args: any, callId: string): Promise<MCPCallToolResponse>
  1. Обеспечить подключение с помощью initClient()
  2. client.callTool({ name, arguments })
  3. Записать длительность вызова
  4. Сгенерировать событие tool:called
  5. Вернуть { isError, content } или сообщение об ошибке

test — проверка подключения

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

Попытаться подключиться и получить список инструментов, вернуть успех/неудачу и список имен инструментов.

discover — обнаружение возможностей

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

После подключения вызвать соответственно listTools(), listPrompts() и listResources() (два последних необязательны, допускается частичный отказ), вернуть полный перечень возможностей.

События

McpService расширяет EventEmitter и генерирует следующие события:

СобытиеАргументыТриггер
server:connectedserverIdСервер успешно подключен
connection:stateserverId, stateСостояние подключения изменилось
servers:changedНетСписок серверов изменился (добавление/удаление/обновление)
tool:called{ serverId, toolName, duration, success }Вызов инструмента завершен

Точки расширения

  • Добавить новый тип транспорта — добавить новую ветку case в оператор switch метода createTransport()
  • Настроить проверку работоспособности — изменить метод isClientHealthy()
  • Стратегия кэширования — изменить CACHE_TTL или реализовать более сложную стратегию инвалидации кэша
  • События подключения — отслеживать, подписываясь на события EventEmitter

Связанные файлы