Files
2026-09-17 22:57:27 +03:00

108 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Живой MCP 2.x клиент: stdio → sdbg MCP → общая DAP-сессия."""
from __future__ import annotations
import argparse
import asyncio
import json
from pathlib import Path
import sys
from mcp import Client, StdioServerParameters
ROOT = Path(__file__).resolve().parents[2]
async def probe(socket: str, source: str, foreign_id: int):
parameters = StdioServerParameters(
command=sys.executable,
args=[str(ROOT / 'toolchain/sdbg_mcp.py'), '--socket', socket])
async with Client(parameters) as client:
listed = await client.list_tools()
names = {tool.name for tool in listed.tools}
expected = {'session_status', 'where', 'read_registers', 'read_memory',
'recent_events', 'set_line_breakpoint', 'clear_breakpoint',
'claim_control', 'release_control', 'list_shares',
'read_share', 'read_vram', 'read_screen_pixels', 'screenshot',
'read_program_memory', 'list_ports'}
if not expected <= names:
raise RuntimeError('Не хватает MCP-инструментов: ' + str(expected - names))
async def call(name: str, arguments: dict | None = None):
result = await client.call_tool(name, arguments or {})
if result.is_error:
raise RuntimeError(name + ': ' + str(result.content))
if result.structured_content is None:
raise RuntimeError(name + ': нет structuredContent: '+repr(result.content))
return result.structured_content
status = await call('session_status')
location = await call('where')
registers = await call('read_registers')
memory = await call('read_memory', {'address': hex(location['pc']), 'length': 4})
events = await call('recent_events', {'after': 0})
if not status['build_id'] or location['status'] != 'mapped' or \
not status['session_id'] or \
registers['registers']['PC'] != location['pc'] or \
len(memory['hex']) != 8 or not events['events']:
raise RuntimeError('Неполный C-снимок MCP: ' + repr((status, location, memory)))
program = await call('read_program_memory', {
'address': hex(0x10000 + location['pc']), 'length': 4})
if program['hex'] != memory['hex'] or program['space'] != 'program':
raise RuntimeError('Raw program и logical Z80 расходятся в main')
ports = await call('list_ports')
if ports['truncated'] or not any(':kbd:ms_naturl:' in item['tag']
for item in ports['ports']):
raise RuntimeError('Порты PC-клавиатуры не найдены: ' + repr(ports)[:500])
shares = await call('list_shares')
vram = [item for item in shares['shares'] if 'vram' in item['tag']]
if len(vram) != 1 or vram[0]['size'] < 4:
raise RuntimeError('VRAM share не найден: ' + repr(shares))
raw_vram = await call('read_vram', {'address': '0', 'length': 4})
same_share = await call('read_share', {
'tag': vram[0]['tag'], 'address': '0', 'length': 4})
if len(raw_vram['hex']) != 8 or raw_vram['hex'] != same_share['hex']:
raise RuntimeError('Чтение VRAM/share различается')
pixels = await call('read_screen_pixels', {
'x': 0, 'y': 0, 'width': 2, 'height': 2})
if len(pixels['hex']) != 16 or not pixels['stale_frame']:
raise RuntimeError('Неверный снимок пикселей: ' + repr(pixels))
shot = await call('screenshot')
snapshot = Path(shot['path'])
if snapshot.suffix != '.png' or snapshot.stat().st_size != shot['size'] or \
snapshot.read_bytes()[:8] != b'\x89PNG\r\n\x1a\n':
raise RuntimeError('Снимок MAME не является PNG: ' + repr(shot))
own = await call('set_line_breakpoint', {'file': source, 'line': 62})
foreign = await client.call_tool('clear_breakpoint', {'identifier': foreign_id})
if not foreign.is_error or 'не принадлежит' not in str(foreign.content):
raise RuntimeError('MCP не подтвердил защиту чужой точки VS Code')
lease = await call('claim_control')
if not lease['owner'].startswith('mcp:'):
raise RuntimeError('MCP не захватил управление CPU: ' + repr(lease))
released = await call('release_control')
if released['released'] != lease['owner']:
raise RuntimeError('MCP не освободил управление CPU: ' + repr(released))
print(json.dumps({'event': 'mcp_shared_session_verified',
'build_id': status['build_id'], 'pc': location['pc'],
'tools': len(names), 'foreign_point_rejected': True,
'owned_point_for_cleanup': own['id'],
'control_claim_release': True,
'shares_pixels_png': True,
'program_and_ports': True},
ensure_ascii=False), flush=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--socket', required=True)
parser.add_argument('--source', required=True)
parser.add_argument('--foreign-id', type=int, required=True)
args = parser.parse_args()
asyncio.run(probe(args.socket, args.source, args.foreign_id))
if __name__ == '__main__':
main()