1. Що сьогодні побудуємо й як це вписується в застосунок
Згадайте наш навчальний застосунок: ми робимо асистента для вибору подарунків. У попередніх модулях ви вже зробили:
- віджет у ChatGPT (Next.js 16 + Apps SDK), який показує UI, відображає стан і вміє викликати callTool;
- простий бекенд (через Apps SDK / маршрути Next.js), який повертав заглушки з ідеями подарунків.
Тепер ми хочемо винести «мозок» нашого асистента в окремий MCP‑сервер. У результаті схема буде такою:
flowchart TD
subgraph ChatGPT
U[Користувач
у чаті]
W["Віджет App
(Apps SDK)"]
end
subgraph MCP‑клієнт
C[ChatGPT MCP client]
end
subgraph OurServer[Наш MCP‑сервер]
T1[Tool: suggest_gifts]
R1[Resource: gift_catalog]
P1[Prompt: birthday_template]
end
U --> W
W -- callTool --> C
C <-- JSON-RPC / HTTP --> OurServer
OurServer --> C
C --> W
Тобто тепер:
- модель усередині ChatGPT бачить наш MCP‑сервер як стандартний набір tools/resources/prompts;
- callTool із віджета логічно перетворюється на внутрішній виклик MCP;
- наш сервер описує контракти (схеми, описи) й реалізує бізнес‑логіку.
Наприкінці цієї лекції у вас буде окремий Node/TypeScript‑проєкт із MCP‑сервером, який:
- запускається локально однією командою;
- реєструє щонайменше один інструмент і один ресурс;
- повертає змістовні дані (хоч і на простих моках);
- організований так, щоб його можна було безболісно розвивати далі.
Водночас поточний бекенд через Apps SDK/Next.js ми зараз не переписуємо. Він лишається як є, а MCP‑сервер запускаємо як окремий сервіс поруч. Пізніше ви зможете підʼєднати його до ChatGPT App і поступово перенести туди «подарункову» логіку замість старих заглушок.
2. Стек: TypeScript + MCP SDK + HTTP‑транспорт
MCP‑сервер ми писатимемо на TypeScript під Node.js. Офіційний JS/TS SDK для MCP доступний у пакеті @modelcontextprotocol/sdk. Він бере на себе рутину з JSON‑RPC, валідації та перетворення схем. Ви описуєте аргументи через Zod‑схеми, а SDK сам перетворює їх на JSON Schema, зрозумілу для моделі.
Для транспорту нам потрібен HTTP‑варіант: ChatGPT спілкується з віддаленими MCP‑серверами мережею, а не через stdio локально. Специфікація MCP описує стандартний формат «потокового HTTP» — по суті, еволюцію старої схеми HTTP+SSE. На практиці це один HTTP‑ендпойнт, який обробляє запит (POST/GET) і, за потреби, стрімить відповідь. У TypeScript SDK для MCP зазвичай уже є готовий транспорт під такий формат. Його можна підʼєднати, наприклад, до Express або Hono.
Щоб не розпорошуватися, припустімо, що в нас є:
- серверний обʼєкт McpServer із @modelcontextprotocol/sdk;
- HTTP‑транспорт (наприклад, StreamableHttpServerTransport або аналогічний), який можна без проблем інтегрувати з Express.
Назви класів можуть трохи різнитися між версіями SDK, але архітектура завжди однакова:
- створюєте обʼєкт MCP‑сервера;
- реєструєте на ньому tools/resources/prompts;
- підʼєднуєте транспорт до HTTP‑застосунку.
3. Структура проєкту й підготовка
Створімо окрему теку для MCP‑сервера. Її зручно тримати поруч із фронтенд‑застосунком, але як окремий Node‑проєкт:
chatgpt-gift-app/
app/ ← Next.js + Apps SDK (віджет)
mcp-server/ ← наш MCP‑сервер
Усередині mcp-server:
mcp-server/
src/
server.ts ← точка входу MCP‑сервера
gifts.ts ← бізнес‑логіка підбору подарунків
package.json
tsconfig.json
Приклад простого файлу gifts.ts ми зробимо трохи пізніше. А зараз зосередимося на server.ts.
Припустімо, що ви вже ініціалізували проєкт:
mkdir mcp-server
cd mcp-server
npm init -y
npm install typescript ts-node-dev zod express @modelcontextprotocol/sdk
tsconfig.json — цілком звичайний (модулі ESNext, ціль Node.js, strict). Можете взяти його з будь‑якого вашого TS‑проєкту.
4. Виносимо бізнес‑логіку в окремий модуль
Іноді дуже хочеться одразу написати server.registerTool(..., async () => {...}) і там же нашвидкуруч накидати всю логіку. Але краще від самого початку розділити:
- модуль, який нічого не знає про MCP, JSON‑RPC та інші «радощі»;
- модуль, який знає лише про MCP, але майже не торкається бізнес‑логіки.
У src/gifts.ts опишемо просту функцію підбору подарунків:
// src/gifts.ts
export type GiftIdea = {
id: string;
title: string;
price: number;
occasion: string;
};
export type SuggestGiftsInput = {
age: number;
relationship: "friend" | "partner" | "child" | "coworker";
budget: number;
};
export function suggestGifts(input: SuggestGiftsInput): GiftIdea[] {
// поки просто моки
return [
{
id: "book-1",
title: "Книга про улюблене хобі",
price: Math.min(input.budget, 30),
occasion: "generic",
},
{
id: "game-1",
title: "Настільна гра для компанії",
price: Math.min(input.budget, 50),
occasion: "party",
},
];
}
Це «чиста» функція: на вході параметри, на виході — масив ідей. Її можна тестувати юніт‑тестами, перевикористовувати в інших місцях, і вона ніяк не залежить від MCP. Саме так і радять робити: серверна обвʼязка — окремо, бізнес‑функції — окремо.
5. Створюємо MCP‑сервер і підʼєднуємо HTTP‑транспорт
Тепер переходимо до точки входу src/server.ts. Схематично потрібно:
- створити екземпляр MCP‑сервера;
- зареєструвати на ньому інструменти, ресурси й промпти;
- підняти HTTP‑сервер (наприклад, Express) і підʼєднати до нього MCP‑транспорт.
Почнемо із заготовки:
// src/server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/sdk/transport/streamable-http";
const app = express();
// 1. Створюємо MCP‑сервер
const mcpServer = new McpServer({
name: "gift-assistant-mcp",
version: "0.1.0",
});
// 2. Тут пізніше зареєструємо tools/resources/prompts
// 3. Налаштовуємо транспорт поверх HTTP
const transport = new StreamableHttpServerTransport({
path: "/mcp", // єдиний endpoint MCP
app, // вбудовуємося в Express‑застосунок
});
transport.attach(mcpServer);
const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
});
Конкретні назви класів транспорту можуть відрізнятися, але підхід один: ви створюєте HTTP‑ендпойнт і підʼєднуєте до нього MCP‑сервер як обробник JSON‑RPC поверх HTTP/стріму.
Поки що сервер не робить нічого корисного, зате вже вміє:
- проходити MCP‑handshake;
- відповідати на базові запити discovery (список tools/resources/prompts — поки порожній).
Наступний крок — зареєструвати перший інструмент.
6. Реєструємо tool suggest_gifts через MCP SDK
Офіційні Apps SDK і документація MCP показують один і той самий підхід до реєстрації інструмента: метод registerTool. Туди ви передаєте імʼя, дескриптор (заголовок, опис, схему аргументів) і обробник.
Ми вже описали тип SuggestGiftsInput у gifts.ts. Тепер додамо Zod‑схему, щоб сервер міг валідувати вхідні аргументи й автоматично віддати LLM коректний JSON Schema.
// src/server.ts (фрагмент)
import { z } from "zod";
import { suggestGifts } from "./gifts";
const suggestGiftsInputSchema = z.object({
age: z.number().int().min(0).max(120),
relationship: z.enum(["friend", "partner", "child", "coworker"]),
budget: z.number().min(0),
});
Тепер реєструємо інструмент:
// все ще в server.ts
mcpServer.registerTool(
"suggest_gifts",
{
title: "Suggest gift ideas",
description:
"Підбирає ідеї подарунків за віком, типом стосунків і бюджетом.",
// SDK сконвертує Zod‑схему в JSON Schema для моделі
inputSchema: suggestGiftsInputSchema,
},
async ({ input }) => {
const ideas = suggestGifts(input);
const text = ideas
.map(
(g) =>
`• ${g.title} — ~${g.price} USD (occasion: ${g.occasion}, id: ${g.id})`
)
.join("\n");
return {
content: [
{
type: "text",
text,
},
],
// structuredContent можна використовувати у віджеті
structuredContent: {
ideas,
},
};
}
);
Ключові моменти:
- inputSchema — Zod‑схема. TS SDK уміє перетворювати її на JSON Schema й таким чином автоматично описує інструмент для моделі.
- Обробник приймає обʼєкт із input (тип якого ви отримуєте зі схеми). Усередині можна викликати свою бізнес‑функцію.
- У result ви повертаєте content — текст, який модель побачить як результат, і, за бажання, structuredContent із JSON‑структурою, яку потім може використати ваш віджет.
Якщо в попередніх модулях ви вже робили інструмент через Apps SDK, цей код має виглядати знайомо: підхід рівно той самий, тільки тепер він живе в окремому MCP‑сервері.
7. Додаємо ресурс gift_catalog для даних
Інструменти — це дії. Іноді хочеться ще надавати дані як ресурс, щоб модель могла їх читати й шукати в них, або щоб ваш віджет міг підвантажувати шаблони, компоненти тощо. MCP окремо описує концепцію ресурсів із URI, MIME‑типами та вмістом.
Зробимо простий ресурс gift_catalog, який повертає список доступних подарунків. Поки це будуть ті самі моки, але в реальному проєкті це могла б бути вивантажена база або фід товарів.
Спочатку сам каталог:
// src/gifts.ts (доповнення)
export const giftCatalog: GiftIdea[] = [
{
id: "book-1",
title: "Книга з програмування",
price: 25,
occasion: "learning",
},
{
id: "lego-1",
title: "Набір LEGO",
price: 60,
occasion: "fun",
},
];
Тепер реєструємо ресурс на сервері:
// src/server.ts (фрагмент)
import { giftCatalog } from "./gifts";
mcpServer.registerResource(
"gift_catalog",
{
title: "Gift catalog",
description: "Простий каталог подарунків для демо та налагодження.",
mimeType: "application/json",
},
async () => {
return {
contents: [
{
uri: "mcp://gift-catalog",
mimeType: "application/json",
text: JSON.stringify(giftCatalog, null, 2),
},
],
};
}
);
Що тут відбувається з погляду логіки:
- імʼя ресурсу gift_catalog буде видно клієнту під час discovery (у MCP‑інспекторі ви потім побачите його у списку ресурсів);
- дескриптор містить зрозумілий опис і MIME‑тип;
- обробник повертає масив contents з URI та текстом — це стандартний формат ресурсу в MCP.
Згодом ви зможете:
- читати цей ресурс із клієнта (наприклад, агент або інспектор);
- використовувати його як шаблони/дані для UI;
- експериментувати з тим, як модель використовує готовий каталог, щоб пояснювати користувачеві варіанти.
8. Реєструємо простий prompt
Третя сутність MCP — промпти, заздалегідь підготовлені підказки. Вони дають змогу не повторювати довгі системні чи користувацькі промпти, а зберігати їх на сервері під іменами.
Зробимо мініприклад: промпт birthday_gift, який можна викликати як передзаповнений шаблон розмови про подарунок на день народження.
// src/server.ts (фрагмент)
mcpServer.registerPrompt("birthday_gift", {
title: "Birthday gift helper",
description: "Шаблон запиту для підбору подарунка на день народження.",
messages: [
{
role: "system",
content:
"Ти асистент з пошуку подарунків. Став уточнювальні запитання й пропонуй кілька варіантів.",
},
{
role: "user",
content:
"Мені потрібен подарунок на день народження. Постав потрібні уточнення й допоможи обрати.",
},
],
});
Під капотом MCP дає клієнтам змогу:
- отримати список промптів (в інспекторі ви побачите birthday_gift);
- запросити його вміст і використати як базову підказку для моделі.
Окремо, у модулі про system‑prompt та інструкції, ми докладно розбираємо, як такі промпти поєднуються з глобальними інструкціями застосунку. Тут же нам важливо просто «побачити» їх як частину MCP‑сервера.
9. Як усе це працює під час виконання
Складімо картинку повністю.
Коли клієнт (наприклад, MCP Inspector або ChatGPT) підʼєднується до нашого HTTP‑ендпойнта /mcp:
- відбувається handshake: клієнт і сервер обмінюються інформацією про підтримувані можливості (tools/resources/prompts тощо);
- клієнт викликає методи discovery: отримує список інструментів, ресурсів і промптів разом із їхніми описами та схемами;
- коли модель вирішує викликати інструмент, вона формує JSON‑RPC‑запит із методом на кшталт tools/call чи подібним — а SDK на боці сервера перетворює це на внутрішній виклик обробника registerTool;
- обробник виконує бізнес‑логіку (у нас це suggestGifts або видача giftCatalog) і повертає результат у стандартизованому форматі;
- SDK серіалізує відповідь назад у JSON‑RPC і надсилає її клієнту через той самий HTTP/стрім‑транспорт.
Усі деталі JSON‑RPC, формування id, маршрутизації методів тощо залишаються всередині @modelcontextprotocol/sdk. Для вас інтерфейс дуже схожий на Apps SDK: ви працюєте з registerTool/registerResource/registerPrompt та обробниками й не думаєте про протокол.
10. Локальний запуск і перший простий тест
Припустімо, ви додали все, що описано вище. Тепер лишається запустити сервер.
У package.json можна додати скрипт:
{
"scripts": {
"dev": "ts-node-dev src/server.ts"
}
}
Запускаємо:
npm run dev
У консолі має зʼявитися щось на кшталт:
MCP server listening on http://localhost:4000/mcp
Повноцінну інспекцію й ручні виклики інструментів робитимемо в наступній лекції через MCP Inspector / MCP Jam. Але навіть зараз можна виконати дуже простий smoke‑тест через curl:
curl -X POST http://localhost:4000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Цей curl — цілком факультативний smoke‑тест для тих, хто любить дивитися на «сирі» JSON‑відповіді. У реальній розробці ви майже завжди спілкуєтеся з MCP‑сервером через SDK, а не вручну складаєте JSON‑RPC‑запити.
Точна назва методу залежить від версії протоколу та SDK, але ідея проста: ви отримаєте JSON‑список, де серед tools буде видно suggest_gifts. Якщо метод не збігається — не страшно. Завдання лекції не в тому, щоб напамʼять знати всі назви, а в тому, щоб ви не боялися дивитися на JSON‑відповіді й розуміли їхню структуру, спираючись на попередні лекції.
11. Звʼязок із нашим ChatGPT App і подальший розвиток
Поки MCP‑сервер живе сам по собі. У наступних модулях ви:
- підʼєднаєте його до MCP Inspector і навчитеся налагоджувати tools/resources/prompts окремо, не чіпаючи ChatGPT;
- налаштуєте ChatGPT App так, щоб він бачив цей MCP‑сервер як джерело інструментів;
- перенесете частину логіки, яка раніше була реалізована всередині Apps SDK (наприклад, через вбудовані tools), у MCP‑шар;
- додасте авторизацію, логування, потокові сценарії — уже поверх готового каркаса.
Зараз важливо, що:
- у вас є окремий сервіс, який відповідає за «вміння» й «дані» застосунку;
- цей сервіс говорить із клієнтами через стандарт MCP, а не через кастомний REST;
- ви вже вмієте вручну реєструвати інструменти, ресурси й промпти — і не боїтеся протоколу.
12. Трохи про структуру коду й найкращі практики
Навіть на такому маленькому прикладі можна закласти хороші звички.
По‑перше, тримайте конфігурацію сервера окремо. Усе, що стосується імені, версії, логування, налаштувань транспорту (порт, шлях /mcp), легко винести в невеликий модуль config.ts. Потім, коли розгортатимете застосунок на Vercel або через MCP‑gateway, доведеться додавати змінні середовища — і ви ще подякуєте собі за таку структуру.
По‑друге, намагайтеся, щоб методи registerTool/registerResource/registerPrompt залишалися максимально «тонкими». Опис схем, тексти й бізнес‑логіка — речі, які добре виглядають в окремих файлах:
- gifts.ts — функції вибору подарунків;
- catalog.ts — робота з каталогом товарів;
- prompts.ts — набір промптів.
Тоді server.ts перетворюється на «провайдера MCP», який просто склеює все докупи.
По‑третє, памʼятайте, що MCP‑сервер за своєю природою реактивний: він очікує підключення клієнтів і їхніх запитів. Це означає, що будь‑які блокувальні або надто довгі операції всередині інструментів безпосередньо впливатимуть на UX у ChatGPT. У наступних модулях ми поговоримо і про таймаути, і про асинхронні операції, і про потокові відповіді. Але вже зараз варто подумати, які операції можна винести у фон, а які мають відповідати швидко.
Нотатка: ChatGPT підтримує лише частину MCP
Важливо розуміти: ChatGPT Apps використовують MCP як транспорт і формат, але не є повноцінними MCP-клієнтами. Якщо орієнтуватися лише на протокол, легко сформувати хибні очікування щодо того, як усе працюватиме в середовищі виконання.
Що обіцяє «чистий» MCP:
- ресурси (resources) можуть читатися динамічно, на запит клієнта, а не «один раз і назавжди»;
- сервер може надсилати resourceChanged/toolChanged‑сповіщення і тим самим передавати оновлення без перезапуску клієнта;
- можна будувати досить гнучку систему, де набір tools/resources/prompts керується конфігами або зовнішнім станом.
У контексті ChatGPT Apps це працює інакше. Для застосунку картина значно статичніша:
- під час реєстрації застосунку ChatGPT один раз зчитує опис усіх tools і resources;
- потім ця конфігурація фактично кешується як частина версії застосунку;
- динамічні оновлення через MCP‑сповіщення не підтримуються — платформа їх просто ігнорує.
13. Типові помилки під час написання першого MCP‑сервера
Помилка № 1: змішати всю бізнес‑логіку прямо в registerTool.
Спокуса «швидко написати все в обробнику інструмента» величезна, особливо в навчальному прикладі. Але потім це перетворюється на важкочитну конструкцію, де впереміш — валідація, робота з базою даних і форматування відповіді. Краще відразу винести бізнес‑функції (suggestGifts, роботу з каталогом) в окремі модулі, а в обробнику робити лише «склеювання».
Помилка № 2: жорстко привʼязатися до конкретних імен JSON‑методів MCP.
Іноді студенти починають писати if (method === "tools/list") і вручну розбирати JSON. Так робити не треба: це робота SDK. Специфікація MCP і назви методів можуть еволюціонувати, а SDK бере цю турботу на себе. Використовуйте registerTool, registerResource, registerPrompt і дайте бібліотеці вирішувати, як саме це виглядає в JSON‑RPC.
Помилка № 3: не думати про транспорт і намагатися підʼєднати до ChatGPT stdio‑сервер.
Stdio‑транспорт ідеальний для локальних клієнтів на кшталт десктопних середовищ, де клієнт може запускати сервер як підпроцес. Але ChatGPT спілкується через HTTPS, і йому потрібен HTTP/стрім‑ендпойнт. Спроба «якось протягнути stdio» через тунель зазвичай закінчується проблемами. Для ChatGPT App одразу робіть HTTP‑транспорт (Streamable HTTP).
Помилка № 4: ігнорувати MIME‑типи й структуру ресурсів.
Для ресурсів важливі не лише вміст, а й тип (mimeType) і URI. Якщо всюди ставити text/plain і бездумно віддавати JSON‑рядки, клієнтам (і інспекторам) буде складніше розуміти, що це за дані. Намагайтеся вказувати коректні MIME‑типи (application/json, text/html для шаблонів UI тощо) і стабільні URI.
Помилка № 5: використовувати MCP‑сервер як «випадковий HTTP‑API».
Іноді виникає спокуса: «Раз у мене вже є Express, я повішу ще /api/whatever і звертатимуся туди напряму». Змішувати MCP‑ендпойнт із довільним REST краще не варто: це ускладнює конфігурацію, роутинг і безпеку. Простіше мати чіткий контракт: /mcp для MCP, окремі шляхи для інших потреб або взагалі інший сервіс. У продакшені це особливо важливо для конфігурування gateway і авторизації. Тож не перетворюйте MCP‑сервер на «випадковий HTTP‑API» — набір ендпойнтів, не повʼязаних із MCP‑контрактом.
Помилка № 6: не логувати вхідні та вихідні MCP‑повідомлення.
Без логів MCP‑сервер перетворюється на «чорну скриньку»: «щось не працює, але я не знаю що». Уже на першому сервері має сенс хоча б у stderr виводити компактні структуровані логи: метод інструмента, статус, час виконання. Головне — не записувати в логи чутливі дані й токени. Це ми окремо обговоримо далі, коли дійдемо до безпеки.
Помилка № 7: намагатися налагоджувати все одразу через ChatGPT, не маючи інспектора.
Типова картина: студент пише MCP‑сервер, одразу підʼєднує його до ChatGPT App — і все ламається без зрозумілої причини. При цьому інспектор ще жодного разу не запускався. У результаті складно зрозуміти, де проблема: у протоколі, у сервері, в Apps SDK чи в поведінці моделі. Правильний шлях — спочатку переконатися, що MCP‑сервер коректно працює в ізоляції (через MCP Jam / Inspector), а вже потім підʼєднувати його до застосунку.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