Написать свой MCP-сервер на TypeScript: тонкая обёртка над API
Готовые MCP-серверы — Postgres, GitHub, файловая система — покрывают типовые задачи. Но как только у вас есть собственный API (внутренний сервис, NestJS-бэкенд, сторонний REST без официального сервера), встаёт вопрос: как дать агенту доступ именно к вашим доменным операциям? Ответ — написать свой MCP-сервер на TypeScript. Это не сложная инфраструктура, а тонкая обёртка: несколько десятков строк кода, которые превращают ваши эндпоинты в инструменты для агента.
Зачем вообще свой сервер, если есть готовые
Возьмём конкретный сценарий. В сквозном проекте курса — CoffeeCRM на Next.js + NestJS — агент мог бы напрямую идти в базу через готовый Postgres MCP-сервер. Но это грубо: агент сам пишет SQL, видит все таблицы, ответы бывают огромными, бизнес-логика и валидация обходятся стороной.
Нам нужнее доменные действия: «дай активные позиции меню», «дай заказ #42 с составом». Это уже язык бизнеса, а не базы данных. Свой MCP-сервер — переводчик между агентом и вашим API: снаружи чистые инструменты, внутри — обычный fetch к существующим эндпоинтам.
Навык переносится на любой внутренний HTTP-сервис. Поменяйте тело клиентского модуля — каркас и принципы остаются теми же.
Подробнее о том, как протокол MCP устроен изнутри и как агент обнаруживает инструменты — в статье что такое MCP.
Три принципа чистого инструмента
Качество сервера определяется качеством его инструментов. Прежде чем писать код, разберём три принципа, по которым инструменты и стоит оценивать.
1. Узкая зона ответственности
Один инструмент — одно понятное действие домена.
Плохо: coffeecrm_api(method, path, body) — универсальный «дёрни любой эндпоинт». По сути вы перекладываете выбор обратно на модель, которая не знает вашего API так, как знаете его вы.
Хорошо: list_products, get_order — каждый делает ровно одно осмысленное действие. Агент не гадает, а выбирает из конкретного словаря.
2. Говорящее имя и описание
Агент выбирает инструмент только по имени и описанию — он не читает исходники вашего сервера. Описание — не комментарий для людей, это интерфейс для модели.
Плохо: get_data / «Returns data». Агент не поймёт ни когда звать, ни что получит.
Хорошо: get_order / «Возвращает заказ CoffeeCRM по его id: статус, клиент, список позиций и сумма. Используй, когда нужны детали конкретного заказа.» — описание явно говорит когда и что.
Параметры тоже описывайте (через .describe() у Zod) — агент по ним поймёт, что подставлять, без дополнительных подсказок в промпте.
3. Компактный, предсказуемый результат
Агент платит контекстом за каждый ответ инструмента. Не вываливайте весь JSON на 200 полей.
Плохо: сырой дамп строки из БД со служебными полями, внутренними id и пустыми ключами.
Хорошо: «Заказ #42 — active, клиент Анна, 2 позиции (Latte ×1, Espresso ×2), итог 540 ₽.» Агент мгновенно понимает результат, контекст не раздувается, следующий шаг выбирается точнее.
Якорь для запоминания: одно действие · говорящее описание · компактный ответ.
Каркас сервера: структура проекта
Минимальная структура для сервера CoffeeCRM:
coffeecrm/mcp-server/
package.json
tsconfig.json
src/
index.ts # каркас + регистрация инструментов
coffeecrm.ts # тонкий HTTP-клиент к API
Сервер намеренно разделён на два файла: index.ts — протокол MCP и описания инструментов, coffeecrm.ts — работа с API. Если завтра поменяется бэкенд, правите только клиентский модуль.
Перед написанием кода — обязательно сверяйтесь с актуальным MCP TypeScript SDK через Context7 (запрос по /modelcontextprotocol/typescript-sdk). Имена импортов и сигнатура registerTool между версиями SDK меняются; ориентироваться на примеры из статей — значит рисковать кодом, который не соберётся через полгода. Ниже — структура, сверенная с SDK на дату записи урока курса.
Скелет src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { coffeeApi } from "./coffeecrm.js";
const server = new McpServer({ name: "coffeecrm", version: "1.0.0" });
server.registerTool(
"list_products",
{
title: "Список товаров CoffeeCRM",
description:
"Возвращает позиции меню CoffeeCRM. Используй, когда нужно узнать, " +
"какие напитки есть и какие из них активны. " +
"По умолчанию возвращает только активные.",
inputSchema: z.object({
onlyActive: z
.boolean()
.default(true)
.describe("true — только активные; false — все"),
}),
},
async ({ onlyActive }) => {
const products = await coffeeApi.listProducts({ onlyActive });
const lines = products.map(
(p) => `#${p.id} ${p.name} — ${p.price}₽ ${p.active ? "(active)" : "(inactive)"}`,
);
return {
content: [{ type: "text", text: lines.length
? `Позиции меню (${lines.length}):\n${lines.join("\n")}`
: "Активных позиций нет." }],
};
},
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// ВАЖНО: логи только в stderr — stdout занят протоколом MCP
console.error("CoffeeCRM MCP server running on stdio");
}
main().catch((e) => { console.error("Fatal:", e); process.exit(1); });
Обратите внимание на console.error вместо console.log. Это не опечатка — это причина номер один, почему самописный сервер «падает или не виден»: любой вывод в stdout ломает бинарный протокол MCP. Все диагностические сообщения — только в stderr.
Тонкий клиент src/coffeecrm.ts
const BASE = process.env.COFFEECRM_API ?? "http://localhost:3000";
export const coffeeApi = {
async listProducts({ onlyActive }: { onlyActive: boolean }) {
const res = await fetch(`${BASE}/products?active=${onlyActive}`);
if (!res.ok) throw new Error(`API ${res.status}`);
return (await res.json()) as Array<{
id: number; name: string; price: number; active: boolean;
}>;
},
async getOrder(id: number) {
const res = await fetch(`${BASE}/orders/${id}`);
if (res.status === 404) return null;
if (!res.ok) throw new Error(`API ${res.status}`);
return (await res.json()) as {
id: number; status: string; customerName: string; total: number;
items: Array<{ productName: string; qty: number }>;
};
},
};
Сервер не знает ничего про SQL и схему базы — он зовёт наш бэкенд CoffeeCRM по HTTP. Вся бизнес-логика и валидация остаются в NestJS, где им и место. MCP-сервер — тонкий переводчик между агентом и API, не более.
Сборка и подключение к Claude Code
# В coffeecrm/mcp-server/
npm install
npm run build # tsc -> dist/index.js
Подключаем в .mcp.json рядом с готовыми серверами (о них — в статье подключить MCP-серверы):
{
"mcpServers": {
"coffeecrm": {
"type": "stdio",
"command": "node",
"args": ["./mcp-server/dist/index.js"],
"env": { "COFFEECRM_API": "http://localhost:3000" }
}
}
}
Проверяем:
claude mcp list # coffeecrm -> connected
/mcp # у coffeecrm видны list_products и get_order
Если сервер не виден — чек-лист: путь в args верный, npm run build завершился без ошибок, нет console.log в stdout, бэкенд CoffeeCRM запущен.
Как агент выбирает инструмент сам
Ключевой момент демо: не называйте инструмент явно в промпте. Пишите на естественном языке — и смотрите, как агент сам сопоставляет задачу с описанием.
«Покажи активные позиции меню CoffeeCRM.» — агент выбирает list_products, не get_order, не SQL. Именно потому, что в описании написано «используй, когда нужно узнать, какие напитки есть».
«Дай детали заказа номер 42 — кто клиент и что заказал.» — агент выбирает get_order с id: 42 и возвращает компактную сводку.
Это и есть критерий качества: если агент сам выбирает нужный инструмент по естественному запросу — описания написаны правильно.
Аналогичная логика работает и со skills в Claude Code — там агент тоже выбирает по описанию, не по имени команды.
Что дальше
Три принципа чистого инструмента — узкая зона, говорящее описание, компактный ответ — это не правила конкретного фреймворка. Это подход к проектированию интерфейса между вашим доменом и языковой моделью. Напишите сервер поверх любого вашего HTTP-сервиса, замените тело coffeecrm.ts на вызовы вашего API — каркас и принципы останутся теми же.
Если хотите разобрать это на практике — от нулевого репозитория CoffeeCRM до рабочего агента с собственными инструментами — смотрите полный курс по Claude Code: модуль 3 целиком посвящён MCP, включая живое демо написания сервера с нуля.
Курс
Освойте Claude Code системно
6 модулей, реальный fullstack-проект до деплоя, свои skills, MCP и агенты.
Смотреть программу курса