Support bounded machine instruction step counts in MCP
This commit is contained in:
+2
-1
@@ -287,7 +287,8 @@ Quick wins:
|
|||||||
проверены живым прогоном. Привязка RPC к session/build/generation и
|
проверены живым прогоном. Привязка RPC к session/build/generation и
|
||||||
экранные API, одиночный `press_key` и читающий список C-точек уже
|
экранные API, одиночный `press_key` и читающий список C-точек уже
|
||||||
добавлены; logical Z80 disassembly и ограниченный `type_string`
|
добавлены; logical Z80 disassembly и ограниченный `type_string`
|
||||||
проверены. Далее natural keyboard, управляемые поля портов и мышь,
|
проверены, машинный `step_instruction(count=1..64)` проверен на `hello`.
|
||||||
|
Далее 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` не подключать к тому же
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`:
|
|||||||
| Список 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/мышь остаются в плане |
|
| Строковый ввод MCP, 2026-09-17 | `tests/gets` с C-картой в видимом MAME получил `Ab9` и Enter через один MCP `type_string`; скриншот показал ввод и печать строки, CPU остановился на строке 16 после `gets()`. 31 автономный инструмент; natural keyboard/мышь остаются в плане |
|
||||||
|
| Счётчик машинных шагов MCP, 2026-09-17 | На видимом `hello` официальный MCP-клиент передал `step_instruction(count=3)`, получил следующую остановку и затем довёл приложение до `getchar()` и выхода; диапазон 1..64 проверяется клиентом, сервером и Lua backend |
|
||||||
| 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 не участвует |
|
||||||
|
|||||||
@@ -544,6 +544,8 @@ DAP-точки отклоняется. Совместный живой прог
|
|||||||
VRAM доступны через общий bridge, что подтверждено снимком работающего `hello`;
|
VRAM доступны через общий bridge, что подтверждено снимком работающего `hello`;
|
||||||
одиночный `press_key` проверен на ожидающем `getchar()`, а `type_string`
|
одиночный `press_key` проверен на ожидающем `getchar()`, а `type_string`
|
||||||
ввёл `Ab9` и Enter в `gets()` с остановкой после печати строки;
|
ввёл `Ab9` и Enter в `gets()` с остановкой после печати строки;
|
||||||
|
машинный `step_instruction` принимает ограниченный `count` 1..64, не меняя
|
||||||
|
семантику DAP source-step;
|
||||||
остальные интерфейсы этого раздела ещё не реализованы.
|
остальные интерфейсы этого раздела ещё не реализованы.
|
||||||
Автономный 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` проверены.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
| `list_breakpoints` | `list_breakpoints` | Проверено на `hello`: логические C-точки всех владельцев, owner, адреса/банк и фактическое guard-условие только для чтения. Точки из родного окна MAME сюда не входят |
|
| `list_breakpoints` | `list_breakpoints` | Проверено на `hello`: логические C-точки всех владельцев, owner, адреса/банк и фактическое guard-условие только для чтения. Точки из родного окна MAME сюда не входят |
|
||||||
| `set_watchpoint` | — | Нужны owner, space/access, bank guard и проверка PC при попадании |
|
| `set_watchpoint` | — | Нужны owner, space/access, bank guard и проверка PC при попадании |
|
||||||
| `clear_watchpoint` | — | Зависит от owner-модели watchpoint |
|
| `clear_watchpoint` | — | Зависит от owner-модели watchpoint |
|
||||||
| `step` | `step_instruction` | Один машинный шаг проверен; счётчик `count` ещё не поддержан |
|
| `step` | `step_instruction` | Проверено на `hello` с `count=3`: 1..64 машинных инструкций при stop, общий control lease и событие остановки; при breakpoint возможно раннее завершение |
|
||||||
| `step_over` | `step_source(kind="over")` | Проверен C-шаг F10; raw машинный `over count` пока отличается |
|
| `step_over` | `step_source(kind="over")` | Проверен C-шаг F10; raw машинный `over count` пока отличается |
|
||||||
| `step_out` | `step_source(kind="out")` | Проверен C-выход; raw выход из одного машинного frame отличается |
|
| `step_out` | `step_source(kind="out")` | Проверен C-выход; raw выход из одного машинного frame отличается |
|
||||||
| `resume` | `continue_execution` | Проверено с control lease и DAP-событиями |
|
| `resume` | `continue_execution` | Проверено с control lease и DAP-событиями |
|
||||||
|
|||||||
@@ -164,6 +164,11 @@ C-строке. Это покрывает типовой строковый вв
|
|||||||
`bank_pages` и generation. Живой MCP-прогон получил инструкции `main` с
|
`bank_pages` и generation. Живой MCP-прогон получил инструкции `main` с
|
||||||
адреса `0x8224`; байты до и после операции совпали. Raw program вне
|
адреса `0x8224`; байты до и после операции совпали. Raw program вне
|
||||||
текущего отображения остаётся открытым.
|
текущего отображения остаётся открытым.
|
||||||
|
Машинный `step_instruction` принимает ограниченный `count=1..64`; backend
|
||||||
|
передаёт это число в native `cpu.debug:step(count)`, общий session server
|
||||||
|
сохраняет control lease и событие остановки. Живой автономный MCP-прогон
|
||||||
|
выполнил `count=3` в `main`, затем продолжил `hello` до `getchar()` и вышел
|
||||||
|
через клавишу. DAP source-step не менялся.
|
||||||
|
|
||||||
## Г. Сделать запуск пригодным для Codex и Claude без VS Code
|
## Г. Сделать запуск пригодным для Codex и Claude без VS Code
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ MCP-сервером MAME и удаляет socket. Закрытие самог
|
|||||||
возвращает принятие команды; итоговую остановку получите через
|
возвращает принятие команды; итоговую остановку получите через
|
||||||
`recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события
|
`recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события
|
||||||
`output`; журнал ограничен 1024 событиями и сообщает `first`/`last`/`lost`.
|
`output`; журнал ограничен 1024 событиями и сообщает `first`/`last`/`lost`.
|
||||||
|
`step_instruction(count)` синхронно выполняет 1..64 машинных шагов
|
||||||
|
(`count=1` по умолчанию) и возвращает позицию остановки. Если раньше
|
||||||
|
сработает breakpoint, выполнение может остановиться до заданного числа.
|
||||||
|
Команды DAP F11/F10/Shift+F11 сохраняют прежнюю C-семантику.
|
||||||
|
|
||||||
Число 29 в attach-режиме (31 в автономном с `start_session`/`stop_session`)
|
Число 29 в attach-режиме (31 в автономном с `start_session`/`stop_session`)
|
||||||
не означает полного переноса более нового `-plugin mamebridge`:
|
не означает полного переноса более нового `-plugin mamebridge`:
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ async def probe() -> None:
|
|||||||
after_dap = await call('session_status')
|
after_dap = await call('session_status')
|
||||||
if after_dap['phase'] != 'ready' or after_dap['session_id'] != status['session_id']:
|
if after_dap['phase'] != 'ready' or after_dap['session_id'] != status['session_id']:
|
||||||
raise RuntimeError('DAP disconnect завершил MCP-owned MAME')
|
raise RuntimeError('DAP disconnect завершил MCP-owned MAME')
|
||||||
|
stepped = await call('step_instruction', {'count': 3})
|
||||||
|
if stepped['pc'] == location['pc'] or \
|
||||||
|
(await call('session_status'))['running']:
|
||||||
|
raise RuntimeError('Три машинных шага не остановились: ' + repr(stepped))
|
||||||
source = str(ROOT / 'tests/hello/hello.c')
|
source = str(ROOT / 'tests/hello/hello.c')
|
||||||
before = await call('set_line_breakpoint', {'file': source, 'line': 62})
|
before = await call('set_line_breakpoint', {'file': source, 'line': 62})
|
||||||
points = await call('list_breakpoints')
|
points = await call('list_breakpoints')
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ class McpAdapterTests(unittest.TestCase):
|
|||||||
self.client.step_source('back')
|
self.client.step_source('back')
|
||||||
self.assertEqual(len(self.calls), before)
|
self.assertEqual(len(self.calls), before)
|
||||||
|
|
||||||
|
def test_machine_step_count_is_validated_before_rpc(self):
|
||||||
|
before = len(self.calls)
|
||||||
|
for count in (0, 65, True):
|
||||||
|
with self.assertRaisesRegex(SessionError, '1..64'):
|
||||||
|
self.client.step_instruction(count)
|
||||||
|
self.assertEqual(len(self.calls), before)
|
||||||
|
self.client.step_instruction(3)
|
||||||
|
self.assertEqual(self.calls[-1][1], 'step')
|
||||||
|
self.assertEqual(self.calls[-1][2]['count'], 3)
|
||||||
|
|
||||||
def test_press_key_releases_shift_after_snapshot_error(self):
|
def test_press_key_releases_shift_after_snapshot_error(self):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
|
|||||||
@@ -111,13 +111,16 @@ class StepBridge:
|
|||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
self.step_counts = []
|
||||||
|
|
||||||
def close(self): pass
|
def close(self): pass
|
||||||
|
|
||||||
def request(self, command, **arguments):
|
def request(self, command, **arguments):
|
||||||
self.calls.append(command)
|
self.calls.append(command)
|
||||||
if command in ('step', 'step_over', 'step_out'):
|
if command in ('step', 'step_over', 'step_out'):
|
||||||
self.owner.index = min(self.owner.index + 1,
|
count = arguments.get('count', 1) if command == 'step' else 1
|
||||||
|
self.step_counts.append(count)
|
||||||
|
self.owner.index = min(self.owner.index + count,
|
||||||
len(self.owner.locations) - 1)
|
len(self.owner.locations) - 1)
|
||||||
return {'accepted': True}
|
return {'accepted': True}
|
||||||
if command == 'snapshot':
|
if command == 'snapshot':
|
||||||
@@ -482,6 +485,21 @@ class ServerTests(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
controller.close()
|
controller.close()
|
||||||
|
|
||||||
|
def test_instruction_step_count_is_bounded_and_forwarded(self):
|
||||||
|
session = StepSession()
|
||||||
|
controller = SessionController(session)
|
||||||
|
try:
|
||||||
|
with self.assertRaisesRegex(SessionError, '1..64'):
|
||||||
|
controller.call('step', {'count': 0, 'owner': 'mcp:test'})
|
||||||
|
for bad in (65, 1.5, True):
|
||||||
|
with self.assertRaisesRegex(SessionError, '1..64'):
|
||||||
|
controller.call('step', {'count': bad, 'owner': 'mcp:test'})
|
||||||
|
result = controller.call('step', {'count': 2, 'owner': 'mcp:test'})
|
||||||
|
self.assertEqual(result['sources'][0]['line'], 4)
|
||||||
|
self.assertEqual(session.bridge.step_counts, [2])
|
||||||
|
finally:
|
||||||
|
controller.close()
|
||||||
|
|
||||||
def test_source_step_preserves_user_breakpoint_on_same_line(self):
|
def test_source_step_preserves_user_breakpoint_on_same_line(self):
|
||||||
session = StepSession()
|
session = StepSession()
|
||||||
controller = SessionController(session)
|
controller = SessionController(session)
|
||||||
|
|||||||
@@ -163,8 +163,10 @@ function exports.startplugin()
|
|||||||
state="running"
|
state="running"
|
||||||
event("running",{reason=command})
|
event("running",{reason=command})
|
||||||
if command=="step" then
|
if command=="step" then
|
||||||
|
local count=number(args.count or 1,64)
|
||||||
|
assert(count>=1,"step count должен быть 1..64")
|
||||||
pending={kind="step",time=now()}
|
pending={kind="step",time=now()}
|
||||||
cpu().debug:step(1)
|
cpu().debug:step(count)
|
||||||
elseif command=="step_over" then
|
elseif command=="step_over" then
|
||||||
pending={kind="step_over",time=now()}
|
pending={kind="step_over",time=now()}
|
||||||
machine().debugger:command("over 1")
|
machine().debugger:command("over 1")
|
||||||
|
|||||||
@@ -201,8 +201,10 @@ class McpSession:
|
|||||||
def pause_execution(self):
|
def pause_execution(self):
|
||||||
return self.call('pause', {'owner': self.owner})
|
return self.call('pause', {'owner': self.owner})
|
||||||
|
|
||||||
def step_instruction(self):
|
def step_instruction(self, count: int = 1):
|
||||||
return self.call('step', {'owner': self.owner})
|
if not isinstance(count, int) or isinstance(count, bool) or count < 1 or count > 64:
|
||||||
|
raise SessionError('Число машинных шагов должно быть 1..64')
|
||||||
|
return self.call('step', {'count': count, 'owner': self.owner})
|
||||||
|
|
||||||
def step_source(self, kind: str = 'into'):
|
def step_source(self, kind: str = 'into'):
|
||||||
if kind not in ('into', 'over', 'out'):
|
if kind not in ('into', 'over', 'out'):
|
||||||
|
|||||||
@@ -406,8 +406,11 @@ class SessionController:
|
|||||||
if method == 'step':
|
if method == 'step':
|
||||||
if self.running:
|
if self.running:
|
||||||
raise SessionError('CPU уже выполняется; сначала Pause')
|
raise SessionError('CPU уже выполняется; сначала Pause')
|
||||||
|
count = arguments.get('count', 1)
|
||||||
|
if type(count) is not int or count < 1 or count > 64:
|
||||||
|
raise SessionError('Число машинных шагов должно быть 1..64')
|
||||||
self._control(arguments)
|
self._control(arguments)
|
||||||
self.session.bridge.request('step')
|
self.session.bridge.request('step', count=count)
|
||||||
self.running = True
|
self.running = True
|
||||||
self._emit('continued', {'reason': 'step'})
|
self._emit('continued', {'reason': 'step'})
|
||||||
location = self.session.where(self.session.bridge.wait_stopped())
|
location = self.session.where(self.session.bridge.wait_stopped())
|
||||||
|
|||||||
@@ -189,9 +189,9 @@ def make_server(client: McpSession | ManagedMcpSession,
|
|||||||
return client.pause_execution()
|
return client.pause_execution()
|
||||||
|
|
||||||
@tool()
|
@tool()
|
||||||
def step_instruction() -> dict[str, Any]:
|
def step_instruction(count: int = 1) -> dict[str, Any]:
|
||||||
"""Выполнить одну машинную инструкцию Z80 и вернуть текущую позицию."""
|
"""Выполнить 1..64 машинных инструкций Z80 и вернуть текущую позицию."""
|
||||||
return client.step_instruction()
|
return client.step_instruction(count)
|
||||||
|
|
||||||
@tool()
|
@tool()
|
||||||
def step_source(kind: str = 'into') -> dict[str, Any]:
|
def step_source(kind: str = 'into') -> dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user