Files
Tom FluxandClaude Opus 4.8 50dcaca1e8 Add MCP server spec; mark phases 1-2 done in plan
mcp.md specs the Phase 3 FastMCP "ai service": tools, the Django API
gaps to close, authless-connector + URL-secret auth, and the
brainstorm->commit path for meta-recipes and cook logs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 21:02:26 +01:00

11 KiB

Implementation Plan

Written 2026-06-22. Turns requirements.md into concrete, sequenced work. Reflects the current priority order: pantry polish → auth → MCP, with the MCP being the highest-value piece (Claude reads and updates the pantry, so manual upkeep mostly goes away). Builds on the completed /simplify pass (shared kitchen/helpers.py, dead config removed).

Altitude: file/model/endpoint level. Not code — decisions and steps.

Sequencing at a glance

Phase Goal Risk Depends on
0 Commit the simplify baseline — done none
1 Pantry: mobile-first, In/Low/Out, fast add — done & deployed medium (model + UI) 0
2 Auth (login-once) + prod baseline — done & deployed low
3 MCP server — read + update pantry from claude.ai (specced → mcp.md) medium 1, 2

Deferred (not in this plan): web what-can-i-cook rework, §4.1 substitutions, shopping flow rethink, web cook-logging. They stay roughly working but are not invested in here.


Phase 0 — Commit the baseline done

Tom has committed the simplify baseline. Starting point is clean.


Phase 1 — Pantry polish (mobile-first)

The heart of the friction problem. Move from "spreadsheet of decimals" to "glance into the fridge", and rebuild the screen phone-first.

1.1 Model change — add presence state

PantryItem today: quantity (required Decimal), unit, location, expiry_date, is_staple.

  • Add state to PantryItem: CharField(choices=IN/LOW/OUT, default="in"). New TextChoices like the existing Location.
  • Change quantity from DecimalField to an optional integer (PositiveIntegerField(null=True, blank=True)) — never going to have 1.234 pieces of toast. Keep unit too, but neither is required on add. (Migration handles the Decimal→Integer + nullable change plus the backfill below in one step.)
  • Migration 0003_pantryitem_state_optional_quantity. Data migration: backfill existing rows → state = "out" where quantity == 0, else "in".
  • Decide the In/Low/Out ↔ legacy quantity==0 / is_staple mapping in one place (see 1.5 — it ripples into matcher + shopping queries).

Decided: quantity becomes an optional integer (not a decimal, not free text). It's secondary metadata; In/Low/Out is the primary signal.

1.2 Presence filtering — stop relying on quantity__gt=0

Several queries use quantity__gt=0 to mean "I have this". With optional quantity that breaks (null ≯ 0). Replace presence checks with state.

  • _pantry_context (views_htmx.py): show all items, don't filter out depleted ones — "Out" rows render greyed (requirements §2.1). Annotate is_expired / expiring_soon as today.
  • helpers.get_pantry_total and the matcher's presence logic (what_can_i_cook, recipes_page): treat state != "out" as "have it"; tolerate quantity = None. Minimal change only — full presence-based matcher rework stays deferred. Goal here is just "don't crash / don't silently show everything as missing."

1.3 Pantry UI — mobile-first rebuild

Decided: the current design is poor — do a proper rethink of the pantry screen (layout, interaction, hierarchy), but keep the palette (Tom likes it). Mockup goes first (1.3.0) before touching Django templates.

1.3.0 Design mockup first. Produce a self-contained HTML mockup of the redesigned pantry (phone width, real palette) as an Artifact Tom can open on his phone and react to. Only wire it into Django templates once the look is agreed.

Replace the table-of-tiny-links (partials/pantry_table.html) with a phone-first card list.

  • Per-item card (single column): ingredient name, location badge, and a 3-way In / Low / Out button group as the primary control — proper buttons, ≥44px tap targets, not inline links. Expiry shown only if set, as a soft "use soon" pill.
  • Grouping: fridge / freezer / cupboard sections, collapsible or just stacked. Keep it one-column; no horizontal scroll.
  • State change endpoint: POST /app/pantry/<id>/state/ (to=in|low|out), re-renders the list partial via HTMX. Replaces the quantity-edit flow as the main interaction.
  • Keep delete and fridge↔freezer move, but as secondary (smaller, maybe behind a tap). Expiry edit becomes optional, de-emphasised.

1.4 Fast add

  • Autocomplete: GET /app/pantry/search/?q= returns matching Ingredients (name + aliases), rendered as an HTMX dropdown under the add box. Tap a result → adds with sensible defaults (unit/location from the Ingredient, state="in", no qty required).
  • Quick-add chips: render a row of one-tap chips from staples + recently-used ingredients (recent = distinct ingredients from recent PantryItem/CookLog). One tap adds/sets to In.
  • Add bar placement: sticky to the bottom of the viewport (thumb reach) on mobile, with the chips just above it. This is the make-or-break detail per requirements.
  • pantry_add already auto-creates unknown ingredients and merges into an existing same-location row — keep that, but set state="in" on add/merge, and stop requiring quantity.

