tv-debug-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tv-debug-mcpPress OK and check video state."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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)
Тул | Что делает |
| Парк из |
| Установка билда ( |
| Debug-запуск + attach по CDP. Режимы: свежий старт / |
| Клавиша пульта. |
| Структурный снимок: url, заголовок, видимые сцены, фокус (текст, класс, путь, индекс/всего), попапы, счётчики |
| Ожидание условия вместо |
| Жать направление, пока сфокусированный элемент не совпадёт с целью. Ограничен |
| Войти в меню приложения и выбрать раздел по имени; без имени — открыть и вернуть список разделов |
| Весь кейс одним вызовом: вердикт, время и результат по каждому шагу, под device-lock |
| PNG кадра. В браузере работает всегда; на Tizen деградирует с пометкой (secure/overlay plane) |
| Консоль / исключения / упавшие запросы с момента launch. Все уровни, фильтр, счётчик отброшенного буфером |
| Программный снимок |
| Произвольный JS в странице (escape hatch). На старых ТВ — только ES5 |
| Запись JS CPU-профиля ( |
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} — с GCexpect — то же, что 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— diffPerformance.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 — почему это два разных эксперимента
ТВ | Браузер по умолчанию | Браузер | |
Механизм | page-side |
| page-side |
| нет | да | нет |
Куда летит |
| реально сфокусированный элемент |
|
Дефолтные действия браузера | нет | да | нет |
Кейс может быть зелёным в браузере и красным на ТВ (ветка 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-канала, поэтому канал закрывается сразу после разбора порта.Надёжный kill —
sdb 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 сериализован — параллельные вызовы не запускают апп дважды.
Проверено
Прогон | Что |
| 40/40 — честный статус офлайн-устройства, перечитка конфига без рестарта, отказ при дублях id, выживание без |
| 18/18 на Samsung UE50TU8510 — launch, движение фокуса, ES5-проба видео, |
| 12/12 на ТВ — |
| 38/38 в Chrome — capabilities, отказ |
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/*.md — catalog-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 синтетика не покрывает.
This server cannot be installed
Maintenance
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
- Flicense-qualityCmaintenanceModular testing and automation MCP servers for Android devices, desktop browsers, canvas games, and visual regression.Last updated
- Alicense-qualityAmaintenanceMCP server for browser automation, exposing tools for tab management, navigation, CDP, action plans, and cleanup.Last updated186217MIT
- Alicense-qualityDmaintenanceMCP server that connects to your browser to capture screenshots, inspect console logs, network requests, and more via Chrome DevTools Protocol.Last updated92MIT
- Alicense-qualityDmaintenanceMCP server for controlling Chromium/Chrome via Chrome DevTools Protocol. Supports cross-platform automation, auto-launch, and automatic reconnection.Last updated1MIT
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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