Capstone: an MCP Server for Your Product API
Design the tool surface around user tasks, wrap a real API with error handling, and ship the server against a production checklist.
Lesson content is currently available in Russian.
What you will learn
- →Проектировать поверхность инструментов от задач пользователя, а не от endpoints API.
- →Оборачивать реальный API: клиент с таймаутами, ошибки как isError-результаты, экономия контекста.
- →Сдавать сервер по production-чеклисту: smoke-тесты, безопасность, README, релиз.
Проектирование поверхности инструментов
Главная ошибка капстоунов — сгенерировать по инструменту на каждый endpoint REST API. У API вашего продукта может быть 80 endpoints; сервер с 80 инструментами — плохой сервер. Каждое определение инструмента съедает контекст модели, а выбор из десятков похожих опций резко повышает процент неверных вызовов. REST проектировался для программистов, читающих документацию; поверхность MCP проектируется для модели, читающей описания на лету.
Метод такой. Выпишите 5–8 задач, которые пользователь реально будет формулировать ассистенту: «найди клиента и покажи его последние заказы», «оформи возврат», «что с выручкой за неделю». Каждой задаче — один инструмент с точным именем-глаголом: search_customers, get_customer_orders, create_refund, get_revenue_report. Один такой инструмент может внутри дёргать три endpoint вашего API — это нормально: агрегируйте на сервере, а не заставляйте модель делать четыре вызова и склеивать результаты в контексте.
Дальше распределите по примитивам: справочные данные без параметров действия (схема тарифов, статус системы) — это resources, а не инструменты; типовые сценарии («еженедельный разбор метрик») — prompts. Для каждого инструмента напишите описание по формуле: что делает, что возвращает, ограничения и хороший пример значения каждого аргумента через describe() у zod-полей. И заложите пагинацию сразу: limit с разумным дефолтом и жёстким максимумом — модель, получившая 10 000 строк, утонет вместе с контекстом сессии.
Реализация: обёртка API и ошибки как данные
Весь поход в ваш API сведите в один типизированный клиент с таймаутом и нормализацией ошибок:
const BASE_URL = process.env.PRODUCT_API_URL ?? 'https://api.acme.dev';
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };
async function callApi<T>(path: string, init?: RequestInit): Promise<ApiResult<T>> {
try {
const res = await fetch(BASE_URL + path, {
...init,
headers: {
Authorization: 'Bearer ' + API_KEY,
'Content-Type': 'application/json',
...init?.headers,
},
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
return { ok: false, error: 'HTTP ' + res.status + ': ' + (await res.text()).slice(0, 300) };
}
return { ok: true, data: (await res.json()) as T };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : 'unknown error' };
}
}
Хендлеры инструментов переводят неудачу в isError-результат с действенным текстом — модель должна понять, что делать дальше:
async ({ customerId }) => {
const result = await callApi<Order[]>('/v1/customers/' + customerId + '/orders');
if (!result.ok) {
return {
content: [{
type: 'text',
text: 'Не удалось получить заказы: ' + result.error +
'. Проверь customerId через search_customers и повтори.',
}],
isError: true,
};
}
const trimmed = result.data.slice(0, 25).map(pickOrderSummary);
return { content: [{ type: 'text', text: JSON.stringify(trimmed) }] };
}
Обратите внимание на два приёма. Текст ошибки подсказывает следующий шаг — «проверь id через search_customers» — это резко повышает способность агента восстановиться. И результат прорежен функцией pickOrderSummary: в контекст уходят пять полей заказа, а не сорок. Экономия контекста — такая же часть контракта, как схема аргументов.
Наблюдаемость и последний рубеж защиты
Production-сервер обязан быть наблюдаемым и ограниченным в ущербе. Минимальный набор:
Структурные логи в stderr. Каждый вызов инструмента — одна строка: время, инструмент, длительность, статус, усечённые аргументы:
function logCall(tool: string, ms: number, ok: boolean, args: unknown) {
console.error(JSON.stringify({
ts: new Date().toISOString(),
tool,
ms,
ok,
args: JSON.stringify(args).slice(0, 200),
}));
}
Никогда не логируйте секреты и полные тела ответов. Этот лог — ваш единственный источник правды при разборе инцидента «агент удалил не тот заказ».
Аннотации и подтверждения. Все мутирующие инструменты — с destructiveHint или idempotentHint по фактическому поведению. Клиент построит на них правильный UX подтверждений.
Пределы ущерба на сервере. Максимум записей на выборку, максимум мутаций в минуту, запрет массовых операций одним вызовом. Модель ошибается иначе, чем человек: она может искренне решить, что «почистить тестовые данные» означает пройтись delete-циклом по проду. Сервер — последний рубеж, где эта ошибка останавливается.
Режим READ_ONLY. Флаг окружения, при котором мутирующие инструменты не регистрируются вовсе. Это лучший ответ на вопрос службы безопасности «а что оно может натворить?» — в read-only-режиме ответ доказуемо «ничего».
Если сервер удалённый — добавьте проверку audience токена и скоупов из урока 4 на каждый запрос, а не только на initialize.
Definition of Done
Капстоун сдаётся не «когда работает», а когда закрыт чеклист. Пройдите его честно — это и есть отличие учебного скрипта от продукта:
Функциональность
- Минимум три инструмента, один ресурс и один промпт, спроектированные от задач пользователя.
- Каждый инструмент: zod-схема с describe() у полей, пагинация с дефолтом и максимумом, прореженные результаты.
- Ошибки API возвращаются как isError-результаты с подсказкой следующего шага.
Безопасность
- Секрет — только из env, fail-fast при отсутствии, ни разу не встречается в результатах и логах.
- Аннотации на всех инструментах соответствуют фактическому поведению; есть режим READ_ONLY.
- Таблица угроз из урока 4 обновлена под финальную поверхность инструментов.
Проверка
- Smoke-скрипт на Inspector CLI: tools/list и по одному вызову каждого инструмента — зелёный в CI.
- Полный ручной круг в Inspector: валидные и невалидные аргументы, чтение ресурса, предпросмотр промпта.
- Сервер подключён и проверен в двух реальных клиентах.
Дистрибуция
- npm-пакет: shebang, bin, files, prepublishOnly; состав проверен через npm pack --dry-run.
- README: что делает сервер, таблица инструментов, блоки конфигурации Claude / Cursor / VS Code, переменные окружения.
- Версия 1.0.0 с зафиксированным контрактом: что будет major-изменением, описано в README.
Финальный тест — попросите коллегу подключить сервер только по README, не задавая вам вопросов. Если получилось — вы построили настоящий MCP-сервер. Это и есть результат курса.
Practice
Капстоун: сервер для API вашего продукта
Постройте production-ready MCP-сервер для API вашего продукта (или публичного API, если своего нет): 5–8 задач пользователя → 3+ инструментов, 1+ ресурс, 1 промпт; клиент API с таймаутами и isError-ошибками; структурные логи; README и npm-пакет. Прогоните полный Definition of Done из последней секции.
Deliverable
Репозиторий с сервером, зелёный smoke-скрипт в CI, скриншоты работы в двух клиентах, README с конфигурациями и закрытый чеклист DoD с отметками по каждому пункту.
Checkpoint
Защитите свой дизайн: почему вы выбрали именно эти инструменты, а не сгенерировали по одному на каждый endpoint API? Покажите, как один из инструментов агрегирует несколько вызовов API.
- ✓ Дизайн обоснован от задач пользователя, а не от структуры REST API.
- ✓ Объяснена экономия контекста: меньше инструментов, прореженные результаты, пагинация с лимитами.
- ✓ Показан инструмент-агрегатор и объяснено, почему склейка данных на сервере надёжнее цепочки вызовов моделью.
Check yourself
Sources