Feat: Hermes PC-Zugriff via MCP (executor + mcp_pc)
- mcp/mcp_pc.py: MCP-Server der PC-Tools an den Executor proxied (pc_shell, pc_screenshot, pc_type, pc_key, pc_open, pc_status) - client/hermes-pc/executor.py: FastAPI auf Port 7777 (Daemon-Reg entfernt) - client/hermes-pc/start.bat, install.bat, requirements.txt - client/hermes-voice/agent_client.py, main.py: agent_client integration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Hermes PC Executor — läuft auf dem Windows PC.
|
||||
Empfängt Tool-Befehle vom MCP-Server der AI Box und führt sie lokal aus.
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import webbrowser
|
||||
from urllib.parse import quote
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI(title="Hermes PC Executor")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
MY_PORT = int(os.environ.get("HERMES_PC_PORT", "7777"))
|
||||
|
||||
|
||||
# ── Shell ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/shell")
|
||||
async def run_shell(req: dict):
|
||||
cmd = req.get("command", "")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
encoding="utf-8", errors="replace",
|
||||
)
|
||||
return {
|
||||
"stdout": result.stdout.strip(),
|
||||
"stderr": result.stderr.strip(),
|
||||
"returncode": result.returncode,
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(408, "Timeout nach 60s")
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Screen ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/screenshot")
|
||||
async def take_screenshot():
|
||||
try:
|
||||
import mss
|
||||
from PIL import Image
|
||||
|
||||
with mss.mss() as sct:
|
||||
monitor = sct.monitors[1]
|
||||
raw = sct.grab(monitor)
|
||||
img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX")
|
||||
img.thumbnail((1280, 720))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=75)
|
||||
return {"screenshot": base64.b64encode(buf.getvalue()).decode()}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Input ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/type")
|
||||
async def type_text(req: dict):
|
||||
text = req.get("text", "")
|
||||
try:
|
||||
import pyautogui
|
||||
pyautogui.write(text, interval=0.03)
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/key")
|
||||
async def press_keys(req: dict):
|
||||
keys = req.get("keys", "")
|
||||
try:
|
||||
import pyautogui
|
||||
parts = [k.strip() for k in keys.split("+")]
|
||||
pyautogui.hotkey(*parts)
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Apps / Browser ─────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/open")
|
||||
async def open_target(req: dict):
|
||||
target = req.get("target", "")
|
||||
try:
|
||||
if target.startswith("http://") or target.startswith("https://"):
|
||||
webbrowser.open(target)
|
||||
else:
|
||||
os.startfile(target)
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/search")
|
||||
async def search_web(req: dict):
|
||||
query = req.get("query", "")
|
||||
url = f"https://www.google.com/search?q={quote(query)}"
|
||||
webbrowser.open(url)
|
||||
return {"ok": True, "url": url}
|
||||
|
||||
|
||||
# ── Health ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "host": socket.gethostname()}
|
||||
|
||||
|
||||
def _get_local_ip() -> str:
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
except Exception:
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
my_ip = _get_local_ip()
|
||||
print(f"Hermes PC Executor läuft auf http://{my_ip}:{MY_PORT}")
|
||||
uvicorn.run(app, host="0.0.0.0", port=MY_PORT)
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
echo === Hermes PC Executor Setup ===
|
||||
python -m venv .venv
|
||||
.venv\Scripts\pip install -r requirements.txt -q
|
||||
echo.
|
||||
echo Fertig. Starte mit start.bat
|
||||
pause
|
||||
@@ -0,0 +1,6 @@
|
||||
fastapi>=0.111.0
|
||||
uvicorn>=0.30.0
|
||||
httpx>=0.27.0
|
||||
mss>=9.0.1
|
||||
Pillow>=10.0.0
|
||||
pyautogui>=0.9.54
|
||||
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
echo === Hermes PC Executor ===
|
||||
echo Port: 7777
|
||||
echo Hermes (AI Box) kann jetzt auf diesen PC zugreifen.
|
||||
echo Fenster offen lassen solange Hermes PC-Zugriff braucht.
|
||||
echo.
|
||||
.venv\Scripts\python executor.py
|
||||
pause
|
||||
Reference in New Issue
Block a user