Skip to main content
Glama
DreamShaded

MCP App Proxyfier

by DreamShaded

MCP App Proxyfier

MCP-сервер, который отдаёт в чат с моделью интерактивные MCP Apps (официальное UI-расширение MCP, рендерится в песочнице-iframe хоста). Вместо текстовой обёртки над запросами зритель получает нативный UI прямо в диалоге.

Реализован один вылизанный флоу на настоящих данных: Megamarket — поиск товаров → грид → детальная страница → корзина → оформление.

Данные статические: каталог собран из сохранённых снапшотов реальных страниц megamarket.ru (pages/) скриптом pnpm update:data. Сеть и браузер на демо не нужны — сервер стартует мгновенно и отвечает одинаково при любом Wi-Fi в зале.

Сценарий живого демо (без MCP → MCP без UI → MCP с UI → MCP с UI и скиллом) — в DEMO.md.

Структура

packages/
  ui/      React + Vite; собирается в самодостаточные HTML (по одному на приложение)
  server/  MCP-сервер, отдаёт UI как ui:// ресурсы + инструменты
pages/     HAR/HTML-снапшоты megamarket.ru — сырьё для pnpm update:data

UI собирается в два самодостаточных HTML-бандла — index (каркасный ping) и megamarket; все JS/CSS встроены, внешних ссылок нет (требование песочницы-iframe). Сервер на старте читает эти HTML и регистрирует как ui:// ресурсы.

Инструменты

Инструмент

Вход

UI

Назначение

ping

echo?

ping.html

Каркасная проверка рендера iframe

search_products

query, filters?

Поиск товаров, результат только текстом (список позиций)

search_products_widget

query, filters?

megamarket.html

Тот же поиск + грид карточек виджетом

search_products_advised

query, filters?

megamarket.html

Тот же поиск + виджет; описание обязывает прочитать skill://shopping-advisor

get_product

id

megamarket.html

Карточка товара: галерея, таблица «О товаре», описание

get_delivery_calendar

Сегодня/завтра + ближайшие 7 дней с днями недели

add_to_cart

id

megamarket.html

Добавляет товар в корзину

view_cart

megamarket.html

Текущее состояние корзины

checkout

megamarket.html

Оформляет заказ по корзине, возвращает подтверждение и очищает её

filters — ценовой коридор priceMin / priceMax и срок доставки deliveryBy (YYYY-MM-DD; оставляет только то, что приедет не позже).

get_delivery_calendar существует потому, что у модели нет часов: «до пятницы» она сама в число не превратит — либо выдумает, либо отсчитает от даты обучения. Скилл обязывает вызвать календарь до поиска, отсюда и порядок вызовов в демо.

Три варианта поиска — это ступени живого демо (без UI → с UI → с UI и методичкой). Они существуют одновременно на одном подключении, переключение идёт формулировкой запроса, без перезапуска сервера. Почему их три, а не один с параметром: привязка UI живёт в _meta.ui.resourceUri на регистрации инструмента и уезжает клиенту в tools/list — результат вызова её изменить не может.

Корзина — in-memory, одна на процесс сервера: перезапуск её обнуляет.

Ресурсы ui://

MCP Apps: самодостаточный HTML, который хост рендерит в песочнице-iframe и кормит structuredContent результата инструмента через мост. MIME — text/html;profile=mcp-app.

URI

Собирается из

Кто рендерит

ui://mcp-app-proxyfier/ping.html

packages/ui/index.htmldist/index.html

ping

ui://mcp-app-proxyfier/megamarket.html

packages/ui/megamarket.htmldist/megamarket.html

все инструменты Megamarket

Приложение Megamarket — мини-SPA: выдача → деталка → корзина → подтверждение. Какой вид показать, оно решает по форме пришедшего structuredContent: products — выдача, product — деталка, cart — корзина.

Виджет интерактивный, а не картинка:

  • чипы фильтров над гридом (бренд, шумоподавление) — фильтруют внутри iframe, без вызова инструмента и без нового пузыря в чате;

  • клик по карточке открывает деталку (get_product через мост), «Назад» возвращает в тот же отфильтрованный список — состояние фильтров переживает переход;

  • деталка открывается и голосом («покажи подробнее вот эти») — вид тот же самый;

  • «В корзину» на карточке и на деталке — app-initiated add_to_cart(id); ответ несёт актуальную корзину, поэтому бейдж обновляется без отдельного view_cart.

Фильтры виджета сознательно не трогают доставку: срок задаёт агент через filters.deliveryBy на сервере. Иначе виджет молча показывал бы то, что агент уже отсёк.

Ресурсы skill://

Методички для агента. В отличие от ui:// это не MCP Apps — рендерить нечего, это обычный текст, который агент читает перед вызовом инструмента.

