JavaRush /Курсы /ChatGPT Apps /Handshake и capabilities: как клиент узнаёт, что сервер у...

Handshake и capabilities: как клиент узнаёт, что сервер умеет

ChatGPT Apps
6 уровень , 2 лекция
Открыта

1. Зачем вообще нужен handshake

Если REST-эндпоинты — это набор отдельных дверей, в которые можно стучаться по URL, то MCP — это скорее постоянный диалог по одному каналу. Клиент не просто шлёт разрозненные запросы, он сначала устанавливает сессию. Handshake — это момент знакомства в начале этой сессии.

В MCP этот момент реализован как специальный запрос initialize, который клиент отправляет сразу после установления транспорта (STDIO, HTTP/stream, WebSocket — неважно). В запросе он сообщает: «Я говорю на такой-то версии MCP, вот, что я умею, и вот, кто я вообще такой». Сервер в ответ говорит: «А я поддерживаю вот такую версию и вот такие возможности, приятно познакомиться».

После успешного обмена клиент шлёт уведомление notifications/initialized и только после этого начинается рабочая жизнь: tools/list, resources/list, tools/call и прочие полезные штуки.

Если пытаться проводить аналогию, handshake MCP — это как договор аренды перед тем, как завезти сервер в датацентр. Пока вы не договорились о правилах (формат протокола, какие услуги предоставляет датацентр, с кого брать деньги) — возить сервера бессмысленно.

С практической точки зрения handshake решает три задачи:

  1. Проверяет совместимость версий протокола.
  2. Объявляет, какие «примитивы» MCP сервер вообще поддерживает: tools, resources, prompts, логирование, нотификации и т.п.
  3. Даёт метаинформацию о клиенте и сервере — имя и версию реализации.

2. Жизненный цикл соединения MCP: где живёт handshake

Чтобы картина не казалась абстрактной, давайте посмотрим на типичный сценарий (flow) соединения, сильно упрощённый:

sequenceDiagram
    participant C as Клиент (ChatGPT/Inspector)
    participant S as MCP-сервер

    C->>S: (1) Устанавливаем транспорт (STDIO/HTTP-stream)
    C->>S: (2) Request: "initialize"
    S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notification: "notifications/initialized"
    C->>S: (5) Request: "tools/list" / "resources/list"
    S-->>C: (6) Result: списки инструментов/ресурсов
    C->>S: (7) Request: "tools/call" и др.

С технической стороны шаги выглядят так:

  1. Транспорт установлен: например, ChatGPT запускает ваш сервер как subprocess и подключается к STDIO, либо Inspector делает HTTP/stream-запрос на /mcp.
  2. Клиент отправляет JSON-RPC-запрос initialize.
  3. Сервер отвечает JSON-RPC-результатом с полями protocolVersion, capabilities и serverInfo.
  4. Клиент отправляет notification notifications/initialized — сигнал: «я всё прочитал, можно работать».
  5. Клиент вызывает методы discovery (tools/list, resources/list, prompts/list) в зависимости от того, что увидел в capabilities сервера.
  6. Сервер отдаёт метаданные инструментов/ресурсов/промптов.
  7. Дальше идут уже «рабочие» запросы: tools/call, resources/read и прочие.

Важно заметить, что handshake — это всего лишь обычный JSON-RPC-вызов initialize. Никакой магии. После лекции про формат MCP‑сообщений вы уже умеете разбирать такие запросы; единственное отличие — здесь метод всегда один и «особенный», и он выполняется первым.

3. Что отправляет клиент в initialize

Разберём запрос initialize по частям. Примерно так может выглядеть минимальный запрос (упрощённый для лекции):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

Этот пример близок к тому, что показано в официальной документации MCP. Основные поля в params:

protocolVersion

Строка с версией MCP-спецификации, чаще всего в формате даты, например "2025-06-18". Это не версия вашего приложения, а версия самого протокола. Клиент говорит: «я ожидаю говорить на такой версии MCP». Сервер в ответе должен либо подтвердить её, либо вернуть ошибку, если такой версии он не знает.

