Skip to main content
Glama

tv-debug-mcp

MCP-сервер для полуручного прогона QA-кейсов на реальных Smart TV (Tizen / webOS) и на локальном Chrome — через Chrome DevTools Protocol. Агент управляет приложением: навигация пультом, лонгтап с точными таймингами, переходы по меню, чтение консоли и состояния плеера. Человек подтверждает то, что можно проверить только глазами.

Задача: VKVIDEO-98474. Закрывает боль ручного тестирования сложных кейсов (лонгтап, перемещения, меню) на всём парке устройств, включая старые.

Зачем не Appium / не playwriter

  • Appium TV-драйверы тянут chromedriver, который мёртв на Tizen с Chrome ≤ 57 и держится на хаке подмены UA на webOS 3. Тяжёлая инфра, два разных драйвера.

  • playwriter / Playwright connectOverCDP требует свежий Chromium — не заведётся на webOS 3/4 (Chrome 38/53).

  • Этот MCP говорит с инспектором по «голому» CDP. Один кодовый путь от Chrome 38 до 120+, ноль зависимостей на устройстве. Тот же набор тулов работает и против браузера на ноуте.

Related MCP server: open_browser_use

Инструменты (14)

Тул

Что делает

tv_devices

Парк из devices.json: доступность и реальные capabilities каждого устройства

tv_install

Установка билда (.wgt / .ipk). uninstallFirst:true лечит «Author certificate not match»

tv_launch

Debug-запуск + attach по CDP. Режимы: свежий старт / reload / relaunch / attach

tv_press

Клавиша пульта. durationMs = лонгтап; repeat+intervalMs = серия. Возвращает фокус до/после и inputMode

tv_state

Структурный снимок: url, заголовок, видимые сцены, фокус (текст, класс, путь, индекс/всего), попапы, счётчики

tv_wait_for

Ожидание условия вместо sleep: focusText / selector / selectorGone / scene / text / expression / videoAdvancing

tv_goto

Жать направление, пока сфокусированный элемент не совпадёт с целью. Ограничен maxSteps, дедлайном и детектом «фокус встал» / «обернулись по кругу»

tv_menu

Войти в меню приложения и выбрать раздел по имени; без имени — открыть и вернуть список разделов

tv_sequence

Весь кейс одним вызовом: вердикт, время и результат по каждому шагу, под device-lock

tv_screenshot

PNG кадра. В браузере работает всегда; на Tizen деградирует с пометкой (secure/overlay plane)

tv_console

Консоль / исключения / упавшие запросы с момента launch. Все уровни, фильтр, счётчик отброшенного буфером

tv_video_state

Программный снимок <video>: тикает ли currentTime (два замера), readyState, размеры, MediaError

tv_evaluate

Произвольный JS в странице (escape hatch). На старых ТВ — только ES5

tv_profile

Запись JS CPU-профиля (start → действия → stop): файл .cpuprofile для DevTools + топ функций и файлов по self time. sourceMap деминифицирует топ на прод-сборке. Плюс метрики Performance.getMetrics (heap, DOM-узлы, слушатели, layout) — снимок на start и на stop, в ответе diff; action:"metrics" снимает их отдельно, без записи профиля

tv_sequence — шаги

{"launch": {"relaunch": true}}            // привести апп в известное состояние
{"press": "RIGHT", "repeat": 2}
{"longpress": "ENTER", "durationMs": 1600}
{"goto": {"direction": "DOWN", "text": "Подписки"}}
{"menu": "Settings"}
{"wait": {"scene": "s-player"}, "timeoutMs": 30000}
{"expect": {"selector": ".w-context-menu"}}
{"eval": "document.title"}
{"sleep": 1500}
{"videoState": true, "expectAdvancing": true}
{"state": true}
{"profileStart": {"samplingIntervalUs": 1000}}
{"profileStop": {"path": "/tmp/scroll.cpuprofile", "sourceMap": "…/app.js.map"}}
{"metrics": true}                         // снимок Performance.getMetrics; {"collectGarbage": true} — с GC

expect — то же, что wait, но невыполнение валит шаг. stopOnFail по умолчанию true.

tv_press — клавиши

