Add bounded MCP text input for Sprinter programs
This commit is contained in:
+4
-3
@@ -280,14 +280,15 @@ Quick wins:
|
|||||||
## Прочий backlog
|
## Прочий backlog
|
||||||
|
|
||||||
- [ ] **Довести MCP C-уровня до полного безопасного контракта.** Первый
|
- [ ] **Довести MCP C-уровня до полного безопасного контракта.** Первый
|
||||||
stdio-адаптер с 28 инструментами использует общую sdbg-сессию и
|
stdio-адаптер с 29 инструментами использует общую sdbg-сессию и
|
||||||
owner ID личных точек. Полный план и матрица всех 30 raw-возможностей —
|
owner ID личных точек. Полный план и матрица всех 30 raw-возможностей —
|
||||||
[mcp-convergence-plan.md](mcp-convergence-plan.md).
|
[mcp-convergence-plan.md](mcp-convergence-plan.md).
|
||||||
control lease для DAP/MCP и cleanup точек по истечении heartbeat
|
control lease для DAP/MCP и cleanup точек по истечении heartbeat
|
||||||
проверены живым прогоном. Привязка RPC к session/build/generation и
|
проверены живым прогоном. Привязка RPC к session/build/generation и
|
||||||
экранные API, одиночный `press_key` и читающий список C-точек уже
|
экранные API, одиночный `press_key` и читающий список C-точек уже
|
||||||
добавлены; logical Z80 disassembly также проверен. Далее очередь
|
добавлены; logical Z80 disassembly и ограниченный `type_string`
|
||||||
строкового ввода, управляемые поля портов и мышь, затем доказанные
|
проверены. Далее natural keyboard, управляемые поля портов и мышь,
|
||||||
|
затем доказанные
|
||||||
watchpoints, запись typed values, disassemble_src и batch-загрузка
|
watchpoints, запись typed values, disassemble_src и batch-загрузка
|
||||||
точек. Raw `mame_mcp.py` с `-plugin mamebridge` не подключать к тому же
|
точек. Raw `mame_mcp.py` с `-plugin mamebridge` не подключать к тому же
|
||||||
MAME в обход общей сессии. Старый `mame_bridge.lua` удалён из MAME.HT
|
MAME в обход общей сессии. Старый `mame_bridge.lua` удалён из MAME.HT
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени
|
|||||||
проверяет сигнатуру entry и запускает session server. Закрытие DAP
|
проверяет сигнатуру entry и запускает session server. Закрытие DAP
|
||||||
завершает только созданные им server/MAME, удаляет временный каталог и
|
завершает только созданные им server/MAME, удаляет временный каталог и
|
||||||
свой Unix socket (с проверкой inode, чтобы не удалить новый сеанс).
|
свой Unix socket (с проверкой inode, чтобы не удалить новый сеанс).
|
||||||
- `sdbg_mcp.py` предоставляет 28 инструментов через официальный MCP SDK
|
- `sdbg_mcp.py` предоставляет 29 инструментов через официальный MCP SDK
|
||||||
2.x поверх того же session server: статус, C-позиция, регистры, logical
|
2.x поверх того же session server: статус, C-позиция, регистры, logical
|
||||||
memory, переменные, события/логи, личные точки и команды исполнения.
|
memory, переменные, события/логи, личные точки и команды исполнения.
|
||||||
MCP-точки имеют owner ID и не могут удалить точки VS Code; при обычном
|
MCP-точки имеют owner ID и не могут удалить точки VS Code; при обычном
|
||||||
@@ -136,7 +136,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени
|
|||||||
шага после клавиши.
|
шага после клавиши.
|
||||||
Повторный запуск на том же фиксированном socket после прежнего stale-файла
|
Повторный запуск на том же фиксированном socket после прежнего stale-файла
|
||||||
прошёл; после Stop путь удалён.
|
прошёл; после Stop путь удалён.
|
||||||
В автономном режиме доступны 30 инструментов: официальный MCP-клиент
|
В автономном режиме доступен 31 инструмент: официальный MCP-клиент
|
||||||
запустил `hello`, ввёл `x` через публичный `press_key` во время `getchar()`
|
запустил `hello`, ввёл `x` через публичный `press_key` во время `getchar()`
|
||||||
и остановился на следующей C-строке. Остальные raw-возможности ввода
|
и остановился на следующей C-строке. Остальные raw-возможности ввода
|
||||||
перечислены в [матрице](mcp-capability-matrix.md).
|
перечислены в [матрице](mcp-capability-matrix.md).
|
||||||
@@ -296,6 +296,7 @@ commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`:
|
|||||||
| Публичный MCP `press_key`, 2026-09-17 | В автономном видимом MAME официальный MCP-клиент поставил C-точку перед `getchar()`, продолжил `hello`, ввёл `x` через `press_key` и остановился на следующей строке 63. В автономном режиме доступны 28 инструментов; строковый ввод и мышь остаются в плане |
|
| Публичный MCP `press_key`, 2026-09-17 | В автономном видимом MAME официальный MCP-клиент поставил C-точку перед `getchar()`, продолжил `hello`, ввёл `x` через `press_key` и остановился на следующей строке 63. В автономном режиме доступны 28 инструментов; строковый ввод и мышь остаются в плане |
|
||||||
| Список C-точек MCP, 2026-09-17 | В автономном видимом MAME MCP увидел собственную точку перед `getchar()` с owner и адресом; `list_breakpoints` также отдаёт guard-условие банка из bridge, если оно есть. Всего 29 автономных инструментов; вручную созданные debugger-точки пока не перечисляются |
|
| Список C-точек MCP, 2026-09-17 | В автономном видимом MAME MCP увидел собственную точку перед `getchar()` с owner и адресом; `list_breakpoints` также отдаёт guard-условие банка из bridge, если оно есть. Всего 29 автономных инструментов; вручную созданные debugger-точки пока не перечисляются |
|
||||||
| Logical Z80 disassembly MCP, 2026-09-17 | Через официальный MCP-клиент в видимом MAME прочитаны инструкции `main` с `0x8224` (`call $852C`); байты памяти до/после совпали. Адрес/длина ограничены, возвращаются bank_pages и generation. Всего 30 автономных инструментов; raw program disassembly ещё не поддержано |
|
| Logical Z80 disassembly MCP, 2026-09-17 | Через официальный MCP-клиент в видимом MAME прочитаны инструкции `main` с `0x8224` (`call $852C`); байты памяти до/после совпали. Адрес/длина ограничены, возвращаются bank_pages и generation. Всего 30 автономных инструментов; raw program disassembly ещё не поддержано |
|
||||||
|
| Строковый ввод MCP, 2026-09-17 | `tests/gets` с C-картой в видимом MAME получил `Ab9` и Enter через один MCP `type_string`; скриншот показал ввод и печать строки, CPU остановился на строке 16 после `gets()`. 31 автономный инструмент; natural keyboard/мышь остаются в плане |
|
||||||
| F10 через `getchar()`, 2026-09-15 | Асинхронный `next` ответил за 21,49 мс; при ручном `x` в окне MAME `hello.c:62` → `:63`, возврат `DE.low=0x78`; обе клавиатуры включены в изолированном cfg |
|
| F10 через `getchar()`, 2026-09-15 | Асинхронный `next` ответил за 21,49 мс; при ручном `x` в окне MAME `hello.c:62` → `:63`, возврат `DE.low=0x78`; обе клавиатуры включены в изолированном cfg |
|
||||||
| VS Code TaskProvider Build, 2026-09-15 | Одиннадцать Node-проверок: выбор Makefile рядом с пакетом/в `build/`, команда make с Python shim, SDCC matcher, успешный и неуспешный код задачи, pyenv вне workspace; реальный make в `tests/hello` прошёл, VS Code показал Build → MAME/DSS → `main` |
|
| VS Code TaskProvider Build, 2026-09-15 | Одиннадцать Node-проверок: выбор Makefile рядом с пакетом/в `build/`, команда make с Python shim, SDCC matcher, успешный и неуспешный код задачи, pyenv вне workspace; реальный make в `tests/hello` прошёл, VS Code показал Build → MAME/DSS → `main` |
|
||||||
| Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует |
|
| Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует |
|
||||||
|
|||||||
@@ -536,13 +536,14 @@ console_log и загрузка набора точек. Возвращать bu
|
|||||||
в арбитраж, а не обходят его.
|
в арбитраж, а не обходят его.
|
||||||
|
|
||||||
Первый stdio MCP-адаптер уже работает через общий session server и отдаёт
|
Первый stdio MCP-адаптер уже работает через общий session server и отдаёт
|
||||||
28 проверенных C-инструментов (30 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой
|
29 проверенных C-инструментов (31 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой
|
||||||
DAP-точки отклоняется. Совместный живой прогон подтвердил чтение состояния,
|
DAP-точки отклоняется. Совместный живой прогон подтвердил чтение состояния,
|
||||||
точки и последующее срабатывание DAP-точки. Добавлен control lease для
|
точки и последующее срабатывание DAP-точки. Добавлен control lease для
|
||||||
команд CPU и ввода; передача MCP→DAP и cleanup точки владельца без heartbeat
|
команд CPU и ввода; передача MCP→DAP и cleanup точки владельца без heartbeat
|
||||||
проверены живым прогоном. RPC привязан к session/build/generation; экран и
|
проверены живым прогоном. RPC привязан к session/build/generation; экран и
|
||||||
VRAM доступны через общий bridge, что подтверждено снимком работающего `hello`;
|
VRAM доступны через общий bridge, что подтверждено снимком работающего `hello`;
|
||||||
одиночный `press_key` проверен на ожидающем `getchar()`;
|
одиночный `press_key` проверен на ожидающем `getchar()`, а `type_string`
|
||||||
|
ввёл `Ab9` и Enter в `gets()` с остановкой после печати строки;
|
||||||
остальные интерфейсы этого раздела ещё не реализованы.
|
остальные интерфейсы этого раздела ещё не реализованы.
|
||||||
Автономный MCP `start_session` поднимает DSS→EXE→`main` без VS Code;
|
Автономный MCP `start_session` поднимает DSS→EXE→`main` без VS Code;
|
||||||
DAP attach к созданному socket и последующий `stop_session` проверены.
|
DAP attach к созданному socket и последующий `stop_session` проверены.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
| `read_screen_pixels` | `read_screen_pixels` | Проверено; до 8192 pen16 пикселей, кадр/время/stale в ответе |
|
| `read_screen_pixels` | `read_screen_pixels` | Проверено; до 8192 pen16 пикселей, кадр/время/stale в ответе |
|
||||||
| `list_ports` | `list_ports` | Проверено; структурированные tag/маска/имя, предел 512 портов и 4096 полей |
|
| `list_ports` | `list_ports` | Проверено; структурированные tag/маска/имя, предел 512 портов и 4096 полей |
|
||||||
| `press_key` | `press_key` | Проверено на `hello` через `getchar()`: один символ/enter/space/tab на 1..60 кадров, только при running CPU и с control lease; удержанные клавиши отпускаются при stop/ошибке. Имена прочих физических клавиш пока не поддержаны |
|
| `press_key` | `press_key` | Проверено на `hello` через `getchar()`: один символ/enter/space/tab на 1..60 кадров, только при running CPU и с control lease; удержанные клавиши отпускаются при stop/ошибке. Имена прочих физических клавиш пока не поддержаны |
|
||||||
| `type_string` | — | Нужна очередь ввода с release при stop/disconnect |
|
| `type_string` | `type_string` | Проверено на `tests/gets`: `Ab9` + Enter напечатаны, CPU остановился на C-строке после `gets()`. До 64 символов из PC-раскладки, предварительная проверка всей строки, пауза между клавишами; набор прекращается при stop. Асинхронная очередь пока не нужна для этого ограниченного вызова |
|
||||||
| `type_text` | — | Нужна отдельная проверка natural keyboard и прямой физической клавиатуры |
|
| `type_text` | — | Нужна отдельная проверка natural keyboard и прямой физической клавиатуры |
|
||||||
| `move_mouse` | — | Нужна owner-очередь относительных осей |
|
| `move_mouse` | — | Нужна owner-очередь относительных осей |
|
||||||
| `click_mouse` | — | Нужна owner-очередь кнопок |
|
| `click_mouse` | — | Нужна owner-очередь кнопок |
|
||||||
@@ -46,6 +46,8 @@
|
|||||||
`run_managed_mcp_probe.py` через официальный MCP-клиент проверяет
|
`run_managed_mcp_probe.py` через официальный MCP-клиент проверяет
|
||||||
`disassemble_logical` в `main`, `list_breakpoints`, `press_key("x")` и
|
`disassemble_logical` в `main`, `list_breakpoints`, `press_key("x")` и
|
||||||
остановку на строке после `getchar()`.
|
остановку на строке после `getchar()`.
|
||||||
|
`run_type_string_probe.py` вводит `Ab9` и Enter в `tests/gets`, проверяет
|
||||||
|
остановку после `gets()` и сохраняет снимок с напечатанной строкой.
|
||||||
`run_raw_mcp_hello_probe.py`
|
`run_raw_mcp_hello_probe.py`
|
||||||
проверяет отдельный raw plugin на том же приложении. Несколько процессов MAME
|
проверяет отдельный raw plugin на том же приложении. Несколько процессов MAME
|
||||||
с MCP одновременно не проверялись.
|
с MCP одновременно не проверялись.
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
`MAME.HT/plugins/mamebridge` вместе с `src/mame_mcp.py` публикует 30
|
`MAME.HT/plugins/mamebridge` вместе с `src/mame_mcp.py` публикует 30
|
||||||
raw-инструментов. Удалённый на этапе А `src/mame_bridge.lua` использовал тот
|
raw-инструментов. Удалённый на этапе А `src/mame_bridge.lua` использовал тот
|
||||||
же Python frontend, но обрабатывал только 16 команд и не отвечал при hard-stop.
|
же Python frontend, но обрабатывал только 16 команд и не отвечал при hard-stop.
|
||||||
`toolchain/sdbg_mcp.py` публикует 28 C-инструментов через общую с DAP сессию
|
`toolchain/sdbg_mcp.py` публикует 29 C-инструментов через общую с DAP сессию
|
||||||
(30 в автономном режиме с командами start/stop).
|
(31 в автономном режиме с командами start/stop).
|
||||||
**Цель — покрыть возможности всех 30 raw-инструментов в C-сессии, а не
|
**Цель — покрыть возможности всех 30 raw-инструментов в C-сессии, а не
|
||||||
механически скопировать имена и небезопасную семантику.** Полный raw MCP
|
механически скопировать имена и небезопасную семантику.** Полный raw MCP
|
||||||
остаётся отдельным режимом для задач без C-пакета и других машин MAME.
|
остаётся отдельным режимом для задач без C-пакета и других машин MAME.
|
||||||
@@ -146,8 +146,14 @@ pass-through и особенности адресов перечислены п
|
|||||||
Одиночный публичный `press_key` теперь требует running CPU и control lease,
|
Одиночный публичный `press_key` теперь требует running CPU и control lease,
|
||||||
удерживает клавишу 1..60 кадров и отпускает её при stop/ошибке. Автономный
|
удерживает клавишу 1..60 кадров и отпускает её при stop/ошибке. Автономный
|
||||||
официальный MCP-клиент ввёл `x` в ожидающий `getchar()` и попал на следующую
|
официальный MCP-клиент ввёл `x` в ожидающий `getchar()` и попал на следующую
|
||||||
C-строку. Очередь строки, другие имена физических клавиш и мышь ещё не
|
C-строку. Асинхронная очередь событий, другие имена физических клавиш и мышь
|
||||||
реализованы; поэтому строка raw `press_key` покрыта частично.
|
ещё не реализованы; поэтому строка raw `press_key` покрыта частично.
|
||||||
|
Ограниченный `type_string` уже выполняет последовательность из 1..64
|
||||||
|
символов PC-раскладки с предварительной проверкой, покадровым удержанием и
|
||||||
|
отпусканием, прерыванием при stop. На `tests/gets` строка `Ab9` и Enter
|
||||||
|
дошли до `gets()`; программа напечатала её и остановилась на следующей
|
||||||
|
C-строке. Это покрывает типовой строковый ввод, но не natural keyboard и
|
||||||
|
не асинхронную очередь произвольных событий мыши/портов.
|
||||||
Читающий `list_breakpoints` теперь показывает логические C-точки всех
|
Читающий `list_breakpoints` теперь показывает логические C-точки всех
|
||||||
владельцев, адреса и полученные от MAME условия банковской страницы;
|
владельцев, адреса и полученные от MAME условия банковской страницы;
|
||||||
удалять чужие точки он не позволяет. Живой MCP-прогон увидел личную точку
|
удалять чужие точки он не позволяет. Живой MCP-прогон увидел личную точку
|
||||||
|
|||||||
+13
-5
@@ -76,7 +76,8 @@ MCP-сервером MAME и удаляет socket. Закрытие самог
|
|||||||
Доступные инструменты: `session_status`, `where`, `read_registers`,
|
Доступные инструменты: `session_status`, `where`, `read_registers`,
|
||||||
`read_memory`, `read_program_memory`, `disassemble_logical`, `list_ports`,
|
`read_memory`, `read_program_memory`, `disassemble_logical`, `list_ports`,
|
||||||
`list_shares`, `read_share`, `read_vram`,
|
`list_shares`, `read_share`, `read_vram`,
|
||||||
`read_screen_pixels`, `screenshot`, `press_key`, `list_variables`, `read_variable`, `recent_events`,
|
`read_screen_pixels`, `screenshot`, `press_key`, `type_string`,
|
||||||
|
`list_variables`, `read_variable`, `recent_events`,
|
||||||
`mame_console_tail`, `set_line_breakpoint`, `set_function_breakpoint`,
|
`mame_console_tail`, `set_line_breakpoint`, `set_function_breakpoint`,
|
||||||
`list_breakpoints`, `clear_breakpoint`, `clear_my_breakpoints`,
|
`list_breakpoints`, `clear_breakpoint`, `clear_my_breakpoints`,
|
||||||
`claim_control`, `release_control`,
|
`claim_control`, `release_control`,
|
||||||
@@ -87,7 +88,7 @@ MCP-сервером MAME и удаляет socket. Закрытие самог
|
|||||||
`recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события
|
`recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события
|
||||||
`output`; журнал ограничен 1024 событиями и сообщает `first`/`last`/`lost`.
|
`output`; журнал ограничен 1024 событиями и сообщает `first`/`last`/`lost`.
|
||||||
|
|
||||||
Число 28 в attach-режиме (30 в автономном с `start_session`/`stop_session`)
|
Число 29 в attach-режиме (31 в автономном с `start_session`/`stop_session`)
|
||||||
не означает полного переноса более нового `-plugin mamebridge`:
|
не означает полного переноса более нового `-plugin mamebridge`:
|
||||||
его `src/mame_mcp.py` публикует 30 raw-инструментов, ориентированных на
|
его `src/mame_mcp.py` публикует 30 raw-инструментов, ориентированных на
|
||||||
машину MAME. Здесь инструменты сгруппированы по операциям C-сессии, а
|
машину MAME. Здесь инструменты сгруппированы по операциям C-сессии, а
|
||||||
@@ -98,7 +99,7 @@ MCP-сервером MAME и удаляет socket. Закрытие самог
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `setmem`, raw `debugger_command`, watchpoints | Проверка прав, банка, диапазона и согласование с DAP-точками; произвольная debugger-команда может нарушить состояние сессии |
|
| `setmem`, raw `debugger_command`, watchpoints | Проверка прав, банка, диапазона и согласование с DAP-точками; произвольная debugger-команда может нарушить состояние сессии |
|
||||||
| VRAM/shares, screen pixels, screenshot | Реализованы через общий `sdbgbridge`; точный tag, пределы 4096 байт/8192 пикселя, PNG только в каталоге сессии |
|
| VRAM/shares, screen pixels, screenshot | Реализованы через общий `sdbgbridge`; точный tag, пределы 4096 байт/8192 пикселя, PNG только в каталоге сессии |
|
||||||
| Клавиатура, мышь, type/press | `list_ports` и одиночный `press_key` уже перенесены; набор строки, произвольные поля портов и мышь требуют отдельного контракта ввода |
|
| Клавиатура, мышь, type/press | `list_ports`, одиночный `press_key` и ограниченный `type_string` перенесены; natural keyboard, произвольные поля портов и мышь требуют отдельного контракта |
|
||||||
| Raw disassembly | Текущее окно logical Z80 уже доступно через `disassemble_logical`; адреса raw program вне отображённого банка требуют отдельного безопасного режима |
|
| Raw disassembly | Текущее окно logical Z80 уже доступно через `disassemble_logical`; адреса raw program вне отображённого банка требуют отдельного безопасного режима |
|
||||||
|
|
||||||
Это следующий этап, а не запрет на функции raw MCP. Нельзя просто загрузить
|
Это следующий этап, а не запрет на функции raw MCP. Нельзя просто загрузить
|
||||||
@@ -146,8 +147,15 @@ CPU, после запуска EXE; применяется общий control le
|
|||||||
Ответ сообщает число прошедших кадров и признак остановки. Живой автономный
|
Ответ сообщает число прошедших кадров и признак остановки. Живой автономный
|
||||||
MCP-прогон ввёл `x` в ожидающий `getchar()` и дошёл до следующей C-строки.
|
MCP-прогон ввёл `x` в ожидающий `getchar()` и дошёл до следующей C-строки.
|
||||||
Прямой физический ввод в MAME проверялся отдельно; `press_key` не заменяет
|
Прямой физический ввод в MAME проверялся отдельно; `press_key` не заменяет
|
||||||
его. Набор строки, произвольные имена клавиш, мышь и порты ввода ещё не
|
его. `type_string` принимает 1..64 символа той же PC-раскладки, включая
|
||||||
публичны в C-MCP.
|
реальный перевод строки `\n` для Enter. Все символы проверяются до первого
|
||||||
|
нажатия. Каждый символ удерживается три кадра и затем отпускается;
|
||||||
|
между символами выдерживаются четыре кадра. При stop дальнейший набор
|
||||||
|
прекращается, ответ сообщает `requested`, `typed`, `complete` и `stopped`.
|
||||||
|
Живой тест `gets()` получил `Ab9` и Enter; снимок показал введённую и
|
||||||
|
напечатанную строку, а CPU остановился на следующей C-строке. Ввод через
|
||||||
|
natural keyboard, произвольные имена физических клавиш, мышь и поля портов
|
||||||
|
пока не публичны в C-MCP.
|
||||||
|
|
||||||
`list_breakpoints` возвращает логические ID, владельца (`mcp:…`, `dap` или
|
`list_breakpoints` возвращает логические ID, владельца (`mcp:…`, `dap` или
|
||||||
`build`), вид C-точки, адреса и фактические условия банковской страницы для
|
`build`), вид C-точки, адреса и фактические условия банковской страницы для
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ launcher задаёт этому процессу `SDL_NO_SIGNAL_HANDLERS=1`, е
|
|||||||
фиксированный `"socket": "/tmp/sprinter-sdbg-hello.sock"` и подключите
|
фиксированный `"socket": "/tmp/sprinter-sdbg-hello.sock"` и подключите
|
||||||
`toolchain/sdbg_mcp.py` после остановки в `main`. MCP использует тот же
|
`toolchain/sdbg_mcp.py` после остановки в `main`. MCP использует тот же
|
||||||
session server; его личные точки не заменяют точки VS Code. Порядок запуска,
|
session server; его личные точки не заменяют точки VS Code. Порядок запуска,
|
||||||
28 доступных инструментов и ограничения совместного управления описаны в
|
29 доступных инструментов и ограничения совместного управления описаны в
|
||||||
[руководстве по MCP](sdbg-mcp.md).
|
[руководстве по MCP](sdbg-mcp.md).
|
||||||
|
|
||||||
Команда палитры `Sprinter: Build Active Project` собирает приложение по
|
Команда палитры `Sprinter: Build Active Project` собирает приложение по
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Живой MCP-прогон: строка с Shift и Enter проходит через DSS gets()."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mcp import Client, StdioServerParameters
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
async def probe() -> None:
|
||||||
|
home = Path(os.environ['MAME_HOME']).resolve()
|
||||||
|
binary = Path(os.environ.get('MAME_BIN', home / 'sprinter')).resolve()
|
||||||
|
package = ROOT / 'tests/gets/.sprinter-cc-gets'
|
||||||
|
with tempfile.TemporaryDirectory(prefix='sprinter-type-mcp-', dir='/tmp') as temp:
|
||||||
|
socket = str(Path(temp) / 'session.sock')
|
||||||
|
parameters = StdioServerParameters(
|
||||||
|
command=sys.executable,
|
||||||
|
args=[str(ROOT / 'toolchain/sdbg_mcp.py'), '--build', str(package),
|
||||||
|
'--socket', socket, '--mame-home', str(home),
|
||||||
|
'--mame-bin', str(binary)])
|
||||||
|
async with Client(parameters) as client:
|
||||||
|
async def call(name: str, arguments: dict | None = None) -> dict:
|
||||||
|
result = await client.call_tool(name, arguments or {})
|
||||||
|
if result.is_error or result.structured_content is None:
|
||||||
|
raise RuntimeError(name + ': ' + repr(result.content))
|
||||||
|
return result.structured_content
|
||||||
|
|
||||||
|
names = {tool.name for tool in (await client.list_tools()).tools}
|
||||||
|
if 'type_string' not in names:
|
||||||
|
raise RuntimeError('MCP не публикует type_string')
|
||||||
|
await call('start_session')
|
||||||
|
try:
|
||||||
|
deadline = time.monotonic() + 90
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
status = await call('session_status')
|
||||||
|
if status['phase'] == 'ready':
|
||||||
|
break
|
||||||
|
if status['phase'] in ('failed', 'stopped'):
|
||||||
|
raise RuntimeError('Launcher: ' + repr(status))
|
||||||
|
await asyncio.sleep(.2)
|
||||||
|
else:
|
||||||
|
raise TimeoutError('gets не дошёл до main')
|
||||||
|
|
||||||
|
source = str(ROOT / 'tests/gets/gets.c')
|
||||||
|
point = await call('set_line_breakpoint', {'file': source, 'line': 16})
|
||||||
|
await call('continue_execution')
|
||||||
|
await asyncio.sleep(.5)
|
||||||
|
if not (await call('session_status'))['running']:
|
||||||
|
raise RuntimeError('gets не ожидает строку')
|
||||||
|
typed = await call('type_string', {'value': 'Ab9\n'})
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
status = await call('session_status')
|
||||||
|
if not status['running']:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(.1)
|
||||||
|
else:
|
||||||
|
shot = await call('screenshot')
|
||||||
|
destination = Path('/private/tmp/sprinter-c-mcp-gets-timeout.png')
|
||||||
|
shutil.copyfile(shot['path'], destination)
|
||||||
|
raise TimeoutError('Enter не завершил gets: ' +
|
||||||
|
repr({'typed': typed, 'status': status,
|
||||||
|
'screenshot': str(destination)}))
|
||||||
|
location = await call('where')
|
||||||
|
if typed['typed'] != 4 or not typed['complete'] or \
|
||||||
|
not any(item['line'] == 16 for item in location['sources']):
|
||||||
|
raise RuntimeError('Неверный результат набора: ' +
|
||||||
|
repr((typed, location)))
|
||||||
|
shot = await call('screenshot')
|
||||||
|
destination = Path('/private/tmp/sprinter-c-mcp-gets.png')
|
||||||
|
shutil.copyfile(shot['path'], destination)
|
||||||
|
await call('clear_breakpoint', {'identifier': point['id']})
|
||||||
|
print(json.dumps({'event': 'mcp_type_string_verified',
|
||||||
|
'tools': len(names), 'typed': typed,
|
||||||
|
'line': 16, 'screenshot': str(destination)},
|
||||||
|
ensure_ascii=False), flush=True)
|
||||||
|
finally:
|
||||||
|
await call('stop_session')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(probe())
|
||||||
@@ -77,6 +77,32 @@ class McpAdapterTests(unittest.TestCase):
|
|||||||
self.assertEqual(keys[1]['tag'], keys[-2]['tag'])
|
self.assertEqual(keys[1]['tag'], keys[-2]['tag'])
|
||||||
self.assertTrue(all(item['owner'] == client.owner for item in keys))
|
self.assertTrue(all(item['owner'] == client.owner for item in keys))
|
||||||
|
|
||||||
|
def test_type_string_checks_layout_before_input_and_releases_each_key(self):
|
||||||
|
calls = []
|
||||||
|
frame = 0
|
||||||
|
|
||||||
|
def rpc(path, method, arguments, timeout=10, **identity):
|
||||||
|
nonlocal frame
|
||||||
|
calls.append((method, arguments))
|
||||||
|
if method == 'status':
|
||||||
|
return {'session_id': 'test-session', 'build_id': 'test-build',
|
||||||
|
'generation': 7, 'running': True}
|
||||||
|
if method == 'snapshot':
|
||||||
|
frame += 1
|
||||||
|
return {'state': 'running', 'frame': frame}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
client = McpSession('/tmp/sprinter-test.sock', rpc=rpc)
|
||||||
|
with self.assertRaisesRegex(SessionError, 'нет раскладки'):
|
||||||
|
client.type_string('a€')
|
||||||
|
self.assertEqual(calls, [])
|
||||||
|
result = client.type_string('ab')
|
||||||
|
self.assertEqual(result, {'requested': 2, 'typed': 2,
|
||||||
|
'complete': True, 'stopped': False})
|
||||||
|
keys = [args for method, args in calls if method == 'input_key']
|
||||||
|
self.assertEqual([item['down'] for item in keys],
|
||||||
|
[True, False, True, False])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -130,6 +130,41 @@ class McpSession:
|
|||||||
self.call('input_key', {'tag': release_tag, 'mask': release_mask,
|
self.call('input_key', {'tag': release_tag, 'mask': release_mask,
|
||||||
'down': False, 'owner': self.owner})
|
'down': False, 'owner': self.owner})
|
||||||
|
|
||||||
|
def type_string(self, value: str):
|
||||||
|
"""Послать строку по физической клавиатуре с паузами между клавишами."""
|
||||||
|
from mame_interactive import resolve
|
||||||
|
|
||||||
|
if not isinstance(value, str) or not value or len(value) > 64:
|
||||||
|
raise SessionError('type_string принимает 1..64 символа')
|
||||||
|
try:
|
||||||
|
for char in value:
|
||||||
|
resolve(char)
|
||||||
|
except ValueError as error:
|
||||||
|
raise SessionError(str(error)) from error
|
||||||
|
if not self.status()['running']:
|
||||||
|
raise SessionError('Ввод строки возможен только при running CPU')
|
||||||
|
typed = 0
|
||||||
|
stopped = False
|
||||||
|
for char in value:
|
||||||
|
result = self.press_key(char)
|
||||||
|
typed += 1
|
||||||
|
stopped = result['stopped']
|
||||||
|
if stopped or typed == len(value):
|
||||||
|
break
|
||||||
|
snapshot = self.call('snapshot')
|
||||||
|
start = snapshot['frame']
|
||||||
|
deadline = time.monotonic() + 4
|
||||||
|
while snapshot['state'] == 'running' and snapshot['frame'] - start < 4:
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise SessionError('Межклавишная пауза не получила кадры за 4 с')
|
||||||
|
time.sleep(.01)
|
||||||
|
snapshot = self.call('snapshot')
|
||||||
|
stopped = snapshot['state'] != 'running'
|
||||||
|
if stopped:
|
||||||
|
break
|
||||||
|
return {'requested': len(value), 'typed': typed,
|
||||||
|
'complete': typed == len(value), 'stopped': stopped}
|
||||||
|
|
||||||
def variables(self):
|
def variables(self):
|
||||||
return self.call('variables')
|
return self.call('variables')
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ def make_server(client: McpSession | ManagedMcpSession,
|
|||||||
"""Нажать один символ/enter/space/tab на 1..60 кадров; отпустить при stop/ошибке."""
|
"""Нажать один символ/enter/space/tab на 1..60 кадров; отпустить при stop/ошибке."""
|
||||||
return client.press_key(key, frames)
|
return client.press_key(key, frames)
|
||||||
|
|
||||||
|
@tool()
|
||||||
|
def type_string(value: str) -> dict[str, Any]:
|
||||||
|
"""Ввести 1..64 символа физической PC-клавиатурой; '\\n' означает Enter."""
|
||||||
|
return client.type_string(value)
|
||||||
|
|
||||||
@tool(annotations=reading)
|
@tool(annotations=reading)
|
||||||
def list_variables() -> dict[str, Any]:
|
def list_variables() -> dict[str, Any]:
|
||||||
"""Перечислить известные global/static переменные и поддержанность типов."""
|
"""Перечислить известные global/static переменные и поддержанность типов."""
|
||||||
|
|||||||
Reference in New Issue
Block a user