JavaRush /Курсы /ChatGPT Apps /Первый MCP‑сервер: от SDK до рабочих tools/resources/prom...

Первый MCP‑сервер: от SDK до рабочих tools/resources/prompts

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

1. Что сегодня построим и как это вписывается в приложение

Вспомним наше учебное приложение: мы делаем ассистента по выбору подарков. В предыдущих модулях у нас уже был:

  • виджет в ChatGPT (Next.js 16 + Apps SDK), который показывает UI, состояние и умеет дергать callTool;
  • простой backend (через 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‑сервером, который:

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

При этом существующий backend через 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‑endpoint, который обрабатывает запрос (POST/GET) и при необходимости стримит ответ. В TypeScript‑SDK для MCP обычно уже есть готовый транспорт под такой формат, который можно прикрутить к Express или Hono.

Чтобы не распыляться, мы будем считать, что у нас есть:

  • серверный объект McpServer из @modelcontextprotocol/sdk;
  • HTTP‑транспорт (например, StreamableHttpServerTransport или аналогичный), который можно подружить с Express.

Точные имена классов могут слегка меняться между версиями SDK, но архитектурно это всегда:

  1. создаёте объект MCP‑сервера;
  2. регистрируете на нём tools/resources/prompts;
  3. подключаете транспорт к 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 modules, target node, 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. Схематично нам нужно:

  1. создать экземпляр MCP‑сервера;
  2. зарегистрировать на нём инструменты, ресурсы и промпты;
  3. поднять 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‑endpoint и подключаете к нему 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‑схема. SDK для TS умеет превращать её в JSON Schema и таким образом автоматически описывает инструмент для модели.
  • Обработчик принимает объект с input (тип которого вы получаете из схемы). Внутри вы можете вызывать свою бизнес‑функцию.
  • В result вы возвращаете content — это текст, который модель увидит как результат, и, при желании, structuredContent с JSON‑структурой, которую потом может потребить ваш виджет.

Если вы в предыдущих модулях уже делали инструмент через Apps SDK, этот код должен выглядеть очень знакомо: паттерн ровно тот же, только теперь он живёт в отдельном MCP‑сервере.

7. Добавляем ресурс gift_catalog для данных

Инструменты — это действия. Иногда хочется ещё предоставлять данные как ресурс, чтобы модель могла их читать, искать по ним или чтобы ваш виджет мог подгружать шаблоны, компоненты и так далее. MCP отдельно описывает концепцию ресурсов с URI, MIME‑типами и содержимым.

Сделаем простой ресурс gift_catalog, который возвращает список доступных подарков. Пока это будут те же моки, но в реале это могла бы быть выгрузка из базы или product feed.

Сначала сам каталог:

// 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‑endpoint /mcp:

  1. происходит handshake: клиент и сервер обмениваются информацией о поддерживаемых возможностях (tools/resources/prompts и т.д.);
  2. клиент вызывает методы discovery: получает список инструментов, ресурсов, промптов вместе с их описаниями и схемами;
  3. когда модель решает вызвать инструмент, она формирует JSON‑RPC запрос с методом вроде tools/call или аналогичным — SDK на стороне сервера превращает это во внутренний вызов registerTool‑обработчика;
  4. обработчик выполняет бизнес‑логику (у нас это suggestGifts или выдача giftCatalog) и возвращает результат в стандартизованном формате;
  5. 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. Немного о структуре кода и best practices

Даже на таком маленьком примере можно заложить хорошие привычки.

Во‑первых, держите конфигурацию сервера отдельно. Всё, что касается имени, версии, логирования, настроек транспорта (порт, путь /mcp), легко вынести в маленький модуль config.ts. Потом, когда будете деплоить на Vercel или за MCP‑gateway, придётся добавлять env‑переменные, и вы себя поблагодарите.

Во‑вторых, старайтесь, чтобы методы registerTool/registerResource/registerPrompt оставались максимально «тонкими». Описание схем, текстов и бизнес‑логика — штуки, которые хорошо смотрятся в отдельных файлах:

  • gifts.ts — функции выбора подарков;
  • catalog.ts — работа с каталогом товаров;
  • prompts.ts — набор промптов.

Сам server.ts тогда превращается в нечто вроде «провайдера MCP», который просто склеивает всё воедино.

В‑третьих, помните, что MCP‑сервер по своей природе реактивен: он ожидает подключения клиентов и их запросов. Это значит, что любые блокирующие или чрезмерно долгие операции внутри инструментов будут напрямую влиять на UX в ChatGPT. В следующих модулях мы поговорим и про таймауты, и про асинхронные операции, и про потоковые ответы, но уже сейчас стоит думать, какие операции можно выделить в фон, а какие должны отвечать быстро.

Insight: ChatGPT поддерживает только часть MCP

Важно понимать: ChatGPT Apps используют MCP как транспорт и формат, но не являются полноценным MCP-клиентом. Если читать только протокол, легко построить неверные ожидания о том, как всё будет работать в рантайме.

Что обещает «чистый» MCP:

  • ресурсы (resources) могут читаться динамически, по запросу клиента, а не один раз навсегда;
  • сервер может отправлять resourceChanged/toolChanged-нотификации и тем самым «пропихивать» обновления без перезапуска клиента;
  • можно строить достаточно гибкую систему, где набор tools/resources/prompts управляется конфигами или внешним состоянием.

В контексте ChatGPT Apps это не так. Для приложения картинка гораздо более статична:

  • при регистрации App 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/стрим‑endpoint. Попытка «как‑нибудь прокинуть 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‑endpoint с произвольным REST лучше не стоит: это усложняет конфигурацию, роутинг и безопасность. Проще иметь чёткий контракт: /mcp для MCP, отдельные пути для других нужд, или вообще другой сервис. В продакшене это особенно важно для конфигурирования gateways и авторизации. То есть не превращайте MCP‑сервер в «рандомный HTTP‑API» — набор случайных HTTP‑ручек, не связанных с MCP‑контрактом.

Ошибка №6: Не логировать входящие и исходящие MCP‑сообщения.
Без логов MCP‑сервер превращается в чёрный ящик: «что‑то не работает, но я не знаю, что». Уже на первом сервере имеет смысл хотя бы в stderr писать компактные структурированные логи: метод инструмента, статус, время выполнения. Главное — не логировать чувствительные данные и токены, это мы дальше отдельно обсудим, когда дойдём до безопасности.

Ошибка №7: Пытаться отлаживать всё сразу через ChatGPT, не имея инспектора.
Частая картина: студент пишет MCP‑сервер, тут же подключает его к ChatGPT App, и всё «непонятно ломается». При этом инспектор даже ни разу не запускался. В результате сложно понять, проблема в протоколе, в сервере, в Apps SDK или в поведении модели. Правильный путь — сначала убедиться, что MCP‑сервер корректно работает в изоляции (через MCP Jam / Inspector), а уже потом подключать его к приложению.

1
Задача
ChatGPT Apps, 6 уровень, 3 лекция
Недоступна
Первый MCP tool “suggest_gifts” (тонкий сервер + чистая бизнес-логика)
Первый MCP tool “suggest_gifts” (тонкий сервер + чистая бизнес-логика)
1
Задача
ChatGPT Apps, 6 уровень, 3 лекция
Недоступна
MCP resource “gift_catalog” + prompt “birthday_gift” (без инструментов)
MCP resource “gift_catalog” + prompt “birthday_gift” (без инструментов)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