MCP App Proxyfier
MCP server delivering interactive MCP Apps UI for Megamarket shopping, enabling search, product details, cart, and checkout through native interfaces within the chat.
README
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.html → dist/index.html |
ping |
ui://mcp-app-proxyfier/megamarket.html |
packages/ui/megamarket.html → dist/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»). Убедитесь визуально:
- В чате нарисован интерактивный iframe (карточка с заголовком «MCP App Proxyfier»), а не только текстовый результат.
- Карточка показывает
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-демо он не блокирует.
-
Соберите и запустите сервер по 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 -
Откройте публичный HTTPS-туннель к этому локальному порту:
cloudflared tunnel --url http://127.0.0.1:3000 # → печатает https://<random>.trycloudflare.com # альтернатива ngrok: # ngrok http 3000 → https://<random>.ngrok-free.appURL коннектора — это origin туннеля плюс путь эндпоинта, например
https://<random>.trycloudflare.com/mcp. -
В 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 является секретом.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。