Add auto-discovery support (UDP discovery responder & subnet scanner) and implement virtual screen MCP server

This commit is contained in:
Adolfo Reyna
2026-06-03 14:29:49 -04:00
parent 81e7b15b61
commit 070390355e
8 changed files with 2115 additions and 2 deletions
+114 -2
View File
@@ -4,13 +4,125 @@ import json
import urllib.request
import argparse
def discover_screen():
import socket
sys.stderr.write("Searching for ESP32 Screen via UDP broadcast on port 5000...\n")
sys.stderr.flush()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.settimeout(1.5)
broadcast_ips = ['255.255.255.255', '<broadcast>']
try:
hostname = socket.gethostname()
ip_list = socket.gethostbyname_ex(hostname)[2]
for ip in ip_list:
if not ip.startswith('127.'):
parts = ip.split('.')
if len(parts) == 4:
subnet_bcast = f"{parts[0]}.{parts[1]}.{parts[2]}.255"
if subnet_bcast not in broadcast_ips:
broadcast_ips.append(subnet_bcast)
except:
pass
for target_ip in broadcast_ips:
try:
s.sendto(b"DISCOVER_SCREEN", (target_ip, 5000))
except:
pass
try:
data, addr = s.recvfrom(128)
if data == b"SCREEN_IP_80":
sys.stderr.write(f"Auto-discovered ESP32 Screen at IP: {addr[0]} via UDP\n")
sys.stderr.flush()
return addr[0]
except:
pass
finally:
s.close()
return None
def scan_subnet():
import socket
import concurrent.futures
try:
hostname = socket.gethostname()
local_ips = [ip for ip in socket.gethostbyname_ex(hostname)[2] if not ip.startswith('127.')]
if not local_ips:
return None
local_ip = local_ips[0]
except Exception as e:
sys.stderr.write(f"Subnet lookup error: {e}\n")
sys.stderr.flush()
return None
parts = local_ip.split('.')
if len(parts) == 4:
base_ip = f"{parts[0]}.{parts[1]}.{parts[2]}."
else:
return None
sys.stderr.write(f"Scanning local subnet {base_ip}1 to {base_ip}254 on port 80...\n")
sys.stderr.flush()
def check_ip(ip_suffix):
target_ip = f"{base_ip}{ip_suffix}"
url = f"http://{target_ip}:80/api/mcp"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
req_data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(
url,
data=req_data,
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=2.0) as response:
resp_data = json.loads(response.read().decode('utf-8'))
tools = resp_data.get("result", {}).get("tools", [])
if any(t.get("name") == "clear_screen" for t in tools):
return target_ip
except:
pass
return None
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
futures = [executor.submit(check_ip, i) for i in range(1, 255)]
for fut in concurrent.futures.as_completed(futures):
res = fut.result()
if res:
executor.shutdown(wait=False)
sys.stderr.write(f"Auto-discovered ESP32 Screen at IP: {res} via subnet scan\n")
sys.stderr.flush()
return res
return None
def main():
parser = argparse.ArgumentParser(description="MCP Stdio-to-HTTP Bridge for ESP32-S3-RLCD-4.2")
parser.add_argument("--ip", required=True, help="IP address of the ESP32 board (e.g. 192.168.1.123)")
parser.add_argument("--ip", help="IP address of the ESP32 board. If omitted, auto-discovery is performed.")
parser.add_argument("--port", type=int, default=80, help="Port the MCP server is listening on (default 80)")
args = parser.parse_args()
url = f"http://{args.ip}:{args.port}/api/mcp"
ip = args.ip
if not ip:
ip = discover_screen()
if not ip:
sys.stderr.write("UDP broadcast discovery timed out. Initializing subnet sweep...\n")
sys.stderr.flush()
ip = scan_subnet()
if not ip:
sys.stderr.write("Subnet sweep failed. Falling back to hostname: 'esp32screen.local'\n")
sys.stderr.flush()
ip = "esp32screen.local"
url = f"http://{ip}:{args.port}/api/mcp"
sys.stderr.write(f"ESP32 MCP Stdio-to-HTTP Bridge started. Routing stdio to {url}\n")
sys.stderr.flush()