# Howto: from a fresh Mealie to an agent that maintains it

The README says what the tools are. This is the order to do things in, and the
handful of behaviors that surprise people on the first run.

Twenty minutes end to end. Requires Mealie 2.0 or newer; 2.x and 3.x both
work.

## 1. Token and backup

In Mealie: **Profile → API Tokens** for a long-lived token, then **Site
Settings → Backups** before anything writes for the first time. Nothing here
is exotic, but merges and deletes are not undoable and a backup is cheaper
than reconstructing a taxonomy.

```bash
export MEALIE_URL=https://mealie.example.org
export MEALIE_API_TOKEN=<your token>
```

Verify the instance answers before wiring up a client:

```bash
curl -s -H "Authorization: Bearer $MEALIE_API_TOKEN" \
  "$MEALIE_URL/api/users/self" -w '\n%{http_code}\n' | tail -2
curl -s "$MEALIE_URL/api/app/about"
```

Your username and `200` is what you want; `401` means the token. The second
call reports the version — the server reads it at startup and refuses to run
against 1.x, which has no `/api/households` endpoints.

## 2. Connect the server, read-only first

```bash
claude mcp add mealie \
  --env MEALIE_URL=$MEALIE_URL \
  --env MEALIE_API_TOKEN=$MEALIE_API_TOKEN \
  --env MEALIE_READ_ONLY=true \
  -- uvx --from git+https://github.com/mgummich/mcp-mealie@v0.3.1 mcp-mealie
```

For Claude Desktop, Cursor, Windsurf, or Zed, the same three variables go into
the `mcpServers` block from the README.

If you would rather not repeat the token in a client config, put the variables
in a `.env` file instead — copy `.env.example` to `.env` in the directory the
server is launched from and drop the `--env` flags. The server reads it at
startup, searching upward from the working directory, and anything already set
in the real environment wins over the file. Keep `.env` out of version control;
the token in it is a live credential.

With `MEALIE_READ_ONLY=true` the write tools are never registered — the model
cannot call them by accident, because it cannot see them. Twelve read tools
remain, which is enough for every question in step 3. Drop the variable once
you like what the assistant proposes.

Self-signed certificate on a homelab instance: add
`MEALIE_VERIFY_SSL=false`. Do not use it against anything reachable from the
internet.

## 3. Look before writing

Ask in plain language; the model picks the tools.

> *What's for dinner this week?*
>
> *Which of my tags are used by nothing?*
>
> *Did I import the same recipe twice?*
>
> *Which recipes link to a page that no longer exists?*

The last three are one call each — `library_stats`, `find_duplicate_recipes`,
`check_recipe_links`. They sweep server-side. If the assistant instead starts
running one search per tag, stop it: that is the pass those tools replace.

`library_stats("foods")` and `library_stats("units")` are the slow ones. They
need every recipe's ingredients, so that sweep is one request per recipe and
honors `max_recipes`. Tags, categories and tools come off the recipe list in a
handful of requests.

What comes back is the fifty most-used items, the total count of used ones,
and every unused item — the unused list being the one you act on. Raise `top`
if you want the full ranking; each row carries a UUID, which is why the tail
is not there by default.

## 4. Turn on writes

Remove `MEALIE_READ_ONLY` and restart the client. Useful first jobs, roughly
in order of how much they repay the effort:

**Import and file a recipe.** `import_recipe_from_url` scrapes it;
`update_recipe` files it. Tags, categories and tools **merge** with what is
already there and names that do not exist yet are created — the response says
which were new, so read that line back before a typo becomes a permanent tag.
Pass `replace_tags=True` to overwrite instead. If the scraper missed the
photo, `set_recipe_image(slug, url)` fetches it from the web, and
`upload_recipe_image(slug, path)` sends a file from the machine the server
runs on.

Two things about that pair are worth knowing before they surprise you. Sites
that build their page in the browser leave the scraper with nothing: the
import still succeeds, but the recipe comes back empty and the response says
so — fill it in with `update_recipe(ingredients=[...], instructions=[...])`,
which takes plain text. And renaming a recipe changes its slug, because Mealie
derives one from the other. The response carries the new slug and names the
old one as `renamed_from`; use the new one for every call after that,
`set_recipe_image` above all, as the old one stops resolving.

