decentespresso-mcp
Your espresso archive, and someone to talk to about it.
Every shot your Decent DE1 pulls is already measured in fine detail — pressure,flow, weight and temperature about four times a second, next to the targets theprofile asked for. That data lives on the tablet. This server keeps all of it ona machine you own, and hands it to Claude so you can just ask.
You: Why was my shot this morning sour and bitter at the same time?
Claude: Sour and bitter together is uneven extraction, so I looked at the puck
rather than the recipe.
The machine did exactly what it was told: it tracked its pressure
target to within 0.03 bar for the whole pour. The puck did not. Its
resistance fell steadily — 0.62 per second across the 22 seconds of
pour, the steepest band the diagnostics have. Water found a path and
widened it, so part of the bed over-extracted while the rest barely got
wet. That is your sour and bitter at once.
The other channeling signs were quiet: no pressure dip after the peak,
flow steady to 0.015 ml/s. Two more need the scale and could not be
computed for this shot.
One thing to correct, though — you noted it probably ran too hot. It
ran 1.6 °C *below* target on average and 2.1 °C under at the worst
point. Temperature was not your problem here.
That exchange is real, from shot a0b7eb21. None of the numbers were typed byhand.
What you get
A complete archive, independent of the tablet. Shots, the full measurementseries, and the exact profile version each one ran on. Decaid is the source;this is the memory. Nothing in that chain leaves your network.
Metrics from the machine's own phase markers. The end of preinfusion is readoff what the DE1 reports, not guessed from a pressure threshold. Where a shotdoes not report it, the fallback says so — you always know whether a number wasmeasured or inferred.
Puck diagnostics. Resistance modelled as pressure over flow squared, andfive independent channeling indicators: a pressure dip after the peak, flow thatwill not settle, the pump out-delivering the scale, drops arriving too early,and a bed losing resistance while the pressure is held. Each keeps its own rawvalue and threshold, so you can disagree with the verdict and still use theevidence. An indicator that could not be measured says so rather than passingquietly.
Target versus actual, per data point. Decaid records what the machine wasasked for alongside what it did. Profile compliance here is thereforemeasured, not estimated — and a shot that followed its profile perfectly andstill tasted wrong tells you to look at the coffee instead of the machine.
Three levels of detail. summary to triage, per_phase to find a cause,detailed for the raw curve. Each step costs roughly twice the one before, sodeep questions stay cheap until you actually ask one.
Profile versioning that understands cosmetics. Two hashes: one identifies aversion exactly, the other groups versions that brew identically. Reformat aprofile and nothing moves; change the pressure by 0.3 bar and it does.
A watchman. Four rules run after every sync — changed the batch but not thegrind, beans past their prime (frozen time subtracted), a shot you never rated,weights that miss their target. With optional ntfy push, capped so the messagesstay worth reading: at most one per shot, only the last 48 hours, at most fiveper run.
Statistics over a period. What you pulled, from which beans, on whichprofiles, how the grind moved, how it tasted — and the same figures for theperiod before.
You: How was the last month?
Claude: 117 shots in 30 days, just under four a day, with 13 flushes and aborts
left out. You rated 18 of them, averaging 53. Typical shot: 17.9 g in,
38.4 g out, 1:2.1 in 29 seconds.
The incahuasi accounts for 57 of those and you moved the grinder eleven
times on it — 4.3 down to 2.5, then settling around 2.7. It averages 47
across nine ratings, while the Arabica Honey Process sits at 67 across
four. That is the one to keep.
Busiest day was 28 August: 34 pulls, 27 of them coffee.
Writing back, by asking. "Give that one a 90 and note it was the best thisweek." Behind a switch that is off by default, with a whitelist of fieldsverified against the real API, and every change read back before it is reportedto you.
How it fits together
Decent DE1 ──BLE──▶ Decaid on the tablet ← the source
│ local network only
▼
decentespresso-mcp (Docker) ← the memory
│ internal Docker network
cloudflared
│ https://<host>/<secret>/mcp
▼
Claude ← the interface
The tunnel exists so Claude can reach the server. The tablet is never reachedthrough it — that connection is local network only, and the server refuses tostart if you point it anywhere else.
The tablet does not need to be on. It runs while you are making coffee; the restof the time the server waits and catches up afterwards. That is a normal state,not an error, and nothing will nag you about it.
Requirements
- A Decent DE1 with Decaid on the tablet (verified against 0.8.5).
- Docker on a machine on the same network as the tablet.
- A Cloudflare tunnel or equivalent, if you want to reach it fromclaude.ai. Optional — everything works locally without one.
- Or just Python 3.12, if you would rather run it without Docker.
Quickstart
git clone https://github.com/The-Walker443/decentespresso-mcp
cd decentespresso-mcp
cp .env.example .env
Two things have to be filled in:
DECAID_URL=http://10.100.100.171:8080 # your tablet, private IP only
MCP_PATH_SECRET=<openssl rand -hex 24> # stands in for authentication
Then:
mkdir -p data && sudo chown -R 10001:10001 data # the container runs as UID 10001
docker compose up -d
docker compose logs -f
The first start backfills everything Decaid has. Point your tunnel at thecontainer:
ingress:
- hostname: coffee.example.com
service: http://decentespresso-mcp:8000
- service: http_status:404
Fetch the connector URL — it carries the secret, so it is never logged:
docker compose exec decentespresso-mcp decentespresso-mcp --print-connector-url
Paste it into claude.ai under Settings → Connectors → Add custom connector,leaving the OAuth fields empty. Then ask it something.
Prefer Portainer and a prebuilt image from ghcr.io? compose.portainer.yamland the specification cover thatroute, including the stack variables.
Security, in three sentences
The connection to your tablet is local network only — DECAID_URL acceptsprivate IP literals and nothing else, and that traffic never touches the tunnel.The MCP endpoint hides behind a secret path instead of a login, every other pathreturns an empty 404, and access logs are off so the path never lands in a logfile. The container runs non-root on a read-only filesystem, publishes no hostport, and the log formatter redacts the only two secrets it holds — includinginside tracebacks from libraries that know nothing about it.
The tools
list_beans · list_shots |
what is in the archive |
get_shot · get_shot_metrics |
one shot, at the detail you ask for |
compare_shots |
two to four side by side, with deltas |
list_profiles · get_profile |
profile versions and their targets |
get_workflow |
what the next shot would run on, live from the tablet |
audit_archive |
what the watchman found |
stats |
a period, with a comparison |
sync_now · status |
housekeeping |
update_shot · update_bean · update_batch · set_workflow |
writing, behind the switch |
Twelve tools cost 8.5 kB of definitions, sixteen with write mode on cost 11 kB —about 700 B each, because the shared vocabulary lives in the server promptrather than being repeated in every docstring. That budget is pinned by tests;it is meant to stay that way.
Configuration
Everything is an environment variable. The server validates all of it at startupand lists every problem at once, rather than one per restart.
| Variable | Default | |
|---|---|---|
DECAID_URL |
— | your tablet, private IP literal only |
MCP_PATH_SECRET |
— | ≥32 characters, stands in for authentication |
SYNC_INTERVAL_MIN |
15 |
0 switches the background sync off |
WRITE_ENABLED |
false |
unlocks the four write tools |
GUARD_RULES |
all | comma-separated rule names, or none |
NTFY_URL · NTFY_TOPIC · NTFY_TOKEN |
— | guard notifications; empty means none |
BEAN_AGE_WARN_DAYS · RATING_GRACE_HOURS · DOSE_TOLERANCE_G |
42 · 36 · 1.0 |
guard thresholds |
PUBLIC_BASE_URL |
— | only for printing the connector URL |
DB_PATH · LOG_LEVEL · TZ |
/data/shots.db · INFO · Europe/Berlin |
storage is always UTC |
HOST · PORT |
0.0.0.0 · 8000 |
bind address inside the container |
Syncing by hand, if you ever need to:
docker compose exec decentespresso-mcp decentespresso-mcp --backfill # everything
docker compose exec decentespresso-mcp decentespresso-mcp --sync-once # new and changed
Both print a JSON summary. waiting_for_tablet means the tablet was off — notan error, just nothing to fetch.
Honest limits
- It cannot taste. The diagnostics describe what the machine and the puckdid. The connection to sour, bitter or thin is inference, and a shot thatlooks clean can still taste wrong.
- Channeling indicators rarely agree. Across 171 real shots no two of themever fired together, which is why they are reported individually rather thancollapsed into one score you would have to trust blindly.
- Thresholds are calibrated against one archive — one machine, one grinder,one operator. They are percentile-based and documented one by one in thespecification, so they can be re-derived rather than believed.
- It does not write profiles. Reading and versioning them, yes; changingthem belongs at the machine.
- Nothing is ever deleted. The API can do it. This project does not buildit.
Development
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]" # Linux/macOS: .venv/bin/pip
.venv/Scripts/python -m pytest
.venv/Scripts/python -m ruff check src tests
Fixtures are anonymised real responses, never invented ones. Numbers pinned intests come from measurement, and the comment says where. The conventions thisproject runs on — verify before you build, whitelist discipline, what a test hasto say — are in CLAUDE.md; the system itself is described inthe specification.
data/shots.db in a checkout is a development copy, not the archive. Numbersfrom it are never a statement about the real one — for that, ask status().
Project status
Personal project, built spec-driven with Claude Code; provided as-is, no supportpromised, issues welcome.
Puck-diagnostic physics and threshold rationales are informed bygaggimate-mcp (MIT) — theresistance model, the idea of independent channeling signatures, the temperaturebands and the three-level detail system. The thresholds themselves are measuredagainst this archive rather than adopted: the sampling rates differ, and thisdata carries per-point targets that theirs does not.
Community project, not affiliated with Decent Espresso International.