diff --git a/CHANGELOG.md b/CHANGELOG.md index 925b11b..fba58c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,11 @@ Alle Änderungen an Grotti. Format nach [Keep a Changelog](https://keepachangelo ## [Unreleased] +## [3.5.1] – 2026-10-20 + ### Geändert +- **MCP:** Server-Anweisung für Clients (Ablauf, confirm, Filament-Pause, Platten-Quittung, kein G-code), damit der Agent die Tools in der richtigen Reihenfolge nutzt. - **Wand:** „Aus der Warteschlange“-Strip nur noch bei Ablauf (mit Schritten); einfacher Queue-Druck zeigt nur DRUCKT. ### Behoben diff --git a/README.md b/README.md index cd46f3c..f40f184 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Version 3.5.0 + Version 3.5.1 Python 3.11+ PostgreSQL Moonraker diff --git a/docs/internal/MCP.md b/docs/internal/MCP.md index 27fb06f..20bec3d 100644 --- a/docs/internal/MCP.md +++ b/docs/internal/MCP.md @@ -1,6 +1,8 @@ # MCP-Server -Stand **15.10.2026**. Agenten steuern Grotti nur über die HTTP-API mit einem API-Token. +Stand **19.10.2026**. Agenten steuern Grotti nur über die HTTP-API mit einem API-Token. + +Beim Connect bekommt der Client die Server-Anweisung aus `grotti/mcp_server.py` (`INSTRUCTIONS`): Deutsch antworten, Host und Datei-id nachschlagen, erst lesen dann schreiben, `confirm=true` nur auf klaren Wunsch, Warteschlange vor Sofortstart, Filament-Pause nicht blind fortsetzen, `clear_bed` ist nur eine Quittung, kein G-code und kein Firmware-Restart. ## Start diff --git a/grotti/mcp_server.py b/grotti/mcp_server.py index 9dd690d..259ef03 100644 --- a/grotti/mcp_server.py +++ b/grotti/mcp_server.py @@ -57,6 +57,51 @@ def _write_guard(): raise RuntimeError("MCP läuft im Read-only-Modus (GROTTI_MCP_READ_ONLY).") +# Wird beim Connect an den Client gegeben (Claude, Cursor, …). +INSTRUCTIONS = """\ +Grotti steuert eine 3D-Drucker-Farm (Anycubic Kobra, Klipper/Moonraker). +Antworte dem Nutzer auf Deutsch. Handle nur über diese Tools. + +Identität +- Drucker über das Feld host (IP). Den Namen nur zur Anzeige nutzen. Hosts nie raten: erst list_printers oder get_printer. +- Dateien über die numerische id aus list_files (Feld id), nicht über den Dateinamen allein. +- Vor jedem Schreibzugriff den aktuellen Stand lesen. + +Lesen +- Farm: list_printers (live=true). Ein Drucker: get_printer(host). +- Läuft: list_running. Warteschlange: list_queue. Ereignisse: list_events. +- Bibliothek: list_files. Spulen: list_spools. Filament-Fehlmenge: bedarf_lesen. +- Kundenaufträge: auftraege_lesen. Verschleißteile: wartung_lesen. + +Zustand (Feld state) +- printing: druckt. paused: Pause. standby: frei. complete: fertig, Platte oft noch belegt. +- error: Fehler, nicht einfach neu starten. offline: nicht erreichbar. +- progress ist 0…1. filament_empty oder pause_reason bei Pause heißt Filament leer. + Dann dem Nutzer sagen, die neue Spule in Grotti dem Drucker zuzuweisen, und erst danach resume_printer. + Eine Filament-Pause nicht blind fortsetzen. + +Schreiben +confirm=true nur setzen, wenn der Nutzer diese Aktion in diesem Zug klar will. Nicht vorauseilend bestätigen. +- enqueue_when_free: Datei in die Warteschlange. host leer = Grotti wählt den nächsten Drucker mit passender Sorte. host gesetzt = dieser Drucker, sobald er frei ist und die Platte klar ist. Das ist der normale Weg. +- enqueue_print: sofort senden und starten. Nur wenn der Nutzer einen Sofortstart auf einem genannten freien Drucker will (state standby, Platte frei). Große Dateien brauchen oft etwa eine Minute bis zum Start. +- Den Material-Match macht Grotti (PLA Meta ist nicht Matte und nicht Silk). Eine falsche Maschine nicht erzwingen. Eine Ablehnung wörtlich weitergeben. +- bed_leveling=true nur auf ausdrücklichen Wunsch (Bettnetz / Auto-Level). +- pause_printer und resume_printer während des Drucks, ohne confirm. +- cancel_printer bricht den Druck ab und braucht confirm=true. Bei Unklarheit erst nachfragen. +- clear_bed ist nur die Quittung „Platte ist leer“ und schiebt nichts physisch. Erst aufrufen, wenn der Nutzer sagt, das Teil ist herunter. Ohne diese Quittung startet der nächste „wenn frei“-Job nicht. +- steer_temp (Düse und Bett in °C), steer_speed (Prozent), steer_fan (0–100): nur diese Werte. Kein freier G-code. + +Grenzen +- Keine G-code-Konsole, kein Firmware-Restart, kein E-Stop, keine Benutzer- oder Token-Verwaltung. Diese Lücken nicht umgehen. +- Im Read-only-Modus (GROTTI_MCP_READ_ONLY) schlagen Schreib-Tools fehl. Das sagen und nicht wiederholen. +- Gesperrte Drucker und ausgeschaltete Automatik starten keine Warteschlangen-Jobs. Den Fehler nennen. + +Antworten +- Kurz und konkret: Druckername, host, Datei, Zustand, was du getan hast. +- Den Fehlertext von Grotti zitieren. Keinen Workaround außerhalb der Tools erfinden. +""" + + def main(): try: from mcp.server.fastmcp import FastMCP @@ -67,23 +112,17 @@ def main(): ) sys.exit(1) - server = FastMCP( - "grotti", - instructions=( - "Grotti steuert die 3D-Drucker-Farm. Destruktive Aktionen brauchen " - "confirm=true. Kein freier G-code, kein Firmware-Restart." - ), - ) + server = FastMCP("grotti", instructions=INSTRUCTIONS) @server.tool() def list_printers(live: bool = True) -> str: - """Druckerliste mit optionalem Live-Zustand.""" + """Alle Drucker. live=true liefert Zustand, Temps, Fortschritt, Dateiname. Host ist die IP.""" q = "?live=1" if live else "?live=0" return json.dumps(_api("GET", "/drucker" + q), ensure_ascii=False, indent=2) @server.tool() def get_printer(host: str) -> str: - """Einen Drucker anhand Host/IP aus der Live-Liste.""" + """Einen Drucker anhand host (IP) aus der Live-Liste. Host vorher über list_printers holen.""" rows = _api("GET", "/drucker?live=1") or [] for row in rows: if row.get("host") == host: @@ -115,7 +154,7 @@ def main(): @server.tool() def list_files(limit: int = 50) -> str: - """Dateien in der Bibliothek.""" + """Dateien in der Bibliothek. Die numerische id ist die file_id fürs Einreihen und Starten.""" rows = _api("GET", "/dateien") or [] return json.dumps(rows[: max(1, min(limit, 200))], ensure_ascii=False, indent=2) @@ -181,7 +220,7 @@ def main(): @server.tool() def clear_bed(host: str, confirm: bool = False) -> str: - """Platte als geräumt quittieren (kein physisches Schieben).""" + """Quittung, dass der Nutzer die Platte geleert hat. Schiebt nichts. confirm muss true sein. Danach kann die Warteschlange den nächsten Job starten.""" _write_guard() if not confirm: raise RuntimeError("Quittierung braucht confirm=true.") @@ -201,7 +240,7 @@ def main(): confirm: bool = False, bed_leveling: bool = False, ) -> str: - """Datei in die Warteschlange („wenn frei“). confirm muss true sein.""" + """Datei in die Warteschlange. host leer: Grotti wählt den nächsten Drucker mit passender Sorte. host gesetzt: dieser Drucker, sobald frei und Platte klar. confirm muss true sein. Normaler Weg zum Drucken.""" _write_guard() if not confirm: raise RuntimeError("Einreihen braucht confirm=true.") @@ -220,7 +259,7 @@ def main(): confirm: bool = False, bed_leveling: bool = False, ) -> str: - """Sofort senden und starten. confirm muss true sein.""" + """Sofort an diesen host senden und starten. Nur bei freiem Drucker (standby, Platte frei) und klarem Sofortstart-Wunsch. confirm muss true sein.""" _write_guard() if not confirm: raise RuntimeError("Start braucht confirm=true.") diff --git a/grotti/version.py b/grotti/version.py index 0f49bb1..d002ae5 100644 --- a/grotti/version.py +++ b/grotti/version.py @@ -1,3 +1,3 @@ """Version von Grotti (SemVer). Bei jeder Änderung CHANGELOG.md mitführen.""" -__version__ = "3.5.0" +__version__ = "3.5.1"