MCP tool получает лишние аргументы: как отделить вход от внутренних полей

Допустим, мы добавляем ассистента в приложение с заметками. Пользователь пишет: «Сохрани план запуска проекта». Чтобы просьба превратилась в действие, приложению мало получить текстовый ответ от модели — нужно вызвать код, который создаст заметку.

В MCP такой код оформляется как инструмент, или tool. Сервер описывает его имя, назначение и схему аргументов. Клиент получает это описание, передаёт его модели, а сформированный вызов отправляет серверному обработчику. Для create_note нам нужны два аргумента: заголовок title и содержимое text.

Есть ещё поле is_public, но у него другой смысл. Создание заметки и её публикация — разные действия. Новую заметку обработчик всегда создаёт частной, поэтому is_public=False не нужно получать вместе с заголовком и текстом. Это внутреннее решение операции create_note.

Сам обработчик хочется написать как можно проще. Задать внутренние значения по умолчанию, распаковать поверх них аргументы и передать готовый словарь дальше:

note = {"is_public": False, **args}

Строка короткая, новые входные поля подхватываются автоматически, а значение по умолчанию видно рядом. Но что произойдёт, если в args окажется ключ с тем же именем? Добавим его в проверочный вызов:

args = {
    "title": "План",
    "text": "Запустить проект",
    "is_public": True,
}

bad_note = {"is_public": False, **args}
print("Слепая распаковка:", bad_note["is_public"])

Python выводит:

Слепая распаковка: True

При объединении словарей последнее значение одинакового ключа побеждает. Сначала мы положили в note значение False, затем распаковали args и заменили его на True. Получается, поле, которого нет в контракте инструмента, всё же влияет на результат работы обработчика.

Можно ли просто переставить словари?

Самое короткое исправление — поменять порядок:

note = {**args, "is_public": False}

Для is_public это сработает: теперь значение обработчика записывается последним. Иногда такой приём уместен, когда функция сознательно принимает произвольный набор полей и переопределяет одно из них.

У create_note другой контракт. Мы знаем весь допустимый вход: title и text. Если сохранить слепую распаковку, любой новый ключ продолжит проходить в запись. Поэтому здесь лучше не исправлять одно совпавшее имя, а закрыть вход целиком.

Схема должна запрещать то, чего нет в контракте

Опишем объект двумя строковыми полями. required требует, чтобы оба присутствовали, а additionalProperties: false запрещает все остальные:

schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "text": {"type": "string"},
    },
    "required": ["title", "text"],
    "additionalProperties": False,
}

Здесь важна именно последняя строка. Раздел properties описывает известные свойства, но не закрывает объект автоматически. Без additionalProperties: false валидатор проверит типы title и text, а неизвестный is_public оставит допустимым.

Схема inputSchema сообщает клиенту, какие аргументы принимает инструмент. Но проверку всё равно должен выполнить серверный обработчик. В нашем случае он проверяет аргументы до создания заметки — иначе лишнее поле успеет повлиять на запись.

После валидации всё равно не распаковываем вход вслепую

Закрытая схема уже остановит лишний ключ. Но код сохранения стоит сделать таким же явным, как контракт: взять два разрешённых поля и отдельно назначить внутреннее.

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
notes = []

def create_note(args):
    validator.validate(args)
    note = {
        "title": args["title"],
        "text": args["text"],
        "is_public": False,
    }
    notes.append(note)
    return note

Теперь схема и обработчик читаются одинаково. Вызывающая сторона задаёт title и text; create_note задаёт is_public. Если позже у инструмента появится новый аргумент, его нужно будет осознанно добавить и в схему, и в сборку записи.

Что произойдёт с двумя вызовами

Сначала отправим объект с лишним is_public. Затем вызовем тот же обработчик только с разрешёнными полями:

from jsonschema.exceptions import ValidationError

try:
    create_note({
        "title": "План",
        "text": "Запустить проект",
        "is_public": True,
    })
except ValidationError:
    print("Лишний аргумент: вызов отклонён")

print("Записей после отказа:", len(notes))

note = create_note({
    "title": "План",
    "text": "Запустить проект",
})
print("Исправленный вызов:", note)

Результат запуска:

Слепая распаковка: True
Лишний аргумент: вызов отклонён
Записей после отказа: 0
Исправленный вызов: {'title': 'План', 'text': 'Запустить проект', 'is_public': False}

В первом вызове валидация заканчивается до notes.append, поэтому список остаётся пустым. Во втором объект соответствует схеме, обработчик создаёт заметку и сам добавляет is_public=False. Мы не молча выбрасываем неизвестное поле, а отклоняем весь вызов: ошибка обнаруживается до изменения данных, и контракт не расходится с фактической записью.

Для запуска установите библиотеку jsonschema:

python -m pip install jsonschema

Сохраните в example-v2.py четыре блока из статьи: проверочный вызов с bad_note, определение schema, обработчик create_note и два вызова из последнего раздела. Расположите их в этом порядке и выполните:

python example-v2.py

Вопрос для следующего tool

Перед подключением изменяющего tool посмотрите на каждое поле будущей записи и спросите: кто должен выбирать его значение? В create_note вызывающая сторона задаёт заголовок и текст, а режим публикации не относится к этой операции. Добавьте к тестам вызов с лишним ключом и убедитесь, что он заканчивается до изменения данных — так удобная распаковка не закрепит случайное поведение.

Собрать интеграцию целиком

На курсе AI Agent Integrations PRO: MCP вы соберёте сервер с инструментами и подключите к нему клиент. Пройдёте весь вызов: клиент получает список инструментов, отправляет аргументы и обрабатывает ответ. Затем добавите проверки ошибок и тесты. Итоговое приложение будет получать внешний контекст и выполнять действия через MCP.

Курс построен как короткая теория, практика и автопроверяемые задания в локальном окружении. Посмотреть программу курса MCP. Для начала можно открыть урок про JSON Schema входа и результата MCP tool.

Источники: MCP Tools, спецификация 2025-06-18; JSON Schema: object.

Следующий шаг: MCP-интеграции: контракты инструментов, ошибки и доступ. По смежным вопросам: Структурированный ответ MCP tool: как не потерять типы данных, Документ в MCP: resource или tool для чтения.

Продолжить на практике

  • AI Agent Integrations PRO: MCP ↗

    Вы уже работали с LLM и понимаете, что подключать их к реальным системам — больно. MCP — это стандарт, который делает эту боль меньше: один контракт вместо десятка костылей, типизированные инструменты вместо голого доступа к базе, тесты и логи вместо молитв. На курсе соберём сервер с нуля и выведем в production-контур.