Files
Sprinter-SDCC/toolchain/sdbg_mcp.py
T
2026-09-16 20:43:50 +03:00

148 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""stdio MCP-адаптер к единственной source-debug сессии Sprinter."""
from __future__ import annotations
import argparse
from functools import wraps
import os
import sys
from typing import Any
from sdbg.mcp_adapter import McpSession
from sdbg.session import SessionError
def make_server(client: McpSession):
try:
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
except ImportError as error:
raise RuntimeError('Нужен официальный MCP SDK 2.x: python -m pip install "mcp>=2.2,<3"') from error
server = MCPServer('sprinter-source-debug',
instructions='Все команды работают через одну sdbg-сессию. '
'Чтение переменных и памяти требует остановленного CPU. '
'Для длинного шага используйте recent_events или pause_execution.')
reading = ToolAnnotations(readOnlyHint=True, openWorldHint=False)
def tool(*, annotations=None):
def register(function):
@wraps(function)
def guarded(*args, **kwargs):
try:
return function(*args, **kwargs)
except (SessionError, OSError, ValueError) as error:
raise ToolError(str(error)) from error
return server.tool(annotations=annotations)(guarded)
return register
@tool(annotations=reading)
def session_status() -> dict[str, Any]:
"""Получить build ID, generation, состояние CPU и ограничения текущей сессии."""
return client.status()
@tool(annotations=reading)
def where() -> dict[str, Any]:
"""Получить проверенное соответствие PC исходнику C и текущему банку."""
return client.where()
@tool(annotations=reading)
def read_registers() -> dict[str, Any]:
"""Читать регистры Z80 и отображение банков на остановленном CPU."""
return client.registers()
@tool(annotations=reading)
def read_memory(address: str, length: int = 16) -> dict[str, Any]:
"""Читать 1..256 байт logical Z80 memory без side effects; адрес decimal или 0xHEX."""
return client.read_memory(address, length)
@tool(annotations=reading)
def list_variables() -> dict[str, Any]:
"""Перечислить известные global/static переменные и поддержанность типов."""
return {'variables': client.variables()}
@tool(annotations=reading)
def read_variable(name: str, module: str | None = None) -> dict[str, Any]:
"""Читать поддержанную global/static переменную; module разрешает неоднозначность."""
return client.read_variable(name, module)
@tool(annotations=reading)
def recent_events(after: int = 0, timeout: float = 0) -> dict[str, Any]:
"""Читать общий журнал stopped/continued/output с курсором seq; timeout 0..30 с."""
return client.events(after, timeout)
@tool(annotations=reading)
def mame_console_tail(count: int = 40) -> dict[str, Any]:
"""Получить последние строки консоли MAME, если bridge поддерживает её чтение."""
return client.console_tail(count)
@tool()
def set_line_breakpoint(file: str, line: int) -> dict[str, Any]:
"""Поставить личную точку по C-файлу и строке без замены точек VS Code."""
return client.break_line(file, line)
@tool()
def set_function_breakpoint(name: str) -> dict[str, Any]:
"""Поставить личную точку по имени C-функции."""
return client.break_function(name)
@tool()
def clear_breakpoint(identifier: int) -> dict[str, Any]:
"""Удалить свою логическую точку по ID; чужие точки отклоняются."""
return client.clear_breakpoint(identifier)
@tool()
def clear_my_breakpoints() -> dict[str, Any]:
"""Удалить только точки текущего MCP-подключения."""
return client.clear_owned_breakpoints()
@tool()
def continue_execution() -> dict[str, Any]:
"""Продолжить CPU; событие остановки читать через recent_events."""
return client.continue_execution()
@tool()
def pause_execution() -> dict[str, Any]:
"""Остановить CPU и вернуть текущую C-позицию."""
return client.pause_execution()
@tool()
def step_instruction() -> dict[str, Any]:
"""Выполнить одну машинную инструкцию Z80 и вернуть текущую позицию."""
return client.step_instruction()
@tool()
def step_source(kind: str = 'into') -> dict[str, Any]:
"""Начать асинхронный C-шаг: into, over или out; результат в recent_events."""
return client.step_source(kind)
return server
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)')
args = parser.parse_args()
if not args.socket:
parser.error('укажите --socket или SDBG_SOCKET')
client = McpSession(args.socket)
try:
client.status() # Не публикуем инструменты для отсутствующей сессии.
server = make_server(client)
server.run(transport='stdio')
return 0
except (SessionError, OSError, RuntimeError) as error:
print('sdbg-mcp: ' + str(error), file=sys.stderr)
return 1
finally:
try:
client.clear_owned_breakpoints()
except (SessionError, OSError):
pass
if __name__ == '__main__':
sys.exit(main())