mcp-mealie

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.

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

Verify the instance answers before wiring up a client:

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

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:

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

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, 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.