Add shared-session MCP source debugger adapter

This commit is contained in:
Александр Петров
2026-09-16 20:43:50 +03:00
parent addc00a0f3
commit 9ad5a018a2
19 changed files with 740 additions and 22 deletions
+77
View File
@@ -0,0 +1,77 @@
"""Тонкий MCP-клиент общей source-debug сессии без зависимости от MCP SDK."""
from __future__ import annotations
from pathlib import Path
import uuid
from .server import rpc_call
from .session import SessionError
class McpSession:
def __init__(self, socket_path: str, rpc=rpc_call):
self.socket_path = str(Path(socket_path).expanduser())
self.owner = 'mcp:' + uuid.uuid4().hex
self._rpc = rpc
def call(self, method: str, arguments: dict | None = None, timeout: float = 10):
return self._rpc(self.socket_path, method, arguments or {}, timeout=timeout)
def status(self):
return self.call('status')
def where(self):
return self.call('where')
def registers(self):
return self.call('registers')
def read_memory(self, address: str, length: int = 16):
try:
parsed = int(address, 16 if address.lower().startswith('0x') else 10)
except ValueError as error:
raise SessionError('Адрес памяти должен быть десятичным или 0xHEX') from error
return self.call('read_memory', {'address': parsed, 'length': length})
def variables(self):
return self.call('variables')
def read_variable(self, name: str, module: str | None = None):
return self.call('read_variable', {'name': name, 'module': module})
def events(self, after: int = 0, timeout: float = 0):
if after < 0 or timeout < 0 or timeout > 30:
raise SessionError('after должен быть неотрицательным, timeout — от 0 до 30 с')
return self.call('events', {'after': after, 'timeout': timeout},
timeout=max(10, timeout + 2))
def console_tail(self, count: int = 40):
if count < 1 or count > 200:
raise SessionError('count должен быть от 1 до 200')
return self.call('mame_console_tail', {'count': count})
def break_line(self, file: str, line: int):
return self.call('break_line', {'file': file, 'line': line, 'owner': self.owner})
def break_function(self, name: str):
return self.call('break_function', {'name': name, 'owner': self.owner})
def clear_breakpoint(self, identifier: int):
return self.call('clear_breakpoint', {'id': identifier, 'owner': self.owner})
def clear_owned_breakpoints(self):
return self.call('clear_owned_breakpoints', {'owner': self.owner})
def continue_execution(self):
return self.call('continue')
def pause_execution(self):
return self.call('pause')
def step_instruction(self):
return self.call('step')
def step_source(self, kind: str = 'into'):
if kind not in ('into', 'over', 'out'):
raise SessionError('kind должен быть into, over или out')
return self.call('source_step', {'kind': kind})
+49 -3
View File
@@ -116,6 +116,13 @@ class SessionController:
self.poller.join(timeout=1)
self.session.bridge.close()
@staticmethod
def _owner(arguments: dict) -> str:
owner = arguments.get('owner', 'cli')
if not isinstance(owner, str) or not owner or len(owner) > 128:
raise SessionError('owner должен быть непустой строкой до 128 символов')
return owner
def call(self, method: str, arguments: dict) -> object:
if method == 'events':
after = int(arguments.get('after', 0))
@@ -131,6 +138,9 @@ class SessionController:
with self.lock:
if self.closed:
raise SessionError('Сессия закрыта или инвалидирована')
if self.running and method in ('where', 'registers', 'read_memory',
'read_variable', 'break_line', 'break_function'):
raise SessionError('Операция требует остановленного CPU; сначала Pause')
if method == 'status':
return {**self.attached, 'running': self.running,
'event_sequence': self.sequence}
@@ -151,16 +161,52 @@ class SessionController:
mapping = self.session.refresh()
return {'generation': mapping.generation, 'registers': mapping.registers,
'bank_pages': mapping.bank_pages}
if method == 'read_memory':
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..65535, длина 1..256 без выхода за 64 КБ')
mapping = self.session.refresh()
data = self.session.bridge.request('memory', address=address, length=length)
return {'address': address, 'length': length, 'hex': data['hex'],
'generation': mapping.generation, 'bank_pages': mapping.bank_pages}
if method == 'variables':
return self.session.model.variables
if method == 'read_variable':
return self.session.read_variable(arguments['name'], arguments.get('module'))
if method == 'break_line':
return self.session.break_line(arguments['file'], int(arguments['line']))
owner = self._owner(arguments)
item = self.session.break_line(arguments['file'], int(arguments['line']))
self.breakpoint_info[item['id']] = {
'kind': 'stop', 'message': None, 'locations': item['locations'],
'hits': 0, 'owner': owner}
return item
if method == 'break_function':
return self.session.break_function(arguments['name'])
owner = self._owner(arguments)
item = self.session.break_function(arguments['name'])
self.breakpoint_info[item['id']] = {
'kind': 'stop', 'message': None, 'locations': item['locations'],
'hits': 0, 'owner': owner}
return item
if method == 'clear_breakpoint':
return self.session.clear_breakpoint(int(arguments['id']))
identifier = int(arguments['id'])
owner = arguments.get('owner')
if owner is not None and self.breakpoint_info.get(identifier, {}).get('owner') != owner:
raise SessionError('Точка не принадлежит этому клиенту')
result = self.session.clear_breakpoint(identifier)
self.breakpoint_info.pop(identifier, None)
return result
if method == 'clear_owned_breakpoints':
owner = arguments.get('owner')
if not isinstance(owner, str) or not owner:
raise SessionError('Для очистки точек требуется owner')
identifiers = [identifier for identifier, info in self.breakpoint_info.items()
if info.get('owner') == owner]
for identifier in identifiers:
self.session.clear_breakpoint(identifier)
self.breakpoint_info.pop(identifier, None)
return {'cleared': identifiers}
if method == 'set_source_breakpoints':
specifications = arguments.get('breakpoints')
if specifications is None:
+6 -3
View File
@@ -246,12 +246,15 @@ class DebugSession:
return self.bridge.request('deactivate_breakpoints')
def clear_breakpoint(self, identifier: int) -> dict:
ids = self.breakpoints.pop(identifier, None)
ids = self.breakpoints.get(identifier)
if ids is None:
raise SessionError('Неизвестная логическая точка остановки')
for backend_id in ids:
backend_ids = list(ids)
for backend_id in backend_ids:
self.bridge.request('clear', id=backend_id)
return {'cleared': identifier, 'backend_ids': ids}
ids.remove(backend_id)
self.breakpoints.pop(identifier)
return {'cleared': identifier, 'backend_ids': backend_ids}
def read_variable(self, name: str, module: str | None = None) -> dict:
matches = [variable for variable in self.model.variables