Это защита от ситуации «клиент думает одно, сервер реализует другое». Если общая версия не найдена, соединение лучше честно разорвать, чем обмениваться несовместимыми сообщениями.

capabilities клиента

Объект, в котором клиент декларирует, какие возможности MCP он сам поддерживает. Например, ChatGPT-клиент часто указывает ключ elicitation, сигнализируя, что может обрабатывать запросы к пользователю (дополнительный ввод, подтверждения и т.п.).

Пример:

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

Сервер может использовать эту информацию, чтобы понимать, какие расширенные возможности протокола вообще имеет смысл использовать. Например, elicitation означает, что клиент (ChatGPT) может задавать пользователю уточняющие вопросы и запрашивать дополнительные данные.

clientInfo

Простая метаинформация: имя и версия клиента.

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

С точки зрения разработчика сервера это золото для логов: вы всегда можете посмотреть, какой именно клиент сейчас подключился — ChatGPT, MCP Inspector, ваш собственный тестовый клиент и какой у него номер версии.

4. Что отвечает сервер: initialize result

Ответ на initialize — это обычный JSON-RPC-результат с тем же id, но в поле result кладётся описание того, что сервер умеет.

В запросе мы смотрели на capabilities со стороны клиента — что он сам поддерживает. Теперь разберём зеркальный объект в ответе: capabilities сервера, то есть что умеет он. Схематично:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {},
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.1.0"
    }
  }
}

Подобную структуру вы увидите и в официальном описании протокола и/или описании SDK. Основные части:

protocolVersion в ответе

Сервер либо повторяет версию, предложенную клиентом, либо (теоретически) мог бы выбрать другую общую версию, если их несколько. В типичных реализациях просто подтверждается версия клиента, если сервер её поддерживает. Если нет — сервер должен вернуть ошибку и прекратить общение.

serverInfo

Метаинформация о сервере: имя, версия.

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

Звучит скучно, но именно по этим данным вы будете потом фильтровать и искать в логах: «почему ChatGPT с версией X не договаривается с нашим сервером версии Y».

capabilities сервера