URI

MIME

Назначение

skill://index.json

application/json

Индекс скиллов — точка входа, по которой агент находит остальные

skill://shopping-advisor/SKILL.md

text/markdown

Подбор товара: уточнить бюджет и сценарий, перевести бюджет в filters, сравнивать по цене и объёму отзывов, не вестись на витринную скидку

Индекс и сами методички собираются из одного SkillDefinition (packages/server/src/skills/skill-registry.ts), поэтому имя и описание в индексе не могут разъехаться с ресурсом.

Данные

Каталог — packages/server/data/market.json (70 товаров, у всех есть детальная карточка). Пересобирается из снапшотов:

pnpm update:data              # разбирает pages/ → packages/server/data/market.json
pnpm update:data -- --dry-run # только показать, что распарсилось, ничего не писать

Скрипт идемпотентен: повторный прогон просто перезаписывает файл. Сеть не трогает.

Выдача поиска ограничена девятью позициями (SEARCH_RESULT_LIMIT) — грид 3×3 в узком чат-iframe.

Сроки доставки — синтетические

packages/server/data/delivery.json не собирается из снапшотов: настоящий срок (calculatedDeliveryDate в SSR-стейте страниц) есть ровно у одного товара каталога из 70, а на выдаче Мегамаркет его не отдаёт вовсе. Формат подписей при этом взят у сайта: он пишет только «Сегодня», «Завтра» и дату вида «15 июля» — «Послезавтра» у него нет.

В файле лежат дни, а не даты: дата считается в рантайме от сегодня, поэтому «Завтра» остаётся завтрашним и через месяц. Товары, которых в файле нет, получают детерминированный срок по хешу id — одинаковый между запусками, чтобы выдача не «дышала».

Флаг anc (активное шумоподавление) поднимается в «плоский» DTO из характеристик, чтобы виджет фильтровал грид без запроса деталки на каждый товар. null означает «характеристики нет в снапшоте», а не «шумоподавления нет».

Требования

  • Node.js 22+ (рекомендуется 24)

  • pnpm 11+

Сборка

pnpm install
pnpm build          # сначала собирает HTML-бандлы UI, затем сервер

pnpm build сначала собирает UI (самодостаточные HTML со встроенными JS/CSS — без внешних источников, как требует песочница-iframe), затем компилирует сервер, который на старте читает эти HTML и регистрирует как ui:// ресурсы.

Превью виджета в браузере

Посмотреть виджет без Claude Desktop:

pnpm --filter @mcp-app-proxyfier/ui exec vite
# → http://localhost:5173/preview.html

Рендерит те же компоненты вью, что и боевое приложение, на настоящем ответе сервера. Работают чипы фильтров, заход в карточку и возврат в отфильтрованный список. Кейс открывается ссылкой: ?case=friday, ?case=detail, ?case=cart.

Фикстуры пересобираются с живого сервера, руками их править не надо:

pnpm build            # превью читает ответы собранного сервера
pnpm update:fixtures  # → packages/ui/src/preview/fixtures.json

Даты доставки в фикстуре заморожены на момент снятия (превью — про вёрстку, не про календарь). Протухли подписи вроде «Завтра» — просто перезапустите update:fixtures.

Чего в превью нет: postMessage-моста и вызовов инструментов — «Подробнее» берёт деталку из фикстуры, а не дёргает get_product; «В корзину» показывает снимок корзины, а не вызывает add_to_cart. Мост и рендер iframe проверяются только вживую, в Claude Desktop (см. «Ручная проверка рендера» ниже). В продакшен-сборку превью не попадает: pnpm build:ui собирает только index и megamarket.

Тесты

pnpm test           # typecheck (включая тесты) + прогон

Тесты проверяют внешнее поведение через швы: инструменты MCP как чёрные ящики (поднимается настоящий McpServer на in-memory транспорте), загрузку статического каталога (включая деградацию товара без богатой детали), сроки доставки, реестр скиллов и выбор транспорта. Рендер iframe проверяется вручную (см. ниже) — в CI его нет.

Тесты тайпчекаются вместе с кодом: гоняет их tsx, который типы не проверяет, поэтому раньше tsc молча зеленел на тестах, ссылающихся на удалённые модули. Сборка идёт отдельным конфигом (tsconfig.build.json), чтобы тесты не попадали в dist/.

Запуск / регистрация в Claude Desktop

Сервер говорит по MCP через stdio: Claude Desktop запускает его как дочерний процесс. После pnpm build пропишите его в конфиг Claude Desktop.

Расположение файла конфигурации:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows: %APPDATA%\Claude\claude_desktop_config.json

  • Linux: ~/.config/Claude/claude_desktop_config.json

