惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Troy Hunt's Blog
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Proofpoint News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - Franky
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
V
Visual Studio Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Azure Blog
Microsoft Azure Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
WordPress大学
WordPress大学
腾讯CDC
J
Java Code Geeks
T
The Blog of Author Tim Ferriss
博客园 - 司徒正美
博客园 - 【当耐特】
小众软件
小众软件
Attack and Defense Labs
Attack and Defense Labs
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
罗磊的独立博客
Schneier on Security
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
O
OpenAI News
TaoSecurity Blog
TaoSecurity Blog
H
Heimdal Security Blog
Blog — PlanetScale
Blog — PlanetScale
Forbes - Security
Forbes - Security
B
Blog
I
InfoQ
D
DataBreaches.Net
月光博客
月光博客
T
Tenable Blog
S
Security Affairs
C
Check Point Blog
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
GenUI: первый взгляд на json-render и MCP Apps
Булат · 2026-06-22 · via Все публикации подряд на Хабре

Сложный

15 мин

51

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

Демо 1: json-render, дашборд собирается из спецификации

Демо 1: json-render, дашборд собирается из спецификации

Вывод: код это плохой интерфейс между 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 } };
});

Архитектура на стороне клиента:

Архитектура MCP App: Host, Sandbox Proxy, Inner iframe и MCP Server

Архитектура MCP App: Host, Sandbox Proxy, Inner iframe и MCP Server

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

Демо 2: MCP Apps, поток сообщений

Демо 2: MCP Apps, поток сообщений

Три эксплуатационных нюанса, которые сэкономят вам день:

  • Песочница блокирует всё по умолчанию, и самая частая проблема «у меня пустой 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 обходятся без него. Чтобы это увидеть, посмотрим на картину целиком, глазами хоста, у которого подключено несколько приложений:

Хост-маршрутизатор: реплика пользователя, LLM выбирает инструмент, рендерится View нужного приложения

Хост-маршрутизатор: реплика пользователя, LLM выбирает инструмент, рендерится View нужного приложения

Каждое приложение это отдельный 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, а инструмент на сервере имеет два режима: вернуть полную спецификацию (первый запрос) или патч к существующей (уточнения). Путь данных в двух фазах:

Две фазы: create рисует интерфейс с нуля, patch обновляет тот же View точечно

Две фазы: create рисует интерфейс с нуля, patch обновляет тот же View точечно

Ключевая деталь, которую легко упустить: повторный вызов того же инструмента не открывает новый 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 }), хост запрашивает подтверждение и проксирует вызов на сервер. Та же модель безопасности, что и у обычных инструментов.

Два обратных канала из View: фильтр уходит в контекст модели, кнопка — вызовом на сервер с подтверждением

Два обратных канала из View: фильтр уходит в контекст модели, кнопка — вызовом на сервер с подтверждением

Хорошая новость: базовый каркас этого упакован. 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 до эксплуатации: где наступили на грабли, что выбрали в итоге.