#!/usr/bin/env python3 import sys import json import urllib.request import argparse 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("--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" sys.stderr.write(f"ESP32 MCP Stdio-to-HTTP Bridge started. Routing stdio to {url}\n") sys.stderr.flush() # Sits in loop reading stdio requests from LLM client and routing them to ESP32 while True: try: line = sys.stdin.readline() if not line: break # Parse request to ensure it's valid JSON req_data = json.loads(line.strip()) # Forward the JSON-RPC request to the ESP32 board via HTTP POST req = urllib.request.Request( url, data=json.dumps(req_data).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST" ) with urllib.request.urlopen(req, timeout=10.0) as response: resp_data = response.read().decode("utf-8") try: resp_json = json.loads(resp_data) result = resp_json.get("result", {}) content_list = result.get("content", []) if len(content_list) > 0 and content_list[0].get("type") == "text": text_val = content_list[0].get("text", "") if text_val.startswith("__PBM_BASE64__:"): b64_pbm = text_val.split(":", 1)[1] import base64 import subprocess import os pbm_bytes = base64.b64decode(b64_pbm) with open("temp_mcp.pbm", "wb") as pf: pf.write(pbm_bytes) # Convert PBM to PNG on macOS using standard sips tool subprocess.run(["sips", "-s", "format", "png", "temp_mcp.pbm", "--out", "temp_mcp.png"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if os.path.exists("temp_mcp.png"): with open("temp_mcp.png", "rb") as pngf: png_bytes = pngf.read() b64_png = base64.b64encode(png_bytes).decode("utf-8") # Replace standard text content with MCP image schema content_list[0] = { "type": "image", "data": b64_png, "mimeType": "image/png" } # Cleanup temp files try: os.remove("temp_mcp.pbm") os.remove("temp_mcp.png") except: pass else: content_list[0] = { "type": "text", "text": "Screenshot captured, but PNG conversion failed on host PC." } resp_data = json.dumps(resp_json) except Exception as ex: sys.stderr.write(f"Bridge image conversion error: {ex}\n") sys.stderr.flush() # Write the response back to stdio for the LLM client sys.stdout.write(resp_data + "\n") sys.stdout.flush() except urllib.error.URLError as e: # Send standard JSON-RPC internal error response err_resp = { "jsonrpc": "2.0", "error": { "code": -32000, "message": f"Bridge failed to reach ESP32: {e.reason}" }, "id": req_data.get("id") if "req_data" in locals() else None } sys.stdout.write(json.dumps(err_resp) + "\n") sys.stdout.flush() sys.stderr.write(f"Bridge error: Failed to connect to ESP32 at {url}: {e}\n") sys.stderr.flush() except Exception as e: err_resp = { "jsonrpc": "2.0", "error": { "code": -32603, "message": f"Bridge Internal Error: {str(e)}" }, "id": req_data.get("id") if "req_data" in locals() else None } sys.stdout.write(json.dumps(err_resp) + "\n") sys.stdout.flush() sys.stderr.write(f"Bridge error: {e}\n") sys.stderr.flush() if __name__ == "__main__": main()