Expand shared Sprinter MCP with managed launch, raw reads and key input
This commit is contained in:
+86
-6
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user