Чат как интерфейс упирается в потолок. Текст хорош, когда ответ это объяснение или код. Но на запрос «покажи продажи за квартал и сравни с прошлым годом» модель отвечает стеной чисел, в которой ничего не покрутить. Хочется другого: интерактивный дашборд прямо в чате, который при следующей реплике не пересобирается с нуля, а аккуратно обновляется. Очевидное решение — попросить модель сгенерировать React-компонент — оказывается тупиком. Вывод нестабилен: один и тот же промпт сегодня даёт useState, завтра — Zustand. Сгенерированный JSX это исполняемый код, то есть открытая дверь для инъекций: в мае 2025 Invariant Labs показали на официальной GitHub MCP-интеграции, как вредоносный issue провоцирует агент на утечку данных из приватных репозиториев. И главное, что нет инкрементальности: на запрос «добавь фильтр» модель регенерирует весь компонент, состояние теряется, ввод сбрасывается.

Вывод: код это плохой интерфейс между LLM и приложением. Нужен промежуточный слой. Эта статья про два таких слоя: json-render отвечает за то, что и как рендерить из декларативной спецификации, MCP Apps — за то, где и как доставить этот интерфейс в ассистент. По официальному анонсу MCP Apps поддерживаются в Claude, Goose, VS Code Insiders и ChatGPT; json-render заявляет MCP-интеграцию для Claude, ChatGPT, Cursor и VS Code. Через всю статью я протащу один пример: дашборд продаж, который пользователь докручивает репликами «добавь разбивку по регионам», «сравни с прошлым годом», «убери блок». Сразу обозначу жанр: это мой первый пилотный заход в тему, поэтому текст ближе к обзору с пробами, чем к боевому отчёту из эксплуатации.
Что проверено руками: генерирование спецификации из каталога, патч-обновления, ручная склейка View с App SDK, базовый пример
@json-render/mcp. Что не проверено: нагрузка уровня эксплуатации, мобильные клиенты, долгоживущие shared-чаты, аудит безопасности. Глава про эксплуатацию это синтез документации, чужого опыта и здравого смысла, а не мои шрамы. Примечание. Здесь речь про vercel-labs/json-render, open source-фреймворк от Vercel Labs (Apache 2.0, январь 2026). Есть также сайт json-render.org; убедитесь, что смотрите документацию именно vercel-labs. Примеры кода иллюстративные: API молодой и меняется, сверяйте с актуальной версией.
json-render: декларативный интерфейс как контракт
Идея простая: модель генерирует не код, а JSON-спецификацию интерфейса в духе «что должно быть», а не «как нарисовать». Спецификация для нашего дашборда:
{
"root": "dashboard",
"elements": {
"dashboard": {
"type": "Card",
"props": { "title": "Продажи Q3 2025" },
"children": ["revenue", "chart"]
},
"revenue": {
"type": "Metric",
"props": { "label": "Выручка", "value": "12 450 000", "format": "currency" }
},
"chart": {
"type": "LineChart",
"props": { "dataPath": "/sales/byMonth", "xKey": "month", "yKey": "revenue" }
}
}
}

