Add bounded MCP text input for Sprinter programs

This commit is contained in:
Александр Петров
2026-09-17 23:17:38 +03:00
parent 958ad0ae0f
commit e8189b137a
11 changed files with 195 additions and 18 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Живой MCP-прогон: строка с Shift и Enter проходит через DSS gets()."""
from __future__ import annotations
import asyncio
import json
import os
from pathlib import Path
import shutil
import sys
import tempfile
import time
from mcp import Client, StdioServerParameters
ROOT = Path(__file__).resolve().parents[2]
async def probe() -> None:
home = Path(os.environ['MAME_HOME']).resolve()
binary = Path(os.environ.get('MAME_BIN', home / 'sprinter')).resolve()
package = ROOT / 'tests/gets/.sprinter-cc-gets'
with tempfile.TemporaryDirectory(prefix='sprinter-type-mcp-', dir='/tmp') as temp:
socket = str(Path(temp) / 'session.sock')
parameters = StdioServerParameters(
command=sys.executable,
args=[str(ROOT / 'toolchain/sdbg_mcp.py'), '--build', str(package),
'--socket', socket, '--mame-home', str(home),
'--mame-bin', str(binary)])
async with Client(parameters) as client:
async def call(name: str, arguments: dict | None = None) -> dict:
result = await client.call_tool(name, arguments or {})
if result.is_error or result.structured_content is None:
raise RuntimeError(name + ': ' + repr(result.content))
return result.structured_content
names = {tool.name for tool in (await client.list_tools()).tools}
if 'type_string' not in names:
raise RuntimeError('MCP не публикует type_string')
await call('start_session')
try:
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
status = await call('session_status')
if status['phase'] == 'ready':
break
if status['phase'] in ('failed', 'stopped'):
raise RuntimeError('Launcher: ' + repr(status))
await asyncio.sleep(.2)
else:
raise TimeoutError('gets не дошёл до main')
source = str(ROOT / 'tests/gets/gets.c')
point = await call('set_line_breakpoint', {'file': source, 'line': 16})
await call('continue_execution')
await asyncio.sleep(.5)
if not (await call('session_status'))['running']:
raise RuntimeError('gets не ожидает строку')
typed = await call('type_string', {'value': 'Ab9\n'})
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
status = await call('session_status')
if not status['running']:
break
await asyncio.sleep(.1)
else:
shot = await call('screenshot')
destination = Path('/private/tmp/sprinter-c-mcp-gets-timeout.png')
shutil.copyfile(shot['path'], destination)
raise TimeoutError('Enter не завершил gets: ' +
repr({'typed': typed, 'status': status,
'screenshot': str(destination)}))
location = await call('where')
if typed['typed'] != 4 or not typed['complete'] or \
not any(item['line'] == 16 for item in location['sources']):
raise RuntimeError('Неверный результат набора: ' +
repr((typed, location)))
shot = await call('screenshot')
destination = Path('/private/tmp/sprinter-c-mcp-gets.png')
shutil.copyfile(shot['path'], destination)
await call('clear_breakpoint', {'identifier': point['id']})
print(json.dumps({'event': 'mcp_type_string_verified',
'tools': len(names), 'typed': typed,
'line': 16, 'screenshot': str(destination)},
ensure_ascii=False), flush=True)
finally:
await call('stop_session')
if __name__ == '__main__':
asyncio.run(probe())
+26
View File
@@ -77,6 +77,32 @@ class McpAdapterTests(unittest.TestCase):
self.assertEqual(keys[1]['tag'], keys[-2]['tag'])
self.assertTrue(all(item['owner'] == client.owner for item in keys))
def test_type_string_checks_layout_before_input_and_releases_each_key(self):
calls = []
frame = 0
def rpc(path, method, arguments, timeout=10, **identity):
nonlocal frame
calls.append((method, arguments))
if method == 'status':
return {'session_id': 'test-session', 'build_id': 'test-build',
'generation': 7, 'running': True}
if method == 'snapshot':
frame += 1
return {'state': 'running', 'frame': frame}
return {}
client = McpSession('/tmp/sprinter-test.sock', rpc=rpc)
with self.assertRaisesRegex(SessionError, 'нет раскладки'):
client.type_string('a€')
self.assertEqual(calls, [])
result = client.type_string('ab')
self.assertEqual(result, {'requested': 2, 'typed': 2,
'complete': True, 'stopped': False})
keys = [args for method, args in calls if method == 'input_key']
self.assertEqual([item['down'] for item in keys],
[True, False, True, False])
if __name__ == '__main__':
unittest.main()