Private Rezeptverwaltung für den Eigengebrauch: Rezepte standardisiert erfassen, Portionen zuverlässig umrechnen, daraus einen Wochenplan und eine Einkaufsliste erzeugen — und das Ganze zusätzlich über einen MCP-Server für einen KI-Assistenten zugänglich machen.
Kein Login, keine Benutzerverwaltung, keine Cloud. Eine SQLite-Datei, ein Container, ein Volume.
- Rezepte verwalten — anlegen, bearbeiten, löschen, durchsuchen (auch nach Zutaten)
- Portionen umrechnen, ohne dass das Ergebnis unbrauchbar wird (siehe unten)
- Importieren aus einer URL, von einem Foto oder aus kopiertem Text — KptnCook-Links vollständig, samt aller Schritte und Bild
- Wochenplan mit Frühstück/Mittag/Abend/Snack
- Einkaufsliste, die gleiche Zutaten über alle Rezepte hinweg zusammenfasst und nach Warengruppen sortiert
- Kochmodus — ein Schritt pro Bildschirm, Wisch-Navigation, hörbarer Timer, Display bleibt an
- Nährwerte je Portion, KI-geschätzt und von Hand korrigierbar
- Fotos je Rezept: hochladen, Titelbild wählen, löschen
- HTTPS — über Tailscale mit echtem Zertifikat, im LAN selbst signiert
- Installierbar (PWA) und offline lesbar — was du schon geöffnet hast, bleibt erreichbar, wenn das Küchen-WLAN aussetzt
- Hell und dunkel, nach Systemeinstellung; Drucklayout für Rezept und Einkaufsliste
- MCP-Server unter
/mcpmit 17 Tools
Jede Zutatenmenge wird in vier getrennten Ebenen gespeichert:
| Ebene | Beispiel | Wofür |
|---|---|---|
| Wie eingegeben | „2 EL" | Anzeige — „1 Tasse Mehl" bleibt eine Tasse |
| Kanonisch | 30 ml | Rechnen, Vergleichen, Zusammenfassen |
| Skalierverhalten | sublinear |
Wie sich die Menge bei anderen Portionen verhält |
| Rundung | nice |
Wie das Ergebnis am Ende dasteht |
Der Unterschied zeigt sich beim Verdoppeln. Ein naiver Portionsrechner macht aus einem Rezept für 4 bei 10 Portionen das hier:
2,5 mal Öl zum Braten 5–7 1/2 Zehen Knoblauch 1 3/8 EL Majoran
ChefMind macht daraus das:
Öl zum Braten (nicht skaliert) 5–8 Zehen Knoblauch 4 TL Majoran
Dahinter stehen vier Regeln:
fixed— „Öl zum Braten", Salzwasser, Mehl zum Ausrollen. Skaliert nie.sublinear— Gewürze mit Exponent 0,8, Triebmittel mit 0,7. Die vierfache Menge Teig will nicht die vierfache Hefe.stepped— Dosen und Packungen. Man kauft keine 1,4 Dosen.linear— alles andere, der Normalfall.
Dazu kommt Küchenlogik statt Mathematik: Stückeinheiten (Zehe, Scheibe, Stange) bleiben ganzzahlig, Löffelmengen runden auf Viertel, Gewichte auf Werte, die eine Küchenwaage anzeigen kann. Gerundet wird sichtbar — neben „5 Eier" steht ein Hinweis, dass rechnerisch 4,5 herauskämen. Aus 4,5 Eiern still 5 zu machen verändert einen Kuchen, und das gehört nicht verschwiegen.
Zeiten und Temperaturen skalieren nie mit. Ein doppelter Braten braucht länger, aber nicht um einen Faktor, den man ausrechnen kann.
Für Backrezepte gibt es den Schalter exakte Mengen: dann bleibt 106,7 g Mehl stehen statt auf 105 g gerundet zu werden, weil beim Teig die Hydration zählt.
Skalierregeln werden pro Zutat kopiert, nicht verknüpft. Wird die Heuristik später verbessert, ändern sich bestehende Rezepte nicht von selbst.
make upDas installiert die Abhängigkeiten, legt .env an, wendet die Migrationen an,
spielt zwei Beispielrezepte ein und startet den Entwicklungsserver auf
http://localhost:3000. Jeder Schritt ist idempotent — make up lässt sich
gefahrlos wiederholen und überschreibt weder .env noch vorhandene Rezepte.
make allein zeigt alle Befehle. Ohne make geht es genauso von Hand:
npm install
cp .env.example .env # für den reinen URL-Import reicht die Datei unverändert
npm run db:migrate
npm run db:seed # zwei Beispielrezepte, optional
npm run dev # http://localhost:3000Alles über .env, Vorlage in .env.example.
| Variable | Bedeutung |
|---|---|
CHEFMIND_DATA_DIR |
Verzeichnis für Datenbank, Fotos und Zertifikate (Compose) |
CHEFMIND_DB_PATH |
Pfad zur SQLite-Datei |
CHEFMIND_UPLOAD_DIR |
Ablage der Fotos |
CHEFMIND_AI_PROVIDER |
anthropic, openrouter oder none |
ANTHROPIC_API_KEY |
Key für Anthropic |
OPENROUTER_API_KEY |
Key für OpenRouter |
CHEFMIND_AI_MODEL |
Modell — bei Anthropic optional (Standard claude-opus-5), bei OpenRouter Pflicht |
CHEFMIND_DEFAULT_SERVINGS |
Portionszahl, mit der Rezepte sich öffnen (Standard 4) |
CHEFMIND_KPTNCOOK_API_KEY |
Nur nötig, wenn KptnCook seinen Schlüssel wechselt |
CHEFMIND_KPTNCOOK_LANG |
Sprache der KptnCook-Rezepte, Standard de |
CHEFMIND_TLS_HOSTS |
Zusätzliche Namen im selbst signierten Zertifikat |
CHEFMIND_ALLOW_PUBLIC_ACCESS |
1 hebt die Beschränkung auf private Adressen auf |
CHEFMIND_ALLOWED_HOSTS |
Zusätzliche Hostnamen ohne Port — nur für eine eigene Domain nötig |
CHEFMIND_MCP_READONLY |
1 = nur lesende MCP-Tools |
CHEFMIND_MCP_TOKEN |
Optionales Bearer-Token für /mcp, leer = kein Auth |
Ohne KI-Key funktioniert alles außer dem Foto- und Textimport. Der URL-Import
läuft trotzdem: er liest die schema.org/Recipe-Daten, die fast jede Rezeptseite
für Google einbettet — exakt, kostenlos und zuverlässiger als jede Bilderkennung.
Die KI wird nur beim Import eingesetzt, nie beim Kochen oder Rechnen. Ein Foto- Import kostet je nach Modell etwa 1–3 Cent.
| Weg | Wie es funktioniert |
|---|---|
| URL | schema.org/Recipe aus der Seite; nur wenn die fehlt, springt die KI ein |
| KptnCook-Link | Über die Schnittstelle der App: alle Schritte, Nährwerte, Bild — ohne KI |
| Rezeptfoto | Kochbuchseite, Rezeptkarte oder Handschrift; mehrere Bilder eines Rezepts werden zusammengesetzt |
| Foto vom Gericht | Die KI erkennt das Gericht und erfindet ein passendes Rezept |
| Text | Kopierter Rezepttext |
Ein aus einem Gerichtsfoto rekonstruiertes Rezept wird als sourceType: 'ai'
gespeichert und überall sichtbar gekennzeichnet — in der Liste, auf der
Detailseite und in der MCP-Ausgabe. Eine Rekonstruktion darf sich nicht als
abgeschriebenes Rezept ausgeben.
Ein in der App geteilter Link (mobile.kptncook.com/recipe/…) wird erkannt und
nicht von der Webseite gelesen. Diese Seite ist Absicht eine Vorschau: Titel,
Portionen, Zeit und alle Zutaten stehen darauf, die Zubereitung bricht nach dem
dritten Schritt ab und verweist auf die App. Stattdessen fragt ChefMind dieselbe
Schnittstelle, die die App benutzt, und bekommt das ganze Rezept:
- alle Arbeitsschritte, mit ausgeschriebenen Zeitangaben statt
<timer> - welche Zutat zu welchem Schritt gehört — das füllt den Kochmodus
- Nährwerte je Portion, Zubereitungs- und Garzeit
- das Titelbild, das gleich als Foto am Rezept hängt
- Grundzutaten wie Salz und Pfeffer als eigene Gruppe, „nach Geschmack"
Das ist eine undokumentierte Schnittstelle, kein Versprechen. Fällt sie aus,
importiert ChefMind die Vorschauseite und schreibt dazu, dass das Rezept deshalb
unvollständig ist. Konfigurieren muss man nichts; die beiden
CHEFMIND_KPTNCOOK_*-Variablen sind für den Fall, dass KptnCook den Schlüssel
der App wechselt.
Die Mengen der Schnittstelle gelten je einer Portion, die App zeigt zwei — ein Import speichert deshalb zwei Portionen als Basis und die verdoppelten Mengen. So steht im Rezept dasselbe wie in der App, und man kann es nachprüfen.
Jeder Import landet als normales Rezept in der Datenbank und lässt sich sofort im Editor korrigieren. Unsichere Stellen werden als Hinweis ausgegeben statt geraten.
Erreichbar unter /mcp, gleicher Port wie die Weboberfläche.
claude mcp add --transport http chefmind https://<server>/mcpClaude Desktop gibt es nur für macOS und Windows, läuft also auf einem anderen Gerät als der Server — und spricht MCP-Server nur über HTTPS an. Wie man dahin kommt, steht unter HTTPS; der bequemste Weg ist Tailscale, weil das Zertifikat dann öffentlich vertraut ist und auf dem Client nichts zu tun ist.
In claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/,
Windows: %APPDATA%\Claude\):
{
"mcpServers": {
"chefmind": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://chefmind.deintailnet.ts.net/mcp"]
}
}
}Mit dem selbst signierten Zertifikat aus dem LAN muss der Client die CA kennen.
mcp-remote läuft unter Node, also:
{
"mcpServers": {
"chefmind": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://192.168.1.250/mcp"],
"env": { "NODE_EXTRA_CA_CERTS": "/Pfad/zu/ca.crt" }
}
}
}data/tls/ca.crt vom Server auf den Client kopieren und den Pfad eintragen.
Danach Claude Desktop vollständig beenden und neu starten.
make mcp-desktop gibt den passenden Block fertig aus — mit dem Tailnet-Namen,
falls Tailscale läuft, sonst mit der LAN-Adresse. Ist CHEFMIND_MCP_TOKEN
gesetzt, kommt "--header", "Authorization: Bearer <token>" dazu.
Eine eigene Domain muss in CHEFMIND_ALLOWED_HOSTS ergänzt werden, sonst
antwortet der Endpunkt mit Unerwarteter Host — das ist der
DNS-Rebinding-Schutz. Private Adressen, .local, .lan, .internal,
.home.arpa und .ts.net gelten ohne Eintrag.
17 Tools: list_recipes, get_recipe, scale_recipe, suggest_recipes,
create_recipe, update_recipe, delete_recipe, import_recipe_from_url,
import_recipe_text, import_recipe_from_photo, get_meal_plan,
set_meal_plan_entry, delete_meal_plan_entry, build_shopping_list,
get_shopping_list, add_shopping_item, check_shopping_item.
get_recipe nimmt optional servings und liefert die Mengen fertig
umgerechnet und formatiert. Sprachmodelle rechnen Einheiten schlecht und
formatieren „1,3333 Tassen" noch schlechter — „1 1/3 Tassen" fertig zu liefern
entfernt diese Fehlerquelle vollständig.
Damit lässt sich zum Beispiel sagen: „Plane mir eine vegetarische Woche aus meinen Rezepten und mach die Einkaufsliste für 3 Personen."
Der Endpunkt hat kein Auth — so gewollt, für ein privates Netz. Zwei Dinge sind trotzdem eingebaut:
- Host- und Origin-Prüfung gegen DNS-Rebinding. Ohne sie könnte eine
beliebige Webseite, die du besuchst, ihren eigenen Namen auf deinen Server
auflösen und
delete_recipeaufrufen. Deshalb mussCHEFMIND_ALLOWED_HOSTSden Hostnamen enthalten, unter dem du die App aufrufst. CHEFMIND_MCP_READONLY=1registriert nur lesende Tools.
Sollte die App je aus dem Internet erreichbar sein, setze zusätzlich
CHEFMIND_MCP_TOKEN — ein gemeinsames Geheimnis, kein Benutzerkonto.
Gebraucht wird es aus zwei Gründen: Claude Desktop spricht MCP-Server nur über
HTTPS an, und Service Worker — also die installierbare PWA — verlangen außerhalb
von localhost einen sicheren Kontext. Über eine LAN-Adresse per http:// gibt
es beides nicht.
Tailscale stellt für den MagicDNS-Namen deines Rechners ein echtes Let's-Encrypt-Zertifikat aus und erneuert es selbst. Auf den Clients ist nichts zu installieren.
make tls-tailscale # entspricht: tailscale serve --bg --https=443 http://127.0.0.1:3000Einmalig nötig: im Tailscale-Admin unter
DNS die Option HTTPS Certificates
aktivieren. Danach läuft ChefMind unter
https://<rechner>.<tailnet>.ts.net/, erreichbar von jedem Gerät im Tailnet und
von sonst niemandem.
Wieder abschalten: tailscale serve --https=443 off.
Für eine private IP kann keine öffentliche CA ein Zertifikat ausstellen, also eine eigene, lokale CA:
make cert # legt data/tls/{ca,server}.{crt,key} an
docker compose up -d # der caddy-Dienst nutzt sieDas Skript nimmt automatisch Loopback, den Hostnamen und alle privaten
Adressen dieses Rechners ins Zertifikat auf. Weitere Namen über
CHEFMIND_TLS_HOSTS. Nach einem Adresswechsel: make cert-force.
Damit Browser und Clients nicht warnen, muss data/tls/ca.crt einmalig als
vertrauenswürdig eingetragen werden — im System-Zertifikatsspeicher oder, für
mcp-remote, über NODE_EXTRA_CA_CERTS.
Die App selbst lauscht nur auf 127.0.0.1 und ist ausschließlich über Caddy
oder tailscale serve erreichbar. Beide überschreiben X-Forwarded-For mit
der echten Gegenstelle. Erst dadurch ist die Prüfung „kommt diese Anfrage aus
einem privaten Netz?" nicht mehr fälschbar — ein Client kann den Header zwar
mitschicken, er wird aber verworfen.
Gebaut und gestartet wird direkt auf dem Server über einen self-hosted GitHub-Runner. Kein Registry-Push, kein eingehendes SSH von GitHub.
Einmalig auf dem Server:
-
Runner installieren, mit den Labels
chefmindundprod:https://github.com/ConstantinTi/ChefMind/settings/actions/runners -
Docker samt Compose v2 installieren und den Dienst starten
-
Runner-Benutzer in die
docker-Gruppe:sudo usermod -aG docker <user>Danach den Runner-Dienst neu starten (
sudo ./svc.sh stop && sudo ./svc.sh startim Runner-Verzeichnis). Eine neue Gruppenmitgliedschaft greift erst in einer neuen Sitzung — ein bereits laufender Dienst sieht sie nicht und scheitert weiter mit „permission denied" oder „command not found". -
~/chefmind.envanlegen (Vorlage.env.example) — im Home des Runner-Benutzers, nicht im eigenen. Secrets bleiben so außerhalb des Repos und überleben jeden Checkout. -
Für HTTPS über das Tailnet einmalig
make tls-tailscale(siehe HTTPS). Das selbst signierte Zertifikat für den LAN-Zugriff legt der Workflow bei jedem Deploy selbst an, falls es fehlt.
Der Workflow prüft diese Punkte vorab und sagt genau, welcher fehlt.
Danach baut und deployt jeder Push auf main automatisch
(.github/workflows/deploy.yml): docker compose build, up -d, Warten auf
healthy, alte Images aufräumen. Schlägt der Healthcheck fehl, bricht der
Workflow mit den Logs ab.
Von Hand:
docker compose up -d --buildDer Port ist absichtlich auf 127.0.0.1 gebunden. Für den Zugriff aus dem LAN in
docker-compose.yml auf die LAN-Adresse ändern und den Hostnamen in
CHEFMIND_ALLOWED_HOSTS ergänzen. Nicht ins offene Internet stellen — es gibt
kein Login.
CHEFMIND_DATA_DIR bestimmt das Verzeichnis; ohne Angabe ./data, was für die
lokale Entwicklung richtig ist.
Auf einem Server, der über CI deployt, muss es außerhalb des Checkouts
liegen. actions/checkout führt git clean -ffdx aus, und das -x entfernt
auch ignorierte Dateien — ./data ist ignoriert. Läge die Datenbank dort, würde
jeder Deploy Rezepte, Fotos, Backups und die TLS-CA löschen. Der Workflow
setzt deshalb CHEFMIND_DATA_DIR=$HOME/chefmind-data und übernimmt beim ersten
Lauf, was noch im Workspace liegt.
Alles liegt in $CHEFMIND_DATA_DIR: die SQLite-Datei und die Fotos. Ein Backup-Container legt
nächtlich einen Snapshot nach data/backups/ und behält 14 Stück. Ein manuelles
Backup:
docker compose exec chefmind sqlite3 /data/chefmind.db ".backup '/data/backups/manuell.db'"sqlite3 .backup ist auch im laufenden Betrieb sicher — die Datei einfach zu
kopieren, während WAL gerade schreibt, ist es nicht.
make zeigt die vollständige Liste. Die wichtigsten:
| Befehl | Wirkung |
|---|---|
make up |
Erststart: einrichten, Beispieldaten, Server |
make dev |
Entwicklungsserver (PORT=4000 für einen anderen Port) |
make stop |
Laufenden Server beenden |
make check |
typecheck + lint + test — dasselbe wie die CI |
make watch |
Tests im Watch-Modus |
make mcp |
Prüft, ob /mcp antwortet, und listet die Tools |
make reset |
Datenbank neu aufbauen (fragt nach, sichert vorher) |
make backup |
Snapshot nach data/backups/ |
make docker-up |
Lokal im Container starten, wie auf dem Server |
make sql |
SQLite-Shell auf der Datenbank |
npm run icons |
App-Icons aus public/icon.svg neu rendern |
Ohne make stehen dieselben Dinge als npm-Skripte bereit (npm run dev,
npm test, npm run typecheck, npm run lint, npm run db:generate,
npm run db:studio).
src/domain/ rein & isomorph: Einheiten, Skalierung, Aggregation. Keine I/O.
src/contracts/ Zod-Schemas — geteilt zwischen Formular, REST und MCP
src/services/ der einzige Ort mit Datenbankzugriff
src/mcp/ MCP-Tools, dünne Hüllen um services/
src/app/ Routen, Server Actions, UI
src/components/ UI-Bausteine; forms.tsx kapselt jeden Server-Action-Absender
src/lib/forms Adapter zwischen HTML-Formularen und den Contracts
Zwei Grenzen sind per ESLint erzwungen: src/domain/** darf keine I/O
importieren (der Portionsrechner läuft im Browser), und src/services/** sowie
src/mcp/** dürfen next/* nicht importieren (der MCP-Server bliebe damit auch
als eigener Prozess lauffähig).
Das gesamte Korrektheitsrisiko steckt in src/domain/ — Brüche, Rundung,
Einheitenwechsel, Zusammenfassen. Deshalb ist genau diese Schicht vollständig
getestet und braucht dafür keine Datenbank.
GPL-3.0-or-later, siehe LICENSE.