Ключевое не сам JSON, а то, что за ним: каталог разрешённых компонентов на Zod. Модель не может прислать <script> или EvilComponent, таких нет в каталоге, и проверка на входе их отрежет:
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';
import { z } from 'zod';
export const catalog = defineCatalog(schema, {
components: {
Card: { props: z.object({ title: z.string() }), hasChildren: true },
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
format: z.enum(['currency', 'percent', 'number']).optional(),
}),
},
LineChart: {
props: z.object({ dataPath: z.string(), xKey: z.string(), yKey: z.string() }),
},
},
actions: {
refresh: { description: 'Обновить данные' },
export_pdf: { params: z.object({ period: z.string() }) },
},
});
Важно не обмануться насчёт того, как именно схема ограничивает модель. Механизма три, по убыванию силы: structured outputs провайдера (грамматика навешивается на сэмплер, модель по токенам не может выйти за рамки), post-hoc проверка Zod (отвергает некорректное до рендера) и промпт из catalog.prompt() (просьба, не гарантия). В проде они работают вместе, и фразу «модель не может прислать EvilComponent» читайте как «такая спецификация не дойдёт до рендера», а не как магию. К нулю ошибки это не сводит, поэтому метрика «доля корректных спецификаций» обязательна в эксплуатации.
Рендер: спецификация проецируется на ваши React-компоненты через registry. Что вы написали руками, то и отрисуется; чего нет в registry, не отрисуется никогда. Это самый сильный защитный барьер в системе, плюс бесплатный бонус: если компоненты каталога написаны с ARIA-атрибутами, то базовую доступность наследует любой сгенерированный интерфейс (семантика композиции, впрочем, остаётся на модели).
Главное для ИИ-сценариев это инкрементальные обновления. Режима три: create (генерация с нуля), merge (мягкое дополнение) и patch (точечные изменения по RFC 6902). На реплику «добавь разбивку по регионам» модель присылает не новую спецификацию, а патч:
[
{ "op": "add", "path": "/elements/regions",
"value": { "type": "BarChart", "props": { "dataPath": "/sales/byRegion", "xKey": "region", "yKey": "revenue" } } },
{ "op": "add", "path": "/elements/dashboard/children/-", "value": "regions" }
]
Обратите внимание: узлы адресуются по строковым id (/elements/regions), а не по позиции. Это сделано сознательно, модели неплохо оперируют именами и плохо считают индексы массивов. Удаление и переупорядочивание лучше отдавать модели в терминах id и резолвить в индексы на своей стороне.
Две оговорки, про которые обычно молчат. Первая: «перерисовывается только затронутый узел» не происходит само, это свойство правильно построенного runtime (стабильные key, мемоизация, structural sharing). Json-render делает это за вас, а в самописном движке придётся обеспечить руками. Вторая: применение патча это миллисекунды, но его генерирование моделью пользователь ждёт секунды, это обычный инференс. Ощущение скорости даёт стриминг, а сам патч экономит токены и сохраняет состояние. Обратная сторона: чтобы патчить, модель должна видеть текущую спецификацию во входном контексте, на длинных сессиях это съедает экономию.

Что в библиотеке действительно полезного. Спецификация, каталог и патчи это каркас. Рабочей лошадкой json-render делают четыре механизма, и у каждого один принцип: декларация в спецификации вместо императивного кода во View.
Привязка к состоянию. Prop можно не задавать значением, а связать со стором через JSON Pointer: { "$bindState": "/form/email" }. Рендерер сам читает значение из состояния и пишет обратно при изменении. Состояние живёт отдельно от спецификации, поэтому патч, меняющий разметку, не затирает то, что пользователь уже ввёл.
Условная видимость. Видимость и значения узлов описываются выражением над состоянием ({ "$cond": "<выражение>", "$then": …, "$else": … }), а не if-ами в коде. Модель умеет это генерировать, потому что синтаксис выражений входит в промпт каталога: «покажи поле X, только если отмечено Y» становится одной декларативной нодой.
Списки. Узел с полем repeat итерируется по массиву из состояния, дочерние элементы ссылаются на текущий через $item и $index. «Выведи таблицу заказов» не требует генерировать узел на каждую строку: спецификация остаётся компактной, строки подставляются из стора.
Развязка событий и действий. Компонент создаёт абстрактное событие (emit("press")), а спецификация через поле on решает, какое действие или цепочку к нему привязать. Один и тот же Button делает разное в разных интерфейсах, при этом белый список действий из каталога остаётся в силе.
Плюс проверка форм описывается в каталоге (правила cross-field, watchers), и рендерер сам подсвечивает ошибки, без дописывания после каждой генерации. Складывается всё это туда, где структура интерфейса заранее неизвестна и зависит от запроса: формы с условной логикой, исследование данных в диалоге, внутренние панели под конкретный тикет. И всё это не привязано к React: рендереры есть для Vue, Svelte, Solid и React Native с той же привязкой, видимостью и стримингом.
Если же структура известна заранее (форма всегда одна и та же), то json-render избыточен, дальше будет видно, почему.
MCP Apps: доставка интерфейса в ассистент
Json-render решает «что рендерить». Остаётся «где»: как этот интерфейс попадёт в Claude или ChatGPT? Здесь работает MCP Apps (SEP-1865), официальное расширение протокола MCP, запущенное в январе 2026. Любой MCP-сервер может вернуть не текст, а интерактивный интерфейс, который рендерится прямо в окне чата. Главный аргумент не «удобно», а «один MCP App работает в нескольких хостах без переписывания интеграции» (официальный анонс называет Claude, Goose, VS Code Insiders и ChatGPT).
Со стороны сервера MCP App это связка из инструмента и UI-ресурса, привязанных через _meta.ui.resourceUri:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
import { z } from 'zod';
const server = new McpServer({ name: 'sales-dashboard', version: '1.0.0' });
const resourceUri = 'ui://sales-dashboard/main.html';
registerAppResource(server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE },
async () => ({ contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: await loadDashboardHtml() }] })
);
registerAppTool(server, 'show_sales_dashboard', {
title: 'Дашборд продаж',
inputSchema: { period: z.string() },
_meta: { ui: {
resourceUri,
csp: {
connectDomains: ['https://api.mycompany.com'],
resourceDomains: ['https://cdn.mycompany.com'],
},
} },
}, async ({ period }) => {
const data = await fetchSalesData(period);
return { structuredContent: { spec: buildDashboardSpec(data), data } };
});
Архитектура на стороне клиента:

