Урок курса
Анатомия MCP tool: схема, вход и результат
AI Agent Integrations PRO: MCPВ прошлом уроке мы разбирали, как правильно именовать инструменты и ресурсы MCP-сервера и какие соглашения делают контракт читаемым. Теперь спускаемся на уровень ниже — к тому, как именно описываются входные параметры конкретного tool и почему это описание является обязательным соглашением, а не просто документацией.
JSON Schema как контракт входных параметров MCP tool
Каждый MCP tool обязан сообщить клиенту, какие параметры он ожидает, ещё до вызова. Для этого используется поле inputSchema — объект в формате JSON Schema, который описывает структуру входных данных. Это не просто подсказка. Клиент (или LLM-агент) читает inputSchema и на его основе формирует запрос: выбирает правильные имена полей, проверяет типы, понимает, что обязательно, а что — нет. Если схема неполная или некорректная, клиент может передать неверные данные или пропустить обязательное поле — и ошибка проявится уже в runtime, а не до вызова. Минимальная inputSchema для tool с двумя числовыми параметрами выглядит так:
{
"type": "object",
"properties": {
"a": { "type": "number", "description": "Первое слагаемое" },
"b": { "type": "number", "description": "Второе слагаемое" }
},
"required": ["a", "b"]
}
Здесь четыре ключевых элемента:
type: "object"— корневой тип всегда объект; параметры передаются как именованные поля, не массивом.properties— словарь, где каждый ключ — имя параметра, значение — его схема с типом и опциональнымdescription.required— массив имён обязательных полей. Именно этот массив чаще всего забывают.descriptionвнутри каждого свойства — помогает LLM понять смысл поля и сформировать корректный аргумент. Отдельно проrequired: если его не указать, JSON Schema по умолчанию считает все поля необязательными. Для статического клиента это может быть незаметно — он всё равно передаст все поля. Но LLM-агент или автоматический генератор запросов вполне может решить, что поле можно опустить. В итоге handler получитundefinedтам, где ожидал число, и поведение станет непредсказуемым. Правило простое: если параметр нужен для работы tool — он должен быть вrequired. Всегда явно, без расчёта на умолчания. ПолеinputSchema— это граница ответственности. Всё, что клиент должен знать о входе инструмента, описано здесь. Handler вправе рассчитывать, что получит именно то, что указано в схеме — и не обязан защищаться от полностью произвольных входов.
Структура результата: content-блоки и флаг isError
Если inputSchema описывает вход tool, то CallToolResult — это выходной контракт. Handler обязан вернуть объект строго определённой формы, иначе клиент не сможет корректно обработать ответ. Минимальная структура результата:
{
content: [
{ type: 'text', text: 'результат' }
]
}
Поле content — это массив блоков. Каждый блок имеет поле type, которое определяет, что именно передаётся:
text— строка с текстовым результатом. Самый частый тип.image— бинарные данные изображения в base64 с указаниемmimeType.resource— ссылка на MCP-ресурс (uri + опциональный текст/blob). В большинстве инструментов достаточноtext. Остальные типы используются, когда tool возвращает файл или связывает результат с ресурсом на сервере. Обработка ошибок через isError
Если что-то пошло не так, не нужно бросать исключение — нужно вернуть результат с флагом isError: true:
return {
content: [{ type: 'text', text: 'Деление на ноль невозможно' }],
isError: true
};
Клиент получит этот объект, увидит флаг и сможет обработать ситуацию — показать сообщение пользователю, залогировать, перейти к fallback-логике. Если вместо этого handler бросает исключение, MCP-сервер вернёт неструктурированную ошибку транспортного уровня — без content, без предсказуемого формата. LLM-агент, получив такой ответ, не сможет осмысленно на него среагировать. Правило: всё, что является штатной ситуацией с точки зрения бизнес-логики (некорректные данные, внешний сервис недоступен, ресурс не найден) — возвращается через isError: true. Исключения оставляй только для действительно непредвиденных сбоев. Типичная ошибка: возврат строки вместо объекта
Часто handler пишут так:
// Неправильно:
async ({ a, b }) => String(a + b)
Это вернёт голую строку, а не CallToolResult. MCP SDK либо выбросит ошибку сериализации, либо клиент получит неожиданный формат и поведение станет непредсказуемым. Правильный вариант — всегда оборачивать результат:
// Правильно:
async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }]
})
Обёртка в content: [{ type: 'text', text: '...' }] — это не церемония, а часть протокола. Клиент не знает, что внутри text, — он знает только структуру CallToolResult и обходит массив content блок за блоком.
Регистрация tool через server.tool() и полный пример
Теперь, когда входной и выходной контракты разобраны отдельно, нужно собрать их вместе. В MCP SDK для TypeScript точка сборки — метод server.tool(). Сигнатура вызова:
server.tool(
name, // строка: имя инструмента
description, // строка: что делает инструмент
inputSchema, // JSON Schema для входных параметров
handler // async-функция, получает типизированные аргументы
);
Все четыре аргумента обязательны. Имя и описание — это то, что клиент и LLM-агент видят при листинге инструментов. inputSchema — контракт входа. Handler — логика инструмента, которая получает уже разобранные аргументы и возвращает CallToolResult. Полный рабочий пример — инструмент add_numbers, который складывает два числа:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({
name: 'math-server',
version: '1.0.0',
});
server.tool(
'add_numbers',
'Складывает два числа и возвращает результат',
{
type: 'object',
properties: {
a: { type: 'number', description: 'Первое слагаемое' },
b: { type: 'number', description: 'Второе слагаемое' },
},
required: ['a', 'b'],
},
async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Несколько моментов, на которые стоит обратить внимание. TypeScript-типизация в handler работает автоматически: SDK выводит типы аргументов из inputSchema, поэтому a и b внутри функции уже имеют тип number. Явно объявлять интерфейс параметров не нужно — схема служит и runtime-контрактом, и источником типов. McpServer — это высокоуровневая обёртка SDK, которая берёт на себя диспетчеризацию вызовов. Когда клиент запрашивает add_numbers, сервер находит зарегистрированный handler по имени, проверяет входные данные по схеме и передаёт аргументы в функцию. Разработчику не нужно вручную разбирать JSON-запрос или маршрутизировать вызовы. StdioServerTransport здесь — стандартный транспорт для локального запуска и тестирования: сервер читает запросы из stdin и пишет ответы в stdout. Именно этот транспорт используется при подключении через тестовый клиент MCP Inspector или при запуске из Claude Desktop. Вызов server.connect(transport) запускает цикл обработки сообщений. После этой строки сервер готов принимать запросы.
Пошаговая процедура реализации и запуск тестового клиента
Все части разобраны по отдельности — теперь нужна воспроизводимая последовательность, которую можно пройти от начала до конца и получить работающий tool. Вот она. Шаг 1. Определить имя и описание
Начни с простого вопроса: что именно делает инструмент, и как это выразить одной фразой? Имя должно быть глаголом или глагольным существительным (add_numbers, fetch_weather, search_documents). Описание — одно предложение, которое LLM-агент увидит при выборе инструмента. Шаг 2. Написать inputSchema
Определяешь, какие параметры нужны, какого они типа, и что из них обязательно. Схему удобно держать отдельной константой — так она не теряется внутри длинного вызова server.tool():
const schema = {
type: 'object' as const,
properties: {
city: { type: 'string', description: 'Название города' },
units: { type: 'string', enum: ['celsius', 'fahrenheit'], description: 'Единицы температуры' },
},
required: ['city'],
};
Здесь units — необязательный параметр с перечислением допустимых значений. city — обязательный. required явно перечисляет только то, без чего handler не может работать. Шаг 3. Реализовать handler
Handler — это async-функция, которая получает уже разобранные аргументы и возвращает CallToolResult. На этом этапе пиши только бизнес-логику, не думая о сериализации:
async ({ city, units = 'celsius' }) => {
const temp = await fetchTemperature(city, units);
if (temp === null) {
return {
content: [{ type: 'text', text: `Город "${city}" не найден` }],
isError: true,
};
}
return {
content: [{ type: 'text', text: `Температура в ${city}: ${temp}°` }],
};
}
Заметь: обе ветки — успех и ошибка — возвращают одинаковую структуру. Клиент различает их по флагу isError. Шаг 4. Зарегистрировать tool на сервере
server.tool(
'get_weather',
'Возвращает текущую температуру в указанном городе',
schema,
handler
);
Если инструментов несколько, каждый регистрируется отдельным вызовом server.tool() до server.connect(transport). Шаг 5. Запустить сервер и проверить через тестовый клиент
Компилируй и запускай сервер:
npx tsc && node dist/server.js
Для верификации используй MCP Inspector — официальный тестовый клиент протокола:
npx @modelcontextprotocol/inspector node dist/server.js
Inspector откроет веб-интерфейс, где ты увидишь список зарегистрированных инструментов, их inputSchema и сможешь вызвать любой из них с произвольными параметрами. Это единственный надёжный способ убедиться, что схема описана корректно и handler возвращает именно то, что ожидается, — до того как инструмент попадёт к реальному клиенту или агенту. На что смотреть в Inspector при тестировании:
- В разделе Tools должен появиться
get_weatherс описанием и схемой. - При вызове с валидными параметрами — ответ с
content[0].textи безisError. - При вызове с намеренно некорректными данными (например, пустой строкой) — ответ с
isError: trueи читаемым сообщением. Если Inspector показывает ошибку при листинге инструментов — проблема в транспорте или в том, чтоserver.connect()не был вызван. Если ошибка при вызове — смотри на handler: либо он бросает исключение вместоisError, либо возвращает не тот формат.
Задача 1
Какое поле JSON Schema отвечает за перечисление обязательных входных параметров MCP tool?
Задача 2
Handler MCP tool получил запрос на деление числа на ноль. Как правильно сообщить клиенту об этой логической ошибке, не нарушая протокол?
Задача 3
Какой из следующих вариантов inputSchema корректен для tool, принимающего обязательный параметр city типа string?
Задача 4
Как называется метод MCP SDK (TypeScript), которым регистрируется инструмент на сервере? Напиши только имя метода без скобок и без префикса объекта.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
