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

Щоб картина не здавалася абстрактною, подивімося на типовий сценарій (перебіг) з’єднання — дуже спрощений:

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 запускає ваш сервер як підпроцес і підʼєднується до STDIO, або Inspector робить HTTP/stream‑запит на /mcp.
  2. Клієнт надсилає JSON-RPC‑запит initialize.
  3. Сервер відповідає JSON-RPC‑результатом із полями protocolVersion, capabilities і serverInfo.
  4. Клієнт надсилає сповіщення 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 — оновити його без повного перепідʼєднання.

Capabilities.resources

Секція resources оголошує, що сервер підтримує роботу з ресурсами: resources/list, resources/read, іноді — пошук. Прапорці всередині:

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

Це особливо важливо для великих каталогів або «живих» даних, що постійно змінюються.

Capabilities.prompts

Якщо сервер публікує заздалегідь задані промпти (наприклад, шаблони звернень до моделі, привʼязані до вашого домену), то в capabilities зʼявляється ключ prompts. Там також може бути прапорець listChanged.

Клієнт, побачивши цей розділ, розуміє, що доступний метод prompts/list і, можливо, prompts/get.

Capabilities.logging та інші

Деякі реалізації серверів оголошують ще й logging. Це означає, що сервер може надсилати клієнту структуровані логи через MCP — наприклад, для налагодження.

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

Insight

На практиці видно, що застосунок ChatGPT ігнорує надіслані йому повідомлення про listChanged. Зараз, під час розробки застосунку, ви не можете оголосити один набір tools, а потім додати або прибрати ще кілька tools. Хоча MCP‑протокол це дозволяє.

На момент написання цього курсу ситуація така: під час реєстрації вашого застосунку в ChatGPT Store платформа запитує у вашого застосунку список 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 навіть не спробує пропонувати ваш застосунок як інструмент. Якщо в capabilities є resources, але там немає прапорця listChanged, ChatGPT може кешувати список ресурсів і не чекати сповіщень про зміни.

Як інспектори та MCP Jam допомагають налагодженню

Інструменти на кшталт MCP Jam / MCP Inspector роблять практично те саме: встановлюють зʼєднання, виконують handshake, показують вам capabilities сервера та дають вручну викликати tools/list, tools/call тощо.

З погляду розробника це те, без чого не обійтися:

  • видно, яку 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 там немає. Посібники з усунення несправностей для 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 варто в міру реальної потреби, а не «про всяк випадок». Це радше організаційний антипатерн, ніж суто протокольна помилка, але в реальних проєктах трапляється доволі часто.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