77 lines
3.6 KiB
Python
77 lines
3.6 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'}
|
|
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)))
|
|
|
|
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},
|
|
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()
|