Initial commit: Mealie MCP server
Ports a working Hermes agent skill to an MCP server. The skill's value was not its Mealie endpoints but ~25 operational rules found by running imports against the live instance; those are now code with tests rather than prompt text. The server owns deterministic mechanics — auth, the non-default User-Agent Cloudflare requires, the `extension` field on cover uploads, PATCH instead of the 500-ing bulk-action endpoints, decimal-comma normalization, and restoring Swedish display text after parsing. Language and taste judgement stay with the model, fed by the four reference notes shipped as MCP resources. verify_recipe runs the finished-import definition as code so an agent cannot report success on a recipe with an English ingredient line, a missing cover, or ingredients still in the "Click Parse" state. Home Assistant is optional and isolated; the Obsidian meal plan is out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Mealie MCP Server
|
||||
|
||||
MCP server for Fredrik's Mealie instance. It exposes recipe search, suggestions,
|
||||
recipe details, ingredient scaling, imports, cover images, ingredient parsing, and a
|
||||
finished-import verification pass.
|
||||
|
||||
It grew out of a working Hermes agent skill. The valuable part of that skill was not
|
||||
the Mealie endpoints — it was roughly 25 operational rules discovered by running
|
||||
imports against the live instance and watching them fail. Those rules are the reason
|
||||
this server exists as code instead of prompt text.
|
||||
|
||||
## What lives where
|
||||
|
||||
The split is deliberate:
|
||||
|
||||
| The server owns (deterministic) | The model owns (judgement) |
|
||||
|---|---|
|
||||
| Auth, and the non-default User-Agent Cloudflare requires | Translating content to Swedish |
|
||||
| The `extension` field on cover uploads | Picking recipes against the taste profile |
|
||||
| PATCH instead of the bulk-action endpoints | Choosing a variant from an article page |
|
||||
| Parsing, then restoring the Swedish display text | Sensible categories and tags |
|
||||
| Decimal-comma normalization before the parser | Judging image quality |
|
||||
|
||||
The token never leaves the server, so agents never handle a bearer credential.
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
export MEALIE_API_TOKEN='...' # required
|
||||
export MEALIE_BASE_URL='https://recept.famfallman.com' # optional
|
||||
export MEALIE_USER_AGENT='Mealie-MCP/0.1' # optional, must be non-default
|
||||
|
||||
export HA_BASE_URL='http://homeassistant.local:8123' # optional, shopping list only
|
||||
export HA_TOKEN='...'
|
||||
export HA_SHOPPING_LIST_ENTITY='todo.ourgroceries_shoppinglista'
|
||||
|
||||
uv run mealie-mcp
|
||||
```
|
||||
|
||||
Create the Mealie token at `/user/profile/api-tokens`.
|
||||
|
||||
Home Assistant is optional and isolated: without `HA_BASE_URL` and `HA_TOKEN`
|
||||
everything except `shopping_list_add` works normally.
|
||||
|
||||
## Tools
|
||||
|
||||
**Read**
|
||||
- `check_auth()` — distinguishes a bad token (401) from a Cloudflare block (403/1010)
|
||||
- `search_recipes(query, limit)`
|
||||
- `suggest_recipes(foods, limit, max_missing_foods)`
|
||||
- `get_recipe(slug_or_id)`
|
||||
- `list_organizers()` — existing categories and tags, so imports reuse the vocabulary
|
||||
- `find_by_source_url(url)` — duplicate check on `orgURL`
|
||||
- `scale_ingredients(slug_or_id, servings)`
|
||||
|
||||
**Import**
|
||||
- `import_recipe_url(url, check_duplicates=True)`
|
||||
- `import_recipe_text(text, source_url=None)`
|
||||
- `import_recipe_image(image_path)`
|
||||
|
||||
Each returns an extraction report flagging `Could not detect ...` placeholders and
|
||||
campaign junk in the title. A response is not proof of a good import.
|
||||
|
||||
**Write**
|
||||
- `patch_recipe(slug_or_id, patch)`
|
||||
- `parse_ingredients(slug_or_id)`
|
||||
- `set_cover_image(slug_or_id, source_url=None, image_path=None)`
|
||||
- `shopping_list_add(items)`
|
||||
|
||||
**Verify**
|
||||
- `verify_recipe(slug_or_id, image_verified=False, taxonomy_skipped_reason=None)`
|
||||
|
||||
Writes are deliberately separate tools so a client can request confirmation.
|
||||
|
||||
## `verify_recipe`
|
||||
|
||||
The one that keeps the rest honest. It runs the finished-import definition as code
|
||||
and returns pass/fail per check, so an agent cannot report success on a recipe with an
|
||||
English ingredient line, a missing cover, or ingredients still in the "Click Parse"
|
||||
state.
|
||||
|
||||
`image_verified` must be passed explicitly by a caller that actually ran an image step.
|
||||
A non-empty `image` field is only a warning: it does not prove the UI shows a cover.
|
||||
|
||||
## Resources
|
||||
|
||||
The reference notes ship as MCP resources rather than prompt text:
|
||||
|
||||
- `mealie://reference/family-taste-profile`
|
||||
- `mealie://reference/import-pitfalls`
|
||||
- `mealie://reference/koket-attribution`
|
||||
- `mealie://reference/cloudflare-hostname`
|
||||
|
||||
## Prompt
|
||||
|
||||
`import_pipeline(source)` spells out the order — import → normalize → image → parse →
|
||||
verify — so no step gets skipped.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The Obsidian meal plan. That is file writing in a vault and belongs to the client's own
|
||||
file tools; folding it in would make this server two things at once.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
The suite covers the verified failure modes: Cloudflare 1010 vs 401, decimal-comma
|
||||
normalization, parser-mangled display text, the required `extension` field on uploads,
|
||||
the 500 on structured ingredient patches, and duplicate detection by `orgURL`.
|
||||
Reference in New Issue
Block a user