diff --git a/docs/TODO.md b/docs/TODO.md index 695a904..99ef7b0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -280,12 +280,13 @@ Quick wins: ## Прочий backlog - [ ] **Довести MCP C-уровня до полного безопасного контракта.** Первый - stdio-адаптер с 26 инструментами использует общую sdbg-сессию и + stdio-адаптер с 27 инструментами использует общую sdbg-сессию и owner ID личных точек. Полный план и матрица всех 30 raw-возможностей — [mcp-convergence-plan.md](mcp-convergence-plan.md). control lease для DAP/MCP и cleanup точек по истечении heartbeat проверены живым прогоном. Привязка RPC к session/build/generation и - экранные API и одиночный `press_key` уже добавлены. Далее очередь + экранные API, одиночный `press_key` и читающий список C-точек уже + добавлены. Далее очередь строкового ввода, управляемые поля портов и мышь, затем доказанные watchpoints, запись typed values, disassemble_src и batch-загрузка точек. Raw `mame_mcp.py` с `-plugin mamebridge` не подключать к тому же diff --git a/docs/mame-source-debug-status.md b/docs/mame-source-debug-status.md index b87e586..3d00bec 100644 --- a/docs/mame-source-debug-status.md +++ b/docs/mame-source-debug-status.md @@ -125,7 +125,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени проверяет сигнатуру entry и запускает session server. Закрытие DAP завершает только созданные им server/MAME, удаляет временный каталог и свой Unix socket (с проверкой inode, чтобы не удалить новый сеанс). -- `sdbg_mcp.py` предоставляет 26 инструментов через официальный MCP SDK +- `sdbg_mcp.py` предоставляет 27 инструментов через официальный MCP SDK 2.x поверх того же session server: статус, C-позиция, регистры, logical memory, переменные, события/логи, личные точки и команды исполнения. MCP-точки имеют owner ID и не могут удалить точки VS Code; при обычном @@ -136,7 +136,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени шага после клавиши. Повторный запуск на том же фиксированном socket после прежнего stale-файла прошёл; после Stop путь удалён. - В автономном режиме доступны 28 инструментов: официальный MCP-клиент + В автономном режиме доступны 29 инструментов: официальный MCP-клиент запустил `hello`, ввёл `x` через публичный `press_key` во время `getchar()` и остановился на следующей C-строке. Остальные raw-возможности ввода перечислены в [матрице](mcp-capability-matrix.md). @@ -294,6 +294,7 @@ commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`: | RPC identity и экран C-MCP, 2026-09-17 | Запросы после handshake привязаны к session/build; мутации сверяют generation. 25 C-инструментов: в живом DAP+MCP-прогоне проверены raw program/logical memory, VRAM/share, порты, пиксели и PNG; при running `hello` MCP снял кадр с `Press any key to exit...`, затем `x` завершил шаг через `getchar()` | | Автономный MCP, 2026-09-17 | `start_session` ответил за 5–7 мс, затем официальный MCP-клиент дождался DSS→`hello.exe`→`main`; DAP attach увидел тот же PC, disconnect не завершил MAME. Повторный start отклонён, stop удалил socket и процесс MAME. Codex/Claude UI ещё не проверены | | Публичный 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-точки пока не перечисляются | | 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` | | Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует | diff --git a/docs/mame-source-debug.md b/docs/mame-source-debug.md index 0eb3f78..c73ab16 100644 --- a/docs/mame-source-debug.md +++ b/docs/mame-source-debug.md @@ -536,7 +536,7 @@ console_log и загрузка набора точек. Возвращать bu в арбитраж, а не обходят его. Первый stdio MCP-адаптер уже работает через общий session server и отдаёт -26 проверенных C-инструментов (28 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой +27 проверенных C-инструментов (29 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой DAP-точки отклоняется. Совместный живой прогон подтвердил чтение состояния, точки и последующее срабатывание DAP-точки. Добавлен control lease для команд CPU и ввода; передача MCP→DAP и cleanup точки владельца без heartbeat diff --git a/docs/mcp-capability-matrix.md b/docs/mcp-capability-matrix.md index 885be63..cec325a 100644 --- a/docs/mcp-capability-matrix.md +++ b/docs/mcp-capability-matrix.md @@ -16,7 +16,7 @@ | `write_memory` | — | Нужны lease, проверка адреса/банка и инвалидация C-карты | | `set_breakpoint` | `set_line_breakpoint`, `set_function_breakpoint` | Только проверенные C-адреса; произвольный raw-адрес и условие ещё не покрыты | | `clear_breakpoint` | `clear_breakpoint`, `clear_my_breakpoints` | Только логические ID текущего владельца, не MAME ID | -| `list_breakpoints` | — | Нужен структурированный список с owner и bank guard | +| `list_breakpoints` | `list_breakpoints` | Проверено на `hello`: логические C-точки всех владельцев, owner, адреса/банк и фактическое guard-условие только для чтения. Точки из родного окна MAME сюда не входят | | `set_watchpoint` | — | Нужны owner, space/access, bank guard и проверка PC при попадании | | `clear_watchpoint` | — | Зависит от owner-модели watchpoint | | `step` | `step_instruction` | Один машинный шаг проверен; счётчик `count` ещё не поддержан | @@ -44,7 +44,7 @@ с `--waitkey --emulated-key --screen-mcp-python` проверяет экран работающего `hello` во время `getchar()` и последующий ввод. Автономный `run_managed_mcp_probe.py` через официальный MCP-клиент проверяет -`press_key("x")` и остановку на строке после `getchar()`. +`list_breakpoints`, `press_key("x")` и остановку на строке после `getchar()`. `run_raw_mcp_hello_probe.py` проверяет отдельный raw plugin на том же приложении. Несколько процессов MAME с MCP одновременно не проверялись. diff --git a/docs/mcp-convergence-plan.md b/docs/mcp-convergence-plan.md index ed39343..1ad29b1 100644 --- a/docs/mcp-convergence-plan.md +++ b/docs/mcp-convergence-plan.md @@ -11,8 +11,8 @@ `MAME.HT/plugins/mamebridge` вместе с `src/mame_mcp.py` публикует 30 raw-инструментов. Удалённый на этапе А `src/mame_bridge.lua` использовал тот же Python frontend, но обрабатывал только 16 команд и не отвечал при hard-stop. -`toolchain/sdbg_mcp.py` публикует 26 C-инструментов через общую с DAP сессию -(28 в автономном режиме с командами start/stop). +`toolchain/sdbg_mcp.py` публикует 27 C-инструментов через общую с DAP сессию +(29 в автономном режиме с командами start/stop). **Цель — покрыть возможности всех 30 raw-инструментов в C-сессии, а не механически скопировать имена и небезопасную семантику.** Полный raw MCP остаётся отдельным режимом для задач без C-пакета и других машин MAME. @@ -148,6 +148,10 @@ pass-through и особенности адресов перечислены п официальный MCP-клиент ввёл `x` в ожидающий `getchar()` и попал на следующую C-строку. Очередь строки, другие имена физических клавиш и мышь ещё не реализованы; поэтому строка raw `press_key` покрыта частично. +Читающий `list_breakpoints` теперь показывает логические C-точки всех +владельцев, адреса и полученные от MAME условия банковской страницы; +удалять чужие точки он не позволяет. Живой MCP-прогон увидел личную точку +перед `getchar()`. Точки, вручную созданные в родном debugger, пока вне списка. ## Г. Сделать запуск пригодным для Codex и Claude без VS Code diff --git a/docs/sdbg-mcp.md b/docs/sdbg-mcp.md index 6340aad..e4fc8d1 100644 --- a/docs/sdbg-mcp.md +++ b/docs/sdbg-mcp.md @@ -77,7 +77,8 @@ MCP-сервером MAME и удаляет socket. Закрытие самог `read_memory`, `read_program_memory`, `list_ports`, `list_shares`, `read_share`, `read_vram`, `read_screen_pixels`, `screenshot`, `press_key`, `list_variables`, `read_variable`, `recent_events`, `mame_console_tail`, `set_line_breakpoint`, `set_function_breakpoint`, -`clear_breakpoint`, `clear_my_breakpoints`, `claim_control`, `release_control`, +`list_breakpoints`, `clear_breakpoint`, `clear_my_breakpoints`, +`claim_control`, `release_control`, `continue_execution`, `pause_execution`, `step_instruction`, `step_source`. Ответы SDK 2.x содержат `structuredContent`. `step_source` принимает `into`, `over` или `out` и @@ -85,7 +86,7 @@ MCP-сервером MAME и удаляет socket. Закрытие самог `recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события `output`; журнал ограничен 1024 событиями и сообщает `first`/`last`/`lost`. -Число 26 в attach-режиме (28 в автономном с `start_session`/`stop_session`) +Число 27 в attach-режиме (29 в автономном с `start_session`/`stop_session`) не означает полного переноса более нового `-plugin mamebridge`: его `src/mame_mcp.py` публикует 30 raw-инструментов, ориентированных на машину MAME. Здесь инструменты сгруппированы по операциям C-сессии, а @@ -139,6 +140,13 @@ MCP-прогон ввёл `x` в ожидающий `getchar()` и дошёл д его. Набор строки, произвольные имена клавиш, мышь и порты ввода ещё не публичны в C-MCP. +`list_breakpoints` возвращает логические ID, владельца (`mcp:…`, `dap` или +`build`), вид C-точки, адреса и фактические условия банковской страницы для +всех точек общей сессии. Это чтение доступно и при running CPU. Точки, +поставленные вручную в родном debugger MAME, в этот список не входят. +Инструмент не даёт удалить чужую точку: `clear_breakpoint` по-прежнему +проверяет owner. + После первого `session_status` клиент отправляет session ID и build ID в каждом RPC; команды, меняющие CPU/точки, дополнительно сверяют generation. При замене сеанса или устаревшей generation команда отклоняется без diff --git a/docs/vscode-sprinter-debug.md b/docs/vscode-sprinter-debug.md index 782560b..f122786 100644 --- a/docs/vscode-sprinter-debug.md +++ b/docs/vscode-sprinter-debug.md @@ -175,7 +175,7 @@ launcher задаёт этому процессу `SDL_NO_SIGNAL_HANDLERS=1`, е фиксированный `"socket": "/tmp/sprinter-sdbg-hello.sock"` и подключите `toolchain/sdbg_mcp.py` после остановки в `main`. MCP использует тот же session server; его личные точки не заменяют точки VS Code. Порядок запуска, -26 доступных инструментов и ограничения совместного управления описаны в +27 доступных инструментов и ограничения совместного управления описаны в [руководстве по MCP](sdbg-mcp.md). Команда палитры `Sprinter: Build Active Project` собирает приложение по diff --git a/tests/sdbg/run_managed_mcp_probe.py b/tests/sdbg/run_managed_mcp_probe.py index 996aa10..bc8bc75 100644 --- a/tests/sdbg/run_managed_mcp_probe.py +++ b/tests/sdbg/run_managed_mcp_probe.py @@ -38,7 +38,7 @@ async def probe() -> None: names = {tool.name for tool in (await client.list_tools()).tools} if not {'start_session', 'stop_session', 'where', 'read_variable', - 'press_key'} <= names: + 'press_key', 'list_breakpoints'} <= names: raise RuntimeError('Нет инструментов автономного запуска') initial = await call('session_status') if initial['phase'] != 'idle': @@ -99,6 +99,12 @@ async def probe() -> None: raise RuntimeError('DAP disconnect завершил MCP-owned MAME') source = str(ROOT / 'tests/hello/hello.c') before = await call('set_line_breakpoint', {'file': source, 'line': 62}) + points = await call('list_breakpoints') + active = [item for item in points['breakpoints'] + if item['id'] == before['id']] + if len(active) != 1 or not active[0]['owner'].startswith('mcp:') or \ + not any(loc.get('line') == 62 for loc in active[0]['locations']): + raise RuntimeError('Личная точка не отражена в списке: ' + repr(points)) await call('continue_execution') deadline = time.monotonic() + 10 while time.monotonic() < deadline: @@ -115,6 +121,13 @@ async def probe() -> None: after = await call('set_line_breakpoint', {'file': source, 'line': 63}) await call('continue_execution') await asyncio.sleep(.4) + running = await call('session_status') + if not running['running']: + raise RuntimeError('CPU не ожидает getchar перед вводом') + while_running = await call('list_breakpoints') + if not any(item['id'] == after['id'] + for item in while_running['breakpoints']): + raise RuntimeError('Не прочитана точка при running CPU') key = await call('press_key', {'key': 'x', 'frames': 3}) deadline = time.monotonic() + 10 while time.monotonic() < deadline: diff --git a/tests/sdbg/test_server.py b/tests/sdbg/test_server.py index 0c4f85d..95faea9 100644 --- a/tests/sdbg/test_server.py +++ b/tests/sdbg/test_server.py @@ -225,6 +225,12 @@ class ServerTests(unittest.TestCase): mine = controller.call('break_line', {'file': '/src/main.c', 'line': 3, 'owner': 'mcp:a'}) other = controller.call('break_function', {'name': 'main', 'owner': 'mcp:b'}) + controller.breakpoint_info[mine['id']]['guard_conditions'] = ['PG3 == 0x12'] + listed = controller.call('list_breakpoints', {})['breakpoints'] + self.assertEqual([(item['id'], item['owner']) for item in listed], + [(mine['id'], 'mcp:a'), (other['id'], 'mcp:b')]) + self.assertEqual(listed[0]['locations'][0]['link_address'], 0x8100) + self.assertEqual(listed[0]['locations'][0]['bank_guard'], 'PG3 == 0x12') self.assertTrue(controller._has_stop_breakpoint(session.where())) with self.assertRaisesRegex(SessionError, 'не принадлежит'): controller.call('clear_breakpoint', {'id': other['id'], 'owner': 'mcp:a'}) diff --git a/toolchain/sdbg/mcp_adapter.py b/toolchain/sdbg/mcp_adapter.py index ad3a68d..22af8d4 100644 --- a/toolchain/sdbg/mcp_adapter.py +++ b/toolchain/sdbg/mcp_adapter.py @@ -77,6 +77,9 @@ class McpSession: def screenshot(self): return self.call('screenshot') + def list_breakpoints(self): + return self.call('list_breakpoints') + def press_key(self, key: str, frames: int = 3): """Один физический PC-key с отпусканием даже при stop/ошибке.""" from mame_interactive import SHIFT_KEY, resolve diff --git a/toolchain/sdbg/server.py b/toolchain/sdbg/server.py index a7cc67f..3693c79 100644 --- a/toolchain/sdbg/server.py +++ b/toolchain/sdbg/server.py @@ -61,6 +61,7 @@ class SessionController: 'kind': 'log', 'message': macro['message'], 'condition': macro['condition'], 'module': macro['module'], 'tag': macro['tag'], 'locations': item['locations'], 'hits': 0, + 'owner': 'build', 'guard_conditions': item['conditions'], } if created: self.session.activate_breakpoints() @@ -233,6 +234,21 @@ class SessionController: 'event_sequence': self.sequence, 'session_id': self.session_id, 'control_owner': self.control_owner if self.control_deadline > time.monotonic() else None} + if method == 'list_breakpoints': + breakpoints = [] + for identifier, info in sorted(self.breakpoint_info.items()): + guards = info.get('guard_conditions', []) + locations = [] + for index, location in enumerate(info['locations']): + locations.append({**location, + 'bank_guard': guards[index] if index < len(guards) + else ''}) + breakpoints.append({'id': identifier, + 'owner': info.get('owner', 'dap'), + 'kind': info['kind'], 'hits': info['hits'], + 'locations': locations, + 'tag': info.get('tag')}) + return {'breakpoints': breakpoints} if method == 'snapshot': return self.session.bridge.request('snapshot') if method == 'mame_console_tail': @@ -322,7 +338,8 @@ class SessionController: item = self.session.break_line(arguments['file'], int(arguments['line'])) self.breakpoint_info[item['id']] = { 'kind': 'stop', 'message': None, 'locations': item['locations'], - 'hits': 0, 'owner': owner} + 'hits': 0, 'owner': owner, + 'guard_conditions': item['conditions']} return item if method == 'break_function': owner = self._owner(arguments) @@ -330,7 +347,8 @@ class SessionController: item = self.session.break_function(arguments['name']) self.breakpoint_info[item['id']] = { 'kind': 'stop', 'message': None, 'locations': item['locations'], - 'hits': 0, 'owner': owner} + 'hits': 0, 'owner': owner, + 'guard_conditions': item['conditions']} return item if method == 'clear_breakpoint': identifier = int(arguments['id']) @@ -480,6 +498,7 @@ class SessionController: self.breakpoint_info[item['id']] = { 'kind': kind, 'message': specification['logMessage'], 'locations': item['locations'], 'hits': 0, + 'owner': 'dap', 'guard_conditions': item['conditions'], } results.append({'line': line, 'verified': True, 'logMessage': specification['logMessage'], **item}) @@ -513,7 +532,9 @@ class SessionController: item = self.session.break_function(name, enabled=False) created.append(item['id']) self.breakpoint_info[item['id']] = { - 'kind': 'stop', 'message': None, 'locations': item['locations'], 'hits': 0} + 'kind': 'stop', 'message': None, 'locations': item['locations'], + 'hits': 0, 'owner': 'dap', + 'guard_conditions': item['conditions']} results.append({'name': name, 'verified': True, **item}) except BaseException: for identifier in created: diff --git a/toolchain/sdbg_mcp.py b/toolchain/sdbg_mcp.py index 9b620f7..d6f3ddd 100644 --- a/toolchain/sdbg_mcp.py +++ b/toolchain/sdbg_mcp.py @@ -143,6 +143,11 @@ def make_server(client: McpSession | ManagedMcpSession, """Поставить личную точку по имени C-функции.""" return client.break_function(name) + @tool(annotations=reading) + def list_breakpoints() -> dict[str, Any]: + """Читать C-точки всех владельцев с логическими ID и условиями банка.""" + return client.list_breakpoints() + @tool() def clear_breakpoint(identifier: int) -> dict[str, Any]: """Удалить свою логическую точку по ID; чужие точки отклоняются."""