Хост (ассистент) заранее видит _meta.ui.resourceUri и упреждающе вызывает HTML параллельно с выполнением инструмента. Получив результат, поднимает двухслойную песочницу: внешний proxy-iframe на whitelisted-домене хоста и внутри него iframe с вашим приложением, между ними message bridge поверх postMessage. Ваш View считается недоверенным и не имеет прямого доступа никуда: произвольные fetch только в домены из CSP, никакого чтения чата за пределами своего structuredContent, никакого localStorage между сессиями. Что View может, так это через App SDK вызвать инструмент своего сервера (app.callServerTool), отправить сообщение в чат (app.sendFollowUpMessage) или открыть внешнюю ссылку, и каждый такой вызов проходит подтверждение пользователем, та же модель безопасности, что у обычных инструментов.

Три эксплуатационных нюанса, которые сэкономят вам день:
Песочница блокирует всё по умолчанию, и самая частая проблема «у меня пустой iframe без ошибок» лечится явным перечислением доменов в
_meta.ui.csp(включая localhost для разработки и CDN со шрифтами).Домен внутреннего iframe у каждого хоста свой (в ChatGPT
*.web-sandbox.oaiusercontent.com, в Claude хеш от server URL), кроссплатформенное приложение должно быть host-aware.Не храните пользовательские auth-токены во View, песочница не персистентна, а долгоживущий токен в iframe это лишняя поверхность атаки. Если View должен ходить в API, то используйте backend-proxy или короткоживущий scoped-токен, выданный под конкретный View, сессию и действие.
MCP Apps без json-render: хост как маршрутизатор намерений
Важно понимать: json-render в этой архитектуре опционален, и большинство MCP Apps обходятся без него. Чтобы это увидеть, посмотрим на картину целиком, глазами хоста, у которого подключено несколько приложений:

