Создайте первый MCP-server как узкий адаптер домена
Хороший server не начинается с LLM и вообще не начинается с протокола. Он берёт уже существующие детерминированные функции домена, описывает их схемами и возвращает проверяемый результат. Такой порядок держит бизнес-правила отдельно от протокольного слоя - и потому их можно тестировать и менять независимо.
Сначала - чистая доменная функция, которая ничего не знает про MCP. В нашем примере это проверка релиза по пяти правилам: CI, тесты, уязвимости, обратимость миграции, документированный откат.
export function listReleaseSnapshots(): readonly ReleaseSnapshot[] {
return snapshots;
}
export function getReleaseSnapshot(service: string, version: string): ReleaseSnapshot | undefined {
return snapshots.find(
(snapshot) => normalize(snapshot.service) === normalize(service) && snapshot.version === version.trim()
);
}
export function reviewRelease(service: string, version: string): ReleaseReview {
const snapshot = getReleaseSnapshot(service, version);
if (!snapshot) {
throw new Error(`Релиз ${service}@${version} не найден`);
}
const checks: ReleaseCheck[] = [
{
id: "ci",
title: "CI завершён успешно",
passed: snapshot.ci === "passed",
evidence: `ci=${snapshot.ci}; commit=${snapshot.commit}`
},
{
id: "tests",
title: "Все тесты прошли",
passed: snapshot.testsFailed === 0,
evidence: `passed=${snapshot.testsPassed}; failed=${snapshot.testsFailed}`
},
{
id: "security",
title: "Критических уязвимостей нет",
passed: snapshot.criticalVulnerabilities === 0,
evidence: `critical=${snapshot.criticalVulnerabilities}`
},
{
id: "migration",
title: "Миграция обратима",
passed: snapshot.migrationReversible,
evidence: `reversible=${snapshot.migrationReversible}`
},
{
id: "rollback",
title: "Откат документирован",
passed: snapshot.rollbackDocumented,
evidence: `documented=${snapshot.rollbackDocumented}`
}
];
const failed = checks.filter((check) => !check.passed);
const decision = failed.length === 0 ? "ready" : "blocked";
const summary = decision === "ready"
? `${snapshot.service}@${snapshot.version} готов к выпуску: пройдены все ${checks.length} проверок.`
: `${snapshot.service}@${snapshot.version} заблокирован: ${failed.map((check) => check.title).join("; ")}.`;
return { service: snapshot.service, version: snapshot.version, decision, checks, summary };
}И только потом эта функция оборачивается в tool - с input/output schema и annotations, которые честно описывают её характер.
server.registerTool(
"review_release",
{
title: "Проверка готовности релиза",
description: "Проверяет существующий снимок релиза по пяти детерминированным правилам.",
inputSchema: z.object({
service: z.string().min(2).max(64),
version: z.string().regex(/^\d+\.\d+\.\d+$/u)
}),
outputSchema: reviewOutputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async ({ service, version }) => {
try {
const output = reviewRelease(service, version);
return {
content: [{ type: "text", text: output.summary }],
structuredContent: output
};
} catch (error) {
return {
isError: true,
content: [{ type: "text", text: error instanceof Error ? error.message : "Unknown release error" }]
};
}
}
);Разворачивается такой сервер по одному и тому же механизму, и его стоит запомнить как рецепт.
- Создайте
McpServerс уникальными name/version и краткими instructions. - Опишите входную и выходную schema - это контракт, а не украшение.
- В handler вызовите доменный сервис, а не пишите бизнес-правила внутри протокольного слоя.
- Верните и человекочитаемый
content, и машиночитаемыйstructuredContent. - Ожидаемую доменную ошибку преобразуйте в
isError; неожиданные ошибки логируйте без секретов.
А проверить, безопасен ли инструмент по конструкции, можно в лаборатории.
Проверьте безопасность tool
Безопасная операция: узкая, типизированная, с подтверждением записи.
Два представления результата - не избыточность, а разделение потребителей.
Почему два представления результата. Текст удобен модели и UI.structuredContentудобен коду, тестам и последующей композиции. Если объявленoutputSchema, структура обязана ему соответствовать - иначе клиент вправе считать ответ невалидным.
И граница ответственности за ошибки, которую нельзя размывать.
Не выдавайте исключение БД за полезный ответ. Пользовательская ошибка, protocol error и infrastructure failure имеют разные retry-семантики. Отдайте клиенту безопасное сообщение, а полный stack сохраните только в защищённой телеметрии.
Сервер готов - осталось решить, как host будет его запускать. Для локального сценария это stdio, и у него своя дисциплина.