Add auto-discovery support (UDP discovery responder & subnet scanner) and implement virtual screen MCP server
This commit is contained in:
+114
-2
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user