1.5 CSS / layout (base.html)

  • Shift base.html to mobile-first: the current max-width: 960px desktop table styling becomes the enhancement; default styles target a narrow phone screen. Bigger touch targets, larger tap-friendly buttons, bottom nav or bottom add bar.
  • Keep the existing dark palette (it's fine) — this is layout/sizing, not a re-theme.
  • Verify on a real phone-width viewport (e.g. 390px) — no horizontal scroll, controls reachable one-handed.

1.6 Tests

kitchen/tests.py is empty. Add the first real tests here:

  • Model: state defaults, optional quantity, backfill migration sanity.
  • Views: add via autocomplete sets state="in"; state toggle endpoint; pantry context includes "Out" rows.

Phase 2 — Auth (login once) + prod baseline

Low risk, can proceed alongside Phase 1. Closes the wide-open /app/.

2.1 Session auth, long-lived

  • One real user account (document the createsuperuser / set-password step; don't commit credentials).
  • Settings: - SESSION_COOKIE_AGE = 60*60*24*365 (~1 year) - SESSION_EXPIRE_AT_BROWSER_CLOSE = False - SESSION_SAVE_EVERY_REQUEST = True (sliding expiry — each visit renews the year, so an active user is effectively never logged out) - LOGIN_URL / LOGIN_REDIRECT_URL
  • Login page: django.contrib.auth.views.LoginView + a minimal template in the existing dark style. Logout link somewhere unobtrusive.
  • Protect /app/: simplest is Django 5.x LoginRequiredMiddleware, but it hits every view — so exempt /api/ (token auth keeps its own) and the login view. If exemptions get fiddly, fall back to wrapping the urls_htmx includes / decorating the page views with login_required. Pick one approach; don't mix.

2.2 Drop the CSRF hacks

  • With a real session + CSRF token (already wired in base.html), remove every @csrf_exempt from views_htmx.py. Verify HTMX POST/DELETE still work (the htmx:configRequest handler already sends X-CSRFToken).

2.3 Production baseline (cheap, fold in here)

  • DEBUG from env (default False in prod).
  • SECRET_KEY from env; drop the committed django-insecure-… literal.
  • Keep ALLOWED_HOSTS as-is.

Phase 3 — MCP server (the goal)

Let claude.ai "cooking mode" read the pantry and write changes back, over the existing /api/ (token auth, IsAuthenticated). This is what kills the "recite my whole pantry every time" chore.

3.1 Shape

  • Separate FastMCP service (decided). Mirrors a pattern Tom already runs elsewhere: a standalone "ai service" FastMCP server that talks to the Django API with the caine token. Keeps concerns decoupled; the API is already the right interface and the /simplify pass left it clean.
  • Tools to expose (map to existing endpoints): - get_pantryGET /api/pantry/ (+ state) - add_to_pantryPOST /api/bulk-pantry-add/ - set_pantry_state → new tiny endpoint or PATCH /api/pantry/<id>/ setting state (in/low/out) — the conversational update path ("used the last of the noodles" → set Out) - what_can_i_cookGET /api/what-can-i-cook/ - log_cookPOST /api/log-cook/ (and make it accept rating, the one API gap from research §5)
  • Add the state field to the pantry serializer so MCP reads/writes it.

3.2 Transport, hosting, auth — decided

  • Transport: remote MCP over HTTP (streamable HTTP / SSE), reachable at food.jihakuz.xyz/mcp. Not stdio.
  • Hosting: same box as Django, behind nginx at /mcp, as its own systemd service alongside food.service.
  • Auth: bearer token — single user, so one long high-entropy token checked by the MCP service (and/or nginx) is enough. It must be tight: this endpoint is internet-exposed (unlike /api/ and /admin/, which nginx blocks externally), so the token is the only thing protecting it. Strong token, kept out of git (env/secret), TLS only.

3.3 Why the cooking blocks still work (no action, just the rationale)

MCP only feeds data/tools into the claude.ai conversation; it doesn't change how cooking mode renders. So Claude pulls real pantry contents and still produces its formatted cooking blocks — grounded instead of guessed.


Decisions (locked)

  1. Quantity → optional integer, secondary to In/Low/Out.
  2. MCP → FastMCP "ai service" on the same box at food.jihakuz.xyz/mcp, bearer-token auth, talks to the Django API.
  3. Design → rethink the pantry screen, keep the palette; mockup first.

Still just an implementation detail (not a blocker): auth enforcementLoginRequiredMiddleware + exemptions vs per-view login_required. Plan: try middleware, fall back if exemptions get fiddly.

Definition of done (per phase)

  • P1: Tom can add an item and flip In/Low/Out on his phone in a couple of taps, one-handed, no horizontal scroll; pantry stays glanceable.
  • P2: Visiting /app/ requires login; logging in once sticks for ~a year; no @csrf_exempt left; DEBUG=False + env SECRET_KEY in prod.
  • P3: In claude.ai cooking mode, Claude can list the pantry and mark items In/Low/Out (and log a cook) without Tom typing the inventory.