Выбор фреймворка (Express.js) и определение эндпоинтов

Теперь, когда вы умеете создавать полезные инструменты для ИИ-агентов, настало время подключить их к реальной системе. До этого мы изучали, как писать функции-инструменты на TypeScript — например, для получения данных по API или генерации PDF. Но агент не сможет ими воспользоваться, пока они не станут доступны через MCP-сервер.

Сервер — это мост между LLM (например, Claude или YandexGPT) и вашими инструментами. Он должен понимать, что он может делать, как обрабатывать запросы и как вызывать нужные функции. Для этого MCP определяет строгий стандарт взаимодействия, который реализуется через HTTP-эндпоинты.

Выбор фреймворка: почему Express.js?

Для создания MCP-сервера на TypeScript нам нужен HTTP-фреймворк. Существует несколько вариантов: NestJS, Fastify, Koa, но мы выбираем Express.js — и вот почему:

  • Простота и минимализм — Express.js не навязывает сложную архитектуру, что идеально для старта.
  • Отличная поддержка TypeScript — легко настроить типизацию, автодополнение и контроль ошибок.
  • Огромное сообщество и документация — тысячи примеров, готовых решений и плагинов.
  • Гибкая middleware-архитектура — позволяет легко добавлять логирование, аутентификацию и обработку ошибок позже.

Express.js — как надёжный каркас для дома: он не делает всё за вас, но даёт прочную основу, на которой можно строить быстро и безопасно.

Хотя NestJS предлагает более строгую структуру и подходит для больших проектов, на этапе создания MVP и первого MCP-агента Express.js — лучший выбор. Он позволяет сосредоточиться на логике, а не на конфигурации.

Основные эндпоинты MCP-сервера

MCP-сервер должен реализовывать три ключевых эндпоинта (или маршрута, routes), чтобы быть совместимым с любым MCP-клиентом:

ЭндпоинтМетодНазначение
/capabilitiesGETОписание возможностей сервера
/promptPOSTОбработка запроса от LLM
/toolPOSTВызов конкретного инструмента

Разберём каждый из них.

/capabilities — визитная карточка сервера

Этот эндпоинт отвечает на GET-запрос и возвращает JSON с описанием всех доступных инструментов. Именно так LLM узнаёт, что может делать ваш агент.

Например, если вы создали инструмент для генерации PDF (как в теме Примеры создания пользовательских инструментов), он должен быть описан здесь — с именем, описанием и параметрами.

Правильный ответ:

{
  "tools": [
    {
      "name": "generatePDF",
      "description": "Генерирует PDF-файл из текста",
      "input_schema": {
        "type": "object",
        "properties": {
          "content": { "type": "string" }
        },
        "required": ["content"]
      }
    }
  ]
}

Неправильный подход — возвращать пустой объект или не реализовывать этот маршрут вовсе. Без /capabilities LLM просто не узнает, какие инструменты у вас есть.

/prompt — мозг агента

Этот POST-эндпоинт получает от LLM контекст и промпт, а затем возвращает либо ответ, либо запрос на вызов инструмента.

LLM анализирует входящий запрос пользователя и решает: ответить самому или использовать один из инструментов. Например:

Пользователь: "Создай PDF с текстом 'Привет, мир!'"
→ LLM понимает, что нужно использовать generatePDF
→ Отправляет запрос на /prompt
→ Наш сервер должен вернуть команду вызова инструмента

Формат ответа MCP:

{
  "type": "tool_call",
  "name": "generatePDF",
  "args": {
    "content": "Привет, мир!"
  }
}

Этот эндпоинт — центральный в архитектуре. Именно здесь происходит "принятие решений" на уровне LLM, а сервер лишь передаёт и получает данные.

/tool — исполнитель

Когда LLM решает вызвать инструмент, он отправляет запрос на /tool. Этот POST-эндпоинт получает имя инструмента и аргументы, выполняет функцию и возвращает результат.

Например:

{
  "name": "generatePDF",
  "args": {
    "content": "Привет, мир!"
  }
}

→ Сервер вызывает функцию generatePDF, получает ссылку на файл и возвращает:

{
  "result": "https://example.com/generated/report.pdf"
}

Затем LLM может использовать этот результат в своём ответе пользователю.

Пример: каркас MCP-сервера на Express.js

Вот минимальный рабочий пример сервера на TypeScript с тремя эндпоинтами:

import express from 'express';
import { generatePDF } from './tools/pdf-tool'; // ваш инструмент из предыдущей темы

const app = express();
app.use(express.json()); // важно: парсинг JSON

// Эндпоинт /capabilities
app.get('/capabilities', (req, res) => {
  res.json({
    tools: [
      {
        name: 'generatePDF',
        description: 'Генерирует PDF из текста',
        input_schema: {
          type: 'object',
          properties: {
            content: { type: 'string' }
          },
          required: ['content']
        }
      }
    ]
  });
});

// Эндпоинт /prompt
app.post('/prompt', (req, res) => {
  const { prompt, context } = req.body;
  
  // Пока возвращаем вызов инструмента (в реальности — логика LLM)
  res.json({
    type: 'tool_call',
    name: 'generatePDF',
    args: { content: 'Пример текста' }
  });
});

// Эндпоинт /tool
app.post('/tool', async (req, res) => {
  const { name, args } = req.body;

  if (name === 'generatePDF') {
    const result = await generatePDF(args.content);
    res.json({ result });
  } else {
    res.status(404).json({ error: 'Инструмент не найден' });
  }
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`MCP-сервер запущен на http://localhost:${PORT}`);
});

Этот код — минимально жизнеспособный сервер, который можно запустить и подключить к MCP-клиенту. Он использует Express.js, обрабатывает все три эндпоинта и уже интегрирует ваш инструмент generatePDF.

Обратите внимание: мы используем express.json() — это middleware, которое автоматически парсит входящие JSON. Без него сервер не сможет читать тела запросов.

Связь с предыдущей темой

Вы уже умеете создавать инструменты на TypeScript — отлично! Теперь вы видите, куда они подключаются: через /tool и /capabilities. Именно так ваш код становится частью ИИ-агента.

Что дальше?

Теперь у нас есть понимание, какие эндпоинты нужны и как они работают. Но текущий код — это один файл. В реальном проекте сервер будет расти: появятся новые инструменты, обработка ошибок, логирование, конфигурации.

Как организовать код, чтобы он оставался читаемым и масштабируемым?
Именно этому посвящена следующая тема — Базовая структура проекта MCP-сервера.

Вы уже почти можете "оживить" своего агента. Скоро он будет не просто теорией — а рабочей системой, готовой к интеграции.