feat: MCP-Tools ask_user_choice + memory_read/write/list, Text-Marker entfernt

MCP-Server erweitert um ask_user_choice (Long-Poll), memory_read, memory_write
und memory_list Tools. MCP-Config wird jetzt immer generiert (nicht nur bei
Remote-Verbindung). InternalEndpoint um PendingChoices + choice-respond Endpoint
ergänzt. Frontend differenziert CLI-Modus (choice-respond API) vs. API-Modus.
Gesamter :::ASK_USER_CHOICE::: Text-Marker-Mechanismus aus ClaudeCliService,
AnthropicService, markdown.js und stream-ui.js entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-03-18 09:54:30 +01:00
1 parent 3b068a7866
commit 3e0a69e7a4
10 files changed
+493 -319

No files matched your search

+205 -93
View File
@@ -19,13 +19,90 @@ import os
API_URL = os.environ.get("GOTT_API_URL", "http://localhost:19000")
SESSION_ID = os.environ.get("GOTT_SESSION_ID", "")
AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "")
AGENT_ID = os.environ.get("GOTT_AGENT_ID", "")
HAS_REMOTE = os.environ.get("GOTT_HAS_REMOTE", "false") == "true"
TOOL_NAME = "remote_execute"
TOOL_DESCRIPTION = (
"Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. "
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
"(z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden."
)
TOOLS = []
# Remote-Execute Tool (nur wenn Remote-Verbindung vorhanden)
if HAS_REMOTE:
TOOLS.append({
"name": "remote_execute",
"description": (
"Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. "
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
"(z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden."
),
"inputSchema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "PowerShell-Befehl zum Ausführen auf dem Remote-PC"}
},
"required": ["command"]
}
})
# ask_user_choice Tool (immer verfügbar)
TOOLS.append({
"name": "ask_user_choice",
"description": (
"Stellt dem User eine Frage mit Auswahlmöglichkeiten (2-5 Optionen). "
"Der User wählt eine Option und du arbeitest mit seiner Antwort weiter. "
"Nutze dies wenn du unsicher bist oder es mehrere sinnvolle Wege gibt."
),
"inputSchema": {
"type": "object",
"properties": {
"question": {"type": "string", "description": "Die Frage an den User"},
"choices": {
"type": "array",
"items": {"type": "string"},
"description": "Liste der Auswahlmöglichkeiten (2-5 Optionen)",
"minItems": 2,
"maxItems": 5
}
},
"required": ["question", "choices"]
}
})
# Memory-Tools (immer verfügbar wenn agentId gesetzt)
if AGENT_ID:
TOOLS.append({
"name": "memory_read",
"description": "Liest eine Memory-Datei des Agents. Nutze dies um gespeicherte Erinnerungen, Notizen oder Kontextinformationen abzurufen.",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relativer Pfad zur Memory-Datei (z.B. 'MEMORY.md' oder 'memory/YYYY-MM-DD.md')"}
},
"required": ["path"]
}
})
TOOLS.append({
"name": "memory_write",
"description": "Schreibt oder überschreibt eine Memory-Datei. Nutze dies um wichtige Informationen, Entscheidungen oder Präferenzen dauerhaft zu speichern.",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relativer Pfad zur Memory-Datei (z.B. 'MEMORY.md' oder 'memory/YYYY-MM-DD.md')"},
"content": {"type": "string", "description": "Inhalt der Datei"},
"append": {"type": "boolean", "description": "Wenn true, wird der Inhalt angehängt statt überschrieben (Default: false)"}
},
"required": ["path", "content"]
}
})
TOOLS.append({
"name": "memory_list",
"description": "Listet alle Memory-Dateien des Agents auf.",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Optionaler Unterpfad zum Auflisten (leer = Root)"}
},
"required": []
}
})
def send_response(response):
@@ -59,112 +136,147 @@ def handle_tools_list(req_id):
"jsonrpc": "2.0",
"id": req_id,
"result": {
"tools": [
{
"name": TOOL_NAME,
"description": TOOL_DESCRIPTION,
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "PowerShell-Befehl zum Ausführen auf dem Remote-PC"
}
},
"required": ["command"]
}
}
]
"tools": TOOLS
}
}
def api_call(endpoint, payload):
"""Macht einen API-Call an gott und gibt das Ergebnis zurück."""
url = f"{API_URL}/api/internal/{endpoint}"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
if AUTH_TOKEN:
req.add_header("X-Internal-Token", AUTH_TOKEN)
with urllib.request.urlopen(req, timeout=200) as resp:
return json.loads(resp.read().decode("utf-8"))
def make_result(req_id, text, is_error=False):
"""Erstellt eine MCP Tool-Result Response."""
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": text}],
"isError": is_error
}
}
def handle_remote_execute(req_id, arguments):
"""Führt remote_execute aus."""
command = arguments.get("command", "")
if not command:
return make_result(req_id, "Fehler: Kein Befehl angegeben", True)
result = api_call("remote-execute", {"command": command, "sessionId": SESSION_ID})
output_parts = []
if result.get("stdout"):
output_parts.append(result["stdout"])
if result.get("stderr"):
output_parts.append(f"[stderr] {result['stderr']}")
if result.get("exitCode", 0) != 0:
output_parts.append(f"[Exit Code: {result['exitCode']}]")
text = "\n".join(output_parts) if output_parts else "(keine Ausgabe)"
return make_result(req_id, text, result.get("exitCode", 0) != 0)
def handle_memory_read(req_id, arguments):
"""Liest eine Memory-Datei."""
path = arguments.get("path", "")
if not path:
return make_result(req_id, "Fehler: path erforderlich", True)
result = api_call("memory-read", {"agentId": AGENT_ID, "path": path})
return make_result(req_id, result.get("content", ""))
def handle_memory_write(req_id, arguments):
"""Schreibt eine Memory-Datei."""
path = arguments.get("path", "")
content = arguments.get("content", "")
append = arguments.get("append", False)
if not path:
return make_result(req_id, "Fehler: path erforderlich", True)
result = api_call("memory-write", {"agentId": AGENT_ID, "path": path, "content": content, "append": append})
return make_result(req_id, result.get("message", "OK"))
def handle_memory_list(req_id, arguments):
"""Listet Memory-Dateien auf."""
path = arguments.get("path", "")
result = api_call("memory-list", {"agentId": AGENT_ID, "path": path})
files = result.get("files", [])
return make_result(req_id, "\n".join(files) if files else "(keine Memory-Dateien vorhanden)")
def handle_ask_user_choice(req_id, arguments):
"""Stellt dem User eine Frage und wartet auf Antwort (long-poll)."""
question = arguments.get("question", "")
choices = arguments.get("choices", [])
if not question:
return make_result(req_id, "Fehler: question erforderlich", True)
if not choices or len(choices) < 2:
return make_result(req_id, "Fehler: Mindestens 2 choices erforderlich", True)
# Long-Poll: Blockiert bis der User antwortet (max 5 Min)
# Der Backend-Endpoint /api/internal/ask-user-choice blockiert selbst
result = api_call_long("ask-user-choice", {"sessionId": SESSION_ID})
choice = result.get("choice", "")
return make_result(req_id, f"User hat gewählt: {choice}")
def api_call_long(endpoint, payload):
"""API-Call mit langem Timeout (für Long-Poll Endpoints)."""
url = f"{API_URL}/api/internal/{endpoint}"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
if AUTH_TOKEN:
req.add_header("X-Internal-Token", AUTH_TOKEN)
with urllib.request.urlopen(req, timeout=330) as resp:
return json.loads(resp.read().decode("utf-8"))
TOOL_HANDLERS = {
"remote_execute": handle_remote_execute,
"ask_user_choice": handle_ask_user_choice,
"memory_read": handle_memory_read,
"memory_write": handle_memory_write,
"memory_list": handle_memory_list,
}
def handle_tools_call(req_id, params):
"""Führt ein Tool aus."""
tool_name = params.get("name", "")
arguments = params.get("arguments", {})
if tool_name != TOOL_NAME:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"Unbekanntes Tool: {tool_name}"}],
"isError": True
}
}
handler = TOOL_HANDLERS.get(tool_name)
if not handler:
return make_result(req_id, f"Unbekanntes Tool: {tool_name}", True)
command = arguments.get("command", "")
if not command:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": "Fehler: Kein Befehl angegeben"}],
"isError": True
}
}
# Prüfen ob das Tool auch in TOOLS registriert ist
if not any(t["name"] == tool_name for t in TOOLS):
return make_result(req_id, f"Tool nicht verfügbar: {tool_name}", True)
# API-Call an gott
try:
url = f"{API_URL}/api/internal/remote-execute"
payload = json.dumps({
"command": command,
"sessionId": SESSION_ID
}).encode("utf-8")
req = urllib.request.Request(url, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
if AUTH_TOKEN:
req.add_header("X-Internal-Token", AUTH_TOKEN)
with urllib.request.urlopen(req, timeout=200) as resp:
result = json.loads(resp.read().decode("utf-8"))
# Ergebnis formatieren
output_parts = []
if result.get("stdout"):
output_parts.append(result["stdout"])
if result.get("stderr"):
output_parts.append(f"[stderr] {result['stderr']}")
if result.get("exitCode", 0) != 0:
output_parts.append(f"[Exit Code: {result['exitCode']}]")
text = "\n".join(output_parts) if output_parts else "(keine Ausgabe)"
is_error = result.get("exitCode", 0) != 0
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": text}],
"isError": is_error
}
}
return handler(req_id, arguments)
except urllib.error.HTTPError as e:
body = ""
try:
body = e.read().decode("utf-8")
except Exception:
pass
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"API-Fehler {e.code}: {body}"}],
"isError": True
}
}
return make_result(req_id, f"API-Fehler {e.code}: {body}", True)
except Exception as e:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"Fehler: {str(e)}"}],
"isError": True
}
}
return make_result(req_id, f"Fehler: {str(e)}", True)
def handle_notifications_initialized(params):