Что такое MCP Apps и зачем они нужны в ChatGPT и Claude
MCP Apps - это расширение Model Context Protocol, при котором сервер вместе с результатом инструмента отдаёт интерактивный HTML. Хост, будь то ChatGPT или Claude, получает разметку и рендерит её в изолированном фрейме прямо в чате. Пользователь работает с формой, ползунком или графиком вместо абзаца текста.
Обычный MCP-сервер возвращает данные: строки, JSON, текст. MCP Apps добавляет слой представления. Тот же инструмент возвращает ещё и ссылку на UI-ресурс, который хост подгружает и показывает рядом с ответом модели.
Главное следствие: интерфейс описывается стандартом, а не под каждый хост отдельно. Один виджет работает и в ChatGPT, и в Claude, если оба поддерживают расширение. Это режет объём работы по сравнению с отдельной интеграцией под каждый клиент.
Как хост рендерит виджеты в sandboxed iframe
HTML не встраивается в DOM чата напрямую. Хост создаёт iframe с атрибутом sandbox и урезанным набором разрешений. У фрейма нет доступа к cookies чата, хранилищу хоста и родительскому DOM, а скрипты виджета исполняются в отдельном origin.
Обмен идёт через postMessage. Хост отправляет в iframe начальные данные и контекст, виджет отвечает сообщениями того же формата. Сообщения валидируются по схеме, поэтому виджет не может вызвать произвольный код хоста. Такой мост даёт виджету возможность запрашивать новые вызовы инструментов, не имея сетевого доступа к серверу.
Практическое следствие: виджет не может напрямую дёрнуть ваш API. Любое действие проходит через хост, который решает, разрешено ли оно. Это одновременная защита и ограничение: логику приходится проектировать в терминах запрос-ответ через хост.
Преимущества перед статичными ответами
Текстовый ответ подходит, когда нужно что-то объяснить. Виджет нужен, когда пользователю надо выбрать, настроить или увидеть.
- форма с параметрами генерации: температура, длина, стиль;
- таблица с чекбоксами для выбора записей;
- график, который перестраивается при смене фильтра;
- голосование с обновлением результатов в реальном времени;
- конфигуратор, где выбор одного поля меняет доступные варианты в другом.
Экономия заметна на многошаговых сценариях. Вместо трёх обменов сообщениями пользователь заполняет одну форму и отправляет результат одним вызовом. Меньше токенов, меньше поводов ошибиться, предсказуемее поведение. Подробнее о том, как MCP меняет продуктовые интерфейсы, есть разбор в материале про MCP как новый интерфейс продукта.
Архитектура решения: MCP-сервер на AgentCore, логика в Lambda, хранение в DynamoDB
Схема делится на три части. MCP-сервер хостится в AgentCore Runtime и отвечает только за протокол. Бизнес-логика лежит в AWS Lambda, куда сервер делегирует работу. Состояние живёт в DynamoDB: сессии, результаты, данные виджетов. Сверху стоит AgentCore Gateway как точка входа для хостов.
Роль AgentCore runtime и Gateway
AgentCore Runtime даёт управляемую среду для запуска MCP-сервера: автомасштабирование, изоляцию сессий, сетевые политики. Сервер можно собрать на любом стеке с поддержкой MCP, а инфраструктурную часть runtime берёт на себя.
Gateway решает другую задачу. Он превращает ваши API и Lambda-функции в MCP-инструменты и маршрутизирует запросы от хостов. Там же живёт аутентификация входящих подключений и исходящая авторизация при вызове бэкендов. Хосту не нужно знать про Lambda и DynamoDB, он видит только список инструментов.
Для MCP Apps критично, чтобы Gateway и runtime корректно пропускали не только вызовы инструментов, но и запросы ресурсов, по которым хост подгружает HTML. Если этот путь обрезан, виджет просто не появится. Как это устроено в реальном проекте, разобрано в статье про AgentCore на практике в кейсе AvioBook, а схему подключения агента к внешним серверам разбирает материал про мост между облаком и локальной машиной.
Почему бизнес-логика вынесена в Lambda и DynamoDB
MCP-сервер держат тонким. Его работа: принять запрос, проверить аргументы, вызвать Lambda, вернуть результат в формате протокола. Он не хранит состояние и не содержит бизнес-правил.
Lambda даёт независимое масштабирование. Если нагрузка растёт на одном инструменте, масштабируется только его функция, остальные не затронуты. Обновить правило можно деплоем функции, не трогая MCP-сервер и не перезапуская runtime.
DynamoDB хранит то, что должно пережить один вызов: идентификатор сессии, промежуточные результаты, агрегаты. Пример: виджет голосования пишет голос через tools/call, Lambda увеличивает счётчик в DynamoDB, а виджет при обновлении читает актуальные числа.
Разделение упрощает тестирование. Логику Lambda прогоняют локально без MCP, а сервер подменяют моком на уровне протокола.
Протокольные вызовы: tools/list, tools/call, resources/read
Три метода закрывают основной сценарий.
tools/list возвращает список инструментов: имя, описание, JSON Schema аргументов. Хост показывает этот список модели, и она решает, какой инструмент вызвать.
tools/call запускает инструмент с аргументами и возвращает результат. В MCP Apps результат может включать указание на UI-ресурс.
resources/read запрашивает ресурс по URI. Для виджета это обычно HTML-шаблон или вспомогательные данные.
Последовательность такая. Хост вызывает tools/list и узнаёт про инструмент. Когда модель решает его использовать, идёт tools/call. Если результат ссылается на UI, хост делает resources/read по указанному URI, получает HTML и рендерит его. Общую механику протокола разбирает статья про MCP в 2026 году.
Как формируется ответ с HTML-виджетом
Сервер возвращает результат с двумя частями: данные для модели и метаданные для интерфейса. Данные - это то, что модель прочитает и на что опрётся в ответе. Метаданные несут URI ресурса, который хост подгрузит.
Упрощённо ответ tools/call выглядит так:
{
"content": [
{ "type": "text", "text": "Показал форму настройки генерации" }
],
"structuredContent": { "widget": "generation-settings", "defaults": { "temperature": 0.7 } },
"_meta": { "ui": { "resourceUri": "ui://settings/generation" } }
}
Точные имена полей зависят от версии расширения UI, поэтому сверяйтесь со спецификацией, которую поддерживает ваш хост. Принцип неизменен: текст для модели, ссылка на ресурс для интерфейса.
Ресурс отдаётся вызовом resources/read:
<!doctype html>
<html>
<body>
<label>Temperature: <input id="t" type="range" min="0" max="1" step="0.1"></label>
<button id="apply">Применить</button>
<script>
document.getElementById("apply").onclick = () => {
window.parent.postMessage({
type: "tool",
name: "apply_settings",
params: { temperature: document.getElementById("t").value }
}, "*");
};
</script>
</body>
</html>
Код приведён как иллюстрация механики. Точный формат сообщений задаёт хост, а не сервер.
Взаимодействие виджета с сервером после рендеринга
После рендеринга виджет живёт в изолированном фрейме и не имеет прямого доступа к сети. Новый вызов инициируется через мост: виджет публикует сообщение в родительское окно, хост его проверяет, при необходимости спрашивает подтверждение у пользователя и вызывает tools/call с новыми аргументами. Результат возвращается в виджет.
Из этого следуют три правила проектирования.
- Каждое действие в виджете соответствует отдельному инструменту с описанной схемой.
- Виджет не предполагает мгновенный ответ: показывайте состояние загрузки.
- Хост вправе отклонить вызов, поэтому интерфейс обязан корректно обрабатывать отказ.
Так собираются многошаговые сценарии: пользователь настраивает параметры, жмёт кнопку, хост вызывает инструмент, Lambda обновляет DynamoDB, виджет получает свежие данные и перерисовывается.
Безопасность: WAF, IAM, изоляция сессий и валидация аргументов
Виджет в чате выполняет код, а инструменты пишут в вашу базу. Границы доверия проходят между хостом, Gateway, runtime, Lambda и DynamoDB.
Настройка AWS WAF для AgentCore Gateway
WAF ставится перед Gateway и фильтрует HTTP-запросы до того, как они дойдут до MCP-сервера. Что имеет смысл включить:
- ограничение скорости по IP, чтобы срезать перебор и флуд;
- правила против SQL-инъекций и XSS в теле и заголовках;
- блокировку по репутационным спискам и геолокации, если аудитория известна;
- лимит на размер тела запроса.
WAF работает на уровне HTTP и не понимает семантику MCP. Корректный по форме JSON с недопустимым значением он пропустит. Поэтому валидация на уровне приложения обязательна, а WAF даёт только первый барьер.
Изоляция сессий и валидация аргументов
Каждая сессия получает уникальный идентификатор, и все записи в DynamoDB привязаны к нему. Инструмент проверяет, что запрошенный объект принадлежит текущей сессии, а не просто существует.
Валидацию делают дважды: на MCP-сервере по JSON Schema инструмента и в Lambda по схеме конкретной операции. Вторая проверка нужна, потому что Lambda может вызываться и по другим каналам.
IAM-роли разводят по функциям. Lambda, читающая результаты голосования, не должна иметь права их удалять. Доступ к таблице DynamoDB выдаётся на конкретные действия и, где возможно, на конкретные ключи через условия политики.
Пример проверки владения записью в Lambda:
def handler(event, context):
user_id = event["identity"]["userId"]
item = table.get_item(Key={"pk": event["pollId"]})["Item"]
if item["owner"] != user_id:
raise PermissionError("access denied")
return update_counter(item, event["choice"])
И ещё одно правило: не доверяйте идентификатору, пришедшему из аргументов инструмента. Берите его из проверенного контекста аутентификации, который Gateway передал в runtime.
Практические ограничения и кому подходит подход
Подход стоит брать осознанно.
Ограничения:
- Поддержка MCP Apps зависит от хоста и версии. То, что работает в одном клиенте, в другом может отобразиться как обычный текст.
- Sandbox урезает возможности: нет прямого сетевого доступа, нет доступа к DOM чата, часть браузерных API недоступна.
- Каждый шаг взаимодействия - это вызов инструмента. Цепочка из пяти действий даёт пять round-trip и заметную задержку, особенно если Lambda холодная.
- Инфраструктура требует навыков AWS: runtime, Gateway, Lambda, DynamoDB, IAM, WAF. Для одного простого инструмента это избыточно.
- Диагностика сложнее: ошибка может сидеть в виджете, в мосте, в сервере, в Lambda или в правах.
Кому подходит: командам, уже живущим в AWS, которым нужен интерактив в чатах без своего фронтенда; продуктам, где один инструмент должен работать в нескольких хостах; сценариям с формами, выборами и визуализацией, где текста мало.
Кому не подходит: если инструмент возвращает одну строку данных, обычного MCP-сервера достаточно. Для простых случаев есть лёгкие ядра, где каталог методов задаётся в YAML и не требует полной облачной обвязки.
Перед стартом проверьте сам сценарий: возвращает ли виджет то, что нельзя уместить в текст, и готов ли хост его отрисовать. Если да, дальше имеет смысл строить тонкий MCP-слой и выносить логику в Lambda.