Каждое приложение это отдельный MCP-сервер, который при подключении отдал хосту описания своих инструментов. Вот два таких сервера, целиком независимых друг от друга:
// Сервер 1: бронирование столиков. Обычный фронтенд в map.html, никакого json-render
registerAppTool(tablesServer, 'book_table', {
title: 'Бронирование столика',
description: 'Ищет свободные столики в ресторанах: по дате, времени, числу гостей. ' +
'Используй, когда пользователь хочет забронировать место, найти ресторан, поужинать.',
inputSchema: {
date: z.string().describe('Дата в формате YYYY-MM-DD'),
guests: z.number().describe('Число гостей'),
},
_meta: { ui: { resourceUri: 'ui://tables/map.html' } },
}, async ({ date, guests }) => ({
structuredContent: { slots: await findSlots(date, guests) },
}));
// Сервер 2: трекер задач. Свой UI, своя команда, ничего не знает про первый сервер
registerAppTool(tasksServer, 'create_task', {
title: 'Создать задачу',
description: 'Создаёт задачу в трекере: заголовок, срок, исполнитель. ' +
'Используй, когда пользователь просит напомнить, запланировать, поставить таску.',
inputSchema: { title: z.string(), due: z.string().optional() },
_meta: { ui: { resourceUri: 'ui://tasks/board.html' } },
}, async ({ title, due }) => ({
structuredContent: { task: await createTask(title, due) },
}));
Где здесь маршрутизатор? Его нет как отдельного компонента, и в этом суть. Хост складывает описания всех инструментов в контекст модели, и на реплику «Найди столик на четверых в пятницу вечером» модель отвечает обычным вызовом инструмента:
{
"type": "tool_use",
"name": "book_table",
"input": { "date": "2026-06-12", "guests": 4 }
}
Выбор инструмента и есть маршрутизация намерений. Модель выбрала book_table, а не create_task, потому что поле description у первого говорит про рестораны и бронирование, а у второго — про задачи и напоминания. Отсюда практическое следствие, которое недооценивают: description инструмента это и есть ваша настройка маршрутизации. Размытое «работает со столиками» даст ложные срабатывания, конкретное «используй, когда пользователь хочет забронировать место, найти ресторан, поужинать» с примерами формулировок даст точный выбор. Тюнинг маршрутизации в этой архитектуре это редактирование текстов, а не кода.
Дальше хост по _meta.ui.resourceUri поднимает View выбранного приложения, и в чате открывается карта со слотами. Следующая реплика «а теперь поставь напоминание про эту бронь» уводит диалог но второй сервер, и рядом рендерится канбан. Несколько View живут в одной переписке параллельно, каждый в своей песочнице, не видя друг друга.
Заметьте, что генеративного интерфейса в этой картине нет вообще. Интерфейсы обоих приложений написаны руками (map.html и board.html это обычный фронтенд на любом фреймворке, в репозитории MCP Apps есть стартеры под React, Vue, Svelte и vanilla). Генеративная здесь только маршрутизация: модель решает, какое приложение показать и с какими аргументами, а не как оно выглядит.
Отсюда правило, когда что брать. Структура интерфейса известна на этапе разработки (карта бронирования, канбан, плеер): пишите обычный фронтенд и упаковывайте в MCP App — меньше абстракций, меньше точек отказа. Структура зависит от запроса пользователя (наш дашборд, который пересобирается репликами): вот тогда внутрь View ставится json-render, и модель управляет уже не только выбором приложения, но и его содержимым. Второй случай заметно реже первого, и про него следующая глава.
Связка в действии: от запроса до патча
Теперь сведём оба слоя. Архитектурно связка это одно MCP App, у которого внутри View вместо рукописного интерфейса стоит json-render, а инструмент на сервере имеет два режима: вернуть полную спецификацию (первый запрос) или патч к существующей (уточнения). Путь данных в двух фазах:

Ключевая деталь, которую легко упустить: повторный вызов того же инструмента не открывает новый View. Хост доставляет результат в уже открытый виджет уведомлением ui/notifications/tool-result, и обработчик ontoolresult срабатывает второй раз. Именно это превращает MCP App из «показал и забыл» в живой интерфейс. Серверная сторона (упрощённый псевдокод, generatePatch и хелперы данных условные):
registerAppTool(server, 'show_sales_dashboard', {
title: 'Дашборд продаж',
description: 'Показывает дашборд продаж. Для уточнений к открытому дашборду ' +
'передай refine и currentSpec, верну патч вместо пересборки.',
inputSchema: {
period: z.string(),
refine: z.string().optional(), // реплика-уточнение
currentSpec: z.any().optional(), // текущая спецификация из контекста
},
_meta: { ui: { resourceUri } },
}, async ({ period, refine, currentSpec }) => {
const data = await fetchSalesData(period);
if (!refine || !currentSpec) {
// Фаза 1: полная спецификация
return { structuredContent: { mode: 'create', spec: buildDashboardSpec(data), data } };
}
// Фаза 2: просим LLM сгенерировать патч, прижатый к каталогу
const patch = await generatePatch({
system: catalog.prompt({ mode: 'patch' }), // правила и компоненты каталога
spec: currentSpec, // что патчить
instruction: refine, // что изменить
});
return { structuredContent: { mode: 'patch', patch, data } };
});
И сторона View, где сходятся оба слоя (тоже псевдокод по мотивам реального API, точные имена сверяйте с документацией). Склейка MCP Apps и json-render занимает считанные строки:
import { App } from '@modelcontextprotocol/ext-apps';
import { applyJsonPatch } from '@json-render/core';
const app = new App({ name: 'sales-dashboard', version: '1.0.0' });
let spec = null;
app.ontoolresult = ({ structuredContent: sc }) => {
if (sc.mode === 'create') {
spec = sc.spec; // первая отрисовка
} else {
spec = applyJsonPatch(spec, sc.patch); // инкрементальное обновление:
} // графики не дёргаются, ввод цел
renderDashboard(spec, sc.data); // <Renderer registry={registry} … />
};
await app.connect();
Обратный путь, из интерфейса в модель, тоже двухканальный, и эти каналы делают разное. Первый канал это контекст модели: пользователь меняет что-то в интерфейсе руками (например, переключает период в фильтре на Q4), и View сообщает об этом модели через app.updateModelContext({ content: [{ type: 'text', text: 'Пользователь выбрал период Q4' }] }). Тогда следующая реплика в чате интерпретируется с учётом правки, и модель патчит актуальное состояние, а не то, что было до того, как пользователь натыкал руками. Второй канал это вызов сервера: пользователь жмёт кнопку-действие, View не ходит в сеть сам, а зовёт app.callServerTool({ name, arguments }), хост запрашивает подтверждение и проксирует вызов на сервер. Та же модель безопасности, что и у обычных инструментов.

