Files
html/api/blade-mcp-server.py
opus bc98f1f0ea
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-commit via WEVIA vault_git intent 2026-04-20T01:44:00+00:00
2026-04-20 03:44:00 +02:00

458 lines
18 KiB
Python

#!/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()