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 @@
-
+
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"