UP DOWN LEFT RIGHT ENTER BACK MENU INFO GUIDE SEARCH TOOLS CAPTION RED GREEN YELLOW BLUE PLAY PAUSE PLAY_PAUSE STOP REWIND FAST_FORWARD TRACK_NEXT TRACK_PREV RECORD CHANNEL_UP CHANNEL_DOWN PAGE_UP PAGE_DOWN VOLUME_UP VOLUME_DOWN VOLUME_MUTE EXIT DIGIT_0..9 (регистр не важен, можно сырой числовой keyCode). Коды взяты из vendor/zombiebox-platform-{tizen,webos}/lib/input.js.

Лонгтап: {"key":"ENTER","durationMs":1600} — keydown, hold, keyup. Механика LongPressService: таймер стартует на keydown, keyup решает «клик или лонгтап». LG SSAP-пульт hold не выражает — поэтому синтетика, а не пульт.

tv_profile — CPU-профиль и метрики

CPU-профиль — единственный перф-домен, который жив на всём парке: Profiler.start/stop есть и в Chromium 69 (tizen55), и в Chrome 38 (webos3) — в отличие от Tracing. Метрики Performance.getMetrics требуют Chromium 60+, поэтому они едут прицепом и никогда не ценой профиля (см. «Метрики» ниже).

tv_profile {"action": "start"}          # опц. samplingIntervalUs, по умолчанию 1000
tv_goto    {"direction": "DOWN", …}     # то, что меряем
tv_profile {"action": "stop", "sourceMap": "…/app.js.map", "topN": 20}

stop отдаёт:

  • path — файл .cpuprofile. Открывается в Chrome DevTools → Performance → Load profile (кнопка ⤒). Сырой профиль в ответ тула не кладётся никогда — это сотни килобайт JSON;

  • summary.topFunctions — self time и % по функциям (аггрегат по одинаковым фреймам; total time рекурсивной функции считается один раз, а не на каждом уровне);

  • summary.topFiles — то же по файлам;

  • summary.special(program) / (garbage collector) / (idle) отдельно, в топ функций они не лезут;

  • metrics — diff Performance.getMetrics за окно записи (или null на движке без домена);

  • warning — если карта не прочиталась, если ни один топовый фрейм в ней не нашёлся, если формат легаси или если метрик на этом движке нет.

Self time = hitCount × средний интервал семплинга, где интервал выводится из самой записи (длительность / число хитов), а не из запрошенного samplingIntervalUs — старый движок вправе его проигнорировать.

Прод-сборка без sourceMap — это топ вида Xy/abc. Карту брать из той же сборки, что стоит на ТВ (dist/sourcemaps/<версия>/<таргет>/app.js.map); деминифицируются только топ-N фреймов, остальное DevTools разберёт сам по файлу.

Внутри tv_sequence — шагами profileStart/profileStop: сценарий держит операционный лок, отдельный tv_profile в него не влезет.

Форматы профиля различаются между поколениями движков и нормализуются оба: современный (nodes[], 0-based строки, микросекунды) и легаси Chrome 38 (head-дерево, 1-based строки, секунды). Строки в саммари всегда 1-based, как показывает DevTools. Файл легаси-формата современный DevTools может не открыть — об этом приходит warning, саммари при этом валидное.

tv_profile — метрики (heap, DOM, layout)

CPU-профиль показывает, где горит JS, и не видит ни память, ни layout. Performance.getMetrics — один дешёвый вызов, который отдаёт JSHeapUsedSize, JSHeapTotalSize, Nodes, Documents, JSEventListeners, LayoutCount, RecalcStyleCount и кумулятивные счётчики времени (LayoutDuration, RecalcStyleDuration, ScriptDuration, TaskDuration).

tv_profile {"action": "metrics"}                      # снимок здесь и сейчас
tv_profile {"action": "metrics", "collectGarbage": true}

start и stop снимают метрики сами, поэтому охота на утечку — это обычная запись:

tv_profile {"action": "start"}
tv_press   {"key": "DOWN", "repeat": 20}
tv_profile {"action": "stop", "collectGarbage": true}

stop вернёт

"metrics": {
  "windowSec": 12.4,
  "collectedGarbage": true,
  "values": {
    "Nodes":            {"before": 1200,     "after": 1650,     "diff": 450},
    "JSEventListeners": {"before": 340,      "after": 352,      "diff": 12},
    "JSHeapUsedSize":   {"before": 20000000, "after": 24500000, "diff": 4500000},
    "LayoutDuration":   {"before": 0.1,      "after": 0.4,      "diff": 0.3}
  }
}

