2 Commits
7 changed files with 77 additions and 23 deletions

No files matched your search

+11
View File
@@ -4,6 +4,17 @@ 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
- **Queue zu früh „Fertig.“:** Nach Start bleibt Phase `starting`, bis Moonraker `printing` meldet — alter `complete`/`standby` schließt den Job nicht mehr.
## [3.5.0] – 2026-10-19
### Neu
+1 -1
View File
@@ -9,7 +9,7 @@
</p>
<p align="center">
<a href="https://git.gundel.dev/dg/grotti"><img src="https://img.shields.io/badge/Version-3.5.0-e85d04?style=flat-square" alt="Version 3.5.0"></a>
<a href="https://git.gundel.dev/dg/grotti"><img src="https://img.shields.io/badge/Version-3.5.1-e85d04?style=flat-square" alt="Version 3.5.1"></a>
<img src="https://img.shields.io/badge/Python-3.11+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
<img src="https://img.shields.io/badge/PostgreSQL-14+-4169E1?style=flat-square&logo=postgresql&logoColor=white" alt="PostgreSQL">
<img src="https://img.shields.io/badge/Moonraker-Klipper-2ecc71?style=flat-square" alt="Moonraker">
+3 -1
View File
@@ -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
+52 -13
View File
@@ -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.")
+5 -4
View File
@@ -421,16 +421,16 @@ class PrintQueueDispatcher:
from grotti import orders
orders.attach_order_item_to_live_run(wconn, host, name, oid)
# Nicht sofort „done“: sonst greift der nächste Wartende, während
# der Drucker noch druckt (Live-Cache kann kurz FREE bleiben).
# Phase „starting“ lassen, bis Moonraker printing meldet.
# Sofort print_active + alter complete/standby → fälschlich „Fertig.“
store.update_print_queue(
wconn,
item_id,
{
"status": "running",
"message": "Druckt auf %s: %s" % (printer_copy["name"], name),
"message": "Startet auf %s: %s" % (printer_copy["name"], name),
"host": host,
"phase": "print_active",
"phase": "starting",
"phase_started_at": "now",
},
)
@@ -498,6 +498,7 @@ class PrintQueueDispatcher:
"message": "Druckt…",
},
)
# complete/standby vom vorherigen Auftrag: noch warten, nicht „Fertig.“
return
if phase != "print_active":
return
+1 -1
View File
@@ -1,3 +1,3 @@
"""Version von Grotti (SemVer). Bei jeder Änderung CHANGELOG.md mitführen."""
__version__ = "3.5.0"
__version__ = "3.5.1"
+4 -3
View File
@@ -322,13 +322,14 @@ function actionsHtml(state, printer) {
function flowHtml(printer) {
const job = printer.queue_running;
const bits = [];
if (job) {
// Nur Ablauf braucht den Strip (Schritte). Einfacher Queue-Druck: Karte zeigt schon DRUCKT.
if (job && job.is_ablauf) {
const rep = repeatText(job);
const title = job.is_ablauf ? "Ablauf" + (rep ? " " + rep : "") : "Aus der Warteschlange";
const title = "Ablauf" + (rep ? " " + rep : "");
bits.push(`<div class="strip flow">
<div class="flow-head">${icon("queue")}<span class="grow">${escapeHtml(title)}</span>
<button type="button" class="icon-btn danger" data-cancel-run="${job.id}" title="Ablauf abbrechen">${icon("x")}</button></div>
${job.is_ablauf ? stepsHtml(job.step_labels, job.display_step_index != null ? job.display_step_index : job.step_index) : ""}
${stepsHtml(job.step_labels, job.display_step_index != null ? job.display_step_index : job.step_index)}
${job.message ? `<div class="flow-msg">${escapeHtml(phaseText(job))}</div>` : ""}
</div>`);
}