Самое интересное поле. Здесь сервер объявляет, какие MCP-примитивы и расширения поддерживает: может ли он обрабатывать tools/*, resources/*, prompts/*, умеет ли отправлять нотификации об изменении списков и т.п.

Если в capabilities нет секции tools, ни один корректно реализованный клиент не станет вызывать tools/list или tools/call. Точно так же отсутствие resources означает, что клиент не будет слать resources/list и resources/read.

Таким образом capabilities — это лёгкий контракт: «что можно, а что нельзя делать с этим сервером».

5. Capabilities как «список суперспособностей»

Дальше нас интересует уже только capabilities сервера — тот объект, который приходит в ответе на initialize и определяет, какие MCP-примитивы этот сервер вообще поддерживает.

Давайте подробнее посмотрим на его структуру. Пример (упрощённый, но близкий к спецификации):

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

Такой пример разбирается в официальной архитектуре MCP. Расшифруем по разделам.

Capabilities.tools

Наличие ключа tools говорит: сервер умеет отвечать на методы tools/list и tools/call. Если там ещё есть флаг listChanged: true, это означает, что сервер в будущем может присылать уведомления tools/list_changed, когда набор инструментов меняется.

Для ChatGPT это полезно: можно кэшировать список инструментов, а при получении list_changed обновить его без полного reconnect.

Capabilities.resources

Секция resources объявляет, что сервер поддерживает работу с ресурсами: resources/list, resources/read, иногда поиск. Флаги внутри:

  • subscribe: true — клиент может подписываться на изменения ресурсов (например, для live-логов или обновлений файлов).
  • listChanged: true — сервер может прислать уведомление resources/list_changed, если добавились или исчезли ресурсы.

Это особенно важно для больших каталогов или «живых» данных, которые постоянно меняются.

Capabilities.prompts

Если сервер регистрирует предзаданные промпты (например, шаблоны обращений к модели, завязанные на ваш домен), то в capabilities появляется ключ prompts. Там тоже может быть флаг listChanged.

Клиент, видя этот раздел, понимает, что доступен метод prompts/list и возможно prompts/get.

Capabilities.logging и другие

Некоторые реализации серверов объявляют ещё и logging — это значит, что сервер может отправлять клиенту структурированные логи по MCP, например, для отладки.

Также могут появляться другие разделы (например, sampling или специфичные расширения). Важно, что протокол изначально спроектирован как расширяемый: вы можете добавлять новые ключи в capabilities, а старые клиенты просто будут их игнорировать, если не знают о них.

Insight

Экспериментально установлено, что ChatGPT App игнорирует отправленные ему listChanged-сообщения. Сейчас при написании приложения вы не можете объявить один набор tools, а потом добавить или убрать еще несколько tools. Хотя MCP-протокол это позволяет.

На момент написания этого курса ситуация такова: в момент регистрации вашего приложения в ChatGPT Store, ChatGPT запрашивает у вашего приложения список tools и resources и кеширует их навсегда. Вероятность что ситуация измениться в течении 2026 года - большая, вероятность что ситуация измениться в течении первого квартала 2026 года - низкая.

6. Discovery после handshake: как получить список инструментов и ресурсов

Handshake отвечает на вопрос «что вообще умеет сервер». А следующий шаг — так называемый discovery: клиент уже по конкретным методам вытаскивает детали — какие именно инструменты есть, какие ресурсы доступны, какие промпты зашиты.

Для этого используются discovery-методы: условно tools/list, resources/list, prompts/list. В документации по MCP-архитектуре так и предлагается рассказывать: handshake → discovery → вызовы инструментов.

Пример запроса tools/list:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

Ответ сервера содержит массив инструментов: имена, описания, JSON Schema аргументов и иногда метаданные, вроде категорий или иконок.

После этого ChatGPT (или другой клиент) кэширует список и уже во время диалога использует его, чтобы:

  • подобрать подходящий инструмент для задачи пользователя;
  • проверить, что имя инструмента существует;
  • валидировать аргументы перед тем, как послать tools/call.

С ресурсами похожая история, только resources/list часто поддерживает пагинацию через курсоры, чтобы не тащить сразу миллион записей. Это тоже описано в спецификации MCP и разбирается как типичный случай для больших каталогов.

7. Handshake и capabilities на примере нашего приложения GiftGen

В предыдущих модулях мы строили учебное приложение, которое помогает подбирать подарки. У нас уже есть виджет, есть инструмент suggest_gifts на бэкенде, есть какой‑то каталог подарков. Сейчас давайте представим, как выглядит handshake для MCP-сервера gift-genius.

Пример handshake для GiftGen

Запрос от клиента:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

Ответ от нашего сервера:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.2.0"
    }
  }
}

По сути мы почти повторяем примеры из официальной архитектуры MCP, просто адаптируя имена под наше приложение.

Что узнаёт клиент из этого ответа:

  • Есть инструменты (tools), причём список может динамически меняться (listChanged: true).
  • Есть ресурсы (наш каталог подарков, возможно сохранённые в файлах или БД).
  • Есть промпты (например, шаблон «Сформулируй короткое описание подарка для пользователя N»).
  • Сервер может слать логи (удобно для инспекторов и отладки).

Дальше клиент делает tools/list и видит там, например, такой инструмент:

{
  "name": "suggest_gifts",
  "description": "Подбирает идеи подарков по профилю получателя.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

И теперь, когда пользователь пишет что‑то вроде: «Подскажи подарок для сестры, 25 лет, бюджет до 50 долларов», модель уже знает: есть инструмент suggest_gifts с таким‑то набором аргументов, его можно вызвать через tools/call.

8. Как SDK прячет handshake (но почему всё равно важно его понимать)

В TypeScript‑SDK для MCP (тот, который мы будем использовать в следующей лекции) вся эта история с initialize и notifications/initialized спрятана в методе connect. Примерный код:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "gift-genius",
  version: "1.0.0",
});

// Регистрация инструмента – SDK на основе этого сам настроит capabilities.tools
server.tool(
  "suggest_gifts",
  {
    description: "Подбирает идеи подарков.",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... логика подбора подарков ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// Здесь SDK:
// 1) принимает initialize от клиента,
// 2) отвечает с serverInfo и capabilities,
// 3) ждёт notifications/initialized,
// 4) затем начинает обрабатывать tools/* вызовы.
await server.connect(transport);

SDK автоматически собирает capabilities на основе того, что вы зарегистрировали: если есть хотя бы один server.tool(...), он добавит в capabilities секцию tools. Если вы зарегистрируете ресурсы или промпты, появятся resources и prompts.

Понимание handshake и capabilities нужно не затем, чтобы руками писать JSON (никогда так не делайте), а чтобы:

  • читать MCP-логи и понимать, почему клиент «не видит» ваши инструменты;
  • диагностировать несовместимость версий протокола;
  • при необходимости реализовать кастомный сервер или нестандартный транспорт.

9. Версии протокола и эволюция возможностей

Поле protocolVersion в handshake — это не декорация. В спецификации MCP прямо подчёркивается: это способ договориться о совместимой версии протокола; если общая версия не найдена, соединение лучше завершить.

Типичный сценарий:

  1. Вы разворачиваете MCP-сервер на проде с SDK, который реализует MCP версии "2025-06-18".
  2. Спустя время выходит новая версия MCP, вы обновляете клиент, но сервер ещё старый.
  3. Клиент шлёт protocolVersion: "2026-02-01", сервер такой версии не знает и возвращает ошибку invalid protocol version (или аналогичную).

Практика показывает: разработчики часто игнорируют это поле и потом удивляются, почему соединение не устанавливается.

Правильное отношение к версиям:

  • Всегда знать, какую версию MCP поддерживает ваш SDK (обычно в документации/релиз-нотах).
  • При обновлении SDK — осознанно обновлять версию протокола.
  • Логи и мониторинг должны явно показывать ошибки инициализации из‑за несовпадения protocolVersion.

Расширение возможностей через capabilities тоже завязано на эволюцию: новые функции MCP добавляются как новые ключи в capabilities. Старые клиенты их игнорируют, а новые могут использовать. Такой паттерн как раз и описывается в официальной документации MCP как способ поддерживать обратную совместимость.

10. Handshake глазами ChatGPT и инспектора

Что делает ChatGPT при подключении MCP

Когда вы в Dev Mode привязываете MCP-сервер к ChatGPT, платформа за сценой делает примерно следующее:

  1. Открывает транспорт (обычно HTTP/stream на /mcp).
  2. Шлёт initialize с protocolVersion, capabilities и clientInfo (что‑то вроде «ChatGPT Enterprise, версия такая‑то»).
  3. Получает ответ, кэширует capabilities сервера.
  4. Делает tools/list, resources/list, prompts/list в зависимости от увиденных capabilities.
  5. Уже во время диалога, когда модель решает вызвать инструмент, она сверяется с этим кэшем: есть ли такой tool, какая у него схема аргументов, и как оформлять вызов.

Если capabilities сервера не содержат tools, ChatGPT даже не попытается предлагать ваш App как инструмент. Если в capabilities есть resources, но в них нет флага listChanged, ChatGPT может кэшировать список ресурсов и не ждать уведомлений об изменениях.

Как инспекторы и MCP Jam помогают отладке

Инструменты вроде MCP Jam / MCP Inspector делают практически то же самое: устанавливают соединение, выполняют handshake, показывают вам capabilities сервера, и дают руками вызвать tools/list, tools/call и прочее.

С точки зрения разработчика это must-have:

  • видно, какой protocolVersion реально отдал сервер;
  • сразу видно, есть ли в capabilities tools, resources, prompts;
  • можно понять, почему ChatGPT не видит инструменты (capabilities не объявлены или handshake не прошёл).

В последней лекции этого модуля вы будете использовать такие инструменты более плотно, но уже сейчас полезно понимать, что они работают ровно поверх того handshake, который мы разбираем.

11. Типичные ошибки при работе с handshake и capabilities

В теории всё выглядит довольно прямолинейно, но на практике именно handshake и объявление capabilities чаще всего становятся источником очень примитивных багов — особенно в Dev Mode или MCP Inspector. Ниже — несколько типичных ошибок, с которыми вы почти наверняка столкнётесь или в своём коде, или в логах коллег.

Ошибка №1: Неправильный формат initialize-запроса.
Очень частая проблема при ручной реализации MCP-сервера без SDK — потерять какое‑нибудь обязательное поле JSON-RPC. Например, забыть jsonrpc: "2.0", перепутать method (написать "init" вместо "initialize"), или сделать capabilities булевым значением вместо объекта. Спецификация MCP ожидает чёткий формат; любые отклонения приводят к ошибкам парсинга и разрыву соединения. Документация и практические гайды отдельно советуют вначале убедиться, что initialize у вас строго соответствует спецификации, прежде чем смотреть на что‑то ещё.

Ошибка №2: Игнорирование protocolVersion.
Иногда разработчики просто копируют пример из документации и ставят туда произвольную строку, не глядя на поддержку в SDK. В результате клиент и сервер говорят на разных версиях MCP, и соединение не устанавливается. Ошибка может маскироваться как «клиент вообще не подключается». Нужно относиться к protocolVersion как к реальному контракту: согласовать эту версию между командой фронтенда/агентной платформы и командой, которая пишет MCP-сервер.

Ошибка №3: Забытые capabilities.
Классическая ситуация: вы зарегистрировали инструмент на сервере, но при ручной реализации handshake забыли добавить "tools": {} в capabilities ответа initialize. В inspector вы видите, что инструменты есть, а ChatGPT показывает «No tools available» — потому что честно верит capabilities и не делает tools/list, если секции tools там нет. Troubleshooting‑гайды для Apps SDK отдельно подчёркивают: если ChatGPT не видит инструменты, первым делом проверяйте capabilities.

Ошибка №4: Попытка использовать методы, не заявленные в capabilities.
Иногда студенты экспериментируют и, например, шлют resources/list к серверу, у которого в capabilities нет секции resources. Формально сервер может ответить Method not found, но корректнее вообще не вызывать такие методы. MCP специально вводит capabilities как защиту от подобных попыток. Клиент должен сначала посмотреть, есть ли соответствующая секция в capabilities, и только потом вызывать методы.

Ошибка №5: Сервер начинает «болтать» до notifications/initialized.
Если сервер сразу после отправки ответа на initialize начинает слать клиенту логи или уведомления, не дождавшись notifications/initialized, некоторые клиенты могут проигнорировать эти сообщения или даже разорвать соединение. В официальной архитектуре MCP подчёркивается, что сначала handshake должен завершиться, и только после уведомления об инициализации начнётся «рабочая» жизнь.

Ошибка №6: Изменение схемы инструментов без сигнала о смене списка.
Когда вы меняете JSON Schema инструмента (делаете поле обязательным, переименовываете аргумент), но не перезапускаете сервер или не отправляете уведомление о том, что список инструментов изменился, кэш клиента может содержать старую версию схемы. Это приводит к странным ошибкам валидации. Спецификация предлагает использовать флаг listChanged и уведомления tools/list_changed и resources/list_changed, чтобы помогать клиенту своевременно обновлять кэш.

Ошибка №7: Преждевременная оптимизация и «магия» вокруг capabilities.
Иногда разработчики начинают придумывать сложные схемы с динамической генерацией capabilities, версионированием по клиентам и прочей экзотикой, не разобравшись в базовых механизмах. На старте достаточно честно объявить, что сервер умеет: tools, resources, prompts, logging. Расширять capabilities стоит по мере реальной необходимости, а не «на будущее». Это скорее организационный анти‑паттерн, чем чисто протокольная ошибка, но в боевых проектах встречается очень часто.

1
Задача
ChatGPT Apps, 6 уровень, 2 лекция
Недоступна
Валидатор запроса initialize (JSON-RPC “детектив”)
Валидатор запроса initialize (JSON-RPC “детектив”)
1
Задача
ChatGPT Apps, 6 уровень, 2 лекция
Недоступна
Ручной handshake endpoint (initialize + notifications/initialized)
Ручной handshake endpoint (initialize + notifications/initialized)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