Читать так: Nodes вырос на 450 после того, как навигация вернулась туда же — сцена не разбирает свой DOM. LayoutDuration — секунды layout-времени именно за окно записи.

Детали:

  • Отдаётся весь список метрик, какой прислал движок, без белых списков: набор в Chromium 69 и в свежем Chrome разный, а фильтр молча съел бы то, чего мы не ждали. Метрика, которую знает только один из двух снимков, остаётся в diff со стороной null — это тоже информация. Нечисловые значения проходят насквозь с diff: null;

  • windowSec — из Timestamp (монотонные часы движка), не из часов хоста: раунд-трипы CDP в окно не входят;

  • кумулятивные *Duration считаются с момента старта движка — смысл имеет только diff, не абсолют;

  • collectGarbage по умолчанию выключен. Форсированный GC — это пауза: внутри записи она искажает и профиль, и поведение слабого ТВ. Включать под охоту за утечкой, где несобранный мусор как раз и подделывает рост heap. Метод, которого на движке нет, даёт warning, а не ошибку;

  • снимок на start берётся до Profiler.start, на stopпосле Profiler.disable, чтобы сами вызовы метрик не попали в запись, которую они описывают.

Платформы: tizen55 (Chromium 69) ✓, pc ✓, webos3 (Chrome 38) ✗ — домена Performance там нет. action:"metrics" на webos3 честно падает с сообщением про Chromium 60+; start/stop при этом работают как раньше и возвращают metrics: null плюс warning — потерять CPU-профиль из-за отсутствующих метрик нельзя. Фолбэка на performance.memory нет намеренно: на webOS значения квантованы и дают стабильную ложь вместо честного отказа.

В tv_sequence — шаг {"metrics": true}: им можно обрамить любой кусок сценария, не только тот, что покрыт записью профиля. Diff между двумя такими шагами считает вызывающий.

Парк устройств

devices.json (или путь в TV_DEBUG_CONFIG). Файл перечитывается по mtime — правка подхватывается без рестарта MCP; дубли id и портов отвергаются с внятной ошибкой.

{
  "defaultDevice": "tizen55",
  "devices": [
    {"id": "tizen55", "platform": "tizen", "app": "vktv", "appId": "KVUrL2ikom.vktv",
     "host": "192.168.1.16", "sdbPort": 26101, "localPort": 9955},
    {"id": "webos7", "platform": "webos", "app": "vktv", "appId": "com.vk.video", "device": "webos7"},
    {"id": "pc-dev", "platform": "pc", "app": "vktv", "url": "http://localhost:1337"},
    {"id": "pc-dev-parity", "platform": "pc", "app": "vktv", "url": "http://localhost:1337",
     "inputMode": "synthetic"}
  ]
}

cliTarget (Tizen) можно не указывать — выводится из третьей колонки sdb devices; он нужен, чтобы tizen install -t попал в нужный ТВ на парке.

App-профиль

apps/<id>.json, привязка полем "app". Здесь живёт всё знание о приложении — чем помечен фокус, как выглядит сцена, где меню. Это то, что делает MCP переносимым: для другого приложения (например, Solid-стека smarttv) заводится второй файл, а не форк.

{
  "focus": ["._active"],
  "scene": {"container": "._scene", "strip": "zb-layer__container|zb-fullscreen"},
  "popup": ["[class*=popup]", "[class*=context-menu]"],
  "menu": {"openKey": "LEFT", "exitKey": "BACK",
           "root": ".ds-menu__primary", "item": ".ds-menu__primary .ds-menu-cell",
           "title": ".ds-menu-cell__middle"},
  "tile": ".w-video-tile, .w-media-tile"
}

Два неочевидных момента из живых прогонов, зашитых в профиль vktv:

  • ZombieBox вешает класс фокуса на всю цепочку scene → container → list → tile, поэтому сфокусированный виджет — это самый глубокий match, а не первый. Первый — это сцена, и по нему навигация выглядит неподвижной.

  • root/item пришиты к первому уровню меню: раздел «Настройки» рисует свои строки теми же .ds-menu-cell во втором уровне, и одна из них называется «Main». Матч по всему меню заставлял tv_menu("Main") выбрать строку настроек и отрапортовать успех, пока апп никуда не уходил.