**File many recipes at once.** `bulk_tag_recipes(slugs, tags=["Weeknight"],
categories=["Dinner"])` runs Mealie's bulk endpoints, so retagging forty
recipes is one call rather than forty. It only adds; to take a tag off, or to
set one recipe's list exactly, use `update_recipe` with `replace_tags=True`.

**Merge duplicate foods.** `manage_taxonomy("foods", "merge", item_id=<loser>,
merge_into=<keeper>)` uses Mealie's own merge endpoint and repoints every
recipe that used the loser. Deleting the duplicate instead strips it from
those recipes — that is the difference worth confirming out loud before
running it. A food or unit that is still on a shopping list cannot be deleted
at all: Mealie answers 409 and the tool says to merge instead. Note that
`library_stats` counts recipe usage only, so a food it reports as unused may
still be sitting on somebody's shopping list.

**Rename in bulk.** Every action except `list` also takes `items=[…]` and runs
the batch in one call, reporting per-item failures rather than stopping at the
first bad id:

```python
manage_taxonomy(
    "foods",
    "update",
    items=[
        {"item_id": "...", "name": "Scallion"},
        {"item_id": "...", "data": {"labelId": "..."}},
    ],
)
```

**Build a cookbook.** A Mealie cookbook is a saved filter, not a folder — it
fills itself as recipes match. Pass names and let the server write the filter:
`create_cookbook(name="Weeknight Dinners", tags=["Quick"], require_all=True)`.
Check it with `get_cookbook_recipes` afterwards; an overly narrow filter
matches nothing, which is easy to miss. To change one, `update_cookbook` —
never delete and recreate, which throws away the id.

**Plan a week.** Read the *previous* week with `get_meal_plan` first; that is
what tells you which dinners would repeat. Then one
`add_meal_plan_entry(date, entry_type, recipe_slug)` per slot — there is no
batch endpoint — or `random_meal_plan` for a whole range at once. Random
*adds to* existing entries rather than replacing them, and is capped at 14
days per call.

## 5. Things that bite

- **Ingredients, instructions and notes replace on update.** Read the recipe
  first and pass the whole list back, not just the new items. Tags,
  categories and tools are the exception — those merge.
- **`manage_taxonomy("…", "list")` is paged at 50** and reports the total. A
  library with 400 foods needs eight calls; conclude nothing from page one.
  For anything table-wide, `library_stats` is cheaper and carries the counts.
- **`update` is a patch.** Fields you do not mention keep their value.
- **`delete_recipe` needs the slug twice** and is permanent. If Mealie's own
  delete answers 500 — some rows its ORM cannot cascade do — the tool retries
  through the bulk endpoint and tells you it did. A slug that does not exist
  still fails, so a typo is never answered by a second attempt.
- **An ingredient line the parser cannot place keeps its full text.** "1
  handful of gribenes" lands as a note rather than a quantity plus a food that
  does not exist, because Mealie's ingredient rows require a real food id. It
  reads correctly; it just will not count toward `library_stats("foods")`.
- **Write requests are never retried.** Mealie has no idempotency key, and a
  retried create would duplicate the recipe. A failed write is reported, not
  re-attempted.
- **An auth error on every tool is configuration**, not a transient failure.
  Same for "server is in read-only mode".

## 6. Add the workflows

The tools are verbs. Deciding *when* to merge, how large a batch stays
reviewable, and what a cookbook rule should contain is the job of
[mealie-skill](https://github.com/mgummich/mealie-skill), which builds for
Claude Code, Antigravity, Cursor and `AGENTS.md`.

It detects this server and uses it as the primary analysis path: with the
server connected it never builds its own local recipe index, because
`library_stats` and friends answer the same questions in one call. It keeps
its own ordered batch (`actions.json` + `apply`) for plans where execution
order matters or a dry run over the whole set is wanted.

One rule when running both: **one write path per plan**. Either every write is
an MCP call or the whole plan goes through `apply` — never half of each.
