Урок курса
Инициализация MCP-клиента и обнаружение возможностей
AI Agent Integrations PRO: MCPВ предыдущем уроке мы замкнули цикл работы с prompt-шаблонами: клиент получал список шаблонов, маршрутизировал запрос, подтягивал готовый prompt и отправлял его в LLM. Всё это уже предполагало наличие активного соединения с MCP-сервером. Теперь разберём, что стоит за этим соединением — как именно клиент появляется на сцене и какова его роль до того, как начнутся любые запросы.
Роль MCP-клиента и его место в архитектуре
В уроке про архитектуру мы уже делили систему на Host, Client и Server. Здесь не повторяем эту схему целиком. Клиент интересует нас как конкретный объект в коде: он открывает транспорт, проходит initialization и превращает удалённый MCP-сервер в понятные приложению методы вроде list_tools() или read_resource().
Его зона ответственности практическая:
- открыть транспортный канал: например, stdio к локальному процессу или SSE к удалённому серверу;
- дождаться корректного
initialize/initializedобмена; - получить списки
toolsиresources, но только если сервер действительно заявил их вcapabilities; - удерживать состояние соединения и кэши так, чтобы приложение не отправляло рабочие запросы слишком рано.
Сервер при этом остаётся поставщиком возможностей, а host-приложение — владельцем пользовательского сценария. Клиент связывает эти две части, но не принимает продуктовых решений за агента: он не выбирает, какой tool нужен пользователю, а даёт приложению достоверную картину того, что сервер реально предоставляет.
Фаза initialize handshake: протокол согласования
Сам порядок handshake уже знаком; на клиентской стороне важнее, какие решения принимаются после ответа сервера. Клиент отправляет initialize, получает protocolVersion, capabilities и serverInfo, затем подтверждает initialized — и только после этого может переходить к discovery.
Типичный ответ сервера выглядит так:
{
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": false, "listChanged": false },
"prompts": {}
},
"serverInfo": { "name": "my-mcp-server", "version": "1.0.0" }
}
Клиенту важно не просто сохранить этот объект, а принять по нему решения. protocolVersion нужно сравнить с ожидаемой версией или хотя бы залогировать расхождение. capabilities определяют, какие discovery-запросы вообще уместны: если сервер не заявил resources, не надо вызывать resources/list «на всякий случай». serverInfo полезен для диагностики: когда в окружении несколько серверов, лог с именем и версией быстро показывает, к чему именно подключился клиент.
Распространённая ошибка — считать successful connect достаточным сигналом готовности. Транспорт мог открыться, но пока initialize не завершён и initialized не отправлен, рабочие запросы ещё преждевременны.
Пошаговая реализация инициализации и discover capabilities
В коде здесь важен один барьер: await session.initialize(). Пока он не завершился, клиент ещё не знает, что именно сервер готов публиковать. Полный цикл выглядит так:
- Создать параметры запуска сервера —
StdioServerParametersс командой и аргументами. - Открыть транспортный канал через
stdio_client— он возвращает пару(read, write)потоков. - Создать сессию
ClientSessionповерх этих потоков. - Вызвать
await session.initialize()— именно здесь происходит весь handshake: клиент отправляетinitialize, получает ответ сервера и отправляетinitialized. - Только после возврата из
initialize()вызыватьlist_tools()иlist_resources().
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command='python', args=['server.py'])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
# Шаг 4: handshake — блокирует до получения initialized
await session.initialize()
# Шаг 5: discover capabilities
tools = await session.list_tools()
resources = await session.list_resources()
print('Tools:', [t.name for t in tools.tools])
print('Resources:', [r.uri for r in resources.resources])
asyncio.run(main())
await session.initialize() — это синхронная точка ожидания. До её завершения сессия находится в состоянии «не инициализирована», и сервер отклонит любой другой запрос с ошибкой not initialized. SDK не защищает от этой ошибки автоматически: если вы вызовете list_tools() до initialize(), код отправит запрос, но получит отказ от сервера. После успешной инициализации list_tools() и list_resources() возвращают объекты с полями tools и resources соответственно. Это отдельные запросы к серверу, а не чтение кэша из ответа initialize. Сам ответ initialize говорит только, какие типы возможностей существуют в принципе; конкретный перечень инструментов и ресурсов приходит уже через tools/list и resources/list.
Работа с динамическими capabilities: listChanged и кэширование
После успешной инициализации у вас на руках есть список инструментов из list_tools(). Возникает соблазн сохранить его один раз и переиспользовать на протяжении всей сессии. Это нормально, пока сервер не меняет опубликованный список в runtime. Именно для этого в ответе initialize есть флаг listChanged внутри секции tools:
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": false, "listChanged": false }
}
Если listChanged: true — сервер явно предупреждает: список инструментов может измениться прямо в ходе работы. Это реальный сценарий, например, когда сервер динамически регистрирует или удаляет инструменты в зависимости от состояния системы. При listChanged: true бесконечное кэширование становится источником трудноуловимых ошибок: AI-приложение продолжает считать, что инструмент существует, пытается его вызвать и получает ошибку «tool not found» — уже не на этапе discover, а в середине работы агента. Правильная стратегия зависит от требований к приложению:
- Если сервер присылает уведомление
notifications/tools/list_changed, можно подписаться на него и инвалидировать кэш только по факту изменения. - Если уведомления не реализованы, разумно перезапрашивать
list_tools()перед каждой новой задачей агента или по истечении TTL. - Если
listChanged: false— кэш на всю сессию вполне оправдан. Отдельная проблема: многие реализации просто не читаютcapabilitiesиз ответаinitializeи кэшируют список безусловно. Это безопасно только для статичных серверов. Если в проекте используются серверы сторонних разработчиков, проверяйте этот флаг явно при старте сессии и закладывайте стратегию обновления на этапе проектирования клиента, а не после первого инцидента.
Задача 1
Что именно отправляет MCP-клиент в самом первом сообщении при установке соединения с сервером?
Задача 2
Сервер вернул в ответе initialize поле "tools": { "listChanged": true }. Что это означает для стратегии работы клиента со списком инструментов?
Задача 3
Что произойдёт, если вызвать session.list_tools() до завершения await session.initialize()?
Задача 4
Как называется уведомление, которое MCP-клиент отправляет серверу после получения успешного ответа на запрос initialize? Напиши точное название одним словом.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