Браузерный режим (platform: "pc")

Тот же набор тулов против локального Chrome. Быстро, и скриншоты реально работают — на Tizen они виснут.

  • Chrome — наш: свой временный --user-data-dir, --remote-debugging-port=0 (порт читается из DevToolsActivePort, а не прибит к 9333), гасится и подчищается на dispose. К обычному браузеру пользователя MCP не цепляется.

  • Dev-сервер — ваш: MCP проверяет, что url отвечает, и не запускает и не гасит его. Запускать npm start в vktv.

  • --disable-web-security обязателен: без него анонимный бутстрап vktv умирает на CORS (oauth.vk.com/get_anonym_token).

  • Network.setCacheDisabled(true) обязателен: zb run отдаёт ES-модули, и переиспользованный браузер молча гоняет вчерашний код.

Trusted vs synthetic — почему это два разных эксперимента

ТВ

Браузер по умолчанию

Браузер inputMode: "synthetic"

Механизм

page-side KeyboardEvent

Input.dispatchKeyEvent

page-side KeyboardEvent

isTrusted

нет

да

нет

Куда летит

document

реально сфокусированный элемент

document

Дефолтные действия браузера

нет

да

нет

Кейс может быть зелёным в браузере и красным на ТВ (ветка TV-keyCode не задействована) — и наоборот (Backspace уводит браузер назад). Поэтому: режим пишется в каждый вердикт, тихого фолбэка между режимами нет, а навигационные кейсы прогоняются ещё и на pc-dev-parity перед выводом «на ТВ будет так же».

