diff --git a/mcp_bridge.py b/mcp_bridge.py index 4e78df3..1fd8648 100755 --- a/mcp_bridge.py +++ b/mcp_bridge.py @@ -24,6 +24,74 @@ def main(): # Parse request to ensure it's valid JSON req_data = json.loads(line.strip()) + # Intercept draw_image tool and pre-process the image on the host PC + if req_data.get("method") == "tools/call" and req_data.get("params", {}).get("name") == "draw_image": + try: + args = req_data.get("params", {}).get("arguments", {}) + image_base64 = args.get("image_base64") + + if not image_base64: + raise ValueError("Missing image_base64 parameter") + + # Strip data URI prefix if present + if image_base64.startswith("data:"): + if ";base64," in image_base64: + image_base64 = image_base64.split(";base64,")[1] + + import base64 + import io + from PIL import Image + + img_bytes = base64.b64decode(image_base64) + img = Image.open(io.BytesIO(img_bytes)) + + # Convert to grayscale + img = img.convert("L") + + # Resize to fit screen dimensions (400x300 max) + max_w = int(args.get("maxWidth", 400)) + max_h = int(args.get("maxHeight", 300)) + + # Prevent going over physical screen bounds + max_w = min(max_w, 400) + max_h = min(max_h, 300) + + img.thumbnail((max_w, max_h)) + + # Convert to 1-bit monochrome (with optional Floyd-Steinberg dithering) + dither = args.get("dither", True) + img_1bit = img.convert("1", dither=Image.FLOYDSTEINBERG if dither else Image.NONE) + + # Save as binary PBM (P4 format) using PPM format writer on mode 1 + pbm_io = io.BytesIO() + img_1bit.save(pbm_io, format="PPM") + pbm_data = pbm_io.getvalue() + + # Encode processed PBM to base64 + pbm_b64 = base64.b64encode(pbm_data).decode("utf-8") + + # Substitute arguments for ESP32 + new_args = { + "pbm_base64": pbm_b64, + "x": args.get("x", 0), + "y": args.get("y", 0) + } + req_data["params"]["arguments"] = new_args + + except Exception as e: + # Return error response directly to client without contacting ESP32 + err_resp = { + "jsonrpc": "2.0", + "error": { + "code": -32602, + "message": f"Bridge image preprocessing failed: {str(e)}" + }, + "id": req_data.get("id") + } + sys.stdout.write(json.dumps(err_resp) + "\n") + sys.stdout.flush() + continue + # Forward the JSON-RPC request to the ESP32 board via HTTP POST req = urllib.request.Request( url, diff --git a/mcp_server.py b/mcp_server.py index 40cf163..3995b02 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -169,6 +169,20 @@ class MCPServer: "name": "get_screenshot", "description": "Capture the current reflective LCD screen rendering as a PNG image.", "inputSchema": {"type": "object", "properties": {}} + }, + { + "name": "draw_image", + "description": "Draw an image (PNG, JPEG, GIF, BMP, etc.) on the screen. The image will be converted to 1-bit monochrome and fit to display boundaries.", + "inputSchema": { + "type": "object", + "properties": { + "image_base64": {"type": "string", "description": "Base64 encoded string of the source image file"}, + "x": {"type": "integer", "description": "X coordinate to place the image (default 0)", "default": 0}, + "y": {"type": "integer", "description": "Y coordinate to place the image (default 0)", "default": 0}, + "dither": {"type": "boolean", "description": "Whether to use Floyd-Steinberg dithering (default true)", "default": True} + }, + "required": ["image_base64"] + } } ] }, @@ -280,6 +294,35 @@ class MCPServer: return f"__PBM_BASE64__:{b64}" except Exception as e: raise RuntimeError(f"Screenshot capture failed: {e}") + + elif name == "draw_image": + self.override_active = True + self.override_timeout = time.ticks_ms() + 30000 # 30-sec override + + pbm_b64 = args.get("pbm_base64") + x = int(args.get("x", 0)) + y = int(args.get("y", 0)) + + if not pbm_b64: + raise ValueError("Missing pbm_base64 parameter (preprocessed by bridge)") + + import binascii + pbm_bytes = binascii.a2b_base64(pbm_b64) + + filename = 'temp_recv.pbm' + with open(filename, 'wb') as f: + f.write(pbm_bytes) + + try: + self.display.draw_pbm(filename, x, y, scale=1) + self.display.show() + finally: + import os + try: + os.remove(filename) + except: + pass + return "Image displayed successfully." else: raise ValueError(f"Unknown tool: {name}")