diff --git a/docs/TODO.md b/docs/TODO.md index 99ef7b0..9cd5d53 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -280,13 +280,13 @@ Quick wins: ## Прочий backlog - [ ] **Довести MCP C-уровня до полного безопасного контракта.** Первый - stdio-адаптер с 27 инструментами использует общую sdbg-сессию и + stdio-адаптер с 28 инструментами использует общую 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` и читающий список C-точек уже - добавлены. Далее очередь + добавлены; logical Z80 disassembly также проверен. Далее очередь строкового ввода, управляемые поля портов и мышь, затем доказанные 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 3d00bec..41b0aa0 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` предоставляет 27 инструментов через официальный MCP SDK +- `sdbg_mcp.py` предоставляет 28 инструментов через официальный MCP SDK 2.x поверх того же session server: статус, C-позиция, регистры, logical memory, переменные, события/логи, личные точки и команды исполнения. MCP-точки имеют owner ID и не могут удалить точки VS Code; при обычном @@ -136,7 +136,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени шага после клавиши. Повторный запуск на том же фиксированном socket после прежнего stale-файла прошёл; после Stop путь удалён. - В автономном режиме доступны 29 инструментов: официальный MCP-клиент + В автономном режиме доступны 30 инструментов: официальный MCP-клиент запустил `hello`, ввёл `x` через публичный `press_key` во время `getchar()` и остановился на следующей C-строке. Остальные raw-возможности ввода перечислены в [матрице](mcp-capability-matrix.md). @@ -295,6 +295,7 @@ commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`: | Автономный 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-точки пока не перечисляются | +| Logical Z80 disassembly MCP, 2026-09-17 | Через официальный MCP-клиент в видимом MAME прочитаны инструкции `main` с `0x8224` (`call $852C`); байты памяти до/после совпали. Адрес/длина ограничены, возвращаются bank_pages и generation. Всего 30 автономных инструментов; raw program disassembly ещё не поддержано | | 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 c73ab16..8e14f32 100644 --- a/docs/mame-source-debug.md +++ b/docs/mame-source-debug.md @@ -536,7 +536,7 @@ console_log и загрузка набора точек. Возвращать bu в арбитраж, а не обходят его. Первый stdio MCP-адаптер уже работает через общий session server и отдаёт -27 проверенных C-инструментов (29 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой +28 проверенных C-инструментов (30 при автономном 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 cec325a..267e628 100644 --- a/docs/mcp-capability-matrix.md +++ b/docs/mcp-capability-matrix.md @@ -25,7 +25,7 @@ | `resume` | `continue_execution` | Проверено с control lease и DAP-событиями | | `pause` | `pause_execution` | Проверено с control lease | | `status` | `session_status`, `where` | Проверено; C-статус содержит session/build/generation | -| `disassemble` | — | Нужен явный logical/raw адрес и банк; строковый debugger pass-through не используется | +| `disassemble` | `disassemble_logical` | Проверено на `hello`: 1..256 байт текущей logical Z80 памяти при stop, ответ содержит bank_pages/generation. Raw program 0..0x3ffff вне текущего окна ещё не дизассемблируется | | `screenshot` | `screenshot` | Проверено на выполняющемся `hello`; только каталог сессии, до 8 МиБ, при stop кадр помечается stale | | `read_screen_pixels` | `read_screen_pixels` | Проверено; до 8192 pen16 пикселей, кадр/время/stale в ответе | | `list_ports` | `list_ports` | Проверено; структурированные tag/маска/имя, предел 512 портов и 4096 полей | @@ -44,7 +44,8 @@ с `--waitkey --emulated-key --screen-mcp-python` проверяет экран работающего `hello` во время `getchar()` и последующий ввод. Автономный `run_managed_mcp_probe.py` через официальный MCP-клиент проверяет -`list_breakpoints`, `press_key("x")` и остановку на строке после `getchar()`. +`disassemble_logical` в `main`, `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 1ad29b1..a49198b 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` публикует 27 C-инструментов через общую с DAP сессию -(29 в автономном режиме с командами start/stop). +`toolchain/sdbg_mcp.py` публикует 28 C-инструментов через общую с DAP сессию +(30 в автономном режиме с командами start/stop). **Цель — покрыть возможности всех 30 raw-инструментов в C-сессии, а не механически скопировать имена и небезопасную семантику.** Полный raw MCP остаётся отдельным режимом для задач без C-пакета и других машин MAME. @@ -152,6 +152,12 @@ C-строку. Очередь строки, другие имена физич владельцев, адреса и полученные от MAME условия банковской страницы; удалять чужие точки он не позволяет. Живой MCP-прогон увидел личную точку перед `getchar()`. Точки, вручную созданные в родном debugger, пока вне списка. +Дизассемблирование текущего logical Z80 окна добавлено как +`disassemble_logical`: фиксированная команда MAME с числовыми аргументами, +пределом 256 байт и изолированным временным файлом; ответ включает +`bank_pages` и generation. Живой MCP-прогон получил инструкции `main` с +адреса `0x8224`; байты до и после операции совпали. Raw program вне +текущего отображения остаётся открытым. ## Г. Сделать запуск пригодным для Codex и Claude без VS Code diff --git a/docs/sdbg-mcp.md b/docs/sdbg-mcp.md index e4fc8d1..0bc7e2b 100644 --- a/docs/sdbg-mcp.md +++ b/docs/sdbg-mcp.md @@ -74,7 +74,8 @@ MCP-сервером MAME и удаляет socket. Закрытие самог непосредственно из UI Codex и Claude ещё не проверено. Доступные инструменты: `session_status`, `where`, `read_registers`, -`read_memory`, `read_program_memory`, `list_ports`, `list_shares`, `read_share`, `read_vram`, +`read_memory`, `read_program_memory`, `disassemble_logical`, `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`, `list_breakpoints`, `clear_breakpoint`, `clear_my_breakpoints`, @@ -86,7 +87,7 @@ MCP-сервером MAME и удаляет socket. Закрытие самог `recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события `output`; журнал ограничен 1024 событиями и сообщает `first`/`last`/`lost`. -Число 27 в attach-режиме (29 в автономном с `start_session`/`stop_session`) +Число 28 в attach-режиме (30 в автономном с `start_session`/`stop_session`) не означает полного переноса более нового `-plugin mamebridge`: его `src/mame_mcp.py` публикует 30 raw-инструментов, ориентированных на машину MAME. Здесь инструменты сгруппированы по операциям C-сессии, а @@ -98,7 +99,7 @@ MCP-сервером MAME и удаляет socket. Закрытие самог | `setmem`, raw `debugger_command`, watchpoints | Проверка прав, банка, диапазона и согласование с DAP-точками; произвольная debugger-команда может нарушить состояние сессии | | VRAM/shares, screen pixels, screenshot | Реализованы через общий `sdbgbridge`; точный tag, пределы 4096 байт/8192 пикселя, PNG только в каталоге сессии | | Клавиатура, мышь, type/press | `list_ports` и одиночный `press_key` уже перенесены; набор строки, произвольные поля портов и мышь требуют отдельного контракта ввода | -| Raw disassembly | Связать адрес/банк с проверенной C-картой; не выдавать физический адрес за logical | +| Raw disassembly | Текущее окно logical Z80 уже доступно через `disassemble_logical`; адреса raw program вне отображённого банка требуют отдельного безопасного режима | Это следующий этап, а не запрет на функции raw MCP. Нельзя просто загрузить `mamebridge` рядом с `sdbgbridge`: два независимых обработчика начнут менять @@ -120,6 +121,14 @@ generation и страницы банков; диапазон не может п и битовые маски; результат ограничен 512 портами/4096 полями и помечает `truncated`, если достигнут предел. Оба инструмента проверены в живом MAME. +`disassemble_logical` принимает числовой адрес (десятичный или `0xHEX`) и +длину 1..256 байт в текущем logical Z80 пространстве, только при stop. +Сервер сверяет загруженный C-код, вызывает фиксированную читающую команду +MAME `dasm` во временном каталоге сессии и возвращает текст, generation и +текущие `bank_pages`. Это не произвольная debugger-команда. Дизассемблирование +raw program вне текущих банков пока не поддержано. На живом `hello` ответ +начинался с адреса `8224`, соответствующего `main`. + `list_shares` возвращает точные tag/размеры. `read_share` требует полный tag, `read_vram` выбирает единственный VRAM share; обе операции читают до 4096 байт без Z80 bank mapping. `read_screen_pixels` возвращает pen16-значения diff --git a/docs/vscode-sprinter-debug.md b/docs/vscode-sprinter-debug.md index f122786..c0a3a67 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. Порядок запуска, -27 доступных инструментов и ограничения совместного управления описаны в +28 доступных инструментов и ограничения совместного управления описаны в [руководстве по 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 bc8bc75..a40e366 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', 'list_breakpoints'} <= names: + 'press_key', 'list_breakpoints', 'disassemble_logical'} <= names: raise RuntimeError('Нет инструментов автономного запуска') initial = await call('session_status') if initial['phase'] != 'idle': @@ -67,6 +67,18 @@ async def probe() -> None: location = await call('where') if location.get('function', {}).get('name') != 'main': raise RuntimeError('MCP не остановился в main: ' + repr(location)) + bytes_before = await call('read_memory', + {'address': hex(location['pc']), 'length': 32}) + disassembly = await call('disassemble_logical', + {'address': hex(location['pc']), 'length': 32}) + bytes_after = await call('read_memory', + {'address': hex(location['pc']), 'length': 32}) + if disassembly['space'] != 'logical_z80' or \ + disassembly['address'] != location['pc'] or \ + not disassembly['text'].upper().startswith( + f"{location['pc']:04X}:") or \ + bytes_before['hex'] != bytes_after['hex']: + raise RuntimeError('Нет дизассемблирования main: ' + repr(disassembly)) await call('read_variable', {'name': 'errno'}) shot = await call('screenshot') if Path(shot['path']).read_bytes()[:8] != b'\x89PNG\r\n\x1a\n': @@ -145,6 +157,7 @@ async def probe() -> None: 'tools': len(names), 'start_ms': start_ms, 'session_id': status['session_id'], 'socket': socket, 'pc': location['pc'], + 'disassembly_head': disassembly['text'][:90], 'key': key['key'], 'after_getchar_line': 63}, ensure_ascii=False), flush=True) finally: diff --git a/tests/sdbg/test_server.py b/tests/sdbg/test_server.py index 95faea9..af91ec4 100644 --- a/tests/sdbg/test_server.py +++ b/tests/sdbg/test_server.py @@ -29,6 +29,10 @@ class DummyBridge: if command == 'snapshot': return {'state': self.state} if command == 'console_print': return {'printed': True} if command == 'memory': return {'hex': '00' * arguments['length']} + if command == 'disassemble_logical': + return {'space': 'logical_z80', 'address': arguments['address'], + 'length': arguments['length'], 'text': '8100: NOP', + 'generation': self.generation} if command == 'key': return {'accepted': True} raise AssertionError(command) @@ -243,6 +247,22 @@ class ServerTests(unittest.TestCase): finally: controller.close() + def test_disassemble_logical_is_bounded_and_labels_bank_mapping(self): + controller = SessionController(DummySession()) + try: + result = controller.call('disassemble_logical', + {'address': 0x8100, 'length': 32}) + self.assertEqual(result['space'], 'logical_z80') + self.assertEqual(result['address'], 0x8100) + self.assertEqual(result['bank_pages'], {}) + with self.assertRaisesRegex(SessionError, 'Дизассемблирование'): + controller.call('disassemble_logical', + {'address': 0xffff, 'length': 2}) + self.assertEqual(len([call for call in controller.session.bridge.calls + if call[0] == 'disassemble_logical']), 1) + finally: + controller.close() + def test_control_lease_blocks_other_client_and_can_be_released_or_expire(self): controller = SessionController(DummySession()) try: diff --git a/toolchain/mcp/sdbgbridge/init.lua b/toolchain/mcp/sdbgbridge/init.lua index 1648c52..d33a575 100644 --- a/toolchain/mcp/sdbgbridge/init.lua +++ b/toolchain/mcp/sdbgbridge/init.lua @@ -100,7 +100,7 @@ function exports.startplugin() instruction_step=true,step_over=true,step_out=true, bank_guard=true,deferred_breakpoints=true,console_print=true, shares=true,screen_pixels=true,screen_snapshot=snapdir~=nil, - program_memory=true,list_ports=true},state=state} + program_memory=true,list_ports=true,disassemble_logical=true},state=state} elseif command=="snapshot" then local result={state=state,time=now(),paused=machine().paused} local screen=first_screen() @@ -200,6 +200,23 @@ function exports.startplugin() end return {space="program",address=address,length=length, hex=table.concat(bytes),generation=generation} + elseif command=="disassemble_logical" then + stopped(request) + local address=number(args.address,0xffff) + local length=number(args.length,256) + assert(length>=1 and address+length<=0x10000, + "Дизассемблирование за пределами logical Z80") + local path=directory.."/_disasm.txt" + os.remove(path) -- Не возвращать файл от прежней команды при ошибке MAME. + machine().debugger:command(string.format("dasm %s,0x%X,%d",path,address,length)) + local size=lfs.attributes(path,"size") + assert(size and size>0 and size<=65536,"MAME не создал ограниченный disasm-файл") + local file=assert(io.open(path,"rb")) + local output=file:read("*a") + file:close() + os.remove(path) + return {space="logical_z80",address=address,length=length, + text=output,generation=generation} elseif command=="list_ports" then local result={ports={},truncated=false} local fields_seen=0 diff --git a/toolchain/sdbg/mcp_adapter.py b/toolchain/sdbg/mcp_adapter.py index 22af8d4..fcfb5dc 100644 --- a/toolchain/sdbg/mcp_adapter.py +++ b/toolchain/sdbg/mcp_adapter.py @@ -62,6 +62,10 @@ class McpSession: return self.call('read_program_memory', {'address': self._address(address), 'length': length}) + def disassemble_logical(self, address: str, length: int = 32): + return self.call('disassemble_logical', {'address': self._address(address), + 'length': length}) + def read_share(self, tag: str, address: str, length: int = 16): return self.call('read_share', {'tag': tag, 'address': self._address(address), 'length': length}) diff --git a/toolchain/sdbg/server.py b/toolchain/sdbg/server.py index 3693c79..96c2ff1 100644 --- a/toolchain/sdbg/server.py +++ b/toolchain/sdbg/server.py @@ -224,7 +224,8 @@ class SessionController: self._emit('control', {'owner': None}) return {'released': owner} if self.running and method in ('where', 'registers', 'read_memory', - 'read_program_memory', 'read_variable', + 'read_program_memory', 'disassemble_logical', + 'read_variable', 'break_line', 'break_function'): raise SessionError('Операция требует остановленного CPU; сначала Pause') if method == 'status': @@ -328,6 +329,16 @@ class SessionController: result = self.session.bridge.request('program_memory', address=address, length=length) return {**result, 'bank_pages': mapping.bank_pages} + if method == 'disassemble_logical': + address = int(arguments['address']) + length = int(arguments['length']) + if address < 0 or address > 0xffff or length < 1 or length > 256 or \ + address + length > 0x10000: + raise SessionError('Дизассемблирование: адрес 0..0xffff, длина 1..256') + mapping = self.session.refresh() + result = self.session.bridge.request('disassemble_logical', + address=address, length=length) + return {**result, 'bank_pages': mapping.bank_pages} if method == 'variables': return self.session.model.variables if method == 'read_variable': diff --git a/toolchain/sdbg_mcp.py b/toolchain/sdbg_mcp.py index d6f3ddd..2790df3 100644 --- a/toolchain/sdbg_mcp.py +++ b/toolchain/sdbg_mcp.py @@ -77,6 +77,11 @@ def make_server(client: McpSession | ManagedMcpSession, """Читать 1..4096 байт raw program space 0..0x3ffff; Z80 окна находятся в 0x10000+.""" return client.read_program_memory(address, length) + @tool(annotations=reading) + def disassemble_logical(address: str, length: int = 32) -> dict[str, Any]: + """Дизассемблировать 1..256 байт текущей logical Z80 памяти при stop; ответ содержит bank_pages.""" + return client.disassemble_logical(address, length) + @tool(annotations=reading) def list_ports() -> dict[str, Any]: """Перечислить порты ввода MAME и поля с масками; результат ограничен по размеру."""