Ключевые находки on-device (Tizen 5.5, sdb 4.2.36)

  • Debug-запуск: sdb -s <serial> shell 0 debug <appId> без аргумента-таймаута. С таймаутом launchpad отвечает closed. Инспектор на device-порту переживает закрытие sdb-канала, поэтому канал закрывается сразу после разбора порта.

  • Надёжный killsdb shell 0 was_kill <appId>. kill_app на retail-шелле молча no-op.

  • attach работает только через живой инспектор: второй debug по уже отлаживаемому аппу отвечает closed. Порт берётся из памяти сессии или из правила sdb forward --list, которое переживает рестарт MCP; поэтому forward намеренно не снимается на dispose.

  • Скриншот Page.captureScreenshot виснет (secure/overlay plane, HDCP) — тул отдаёт ok:false с пометкой. Для плейбека — tv_video_state + взгляд на ТВ.

  • localStorage переживает debug-релонч на 5.5 (проверено: маркер на месте после was_kill + свежего debug).

  • Загрузка каталога — 3.6–6.1 с, а не «22 секунды на всякий случай»: tv_wait_for быстрее и детерминированнее слепой паузы.

  • relaunch в браузерном режиме переиспользует ту же throwaway-профиль-директорию, а Chrome оставляет в ней DevToolsActivePort от прошлого запуска. Файл сносится перед спавном — иначе адаптер отдаёт порт, на котором уже никто не слушает (no inspectable page at http://127.0.0.1:…).

  • Весь page-side JS — строго ES5: Array.prototype.find появился в Chrome 45, а webOS 3 — это Chrome 38, и одна такая строчка роняла tv_video_state ровно на самом старом устройстве парка.

Как это устроено

Claude Code ── stdio ── server.js
                         ├── config.js      devices.json (перечитка по mtime + валидация)
                         ├── appprofile.js  apps/<app>.json — знания о приложении
                         ├── adapters/
                         │    tizen.js      sdb -s: install/was_kill/debug/forward
                         │    webos.js      ares: close→launch→inspect
                         │    pc.js         свой Chrome + navigate + setCacheDisabled
                         │    spawn-until-match.js  общий супервизор CLI-детей
                         ├── input/
                         │    synthetic.js  page-side KeyboardEvent (ТВ + parity)
                         │    trusted.js    Input.dispatchKeyEvent (браузер)
                         ├── cdp.js         CDP по WebSocket, единый путь дисконнекта
                         ├── keymaps.js     KeySpec {code, key, domCode} по платформам
                         ├── inject.js      page-side ES5: key dispatch, focus, video-state
                         ├── state.js       page-side ES5: снимок состояния и фокуса
                         ├── wait.js        поллинг условий (общий для wait/goto/sequence)
                         ├── profile.js     CPU-профиль: оба формата, саммари, sourcemap
                         ├── ports.js       свободный локальный порт под forward
                         └── session.js     живая сессия: два лока, авто-реконнект, навигация

Устойчивость: упавший ТВ, выдернутый сокет или отсутствующий sdb валят один вызов тула, а не процесс MCP. ensureConnected сериализован — параллельные вызовы не запускают апп дважды.

Проверено

Прогон

Что

node test/phase0-offline.mjs

40/40 — честный статус офлайн-устройства, перечитка конфига без рестарта, отказ при дублях id, выживание без sdb; парсер CPU-профиля на фикстурах обоих форматов (совпадающие числа, спец-узлы отдельно, рекурсия не удваивается) и деминификация топа с деградацией до warning

node test/phase0-check.mjs

18/18 на Samsung UE50TU8510 — launch, движение фокуса, ES5-проба видео, limit:1, attach из другого процесса с сохранением состояния, выживание при обрыве сокета

node test/phase1-check.mjs

12/12 на ТВ — wait_for вместо сна, структурный фокус, goto до цели и его границы, меню Settings ⇄ Main, кейс лонгтапа целиком

node test/phase2-check.mjs

38/38 в Chrome — capabilities, отказ tv_install, свой Chrome на порту 0, навигация, реальный скриншот, кейс лонгтапа в trusted и synthetic, CPU-профиль (именованная busy-функция видна в топе, двойной start и сиротский stop отвергнуты, профилирование шагами сценария), уборка за собой

webOS-адаптер переписан (close → launch → inspect, честный freshLaunch), но on-device не прогонялся: LG из ares-setup-device --list сейчас недоступны (connection timed out).

test/smoke.mjs — ad-hoc прогон произвольного списка вызовов; test/harness.mjs — общий stdio-клиент для всех проверок. nav-probe.mjs, longtap-probe.mjs, localstorage-trap.mjs, demo-case.mjs — одноразовые зонды из первой итерации, их выводы уже зафиксированы в проверках выше.

Демо-кейсы

cases/*.mdcatalog-longtap.md, playback.md, menu-navigation.md. Формат и правила, выведенные из прогонов, — в cases/README.md.

Регистрация в Claude Code

claude mcp add tv-debug --scope user -- node "$HOME/.claude/mcp-servers/tv-debug-mcp/src/server.js"

Тулы появятся как mcp__tv-debug__*.

Установка

env -u HTTP_PROXY -u HTTPS_PROXY npm install   # корп-прокси режет npmjs

Зависимости: @modelcontextprotocol/sdk, ws, source-map-js (чистый JS-порт source-map 0.6, без wasm — важно для офлайн-запуска). Node ≥ 18.

Экспорт команде

Пакет самодостаточный: скопировать директорию, поправить devices.json под парк, npm install, claude mcp add. Знания о приложении — в apps/, не в коде.

Дальше

  • webOS on-device прогон (в т.ч. webOS 3 = Chrome 38: ES5-инъекция, работоспособность скриншота и легаси-формат CPU-профиля — парсер написан по спецификации Chrome 38 и проверен на фикстуре, но не на живом LG).

  • Прогон tv_profile на tizen55 с sourceMap от прод-сборки (карта Closure парсится и позиции разрешаются — проверено офлайн на dist/sourcemaps/2.4.64/vktv-tizen-upgradable).

  • Остальные перф-инструменты (FPS, Performance.getMetrics, Tracing) — отдельным заходом, они не покрывают весь парк.

  • Авто-повтор удержанной d-pad-клавиши (holdRepeatMs): сейчас durationMs шлёт один keydown, что верно для лонгтапа, но не воспроизводит скролл ленты зажатой стрелкой. Обход списков закрывает tv_goto.

  • Параллельный прогон одного кейса на N ТВ (адресация -s для этого уже есть).

  • Allure TestOps (чтение кейсов) + allurectl (заливка результатов).

  • WS-пульт (SSAP / Samsung remote) для системных кейсов HOME/suspend, которые page-level синтетика не покрывает.

F
license - not found
-
quality - not tested
C
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.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • MCP server for understanding Javascript internals from ECMAScript specification.

  • MCP server for Appcircle mobile CI/CD platform.

  • MCP server for Klever blockchain smart contract development.

View all MCP Connectors

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/Ediand11/tv-debug-mcp'

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