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:
Hitonabi
2026-06-26 23:32:18 +02:00
parent a64b30c429
commit 483eb0b2fb
7 changed files with 307 additions and 2 deletions
+132
View File
@@ -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)
+8
View File
@@ -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
+6
View File
@@ -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
+9
View File
@@ -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
+31
View File
@@ -0,0 +1,31 @@
"""Spricht mit dem Hermes Agent Daemon auf der AI Box."""
import httpx
TIMEOUT = 120
def ask_agent(text: str, screenshot_b64: str | None, settings: dict) -> tuple[str, list]:
"""Schickt Nachricht an den Daemon, gibt (response, tool_calls) zurück."""
daemon_url = settings.get("agent_daemon_url", "http://192.168.178.151:8765")
try:
with httpx.Client(timeout=TIMEOUT) as c:
resp = c.post(f"{daemon_url}/chat", json={
"message": text,
"screenshot": screenshot_b64,
})
resp.raise_for_status()
data = resp.json()
return data.get("response", ""), data.get("tool_calls", [])
except httpx.TimeoutException:
return "Antwort hat zu lange gedauert.", []
except Exception as e:
return f"Agent nicht erreichbar: {e}", []
def get_agent_status(settings: dict) -> dict:
daemon_url = settings.get("agent_daemon_url", "http://192.168.178.151:8765")
try:
with httpx.Client(timeout=5) as c:
return c.get(f"{daemon_url}/status").json()
except Exception:
return {"alive": False}
+7 -2
View File
@@ -12,7 +12,7 @@ import config_manager as cfg
from audio_input import start_recording, stop_recording, transcribe
from audio_output import speak, stop_speaking, play_ding
from screen_capture import capture_screen
from ai_client import ask
from agent_client import ask_agent, get_agent_status
from hotkey_listener import HotkeyListener, KeyCapturer, str_to_display
ctk.set_appearance_mode("dark")
@@ -145,7 +145,12 @@ class HermesApp(ctk.CTk):
ui_event("log_user", text)
screenshot = (capture_screen()
if self.settings["screenshot_enabled"] else None)
response = ask(text, screenshot, self.settings)
ui_event("status", ("thinking", "💭 Denke…", "#3b82f6"))
response, tool_calls = ask_agent(text, screenshot, self.settings)
for tc in tool_calls:
ui_event("log_tool", tc)
ui_event("log_hermes", response)
ui_event("status", ("speaking", "🔊 Spricht…", "#a855f7"))