Expand shared Sprinter MCP with managed launch, raw reads and key input

This commit is contained in:
Александр Петров
2026-09-17 22:57:27 +03:00
parent a43e7bda89
commit 5dc7998324
22 changed files with 1120 additions and 67 deletions
+86 -6
View File
@@ -9,11 +9,14 @@ import sys
import threading
from typing import Any
from mame_profile import add_arguments
from sdbg.mcp_adapter import McpSession
from sdbg.session import SessionError
from sdbg.supervisor import ManagedMcpSession, SessionSupervisor
def make_server(client: McpSession):
def make_server(client: McpSession | ManagedMcpSession,
supervisor: SessionSupervisor | None = None):
try:
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
@@ -43,6 +46,17 @@ def make_server(client: McpSession):
"""Получить build ID, generation, состояние CPU и ограничения текущей сессии."""
return client.status()
if supervisor is not None:
@tool()
def start_session() -> dict[str, Any]:
"""Асинхронно запустить MAME→DSS→EXE→main из --build; готовность читать через session_status."""
return supervisor.start()
@tool()
def stop_session() -> dict[str, Any]:
"""Завершить только MAME, созданный этим MCP-сервером."""
return supervisor.stop()
@tool(annotations=reading)
def where() -> dict[str, Any]:
"""Получить проверенное соответствие PC исходнику C и текущему банку."""
@@ -58,6 +72,47 @@ def make_server(client: McpSession):
"""Читать 1..256 байт logical Z80 memory без side effects; адрес decimal или 0xHEX."""
return client.read_memory(address, length)
@tool(annotations=reading)
def read_program_memory(address: str, length: int = 16) -> dict[str, Any]:
"""Читать 1..4096 байт raw program space 0..0x3ffff; Z80 окна находятся в 0x10000+."""
return client.read_program_memory(address, length)
@tool(annotations=reading)
def list_ports() -> dict[str, Any]:
"""Перечислить порты ввода MAME и поля с масками; результат ограничен по размеру."""
return client.list_ports()
@tool(annotations=reading)
def list_shares() -> dict[str, Any]:
"""Перечислить точные tag и размеры memory shares/regions MAME."""
return client.list_shares()
@tool(annotations=reading)
def read_share(tag: str, address: str, length: int = 16) -> dict[str, Any]:
"""Читать 1..4096 байт из share по точному tag; адрес decimal или 0xHEX."""
return client.read_share(tag, address, length)
@tool(annotations=reading)
def read_vram(address: str, length: int = 16) -> dict[str, Any]:
"""Читать 1..4096 байт VRAM без Z80 bank mapping."""
return client.read_vram(address, length)
@tool(annotations=reading)
def read_screen_pixels(x: int, y: int, width: int = 1,
height: int = 1) -> dict[str, Any]:
"""Читать до 8192 видимых pen16 пикселей; при stop кадр может быть старым."""
return client.read_screen_pixels(x, y, width, height)
@tool()
def screenshot() -> dict[str, Any]:
"""Сохранить PNG последнего кадра в изолированном каталоге текущей сессии."""
return client.screenshot()
@tool()
def press_key(key: str, frames: int = 3) -> dict[str, Any]:
"""Нажать один символ/enter/space/tab на 1..60 кадров; отпустить при stop/ошибке."""
return client.press_key(key, frames)
@tool(annotations=reading)
def list_variables() -> dict[str, Any]:
"""Перечислить известные global/static переменные и поддержанность типов."""
@@ -134,15 +189,38 @@ def make_server(client: McpSession):
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--socket', default=os.environ.get('SDBG_SOCKET'),
help='Unix socket уже запущенного sdbg_server.py (или SDBG_SOCKET)')
help='Unix socket сеанса; с --build можно не указывать')
parser.add_argument('--build', help='Debug-пакет для автономного MCP запуска')
add_arguments(parser)
parser.add_argument('--app-hdd')
parser.add_argument('--launch-path')
parser.add_argument('--data', action='append', default=[])
parser.add_argument('--debugger', default='sdbg')
parser.add_argument('--launch-at', type=float)
parser.add_argument('--dss-timeout', type=float)
args = parser.parse_args()
if not args.socket:
if not args.socket and not args.build:
parser.error('укажите --socket или SDBG_SOCKET')
client = McpSession(args.socket)
supervisor = None
if args.build:
options = []
for name in ('mame_home', 'mame_bin', 'mame_rompath', 'mame_dss_image',
'mame_system_hdd_image', 'mame_bios', 'app_hdd', 'launch_path',
'debugger', 'launch_at', 'dss_timeout'):
value = getattr(args, name)
if value is not None:
options += ['--' + name.replace('_', '-'), str(value)]
for filename in args.data:
options += ['--data', filename]
supervisor = SessionSupervisor(args.build, args.socket, options)
client = ManagedMcpSession(supervisor)
else:
client = McpSession(args.socket)
heartbeat_stop = threading.Event()
heartbeat = None
try:
client.status() # Не публикуем инструменты для отсутствующей сессии.
if supervisor is None:
client.status() # Attach-режим требует уже работающий session server.
def renew_control():
while not heartbeat_stop.wait(10):
try:
@@ -153,7 +231,7 @@ def main() -> int:
heartbeat = threading.Thread(target=renew_control, name='sdbg-mcp-heartbeat',
daemon=True)
heartbeat.start()
server = make_server(client)
server = make_server(client, supervisor)
server.run(transport='stdio')
return 0
except (SessionError, OSError, RuntimeError) as error:
@@ -171,6 +249,8 @@ def main() -> int:
client.clear_owned_breakpoints()
except (SessionError, OSError):
pass
if supervisor is not None:
supervisor.close()
if __name__ == '__main__':