MCP-клиент вызывает неподдерживаемый метод: читаем capabilities после initialize
Клиент успешно подключается к MCP-серверу, получает список ресурсов, а затем падает на resources/subscribe. Разработчик видит resources в ответе initialize и считает подписку частью этой возможности. Но чтение ресурса и получение уведомлений о его изменении — разные функции. Сервер вправе поддерживать первое и не поддерживать второе.
Такой сбой особенно заметен при смене сервера. Одна реализация умеет подписки, другая отдаёт тот же текст по resources/read, но работает без них. Если host жёстко связал две функции, обычный сценарий чтения становится недоступным из-за необязательного улучшения. Исправление начинается с точного чтения capabilities, а не с повторов ошибочного метода.
Пустой объект тоже сообщает возможность
В ответе initialize поле capabilities содержит заявленные возможности сервера. Наличие resources: {} означает поддержку ресурсов без дополнительных флагов. Проверка bool(capabilities.get("resources")) в Python ошибочна: пустой словарь даст False. Нужно отдельно проверять наличие ключа resources, а затем читать флаг subscribe внутри объекта.
Уведомления об изменении списка ресурсов задаются listChanged. Они тоже не заменяют subscribe: изменение перечня и изменение содержимого конкретного документа — разные события. Спецификация ресурсов MCP явно разделяет эти возможности. Поэтому обработчик не должен выводить одну функцию из наличия другой.
В примере разбирается ответ initialize и строится следующий протокольный запрос для трёх вариантов. Это локальная проверка принятия решения по JSON-RPC, а не запуск MCP SDK. Для простоты предыдущий запрос initialize имеет id 1, а поддерживаемая версия протокола в клиенте задана явно. После успешного ответа клиент формирует notifications/initialized.
import json
def initialize_result(capabilities):
wire = json.dumps({"jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2025-11-25", "capabilities": capabilities,
"serverInfo": {"name": "fixture-server", "version": "1.0"}}})
response = json.loads(wire)
assert response["id"] == 1
result = response["result"]
if result["protocolVersion"] not in {"2025-11-25"}:
raise ValueError("unsupported protocol")
ready = {"jsonrpc": "2.0", "method": "notifications/initialized"}
caps = result["capabilities"]
if "resources" not in caps:
method = None
elif caps["resources"].get("subscribe") is True:
method = "resources/subscribe"
else:
method = "resources/read"
request = None if method is None else {
"jsonrpc": "2.0", "id": 2, "method": method,
"params": {"uri": "docs://manual/current"}}
return ready, request
for caps in ({"resources": {}}, {"resources": {"subscribe": True}}, {}):
ready, request = initialize_result(caps)
print(ready["method"], None if request is None else request["method"])
При resources: {} клиент выбирает resources/read. При subscribe: true он может сформировать resources/subscribe. Если ключ resources отсутствует, чтение не планируется. В выводе также виден метод notifications/initialized: это обязательное завершение успешной инициализации со стороны клиента. В рабочем соединении уведомление отправляется прежде обычных операций.
Не теряйте согласованную версию
Сервер может ответить поддерживаемой им версией, отличающейся от предложенной клиентом. Если клиент её не поддерживает, соединение нужно завершить, а не продолжать с догадкой о структуре сообщений. Для HTTP после согласования версия передаётся в заголовке MCP-Protocol-Version. Правила жизненного цикла MCP задают эту последовательность независимо от бизнес-назначения сервера.
До получения ответа initialize обычные запросы не должны уходить в транспортную очередь как будто подключение уже готово. Иначе быстрый сервер скроет ошибку, а более медленный начнёт отклонять операции. Host удобно хранить явные состояния подключения: начато согласование, готово к работе, закрыто. Доступные действия вычисляются из успешно завершённого согласования, а не из имени подключённого продукта.
В нашем сценарии пользователь просит «Открой текущую инструкцию приёмки». Если подписки нет, это не мешает однократному чтению разрешённого ресурса. Host показывает полученную редакцию и время чтения. Для задачи постоянного наблюдения уже потребуется другой механизм: повторное разрешённое чтение по принятой политике или сервер с поддержкой подписок.
Как проверить исправление
Подключите тестовые ответы initialize с пустым resources, с subscribe и совсем без resources. Проверьте не только выбранный метод, но и интерфейс host: отсутствие подписки не должно скрывать обычное чтение. Отдельно подайте неподдерживаемую версию протокола и убедитесь, что запрос ресурса не отправлен после отказа согласования.
Если сервер объявляет subscribe, но отвечает method not found, сохраните оба сообщения и версию реализации. Это уже несоответствие заявленного контракта поведению сервера. Бесконечно повторять такой запрос бессмысленно. Клиенту нужен понятный отказ функции либо переход к явно предусмотренному способу чтения, а серверу — исправление capability или реализации.
В платном курсе «AI Agent Integrations PRO: MCP» вы спроектируете связку host, client и server, подключите discovery и обработку результатов, добавите тесты и базовые production-проверки. Следующий шаг — перенести эту матрицу возможностей в настоящий клиент и проверить её через транспорт. Откройте программу курса и соберите подключение с явным контрактом возможностей.
Продолжить на практике
- AI Agent Integrations PRO: MCP ↗
Вы уже работали с LLM и понимаете, что подключать их к реальным системам — больно. MCP — это стандарт, который делает эту боль меньше: один контракт вместо десятка костылей, типизированные инструменты вместо голого доступа к базе, тесты и логи вместо молитв. На курсе соберём сервер с нуля и выведем в production-контур.