Добавьте (замените путь на абсолютный путь к этому репозиторию):

{
  "mcpServers": {
    "mcp-app-proxyfier": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-app-proxyfier/packages/server/dist/index.js"]
    }
  }
}

Затем полностью закройте и заново откройте Claude Desktop.

Ручная проверка рендера (главный риск демо)

Главный риск — баг рендера iframe на хосте (ext-apps #671): клиент согласует UI-возможность и тянет ресурс, но не рисует iframe. Поэтому его проверяют глазами на боевой сборке.

В чате Claude Desktop попросите модель вызвать инструмент ping (например, «вызови инструмент ping с echo hello»). Убедитесь визуально:

  1. В чате нарисован интерактивный iframe (карточка с заголовком «MCP App Proxyfier»), а не только текстовый результат.

  2. Карточка показывает message: pong, echo: hello и таймстамп — то есть structuredContent инструмента дошёл до UI через мост.

Если виден только текст и iframe не рисуется — баг #671 воспроизведён: зафиксируйте версию Claude Desktop и держите наготове запасной текстовый сценарий для демо.

Абстракция транспорта

Сервер не зависит от транспорта. Инструменты и ресурсы регистрируются на McpServer без знания о канале. Транспорт выбирается за швом ServerTransportProvider (packages/server/src/transport/): stdio (по умолчанию) и http (Streamable HTTP). Оба провайдера регистрируют ровно те же инструменты и ui:// ресурсы — добавление HTTP не потребовало правок кода инструментов или ресурсов.

Транспорт выбирается флагом или переменной окружения (флаг приоритетнее):

Параметр

Флаг

Env

По умолчанию

Транспорт

--transport stdio|http

MCP_TRANSPORT

stdio

Интерфейс прослушивания

--host

MCP_HTTP_HOST

127.0.0.1

Порт

--port

MCP_HTTP_PORT

3000

Путь эндпоинта

--path

MCP_HTTP_PATH

/mcp

Bearer-токен

--token

MCP_HTTP_TOKEN

(выкл.)

Флаги понимают обе формы: --port 3000 и --port=3000.

Удалённый коннектор (Streamable HTTP)

Для демо, где владелец подключает коннектор сам (custom connector в claude.ai), а не Claude Desktop запускает его локально. Это альтернативный канал к тому же серверу — stdio-демо он не блокирует.

  1. Соберите и запустите сервер по HTTP (слушает на 127.0.0.1:3000/mcp). Туннель делает порт публичным, поэтому задайте MCP_HTTP_TOKEN — запросы без Authorization: Bearer <token> отклоняются с 401:

    pnpm build
    MCP_TRANSPORT=http MCP_HTTP_TOKEN="$(openssl rand -hex 16)" pnpm start
    # токен выкл. (только локально, без туннеля): pnpm start -- --transport http
  2. Откройте публичный HTTPS-туннель к этому локальному порту:

    cloudflared tunnel --url http://127.0.0.1:3000
    #   → печатает https://<random>.trycloudflare.com
    # альтернатива ngrok:
    #   ngrok http 3000   → https://<random>.ngrok-free.app

    URL коннектора — это origin туннеля плюс путь эндпоинта, например https://<random>.trycloudflare.com/mcp.

  3. В claude.ai → Settings → Connectors → Add custom connector вставьте этот URL (и Bearer-токен в поле авторизации коннектора, если вы его задали). Claude инициализирует сессию Streamable HTTP и показывает те же инструменты и ui:// приложения, что и stdio.

Ручная проверка (рендер iframe на хосте, ext-apps #671). Как и для stdio, убедитесь визуально, что результат инструмента рисует интерактивный iframe в чате claude.ai, а не только текст. Баг рендера #671 — клиентский и не связан с транспортом, но его нужно перепроверить на claude.ai: сборка хоста отличается от Claude Desktop.

Замечания:

  • Один запущенный процесс держит одну сессию Streamable HTTP — одного докладчика за туннелем.

  • Реконнект = перезапуск. При чистом отключении claude.ai шлёт завершение сессии и повторное подключение работает; после грязного обрыва (туннель умер) проще всего Ctrl-C и заново --transport http, если коннектор потерял сессию.

  • Прослушивание остаётся на localhost намеренно; не слушайте 0.0.0.0 — доступ к серверу только через туннель. Защита от DNS-rebinding выключена намеренно (host туннеля динамический); доступ охраняет Bearer-токен MCP_HTTP_TOKEN.

  • После демо погасите туннель — URL является секретом.

-
license - not tested
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DreamShaded/MCP-Apps-data-proxyfier'

If you have feedback or need assistance with the MCP directory API, please join our Discord server