Хорошая новость: базовый каркас этого упакован. createMcpApp даёт generic-инструмент + рендер спецификации из коробки (хост-LLM генерирует spec), а наш ручной пример показывает более продвинутую серверную схему с доменным инструментом и патчами — её собирают поверх registerJsonRenderTool:
import { createMcpApp } from '@json-render/mcp';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = await createMcpApp({
name: 'sales-dashboard',
version: '1.0.0',
catalog, // ваш каталог компонентов и actions
html: bundledHtml, // self-contained HTML, собранный Vite + vite-plugin-singlefile
});
createMcpApp сам экспонирует каталог как инструмент со схемой спецификации и регистрирует UI-ресурс. На стороне iframe вместо ручного ontoolresult берётся хук useJsonRenderApp, который подключается к хосту, принимает результаты и сам ведёт текущую спецификацию:
import { useJsonRenderApp } from '@json-render/mcp/app';
import { JSONUIProvider, Renderer } from '@json-render/react';
function McpAppView({ registry }) {
const { spec, loading, error } = useJsonRenderApp({ name: 'sales-dashboard', version: '1.0.0' });
if (error) return <div>Ошибка: {error.message}</div>;
if (!spec) return <div>Ожидание…</div>;
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
);
}
Для сборки одностраничного HTML под UI-ресурс в пакете есть buildAppHtml, а в репозитории лежит полный рабочий пример examples/mcp с каталогом shadcn/ui, который подключается к Claude или Cursor одной строчкой в конфиге. Понимать механику из ручного варианта всё равно стоит: когда что-то пойдёт не так (а на молодом API пойдёт), отлаживать придётся именно её. Но начинать в 2026 году разумно с официального пакета.
Появление @json-render/mcp, кстати, лучшее подтверждение главного тезиса статьи: «два уровня одной архитектуры» это уже не интерпретация, а официальная позиция авторов самой библиотеки, выпустивших мост к MCP Apps как штатный канал доставки.
Выглядит гладко, но в эксплуатации первыми ломаются четыре вещи.
Где живёт спецификация между репликами. Центральный вопрос архитектуры. В истории сообщений: sharing чата работает сам, но спецификация тащится во входной контекст каждого следующего запроса, и суммарная стоимость диалога может расти близко к квадратичной, если на каждой реплике отправлять полную спецификацию, которая сама растёт от реплики к реплике. На сервере по conversation ID: контекст компактный, но сервер перестаёт быть stateless, появляется состояние, которое надо реплицировать. Серединный путь, к которому приходят при масштабировании: клиент кеширует спецификацию локально, сервер хранит snapshot для sharing— f, а в контекст модели идёт компактное представление (id и типы узлов без данных). Бесплатного варианта нет.
Версионирование каталога. Спецификации живут в истории чатов. Переименовали свойства у компонента, и старые чаты сломались. Каталог это публичный API: версионируйте, держите обратную совместимость хотя бы одну мажорную версию.
Fallback при некорректной спецификации. Даже со structured outputs модель промахивается. Runtime должен уметь graceful degradation: некорректные узлы рендерятся как заглушки, а корректное окружение остаётся.
Наблюдаемость. Без журнала патчей невозможно понять, в какой момент состояние разъехалось, а без метрики «доля корректных спецификаций» вы не заметите, что провайдер обновил модель и генерация поплыла. С первого дня журналируйте патчи и токены.
Альтернативы
Картина на момент написания (середина 2026). Показательный штрих: на стороне Vercel видно движение от RSC-based Generative UI к декларативным JSON-спецификациям, прежний AI SDK RSC сейчас помечен как paused, а развивается json-render.
Связка из статьи (json-render + MCP Apps) | Vercel AI SDK (Generative UI) | CopilotKit | Thesys C1 | A2UI (Google) | |
|---|---|---|---|---|---|
Модель UI | Декларативная спецификация + каталог на Zod | Стриминг компонентов из tool calls | Боковая ИИ-панель видит и меняет ваше приложение | API возвращает готовый интерфейс | Декларативный протокол, шире веба (Flutter, native) |
Куда доставляется | Claude, ChatGPT, Cursor, VS Code, Goose | Только ваше приложение | Только ваше приложение | Ваше приложение | Кроссплатформенно |
Инкрементальные обновления | JSON Patch, стриминг | Регенерация/частично | Через actions | Регенерация | Зависит от реализации |
Привязка | Открытый стандарт (SEP-1865) | Экосистема Vercel | Open source | Vendor lock-in на их API | Открытый протокол |
Зрелость | Молодой, API меняется | Зрелый SDK | Зрелый | Прод-сервис | Ранняя стадия |
Когда брать | Продукт должен жить в нескольких ассистентах | ИИ-фичи внутри своего Next.js-приложения | Копилот поверх существующего React-приложения | Быстрый прототип без своего рендера | Ставка на мультиплатформенность за пределами веба |
Эти инструменты скорее комплементарны, чем воюют: json-render умеет работать с каталогами A2UI, CopilotKit интегрируется с A2UI, MCP Apps опираются на mcp-ui SDK. Выбор определяется одним вопросом: ваш интерфейс живёт в вашем приложении или в чужих ассистентах? Если первое, то Vercel AI SDK или CopilotKit дадут результат быстрее. Если второе, то альтернатив MCP Apps по сути нет.
Финализация
Где эта связка уместна: ассистенты для работы с данными, внутренние инструменты с динамической логикой, продукты, которые должны жить сразу в нескольких ассистентах, формы со сложной условной логикой. Где избыточна: лендинги и статические дашборды (обычный фронтенд), чисто текстовые ассистенты, прототипы (v0 и Lovable быстрее), копилот к своему веб-приложению (CopilotKit быстрее). Маленьким командам без требования портируемости достаточно json-render без обёртки в MCP Apps: 80% выгоды без 20% сложности.
Главный тезис: AI-UI это не «пусть LLM нарисует фронт», а архитектурный сдвиг от генерации артефактов к управлению состоянием интерфейса через декларативные спецификации. json-render и MCP Apps это два уровня одной архитектуры: первый отвечает за «что и как рендерить», второй за «где и как доставить». По отдельности каждый даёт половину решения, вместе они фундамент, на котором интерактивные ИИ-продукты не разваливаются на второй неделе эксплуатации.
Всё это очень новое: SEP-1865 и json-render появились в январе 2026, SDK ломают совместимость регулярно. Хотите спокойно? Дайте экосистеме ещё пару кварталов. Хотите быть впереди? Пишите сейчас и будьте готовы переписывать.
И это для меня не отвлечённая теория. Мы уже создаём NoCode-платформу поверх json-render, а инфраструктуру MCP Apps начинаем трогать прямо сейчас. Так что в этой статье был обзор и первые пробы, а с боевыми примерами реализации, граблями и числами я вернусь в следующих материалах.
Вопросы и возражения пишите в комментарии. Особенно интересен опыт тех, кто уже довёл MCP Apps до эксплуатации: где наступили на грабли, что выбрали в итоге.























