MCP-Server (Model Context Protocol)¶
Teilweise verfügbar
Das MCP-Framework, die Authentifizierung, das Berechtigungsmodell und ein erster Kern-Werkzeugsatz sind implementiert und aktiv nutzbar. Der vollständige, in der Spezifikation vorgesehene Werkzeugkatalog (rund 30 Werkzeuge, u. a. Setup-Makros, Massen-Anlage von Pflanzen, IPM- und Ernte-Schreibwerkzeuge, die Wissensbasis-Brücke) sowie ein eigenständiger MCP-Prozess mit eigenem Helm-Chart sind noch nicht umgesetzt — der Server läuft heute im Backend-Prozess mit (interne Referenz: REQ-033). Die betroffenen Abschnitte sind unten einzeln gekennzeichnet.
Der MCP-Server macht ausgewählte Kamerplanter-Funktionen für externe LLM-Clients (z. B. Claude Desktop, Claude Code, eigene Agenten) nutzbar — über das offene Model Context Protocol (MCP), ein Protokoll, mit dem Sprachmodelle strukturierte "Werkzeuge" (Tools) eines Systems aufrufen können. So kann ein LLM-Client zum Beispiel direkt fragen "Welche Pflanzen muss ich heute gießen?" und bekommt eine strukturierte Antwort aus deinen echten Daten — ohne dass du dafür eine eigene App öffnen musst.
Was ist der MCP-Server?¶
Anders als die generische REST-API spiegelt der MCP-Server keine 1:1-CRUD-Endpunkte, sondern stellt eine kuratierte, semantisch hochstufige Werkzeugpalette bereit: Ein Werkzeug wie get_due_care_tasks kapselt einen kompletten Anwendungsfall und liefert kompaktes, LLM-freundliches JSON zurück — statt dass das LLM mehrere REST-Aufrufe verketten müsste.
Der MCP-Server ist eine ergänzende, rein maschinelle Schnittstelle für externe Clients — er ersetzt nicht den in die Anwendung eingebauten KI-Assistenten, der Kamerplanter-Nutzer:innen direkt in der App bei Wissensfragen und Chat unterstützt. Beide Funktionen sind komplementär: Der KI-Assistent ist intern für App-Nutzer:innen gedacht, der MCP-Server ist die externe Schnittstelle, über die fremde LLM-Clients Kamerplanter als Werkzeug benutzen.
Aktivieren¶
Der MCP-Server ist standardmäßig deaktiviert. Solange MCP_SERVER_ENABLED nicht auf true gesetzt ist, antworten alle /mcp/*-Endpunkte mit 404 Not Found — die Schnittstelle existiert dann faktisch nicht, analog zum Freischalt-Mechanismus des KI-Assistenten. Details zur Umgebungsvariable siehe Umgebungsvariablen — MCP-Server.
Transport & Endpunkte¶
Der MCP-Server läuft im Backend-Prozess mit und stellt seine Werkzeuge unter /api/v1/mcp/ bereit:
| Methode | Pfad | Zweck |
|---|---|---|
GET | /mcp/tools | REST-freundliche Werkzeug-Übersicht — zeigt die Werkzeuge, die die Rollen des Aufrufers freischalten, samt der Gärten, die der Key abdeckt |
POST | /mcp/tools/{tool_name} | REST-freundlicher Werkzeug-Aufruf mit JSON-Body als Argumenten |
POST | /mcp | Der MCP-Endpunkt: JSON-RPC 2.0 über Streamable HTTP — initialize, tools/list, tools/call, ping |
GET | /mcp | 405 — dieser Server sendet keine server-initiierten Nachrichten; der Transport erlaubt diese Antwort ausdrücklich |
DELETE | /mcp | Beendet die Sitzung aus dem Mcp-Session-Id-Header |
POST | /mcp/rpc | Beibehaltener Alias von POST /mcp (veraltet) |
Nur über API / Betreiber-Konfiguration: Transport
Ein eigenständiger stdio-Transport (Server wird lokal vom Client gestartet, wie es für Claude-Desktop-Konfigurationen typisch ist) ist in der Spezifikation vorgesehen, aber noch nicht umgesetzt — aktuell ist ausschließlich Streamable HTTP verfügbar. Ein MCP-Client verbindet sich über die volle Backend-URL, z. B. https://api.kamerplanter.example.com/api/v1/mcp.
Authentifizierung: dein eigener API-Key¶
Der MCP-Server akzeptiert API-Keys — deinen persönlichen ebenso wie den eines Service Accounts. Niemals akzeptiert werden ein JWT-Access-Token oder eine interaktive Sitzung. Der Key wird als X-API-Key-Header oder als Authorization: Bearer kp_... gesendet und trägt immer das Präfix kp_ (siehe auch Authentifizierung — API-Keys (M2M-Integration)).
Deinen persönlichen Key erstellst du dir selbst über POST /api/v1/auth/api-keys; du kannst ihn jederzeit einzeln widerrufen, ohne dein Passwort zu ändern.
Ein API-Key ist ein Dauerschlüssel
Anders als ein Login-Token läuft ein API-Key nicht nach Minuten ab — genau deshalb eignet er sich für einen dauerhaft laufenden MCP-Client. Behandle ihn wie ein Passwort: Wer ihn hat, kann alles, was du in deinen Gärten kannst. Lege für jeden Client einen eigenen Key an, dann kannst du einzelne gezielt widerrufen.
Du siehst ausschließlich deine eigenen Daten¶
Ein Key gewährt genau die Gärten (Mandanten), in denen sein Konto aktives Mitglied ist — aufgelöst über dieselbe Quelle, auf die auch die normale API zugreift. Über MCP ist dadurch nichts erreichbar, was du nicht auch in der Weboberfläche siehst. Ein Garten, in dem du nicht Mitglied bist, verhält sich exakt so, als gäbe es ihn nicht (not_found) — die Schnittstelle verrät also nicht einmal seine Existenz.
Innerhalb eines Gartens gilt die normale Sichtbarkeit: In einem Gemeinschaftsgarten sehen alle Mitglieder dieselben Pflanzen, in der App wie über MCP.
POST /api/v1/mcp/tools/get_due_care_tasks
X-API-Key: kp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{"urgency": "actionable", "tenant": "mein-garten"}
Mehrere Gärten: der tenant-Parameter¶
Bist du Mitglied in mehreren Gärten, gilt dein Key für alle. Welcher gemeint ist, entscheidest du pro Aufruf über das Argument tenant (den Slug des Gartens). Bei genau einer Mitgliedschaft kannst du es weglassen; bei mehreren ist es Pflicht — der Server rät nicht, sondern fordert dich auf, den Garten zu benennen. Welche Slugs zur Verfügung stehen, verrät dir das Werkzeug list_tenants.
Wichtig ist die Reihenfolge dahinter: Der Server bestimmt erst den Garten und prüft danach, was du dort darfst. Denn deine Rolle kann sich je Garten unterscheiden — im eigenen Garten Admin, im Gemeinschaftsgarten nur Betrachter. Umgekehrt geprüft, hättest du überall die stärkste deiner Rollen.
Key-Kontext auflösen (M2M)¶
Ein separater Endpunkt löst einen rohen Key in seinen Kontext auf — nützlich für einen zukünftigen eigenständigen MCP-Prozess (siehe Statushinweis oben), der den Key nicht selbst validieren kann:
POST /api/v1/auth/service-accounts/validate
Content-Type: application/json
{"api_key": "kp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
Antwort (200):
{
"service_account_key": "sa-abc123",
"display_name": "Diagnose-Bot",
"tenants": [
{
"tenant_key": "t-home",
"tenant_slug": "home",
"role": "viewer",
"mcp_permissions": ["mcp.read"]
}
]
}
Dieser Endpunkt ist bewusst auf Service Accounts beschränkt. Ein ungültiger, widerrufener oder persönlicher Key liefert hier denselben generischen 401 Unauthorized — die API verrät nie, ob überhaupt ein gültiger Key mit anderen Eigenschaften existiert. Für den MCP-Server selbst gilt diese Einschränkung nicht: dort funktioniert dein persönlicher Key.
Key beziehen¶
Über die Oberfläche: Kontoeinstellungen → API-Schlüssel → Anlegen. Der Schlüssel wird genau einmal angezeigt — danach ist nur noch sein Hash gespeichert, er lässt sich nicht erneut anzeigen. Kopieren und direkt eintragen. In derselben Tabelle kannst du jeden Schlüssel einzeln widerrufen.
Über die API: POST /api/v1/auth/api-keys mit {"label": "claude-code"}. Der Schlüssel gilt sofort für alle deine Gärten mit genau den Rollen, die du dort hast. Mit tenant_scope begrenzt du ihn beim Anlegen auf einen einzigen Garten, wenn ein Client nur dort arbeiten soll.
Auch im Light-Modus verfügbar
Eine Light-Instanz kennt keine Benutzerkonten — Anmeldung, Sitzungen und Sicherheitseinstellungen fehlen dort bewusst. Die API-Schlüssel-Verwaltung gibt es trotzdem, denn sie ist die einzige Anmeldeform, die der MCP-Server akzeptiert. Der Schlüssel gehört dort dem System-Benutzer, der Mitglied des Standard-Gartens ist. Zusätzliche Rechte entstehen dadurch nicht: Wer eine Light-Instanz erreicht, hat ohnehin vollen Zugriff — der Schlüssel macht diesen Zugriff nur von außen nutzbar. Genau deshalb gehört eine Light-Instanz nicht ins offene Internet.
Als Service Account — Betreiber-Schritt:
Noch nicht implementiert
Die vollständige, selbstständige Service-Account-Verwaltung (Erstellen, Rotieren, Deaktivieren über die API — siehe Service Accounts & API-Keys) ist spezifiziert, aber noch nicht umgesetzt. Aktuell ist das Anlegen eines Nutzerkontos mit account_type: "service" ein Betreiber-Schritt außerhalb der öffentlichen API, kein Selbstbedienungsfluss (interne Referenz: REQ-023).
Damit ein maschineller MCP-Client heute einen funktionierenden Key bekommt, sind folgende Zutaten nötig:
- Ein Nutzerkonto mit
account_type: "service"(kein Passwort, kein interaktiver Login) — vom Betreiber der Instanz angelegt. - Eine Mandanten-Mitgliedschaft dieses Kontos mit genau der Rolle (
viewer/grower/admin), die dem gewünschten Berechtigungsniveau entspricht. - Ein API-Key für dieses Konto, technisch derselbe Mechanismus wie unter Service Accounts & API-Keys — API-Key verwenden beschrieben — da ein Service Account jedoch nie interaktiv angemeldet ist, kann er den Key nicht selbst über den
/auth/api-keys-Endpunkt anfordern; auch dieser Schritt läuft heute über den Betreiber.
Client einrichten (Claude Code)¶
Claude Code liest MCP-Server aus einer .mcp.json im Projektverzeichnis (oder aus deiner globalen Konfiguration). Trage den Kamerplanter-Server als HTTP-Server ein:
{
"mcpServers": {
"kamerplanter": {
"type": "http",
"url": "https://kamerplanter.example.com/api/v1/mcp",
"headers": {
"X-API-Key": "kp_dein_persoenlicher_api_key"
}
}
}
}
Für eine lokale Entwicklungsumgebung ist die URL http://localhost:8000/api/v1/mcp.
Der Key steht im Klartext in der Datei
.mcp.json gehört nicht ins Git-Repository, wenn ein echter Key darin steht — trage sie in .gitignore ein oder nutze die globale Claude-Code-Konfiguration außerhalb des Projekts. Wer die Datei liest, kann alles, was du in deinen Gärten kannst. Widerrufen kannst du einen Key jederzeit einzeln.
Vorher prüfen, ob die Verbindung steht¶
Bevor du den Eintrag hinzufügst, lohnt ein direkter Test — er zeigt sofort, ob URL und Key stimmen:
# 1. Handschlag: Antwortet der Server als MCP-Server?
curl -sS -X POST https://kamerplanter.example.com/api/v1/mcp \
-H "X-API-Key: kp_dein_persoenlicher_api_key" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}'
# 2. Welche Werkzeuge schaltet mein Key frei?
curl -sS -X POST https://kamerplanter.example.com/api/v1/mcp \
-H "X-API-Key: kp_dein_persoenlicher_api_key" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 3. Ein echter Aufruf: Welche Gärten deckt der Key ab?
curl -sS -X POST https://kamerplanter.example.com/api/v1/mcp \
-H "X-API-Key: kp_dein_persoenlicher_api_key" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_tenants","arguments":{}}}'
Kommt bei Schritt 1 ein 404, ist der MCP-Server auf dieser Instanz nicht freigeschaltet (MCP_SERVER_ENABLED). Ein 401 bedeutet, dass der Key ungültig, widerrufen oder abgelaufen ist.
Danach kannst du im Dialog direkt fragen: "Welche Pflanzen muss ich heute gießen?" — Claude ruft get_due_care_tasks selbst auf. Deckt dein Key mehrere Gärten ab, nenne den gewünschten mit ("…in meinem Balkongarten"), damit das Modell den tenant-Parameter füllen kann.
Transport: Streamable HTTP
Der Server implementiert den Streamable-HTTP-Transport (Protokollrevisionen 2025-06-18, 2025-03-26 und 2024-11-05). Beim initialize handelt er die Revision mit deinem Client aus und vergibt eine Mcp-Session-Id, die der Client danach mitsendet; eine abgelaufene Sitzung beantwortet er mit 404, worauf der Client neu initialisiert. Antworten kommen immer als application/json — vom Transport ausdrücklich erlaubt. Einen server-initiierten Stream bietet er nicht: GET auf den Endpunkt antwortet mit 405. Das ist die vom Transport vorgesehene Antwort und bedeutet, dass Fortschrittsmeldungen bei langen Operationen derzeit nicht möglich sind.
Noch nicht implementiert: Claude Desktop
Claude Desktop startet einen MCP-Server als lokalen Unterprozess und spricht ihn über stdio an — es kann eine HTTP-URL nicht direkt einbinden. Der dafür nötige schlanke Brücken-Client ist spezifiziert, aber noch nicht umgesetzt (interne Referenz: REQ-033). Die obige Konfiguration gilt daher für Claude Code und andere Clients mit HTTP-Transport.
Berechtigungsmodell: mcp.read / mcp.write / mcp.setup¶
Jedes Werkzeug verlangt genau eine von drei MCP-Berechtigungen. Diese sind nicht separat vergebbar, sondern an die Rolle gekoppelt, die dein Konto in dem gerade angesprochenen Garten hat — dieselbe Rolle, die auch für menschliche Mitglieder gilt (Mandanten & Gärten):
| Mandanten-Rolle | mcp.read | mcp.write | mcp.setup | Typischer Einsatz |
|---|---|---|---|---|
| viewer | ✓ | ✗ | ✗ | Nur-Lese-Diagnose-Bot |
| grower | ✓ | ✓ | ✗ | Tagesbetrieb (Pflege quittieren, Pflanzen verschieben/archivieren) |
| admin | ✓ | ✓ | ✓ | Einmaliges Onboarding, Standort-Anlage |
Ein Aufruf ohne die erforderliche Berechtigung wird mit dem Fehlercode permission.denied abgelehnt und im Audit-Log als status: "denied" festgehalten (siehe Audit-Trail & Datenschutz). mcp.setup ist absichtlich die restriktivste Klasse: Sie steuert Standort-Anlage — Zugriffe, die eine ganze Pflanzendaten-Hierarchie betreffen können — und ist deshalb ausschließlich der Rolle admin vorbehalten.
Weil die Rolle je Garten gilt, kann derselbe Key in deinem eigenen Garten schreiben und im Gemeinschaftsgarten, in dem du nur Betrachter bist, dieselbe Aktion verweigert bekommen. Die Werkzeug-Übersicht (GET /mcp/tools) zeigt deshalb alles, was du irgendwo darfst; verbindlich ist die Prüfung beim einzelnen Aufruf.
Werkzeug-Katalog (aktueller Stand)¶
Teilweise verfügbar: Werkzeug-Umfang
Umgesetzt sind derzeit 56 Werkzeuge — sie decken das Lesen weitgehend ab, dazu die fünf Tagebuch-Analyse-Werkzeuge (siehe Tagebuch-Analyse: externe Agenten) und die acht Werkzeuge der Wachstumsphasen-Ebene (siehe Wachstumsphasen und Lebenszyklus). Beim übrigen Schreiben fehlt weiterhin, was die Spezifikation vorsieht: Setup-Makros für Wohnung/Growbox/Freiland-Garten, Massen-Anlage von Pflanzen, Standort- und Bereichsverwaltung sowie das Zurückschreiben einer Ernte (record_harvest) und einer angewandten Behandlung (apply_treatment). Erweiterung ist ein dokumentierter Folgeschritt.
Neu: was ein Analyse-Agent zurückschreiben kann
Fünf Werkzeuge sind hinzugekommen, die extern betriebene Analyse-Agenten brauchten. Jedes ist mit dem Lese-Werkzeug gepaart, das sein Ergebnis wiederfindet — ein Schreibvorgang, den anschließend kein Lese-Werkzeug mehr sichtbar macht, gilt hier als Fehler:
record_feeding_eventerfasst Menge, EC und pH eines Düngevorgangs samt Tankbezug. Bisher stand im Pflegeprotokoll nur „quittiert" — ein Ja/Nein, aus dem sich Unter- und Überversorgung nicht unterscheiden lassen, obwohl beide gegensätzlich zu korrigieren sind. Sichtbar überget_plant_diagnostics.get_plant_diagnosticsliefert den Verlauf statt nur des letzten Werts: EC- und pH-Reihen über einen wählbaren Zeitraum (Zulauf, Nachmessung und Drainage getrennt), Sensor-Momentaufnahme, IPM-Inspektionen, Karenz und jüngste Pflegevorgänge — in einem Aufruf.create_inspectionlegt eine IPM-Inspektion an und behält dabei je Befund die Sicherheit und den betroffenen Pflanzenteil. Ohne sie blieb die Befallshistorie einer rein per Agent betreuten Pflanze für immer leer. Sichtbar überget_plant_inspections.search_plant_knowledgedurchsucht die Wissensbasis und liefert zitierfähige Quellenverweise, damit eine Begründung ihre Quelle benennen kann.assign_nutrient_planbindet einen vorhandenen Nährstoffplan an eine Pflanze. Sichtbar überget_plant_nutrient_plan.clone_nutrient_planleitet aus einem Plan — auch einer globalen Vorlage — eine eigene Kopie ab; die Vorlage bleibt unverändert.set_nutrient_plan_phase_targetssetzttarget_ec_msbzw.reference_ec_msan einer Phase eines eigenen Plans. Einen Plan von Grund auf anzulegen bleibt Aufgabe der Oberfläche.validate_nutrient_plan_coveragemeldet lesend, welche Phasen der Pflanze der gebundene Plan namentlich deckt — deckt er eine nicht, greift eine stille Kalenderwochen-Ersatzregel.
Lese-Werkzeuge (mcp.read)¶
| Werkzeug | Zweck |
|---|---|
list_tenants | Deine Gärten auflisten, mit deiner Rolle je Garten — liefert die Slugs für den tenant-Parameter |
list_plants | Pflanzen auflisten, optional nach Name gefiltert — so wird aus „meine Tomate" der plant_key, den die Schreib-Werkzeuge brauchen |
get_plant | Eine Pflanze im Detail: Art (mit aufgelöstem Namen), Phase, Standort, Substrat (mit aufgelöstem Typ und Namen), Pflanz- und Entfernungsdatum |
get_plant_care_log | Pflegehistorie einer Pflanze — mit reminder_type: "watering" das Gießprotokoll |
get_plant_diagnostics | Diagnose-Momentaufnahme einer Pflanze in einem Aufruf: EC-/pH-Verlauf über einen wählbaren Zeitraum (Zulauf, Nachmessung und Drainage getrennt), Sensorwerte am Standort, IPM-Inspektionen, Karenz und jüngste Pflegevorgänge |
list_diary_entries | Tagebuch-Einträge durchsehen, filterbar nach Pflanze, Art, Eintragstyp, Tag, Analyse-Zustand und Zeitraum — neueste zuerst, mit Messwerten, aber ohne Freitext |
list_plants_at_location | Alle Pflanzen an einem Standort, Beet oder Slot |
list_nutrient_plans | Verfügbare Nährstoffpläne — eigene plus globale Vorlagen |
get_nutrient_plan | Ein Plan mit allen Phasen: NPK-Verhältnis, Ziel-EC, Nährstoffe, Wochenfenster |
get_plant_nutrient_plan | Der Plan, der für eine bestimmte Pflanze gilt |
get_sowing_calendar | Aussaat-, Auspflanz- und Erntefenster je Art, verschoben gegen die Frostdaten deines Standorts |
list_pests / get_pest | Schädlinge suchen — auch nach Schadbild. Das Detail zeigt Gegenmaßnahmen (sanfteste zuerst) und passende Nützlinge, Spezialisten zuerst |
list_diseases / get_disease | Krankheiten: Erreger, Inkubationszeit, auslösende Bedingungen, betroffene Pflanzenteile |
get_treatment | Eine Behandlung im Detail — mit Karenzzeit vor der Ernte, Schutzausrüstung und Anwendung |
get_plant_inspections | Die IPM-Inspektionen einer Pflanze: Befallsdruck, Funde, beobachtete Symptome |
list_fertilizers | Verfügbare Dünger mit EC-Beitrag und Maximaldosis |
calculate_mixing_protocol | Düngerechner: Dosierung je Produkt für dein Zielvolumen und deine Ziel-EC, in der richtigen Mischreihenfolge |
list_cultivars / get_cultivar | Sorten einer Art: Züchter, Merkmale, Saatgut-Typ, Tage bis zur Reife |
list_substrates | Substratkatalog: Medien und ihre Eigenschaften — seit #1195 ein Hybrid-Katalog: die geseedeten Basis-Medien plus die eigenen Mischungen deines Mandanten, sobald du tenant angibst |
get_substrate | Ein einzelnes Substrat im Detail: pH- und EC-Basis, Porosität, CEC, Pufferkapazität, Wiederverwendungsgrenzen. Genau die Zahlen, die zwischen zwei Medien desselben Typs verschieden sind — und über eine sichere Dosierung entscheiden |
preview_substrate_mix | Was eine gewichtete Mischung ergäbe, ohne sie anzulegen. Reine Rechnung; die richtige Gewichtung ist je Eigenschaft eine andere (Volumen für Porosität, Masse für CEC, puffergewichtet für pH), selbst ausrechnen führt zu plausiblen falschen Zahlen |
create_substrate_mix | Eine Mischung anlegen und speichern — sie gehört deinem Mandanten, nicht dem geteilten Katalog |
list_substrate_batches / get_substrate_batch | Die angemischten Chargen eines Substrats mit pH/EC-Verlauf und Zyklenzahl. Damit ist substrate_batch_key einer Pflanze erstmals auflösbar |
check_batch_reusability | Ob diese Charge wiederverwendet werden darf — eine Entscheidung aus ihrer Historie, nicht aus dem Katalog — und welche Behandlungen sie vorher braucht |
set_plant_substrate | Substrat und/oder Charge einer Pflanze setzen, ohne ein anderes Feld anzufassen. Der einzige vorherige Weg war der Voll-Ersatz-PUT, der dabei Standort, Sorte, Name und ausgerechnet die Chargen-Referenz gelöscht hat |
get_location | Die Eigenschaften eines Standorts selbst — Typ, Klimazone, Frostexposition. list_plants_at_location liefert die Pflanzen und nie den Standort, und ob ein Beet frostexponiert ist, entscheidet über die Substratwahl |
list_overwintering_profiles | Überwinterungsprofile: Schutzmethode, Lagerbedingungen, Zeitpunkte |
list_starter_kits | Starter-Kits für den Einstieg |
list_phase_definitions | Wachstumsphasen-Definitionen der Lebenszyklus-Logik — die einzelnen Bausteine, nicht die Abfolgen |
get_species_phase_sequence | Die Phasen-Abfolge, auf der eine Art tatsächlich läuft: Zyklustyp, ob sie sich wiederholt, ob eine Ruhephase nötig ist — dazu die geordneten Phasen mit ihrer effektiven Dauer und den Kennzeichen „Endphase" und „Ernte erlaubt" |
list_phase_sequences | Der Katalog aller Phasen-Abfolgen — damit sich nicht nur feststellen lässt, dass eine Zuordnung falsch ist, sondern auch die richtige benennen |
list_species_by_phase_sequence | Die Umkehrung: alle Arten, die an einer Abfolge hängen. Sitzt eine Zimmerpflanze in derselben Gruppe wie Rosenkohl und Porree, ist das kein Einzelfall, sondern eine Vorlagen-Kollision |
get_species_lifecycle | Der Lebenszyklus einer Art: ein- oder mehrjährig, ob sie nach der Blüte stirbt (monokarp) oder wieder blüht (polykarp), Lebenserwartung, Ruhebedarf |
get_plant_phase_status | Der Phasenstand einer Pflanze: Tage in der Phase, nächste Phase, Zyklusnummer, ob eine Ernte vorgesehen ist — und ein phase_state, das „nie gestartet", „steckt in einer nicht auflösbaren Phase", „zwischen zwei Zyklen" und „läuft" auseinanderhält |
get_plant_phase_history | Der Phasenverlauf einer Pflanze mit Grund, Datum und tatsächlicher Dauer je Übergang |
list_hardiness_zones | Winterhärtezonen mit Temperaturbereichen |
search_glossary | Fachbegriffe aus dem Glossar nachschlagen (VPD, EC, Karenz …) |
search_plant_knowledge | Wissensbasis durchsuchen (RAG) — liefert zitierfähige Quellenverweise mit Score, damit eine Begründung ihre Quelle benennen kann. Mandantenunabhängig; nur die Suchanfrage verlässt die Instanz |
list_species | Pflanzenarten-Katalog auflisten (paginiert) |
get_species_info | Vollständige Stammdaten einer Art: Aussaat-, Blüte- und Erntefenster, Winterhärte, Frostempfindlichkeit, Nährstoffbedarf, Giftigkeit, Mischkultur-Hinweise und die zugehörigen Sorten |
list_planting_runs | Pflanzdurchläufe des Mandanten auflisten, optional nach Status gefiltert |
list_tasks | Aufgaben des Mandanten auflisten, optional nach Status gefiltert |
get_due_care_tasks | Heute fällige/überfällige Pflegeerinnerungen, gruppiert nach Dringlichkeit |
get_harvest_readiness | Erntebereitschafts-Überblick über alle aktiven Pflanzen |
get_mcp_activity | Eigener MCP-Aufrufverlauf des Kontos (Selbstauskunft, siehe unten) |
list_pending_diary_analyses | Arbeitsvorrat der zur KI-Analyse markierten Tagebuch-Einträge — ohne Freitext und ohne Bilder (siehe Tagebuch-Analyse: externe Agenten) |
get_diary_entry | Ein Tagebuch-Eintrag samt Pflanzenkontext, ohne Bilddaten |
get_diary_entry_photos | Die Fotos eines Tagebuch-Eintrags als Bild-Content-Blöcke — das einzige Werkzeug der Palette, das etwas anderes als Text liefert |
Schreib-Werkzeuge (mcp.write)¶
| Werkzeug | Zweck |
|---|---|
confirm_care_task | Pflegeerinnerung für eine Pflanze quittieren ("ich habe gegossen") |
archive_plant | Pflanze als entsorgt/abgegeben/gestorben kennzeichnen — kein Hard-Delete, Verlauf bleibt erhalten |
set_plant_location | Pflanze zu einem anderen Standort/Bereich/Slot verschieben |
add_plant_diary_entry | Einen Tagebuch-Eintrag zu einer Pflanze erfassen (Beobachtung, Problem, Messwert) — nur Text, keine Fotos. measurements benennt die erkannten Größen mit ihrer Einheit im Schlüssel (ec_ms_cm, ph, temperature_c, humidity_percent, height_cm, leaf_count) und nimmt darüber hinaus weiterhin beliebige eigene Schlüssel an |
claim_diary_analysis | Einen wartenden Tagebuch-Eintrag exklusiv beanspruchen (Lease) |
submit_diary_analysis | Das Analyse-Ergebnis eines beanspruchten Tagebuch-Eintrags zurückschreiben |
record_feeding_event | Einen Düngevorgang erfassen: Menge in Litern, EC und pH vor und nach der Gabe, Drainage-EC/-pH und der Tankbezug. Das Pflegeprotokoll kennt nur „quittiert" — hier stehen die Zahlen |
create_inspection | Eine IPM-Inspektion anlegen: Befallsdruck, Symptome und strukturierte Befunde mit Sicherheit (0.0–1.0) und betroffenem Pflanzenteil |
assign_nutrient_plan | Einen vorhandenen Nährstoffplan an eine Pflanze binden (eigener Plan oder globale Vorlage) |
clone_nutrient_plan | Einen Plan als mandanteneigene Kopie ableiten — der einzige richtige Weg, eine geteilte Vorlage zu korrigieren |
set_nutrient_plan_phase_targets | An einer Phase eines eigenen Plans target_ec_ms / reference_ec_ms setzen. Ein ausgelassenes Feld bleibt unverändert; leeren kann das Werkzeug nichts |
transition_plant_phase | Eine Pflanze in eine Phase setzen oder eine falsche korrigieren. Das Ziel wird gegen die Abfolge geprüft, auf der die Art dieser Pflanze läuft — ein fremder Phasenschlüssel würde die Pflanze in einer Phase parken, aus der ihr Lebenszyklus nie wieder herausfindet |
Setup-Werkzeuge (mcp.setup)¶
| Werkzeug | Zweck |
|---|---|
create_site | Standort-Wurzel anlegen (Wohnung, Garten, Balkon, Gewächshaus, Fensterbank, Growzelt) |
assign_species_phase_sequence | Eine Art an eine vorhandene Phasen-Abfolge binden. Verlangt mcp.setup und nicht nur mcp.write, weil Arten und Abfolgen zum gemeinsamen Katalog gehören: eine einzige Bindung ändert den Zeitplan aller Pflanzen dieser Art in jedem Garten. Abfolgen zu definieren bleibt bewusst Aufgabe der Oberfläche |
Jedes Werkzeug prüft die referenzierten Schlüssel (Pflanze, Standort, Bereich, Slot) grundsätzlich gegen den für diesen Aufruf aufgelösten Mandanten. Ein Fremdschlüssel aus einem anderen Mandanten liefert konsequent not_found — niemals permission.denied — damit kein Werkzeug die Existenz fremder Ressourcen verrät.
Wachstumsphasen und Lebenszyklus¶
Die Phasen-Logik steuert Aufgabenplanung, Düngefenster, Erntebereitschaft und Überwinterung (interne Referenz: REQ-003). Über MCP war davon lange nur list_phase_definitions sichtbar — der Katalog der einzelnen Bausteine. Nicht die Abfolgen, zu denen sie zusammengesetzt sind, nicht welche Abfolge für eine Art gilt, und nicht der Phasenstand einer konkreten Pflanze. Eine gewöhnliche Frage wie „läuft diese Pflanze auf der botanisch richtigen Phasen-Abfolge?" war darüber schlicht nicht zu beantworten.
Acht Werkzeuge schließen diese Lücke. Sechs lesen, zwei schreiben — und jedes Schreib-Werkzeug ist mit dem Lese-Werkzeug gepaart, das seine Verweise auflöst, und dem, das sein Ergebnis wiederfindet:
| Schreib-Werkzeug | Löst seine Verweise auf | Macht sein Ergebnis sichtbar |
|---|---|---|
transition_plant_phase | get_species_phase_sequence (liefert die gültigen Phasenschlüssel) | get_plant_phase_status |
assign_species_phase_sequence | list_phase_sequences | get_species_phase_sequence |
Warum phase_state mehr sagt als „keine Phase"¶
get_plant liefert für eine Pflanze ohne Phase nur current_phase_key: null. Darin stecken aber drei verschiedene Zustände, die jeweils etwas anderes verlangen:
phase_state | Bedeutung | Was zu tun ist |
|---|---|---|
never_initialised | Es gibt überhaupt keinen Phasenverlauf — der Lebenszyklus wurde nie gestartet | Eine Startphase setzen |
unresolved | Es gibt einen offenen Verlaufseintrag, aber er zeigt auf keine auflösbare Phase. Die Pflanze steht still, ohne dass es auffällt | Auf eine gültige Phase der eigenen Abfolge korrigieren |
between_cycles | Alle Phasen sind abgeschlossen, keine ist offen — bei einer mehrjährigen Pflanze ein normaler Zustand | Nichts; der nächste Zyklus startet |
in_phase | Die Pflanze läuft | Nichts |
Wie eine Art zu ihrer Abfolge kommt¶
Ist eine Art nicht ausdrücklich gebunden, leitet Kamerplanter die Abfolge aus ihren botanischen Merkmalen ab. Die Regeln greifen in dieser Reihenfolge; die erste, die passt, gewinnt:
- Kurztag-Zierpflanzen, die mehrjährig sind → photoperiodischer Zierpflanzen-Zyklus (Weihnachtsstern, Kalanchoe). Die Einschränkung auf Mehrjährige ist Absicht: einjährige Kurztag-Nutzpflanzen sollen ihren Erntezyklus behalten.
- Monokarpe mehrjährige Aufsitzerpflanzen (Bromelien) → Kindel-Zyklus: Die Mutterpflanze stirbt nach der Blüte, der Nachwuchs führt weiter.
- CAM-Sukkulenten → Zyklus mit kühl-trockener Winterruhe.
- Wuchsform: Farne, Zwiebelpflanzen und Palmen bekommen ihren jeweils eigenen Zyklus.
- Jede weitere mehrjährige Art → immergrüner Blattschmuck-Zyklus (die größte Gruppe im Zimmer).
- Bekannt ein- oder zweijährig → der einjährige Standardzyklus mit Ernte am Ende.
- Alles andere → ebenfalls der immergrüne, sich wiederholende Zyklus.
Fehlende Angaben führen nicht mehr zu einer Ernte
Punkt 7 ist eine Sicherheits- und keine botanische Regel. Fehlt einer Art die Lebenszyklus-Angabe ganz, ist das keine Antwort und darf nicht als „einjährig" gelesen werden. Früher landete eine solche Art auf dem einjährigen Standardzyklus — 126 Tage, mit Ernte und Lebensende am Schluss. Ein immergrüner, mehrjähriger Yucca-Baum war damit 126 Tage nach dem Pflanzen als erntereif und abgeschlossen eingeplant.
Die beiden Irrtümer kosten nicht gleich viel: Eine einjährige Pflanze auf einem mehrjährigen Zyklus verpasst nur einen Ernte-Hinweis, den du weiterhin selbst auslösen kannst. Eine mehrjährige Pflanze auf einem einjährigen Zyklus bekommt eine Ernte und ein Lebensende erfunden, die niemand vorgesehen hat. Deshalb fällt der Zweifelsfall auf den sich wiederholenden Zyklus.
Welche Angaben in diese Entscheidung eingehen — und was passiert, wenn eine davon fehlt — zeigt get_species_info: Es liefert plant_category, photosynthesis_type, growth_habit, indoor_suitable, mature_height_cm und frost_sensitivity. Leere Felder werden weggelassen; ein dünn besetzter Datensatz sieht also auch dünn aus — was selbst schon die Antwort auf „reicht dieser Datensatz für eine verlässliche Zuordnung?" ist.
Ausnahme: Sicherheitsfelder werden nie weggelassen
toxicity, toxicity_severity, allergen_info und allows_harvest stehen immer in der Antwort — ist nichts erfasst, als null. Sonst wäre „zu dieser Art liegen keine Giftigkeitsdaten vor" nicht von „diese Art ist nicht giftig" zu unterscheiden, und ein Agent läse die Lücke als Entwarnung, die niemand gegeben hat. Ausgenommen sind nur diese vier Felder; alle übrigen werden weiterhin weggelassen.
Eine falsche Zuordnung korrigierst du mit assign_species_phase_sequence; welche Arten sonst noch auf derselben Abfolge sitzen, verrät list_species_by_phase_sequence.
Tagebuch-Analyse: externe Agenten¶
Die fünf *_diary_*-Werkzeuge sind der vollständige technische Vertrag für einen extern betriebenen KI-Agenten, der Tagebuch-Einträge analysiert, die ein Nutzer markiert hat (interne Referenz: REQ-050). Die Endnutzer-Sicht — wie du einen Eintrag markierst und wo du ein Ergebnis liest — steht unter Tagebuch. Dieser Abschnitt beschreibt die Gegenseite: das Rezept eines Agenten, der diese Einträge abholt, bearbeitet und das Ergebnis zurückschreibt.
Kamerplanter ruft selbst kein Sprachmodell auf
Diese fünf Werkzeuge sind der einzige Weg, über den ein Sprachmodell überhaupt Tagebuchinhalte zu sehen bekommt — und selbst dabei bleibt die Instanz eine reine Datenquelle und -senke. Es gibt weder einen eingebauten Modellaufruf noch einen Modellschlüssel für diesen Pfad. Ein Agenten-Rezept für dieses Werkzeug-Set liegt in einem eigenen, von Kamerplanter getrennten Repository (kamerplanter-goose) und ist nicht Bestandteil dieses Produkts.
Der Ablauf¶
- Ein Nutzer markiert einen Tagebuch-Eintrag in der Weboberfläche — der Eintrag wechselt in den Zustand
requested. list_pending_diary_analyses(mcp.read) liefert den Arbeitsvorrat — ohne Freitext und ohne Bilder, damit die Antwort klein bleibt.claim_diary_analysis(mcp.write) beansprucht einen Eintrag exklusiv über ein Lease (Vorgabe 15 Minuten, Obergrenze 60 Minuten) und liefert einenlease_token. Ein zweiter Beanspruchungsversuch auf denselben Eintrag scheitert mitconflict.already_claimed. Läuft das Lease ab, ohne dass ein Ergebnis zurückgeschrieben wurde, erscheint der Eintrag wieder im Arbeitsvorrat.get_diary_entry(mcp.read) liefert Text, Tags, Messwerte, den Umgebungs-Schnappschuss (environment) und den Pflanzenkontext — ohne Bilddaten. Der Schnappschuss steht in einem eigenen Feld nebenmeasurements, nie darin:measurementsist, was ein Mensch getippt hat,environmentist, was ein Gerät gemeldet hat, und jeder Wert trägtsource,measured_atundorigin(location|site|weather).environment_statussagt, was eine leere Liste bedeutet —no_source(„nichts misst diese Pflanze") ist etwas anderes alsunavailable(„die Messung kam nicht durch").get_diary_entry_photos(mcp.read) liefert die Fotos als Bild-Content-Blöcke, damit ein bildverstehendes Modell sie direkt sieht (siehe Bild-Auslieferung unten).- Der Agent ruft das Sprachmodell auf, das der Nutzer selbst betreibt und bezahlt.
submit_diary_analysis(mcp.write) schreibt das Ergebnis mit gültigemlease_tokenzurück und setzt den Zustand aufcompletedoderfailed.
Bild-Auslieferung¶
get_diary_entry_photos ist das einzige Werkzeug der Palette, dessen Nutzdaten nicht vollständig in structuredContent liegen: Die Antwort trägt zusätzlich image-Content-Blöcke (Basis-64, mimeType: image/webp), einen je geliefertem Foto, in derselben Reihenfolge wie im strukturierten photos-Feld.
Ausgeliefert werden ausschließlich die vorhandenen 512- oder 1280-px-WebP-Renditions — niemals das Originalfoto. Renditions tragen keine EXIF-Daten, auch dann nicht, wenn die Instanz STORAGE_STRIP_EXIF=false gesetzt hat; jenes Setting betrifft nur die Originaldatei (siehe Umgebungsvariablen — Object Storage).
Die Gesamt-Nutzlast eines Aufrufs ist über MCP_MAX_IMAGE_PAYLOAD_MB gedeckelt (Vorgabe 4 MB, Base-64-kodiert; siehe Umgebungsvariablen — MCP-Server). Wird sie überschritten, antwortet das Werkzeug mit payload.too_large und benennt die betroffenen Fotos sowie eine kleinere Rendition, mit der der Abruf passen würde — es wird nie still gekürzt. Fehlt eine Rendition noch, erscheint das betroffene Foto in pending mit status: "thumbnail_pending" (Erzeugung angestoßen, später erneut abrufbar) oder status: "unavailable" (wird nie entstehen, z. B. weil der Anhang fehlt) — der Aufruf selbst bleibt in beiden Fällen erfolgreich.
Fehlercodes¶
Jeder Fehler eines der fünf Werkzeuge kommt als Werkzeug-Ergebnis mit isError: true, nie als JSON-RPC-error — derselbe Vertrag wie für die übrige Palette (siehe Fehlerbehandlung). error_code ist maschinenlesbar und stabil, message ist für Menschen gedacht und darf sich ändern.
error_code | Bedeutung |
|---|---|
not_found | Mandant oder Eintrag existiert nicht oder liegt außerhalb des aufgelösten Mandanten — auch bei einem fremden Eintrag, nie permission.denied |
permission.denied | Die Rolle im aufgelösten Mandanten reicht für das Werkzeug nicht |
validation.error | Eingabe verletzt eine Feldregel (z. B. confidence außerhalb 0.0–1.0, fehlendes summary bei status: completed) |
validation.tenant_required | tenant fehlt, obwohl der Schlüssel mehr als eine Mitgliedschaft hat |
conflict.already_claimed | Der Eintrag ist bereits beansprucht und das Lease noch gültig |
conflict.concurrent_update | Die Dokumentrevision hat sich zwischen Lesen und Setzen geändert — ein sofortiger Wiederholungsversuch ist hier richtig |
conflict.not_claimed | submit_diary_analysis auf einen Eintrag, der nicht in_progress ist |
conflict.lease_expired | Der lease_token passt nicht (mehr) zum aktuellen Lease |
payload.too_large | Die Bild-Nutzlast des Aufrufs überschreitet MCP_MAX_IMAGE_PAYLOAD_MB |
Was ein Rezept nicht selbst entscheiden muss¶
- Der Vorbehalt (
disclaimer) im Ergebnis wird serverseitig gesetzt — ein Agent kann ihn weder weglassen noch abschwächen. - Ob ein Nutzer überhaupt markieren darf, prüft der Server bei jedem
mcp.write-Aufruf serverseitig (Rolle, Autorschaft, Einwilligungdiary_ai_analysis, Betriebsmodus) — ein Rezept muss diese Regel nicht nachbilden. - Ein Eintrag ohne Fotos ist kein Fehlerfall;
get_diary_entry_photosliefert dannphotos: []mit nur dem Text-Block.
Einen Eintrag schreiben ist eine andere Aufgabe¶
add_plant_diary_entry (mcp.write) gehört zur allgemeinen Werkzeug-Palette, nicht zu den fünf oben: Damit dokumentiert ein Agent eine Beobachtung — Text, Tags, Messwerte — statt sie zu analysieren. Daraus folgen zwei Grenzen:
- Ein neu geschriebener Eintrag wird nicht zur Analyse eingereiht. Das Markieren bleibt eine Nutzerhandlung; ein Agent kann sich also keine eigene Arbeit erzeugen, und die Einwilligungsprüfung auf dem Markier-Pfad wird nie umgangen.
- Das Werkzeug nimmt keine Foto-Referenzen entgegen. Ein Foto anzuhängen setzt voraus, es selbst hochgeladen zu haben (oder die Rolle Leitung zu tragen), und MCP hat keinen Upload-Weg — Fotos kommen über die Weboberfläche an den Eintrag.
Wiederfinden lässt sich das Geschriebene mit list_diary_entries (mcp.read). Das Werkzeug durchsucht die Einträge des Gartens nach Pflanze, Eintragstyp, Tag, Analyse-Zustand und Zeitraum und liefert je Zeile Titel, Tags und Messwerte — den Freitext aber nicht. Der kommt aus get_diary_entry, einem bewussten Einzelabruf. Eine durchblätterbare Liste aller Beobachtungstexte wäre etwas anderes als das gezielte Lesen eines Eintrags.
Antwortformat¶
Jedes Werkzeug liefert ein kompaktes, LLM-freundliches JSON mit drei Pflichtfeldern:
{
"summary": "3 Pflanzen müssen heute gegossen werden.",
"data": { "count": 3, "items": [ /* ... */ ] },
"links": [
{ "type": "ui", "url": "/t/home/care" },
{ "type": "api", "url": "/api/v1/t/home/care/dashboard" }
]
}
summary ist eine Ein-Satz-Zusammenfassung für das LLM, data das strukturierte Ergebnis, links verweisen den Endnutzer auf die passende Stelle in der Oberfläche bzw. der REST-API.
Dry-Run und Idempotenz¶
Jedes Schreibwerkzeug akzeptiert zwei zusätzliche, optionale Argumente:
dry_run: bool(Standardfalse) — beitruewird nur der geplante Effekt zurückgeliefert, ohne dass irgendetwas gespeichert wird. Damit kann ein LLM-Client eine geplante Aktion erst dem Menschen zur Bestätigung vorlegen, bevor er sie tatsächlich ausführt.idempotency_key: str(optional) — identische Keys desselben Kontos, Mandanten und Werkzeugs liefern innerhalb von 24 Stunden das ursprüngliche Ergebnis erneut, statt eine zweite Ressource anzulegen. Das schützt vor Doppel-Aktionen bei LLM-Retries (z. B. wenn eine Netzwerkantwort verloren geht).
Eine Wiederholungs-Antwort ist an "idempotent_replay": true erkennbar:
{
"summary": "Confirmed 'watering' for plant 'p-42'.",
"data": { "plant_key": "p-42", "reminder_type": "watering" },
"dry_run": false,
"idempotency_key": "confirm-2026-07-12-001",
"idempotent_replay": true,
"links": [{ "type": "ui", "url": "/t/home/care" }]
}
Idempotenz-Datensätze werden automatisch nach 24 Stunden gelöscht.
Audit-Trail und Datenschutz¶
Jeder Werkzeug-Aufruf wird protokolliert — unabhängig davon, ob er erfolgreich war, wegen fehlender Berechtigung abgelehnt wurde oder ein dry_run war. Der Eintrag enthält Konto, Mandant, Werkzeugname, einen SHA-256-Hash der Argumente (niemals Klartext), Antwortgröße, Dauer und Status — nie den API-Key selbst und keine personenbezogenen Freitextinhalte wie Tagebucheinträge.
Ein Konto kann seinen eigenen Aufrufverlauf über das Werkzeug get_mcp_activity oder direkt per REST einsehen:
Die Antwort enthält die letzten Einträge (Werkzeugname, Status, Antwortgröße, Dauer, Fehlerklasse, Zeitstempel) — keine Argumente im Klartext. Audit-Einträge werden nach 90 Tagen automatisch entfernt (siehe Datenschutz & DSGVO).
Häufige Fragen¶
Kann ich mein persönliches Konto am MCP-Server nutzen?
Ja — erstelle dir über POST /api/v1/auth/api-keys einen API-Key und sende ihn als X-API-Key. Nicht verwendbar sind ein JWT-Access-Token oder eine interaktive Sitzung; MCP authentifiziert ausschließlich API-Keys.
Kann ein MCP-Client auf mehrere Mandanten gleichzeitig zugreifen?
Ja, sofern das Konto in mehreren Mitglied ist. Der Key gilt dann für alle, und jeder Aufruf benennt den handelnden Garten über das Argument tenant. Wer einen Key auf einen einzigen Garten begrenzen will, setzt beim Anlegen tenant_scope.
Kann ein MCP-Client fremde Gärten sehen?
Nein. Ein Key erreicht genau die Gärten, in denen sein Konto aktives Mitglied ist. Jeder andere Garten liefert not_found — dieselbe Antwort wie für einen Garten, den es gar nicht gibt. Die Schnittstelle taugt damit nicht dazu, fremde Gärten aufzuspüren.
Was passiert, wenn ich versehentlich einen idempotency_key wiederverwende, den ich schon für ein anderes Werkzeug genutzt habe?
Nichts Falsches — die Wiederholungserkennung ist zusätzlich nach Werkzeugname und Mandant gescopet. Derselbe Schlüssel bei einem anderen Werkzeug oder in einem anderen Mandanten löst also keine Wiederholung aus.
Läuft der MCP-Server als eigener Prozess, den ich separat skalieren kann?
Aktuell nicht — der MCP-Server läuft im selben Backend-Prozess mit und teilt sich dessen Ressourcen. Ein eigenständiger Prozess mit eigenem Helm-Chart ist als Erweiterung vorgesehen (siehe Statushinweis oben).