#!/usr/bin/env python3 """WEVIA Blade MCP Server v1.1 - Extended with apple_* tools Exposes Razer Blade + Apple/iCloud scraping as MCP tools. Run: python3 blade-mcp-server.py Listens: http://0.0.0.0:8765 """ import json, sys, uuid, time, requests from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer BLADE_API = "https://weval-consulting.com/api/blade-api.php" BLADE_KEY = "BLADE2026" WEVIA_APPLE_API = "https://weval-consulting.com/api/wevia-apple-ingest.php" TOOLS = [ # === BLADE CORE (v1.0) === { "name": "blade_exec", "description": "Execute a PowerShell command on the Razer Blade Windows machine. Returns stdout/stderr/exit_code.", "inputSchema": { "type": "object", "properties": { "cmd": {"type": "string", "description": "PowerShell command to execute"}, "timeout": {"type": "integer", "default": 60} }, "required": ["cmd"] } }, { "name": "blade_status", "description": "Get current Razer Blade health: CPU, RAM, disk, uptime, last heartbeat.", "inputSchema": {"type": "object", "properties": {}} }, { "name": "blade_screenshot", "description": "Take a screenshot of the Razer desktop, return URL.", "inputSchema": { "type": "object", "properties": {"label": {"type": "string", "default": "screenshot"}} } }, { "name": "blade_chrome_cdp", "description": "Send a Chrome DevTools Protocol command to the Razer Chrome browser via debug port 9222.", "inputSchema": { "type": "object", "properties": { "url_filter": {"type": "string"}, "js": {"type": "string"} }, "required": ["js"] } }, { "name": "blade_open_url", "description": "Open a URL in the Razer Chrome (keeping user session cookies).", "inputSchema": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]} }, { "name": "blade_send_keys", "description": "Simulate keyboard input on the Razer (via SendKeys).", "inputSchema": {"type": "object", "properties": {"keys": {"type": "string"}}, "required": ["keys"]} }, { "name": "blade_file_read", "description": "Read a file from the Razer filesystem (text files under 1MB).", "inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]} }, { "name": "blade_file_write", "description": "Write a text file to the Razer filesystem.", "inputSchema": { "type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"] } }, # === APPLE / WEVIA (v1.1 NEW) === { "name": "apple_ingest_note", "description": "Ingest a note into WEVIA Apple (auto-extracts entities: people, deadlines, money, OSS, tasks). Available everywhere.", "inputSchema": { "type": "object", "properties": { "title": {"type": "string"}, "body": {"type": "string", "description": "Note content - will be analyzed by IA"} }, "required": ["body"] } }, { "name": "apple_ingest_message", "description": "Ingest a message (SMS/iMessage/WhatsApp) into WEVIA Apple for AI analysis (urgency, deadlines, reco).", "inputSchema": { "type": "object", "properties": { "from": {"type": "string"}, "to": {"type": "string", "default": "me"}, "body": {"type": "string"}, "date": {"type": "string"} }, "required": ["from", "body"] } }, { "name": "apple_status", "description": "Get WEVIA Apple ingestion status: items, tasks pending, alerts, entities count.", "inputSchema": {"type": "object", "properties": {}} }, { "name": "apple_search", "description": "Search across all ingested Apple data (OCR, messages, contacts, notes).", "inputSchema": { "type": "object", "properties": {"q": {"type": "string", "description": "Search query (min 2 chars)"}}, "required": ["q"] } }, { "name": "apple_recommendations", "description": "Get top AI-generated recommendations from ingested iPhone data, sorted P0→P3.", "inputSchema": { "type": "object", "properties": {"priority_filter": {"type": "string", "enum": ["P0","P1","P2","P3","all"], "default": "all"}} } }, { "name": "apple_tasks_pending", "description": "Get pending tasks auto-generated from Apple data (deadlines → task_create).", "inputSchema": {"type": "object", "properties": {}} }, { "name": "apple_mark_task_done", "description": "Mark an Apple-generated task as done.", "inputSchema": { "type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"] } }, { "name": "apple_mac_scrape_photos", "description": "Scrape recent iCloud Photos via AppleScript on Mac (requires Blade agent installed on Mac, NOT Razer Windows).", "inputSchema": { "type": "object", "properties": {"limit": {"type": "integer", "default": 20}} } }, { "name": "apple_mac_scrape_messages", "description": "Scrape recent iMessage/SMS from chat.db on Mac (requires Blade Mac agent + Full Disk Access perm).", "inputSchema": { "type": "object", "properties": {"limit": {"type": "integer", "default": 50}} } } ] def push_blade_task(ps_cmd, label="mcp", priority=100, timeout=60): try: r = requests.post(BLADE_API, data={ "k": BLADE_KEY, "action": "push", "type": "powershell", "cmd": ps_cmd, "label": f"mcp_{label}", "priority": priority }, timeout=15) task_id = r.json().get("task", {}).get("id") if not task_id: return {"ok": False, "error": f"push failed: {r.text[:300]}"} deadline = time.time() + timeout while time.time() < deadline: time.sleep(2) r2 = requests.get(BLADE_API, params={"k": BLADE_KEY, "action": "list", "id": task_id}, timeout=8) try: d = r2.json() for t in d.get("tasks", []): if t.get("id") == task_id: st = t.get("status") if st in ("done", "failed"): return { "ok": st == "done", "status": st, "result": t.get("result", "")[:4000], "error": t.get("error"), "task_id": task_id } break except: pass return {"ok": False, "error": "timeout waiting for blade", "task_id": task_id} except Exception as e: return {"ok": False, "error": f"exception: {str(e)[:200]}"} # ==== Blade core handlers (v1.0) ==== def tool_blade_exec(args): return push_blade_task(args["cmd"], label="exec", priority=200, timeout=args.get("timeout", 60)) def tool_blade_status(args): try: r = requests.get(BLADE_API, params={"k": BLADE_KEY, "action": "status"}, timeout=8) return {"ok": True, "status": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:200]} def tool_blade_screenshot(args): label = args.get("label", "shot").replace(" ", "_") cmd = f""" $dir = "C:\\\\ProgramData\\\\WEVAL\\\\shots" New-Item -ItemType Directory -Path $dir -Force -ErrorAction SilentlyContinue | Out-Null $f = "$dir\\\\{label}-$(Get-Date -Format 'yyyyMMdd_HHmmss').png" Add-Type -AssemblyName System.Windows.Forms,System.Drawing $b = New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height) $g = [System.Drawing.Graphics]::FromImage($b) $g.CopyFromScreen(0, 0, 0, 0, $b.Size) $b.Save($f, 'Png') Write-Host "SHOT_PATH=$f" """ return push_blade_task(cmd, label="shot", priority=200, timeout=30) def tool_blade_chrome_cdp(args): js = args["js"].replace("`", "\\`").replace("$", "\\$") url_filter = args.get("url_filter", "") cmd = f""" try {{ $targets = Invoke-RestMethod -Uri "http://localhost:9222/json" -Method GET -TimeoutSec 5 }} catch {{ Write-Host "NO_CHROME_DEBUG_PORT"; exit 1 }} $filter = '{url_filter}' $tgt = if ($filter) {{ $targets | Where-Object {{ $_.url -match $filter }} | Select-Object -First 1 }} else {{ $targets | Where-Object {{ $_.type -eq 'page' }} | Select-Object -First 1 }} if (!$tgt) {{ Write-Host "NO_TAB_MATCH"; exit 1 }} $wsUrl = $tgt.webSocketDebuggerUrl $ws = New-Object System.Net.WebSockets.ClientWebSocket $cts = New-Object System.Threading.CancellationTokenSource $cts.CancelAfter(30000) $ws.ConnectAsync([Uri]$wsUrl, $cts.Token).Wait() $cmd = @{{id=1; method="Runtime.evaluate"; params=@{{expression=@' {js} '@; awaitPromise=$true; returnByValue=$true}}}} | ConvertTo-Json -Depth 5 -Compress $buf = [Text.Encoding]::UTF8.GetBytes($cmd) $seg = New-Object 'System.ArraySegment[byte]' (,$buf) $ws.SendAsync($seg, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $cts.Token).Wait() $rxBuf = New-Object byte[] 131072 $rxSeg = New-Object 'System.ArraySegment[byte]' (,$rxBuf) $result = $ws.ReceiveAsync($rxSeg, $cts.Token).Result Write-Host ([Text.Encoding]::UTF8.GetString($rxBuf, 0, $result.Count)) """ return push_blade_task(cmd, label="cdp", priority=200, timeout=45) def tool_blade_open_url(args): return push_blade_task(f'Start-Process "{args["url"]}"', label="open", priority=200, timeout=15) def tool_blade_send_keys(args): keys = args["keys"].replace("'", "''") return push_blade_task(f"""Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.SendKeys]::SendWait('{keys}') Write-Host "KEYS_SENT" """, label="keys", priority=200, timeout=15) def tool_blade_file_read(args): path = args["path"].replace("'", "''") return push_blade_task(f"Get-Content -Path '{path}' -Raw -ErrorAction Stop", label="read", priority=200, timeout=15) def tool_blade_file_write(args): path = args["path"].replace("'", "''") import base64 b64 = base64.b64encode(args["content"].encode()).decode() return push_blade_task(f"""$c = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{b64}')) Set-Content -Path '{path}' -Value $c -Force -Encoding UTF8 Write-Host "WROTE" """, label="write", priority=200, timeout=15) # ==== Apple / WEVIA handlers (v1.1) ==== def tool_apple_ingest_note(args): try: r = requests.post(f"{WEVIA_APPLE_API}?action=ingest_structured", headers={"Content-Type": "application/json"}, json={"type": "note", "items": [{"title": args.get("title", "MCP capture"), "body": args["body"]}]}, timeout=30) return {"ok": True, "result": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_ingest_message(args): try: item = { "from": args["from"], "to": args.get("to", "me"), "body": args["body"], "date": args.get("date", time.strftime("%Y-%m-%dT%H:%M:%S")) } r = requests.post(f"{WEVIA_APPLE_API}?action=ingest_structured", headers={"Content-Type": "application/json"}, json={"type": "message", "items": [item]}, timeout=30) return {"ok": True, "result": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_status(args): try: r = requests.get(f"{WEVIA_APPLE_API}?action=status", timeout=10) return {"ok": True, "status": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_search(args): try: r = requests.get(f"{WEVIA_APPLE_API}?action=search", params={"q": args["q"]}, timeout=10) return {"ok": True, "result": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_recommendations(args): try: r = requests.get(f"{WEVIA_APPLE_API}?action=recommendations", timeout=10) data = r.json() recos = data.get("recommendations", []) pf = args.get("priority_filter", "all") if pf != "all": recos = [x for x in recos if x.get("priority") == pf] return {"ok": True, "total": len(recos), "recommendations": recos[:20], "by_priority": data.get("by_priority", {})} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_tasks_pending(args): try: r = requests.get(f"{WEVIA_APPLE_API}?action=tasks", params={"status": "open"}, timeout=10) return {"ok": True, "result": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_mark_task_done(args): try: r = requests.get(f"{WEVIA_APPLE_API}?action=mark_done", params={"id": args["task_id"]}, timeout=10) return {"ok": True, "result": r.json()} except Exception as e: return {"ok": False, "error": str(e)[:300]} def tool_apple_mac_scrape_photos(args): # Placeholder - needs Blade Mac agent (not Windows Razer). # Returns guidance + passes AppleScript via Blade task (if Mac hostname matches) osa = f""" osascript -e 'tell application "Photos" set recentPhotos to (get last {args.get("limit", 20)} media items of container "Library") set output to "" repeat with p in recentPhotos set output to output & (get filename of p) & "|" & (get creation date of p as string) & "\\n" end repeat return output end tell' """ return { "ok": False, "status": "requires_mac_agent", "message": "This tool requires Blade MCP agent on a Mac (not Razer Windows). Install blade-agent-v4-mac.sh on macOS first.", "applescript_to_run": osa.strip(), "fallback": "Use iPhone Shortcut 'Scan WEVIA' + automation iCloud album instead (see /downloads/wevia-shortcut-photos.json)" } def tool_apple_mac_scrape_messages(args): return { "ok": False, "status": "requires_mac_agent", "message": "This tool requires Blade MCP agent on a Mac with Full Disk Access permission.", "sql_to_run": f"SELECT datetime(date/1000000000 + 978307200, 'unixepoch') AS ts, handle.id AS contact, text, is_from_me FROM message LEFT JOIN handle ON message.handle_id = handle.ROWID ORDER BY date DESC LIMIT {args.get('limit', 50)};", "db_path": "~/Library/Messages/chat.db", "fallback": "Manually share messages via iOS Share Sheet using /downloads/wevia-shortcut-messages.json guide" } TOOL_HANDLERS = { "blade_exec": tool_blade_exec, "blade_status": tool_blade_status, "blade_screenshot": tool_blade_screenshot, "blade_chrome_cdp": tool_blade_chrome_cdp, "blade_open_url": tool_blade_open_url, "blade_send_keys": tool_blade_send_keys, "blade_file_read": tool_blade_file_read, "blade_file_write": tool_blade_file_write, "apple_ingest_note": tool_apple_ingest_note, "apple_ingest_message": tool_apple_ingest_message, "apple_status": tool_apple_status, "apple_search": tool_apple_search, "apple_recommendations": tool_apple_recommendations, "apple_tasks_pending": tool_apple_tasks_pending, "apple_mark_task_done": tool_apple_mark_task_done, "apple_mac_scrape_photos": tool_apple_mac_scrape_photos, "apple_mac_scrape_messages": tool_apple_mac_scrape_messages } class MCPHandler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): pass def do_POST(self): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length).decode() try: req = json.loads(body) except: self.send_error(400, "invalid json") return method = req.get("method") req_id = req.get("id") params = req.get("params", {}) result = None error = None if method == "initialize": result = { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "serverInfo": {"name": "wevia-blade-mcp", "version": "1.1.0"} } elif method == "tools/list": result = {"tools": TOOLS} elif method == "tools/call": name = params.get("name") args = params.get("arguments", {}) handler = TOOL_HANDLERS.get(name) if not handler: error = {"code": -32601, "message": f"Unknown tool: {name}"} else: try: r = handler(args) result = {"content": [{"type": "text", "text": json.dumps(r, indent=2, ensure_ascii=False)}]} except Exception as e: error = {"code": -32000, "message": str(e)[:300]} elif method == "ping": result = {} else: error = {"code": -32601, "message": f"Unknown method: {method}"} resp = {"jsonrpc": "2.0", "id": req_id} if error: resp["error"] = error else: resp["result"] = result payload = json.dumps(resp).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) def do_GET(self): if self.path == "/health": payload = json.dumps({"ok": True, "server": "wevia-blade-mcp", "version": "1.1.0", "tools": len(TOOLS)}).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) else: self.send_error(404) if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765 server = ThreadingHTTPServer(("0.0.0.0", port), MCPHandler) sys.stderr.write(f"[WEVIA-BLADE-MCP v1.1] Listening on 0.0.0.0:{port}\n") sys.stderr.write(f"[WEVIA-BLADE-MCP] {len(TOOLS)} tools exposed\n") sys.stderr.flush() server.serve_forever()