#!/usr/bin/env python3 """Host smoke test for the Tactility MCP Screen Phase 1 API.""" import argparse import json import socket import urllib.request def discover(timeout: float = 2.0) -> str | None: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.settimeout(timeout) try: sock.sendto(b"DISCOVER_SCREEN", ("255.255.255.255", 5000)) data, address = sock.recvfrom(128) if data == b"SCREEN_IP_80": return address[0] except TimeoutError: return None finally: sock.close() return None def rpc(ip: str, request_id: int, method: str, params: dict | None = None) -> dict: payload = { "jsonrpc": "2.0", "id": request_id, "method": method, } if params is not None: payload["params"] = params request = urllib.request.Request( f"http://{ip}/api/mcp", data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(request, timeout=5) as response: return json.loads(response.read()) def require_result(response: dict, label: str) -> None: if "error" in response: raise RuntimeError(f"{label} failed: {response['error']}") if "result" not in response: raise RuntimeError(f"{label} returned no result: {response}") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--ip", help="Device IP; omit to use UDP discovery") parser.add_argument( "--draw", action="store_true", help="Also clear the app canvas and draw visible test text", ) args = parser.parse_args() ip = args.ip or discover() if not ip: raise SystemExit("MCP Screen was not discovered. Pass --ip DEVICE_IP to test directly.") print(f"Testing MCP Screen at {ip}") tools = rpc(ip, 1, "tools/list") require_result(tools, "tools/list") names = [tool["name"] for tool in tools["result"]["tools"]] expected = {"get_capabilities", "clear_screen", "draw_text"} if not expected.issubset(names): raise RuntimeError(f"Missing tools: {sorted(expected - set(names))}") print(f"tools/list: {', '.join(names)}") capabilities = rpc( ip, 2, "tools/call", {"name": "get_capabilities", "arguments": {}}, ) require_result(capabilities, "get_capabilities") print("get_capabilities:", capabilities["result"]["content"][0]["text"]) if args.draw: cleared = rpc( ip, 3, "tools/call", {"name": "clear_screen", "arguments": {"color": 1}}, ) require_result(cleared, "clear_screen") drawn = rpc( ip, 4, "tools/call", { "name": "draw_text", "arguments": { "text": "Tactility MCP is alive", "x": 20, "y": 30, "size": 2, }, }, ) require_result(drawn, "draw_text") print("Visible draw test sent.") print("Phase 1 smoke test passed.") if __name__ == "__main__": main()