Sabor
Provides tools for interacting with a self-hosted Mealie instance, enabling management of recipes, meal plans, shopping lists, tags, comments, and cooking history. Mealie remains the source of truth for the kitchen data.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SaborPlan pasta for Monday and add the ingredients to my shopping list"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Sabor
A kitchen-intelligence layer on top of Mealie. Find or import recipes, plan the week, build a scaled shopping list, estimate its cost, then sync to Bring! with an explicit preview and digest-bound confirmation. Mealie is the first recipe store; it remains the source of truth for cooking data.
The everyday flow
Call
sabor_capabilities(); use only tools indata.registered_tools.Find recipes with
search_recipesorsuggest_recipes; inspect structured ingredients and instructions withget_recipe. Alternatively previewimport_recipe(source="https://example.org/recipe", kind="url"), ask the user, then repeat withdry_run=False.Agree a menu and use
set_mealplan_entryfor each approved entry.Create or select a Mealie list, then call
add_to_shopping_list(list_id="MEALIE_ID", mealplan_start="2026-10-19", mealplan_end="2026-10-25", servings_multiplier=2.0). Dates are inclusive. Each recipe occurrence is added; free-text entries are skipped with reasons. The multiplier scales native recipe yield, not a guessed diner count. This call writes immediately: obtain approval first.Reread
get_shopping_list_itemsand estimate the actual list withestimate_cost(source={"kind":"shopping_list","list_id":"MEALIE_ID"}, store={"provider":"mercadona"}). Show consumption and procurement subtotals, coverage and uncertainties. If over budget, propose changes and obtain new approval before changing the plan.Discover
get_shopping_sync_targets. Previewsync_shopping_list(mealie_list_id="MEALIE_ID", target={"provider":"bring","list_id":"BRING_ID"}). Show additions, removals, conflicts, skipped checked items andpreview_digest. Ask; repeat the same call withconfirm=True, preview_digest="APPROVED_DIGEST". A stale digest or any conflict blocks writes and returns a fresh preview; review and ask again rather than blindly retrying or switching to replace.
This is documented agent orchestration, not a composite tool or transaction. Plan → list → estimate_cost → sync crosses independent approval boundaries. List creation/additions write immediately; cost approval authorizes no purchase. Report each system's outcome; uncertain writes require readback/reconciliation, not automatic retries or an assumed cross-system rollback.
Related MCP server: Mealie MCP Server
Capabilities and providers
Capability | Provider / requirements | Limits |
Core | Mealie URL + token | Recipes, suggestions, planning, shopping items and history; API baseline Mealie 3.22.0 |
Import: URL | Mealie native scraper | Real read-only extraction preview; unsupported sites fail without paid-provider fallback |
Import: text | OpenAI-compatible endpoint + model (+ key remotely) | Typed recipe extraction; preview can incur LLM charges |
Import: image | Mealie-native image import, using Mealie's OpenAI configuration | Explicit |
Import: video |
| URL download; up to 100 MiB / 10 minutes; no playlists or local paths; TikTok URLs unsupported (paste the caption/transcript with |
Shopping sync | Bring! email + password and Mealie | Only unchecked source items; does not check off Mealie items |
Pricing |
| Read-only quantity-aware estimates; Mercadona only in 0.3.0 |
Legacy | Explicit toolset selection plus provider configuration | Promotions, memory, nutrition and Spanish catalog |
Gemini-native uploads the whole video for audio + vision processing using
GEMINI_API_KEY (VIDEO_RECIPE_GEMINI_MODEL defaults to gemini-2.5-flash-lite).
OpenAI-compatible video uses sampled frames and available captions; visual support
depends on the model, and without captions spoken quantities are unavailable.
No generic audio/transcription endpoint is assumed. Text-only models can use
captions with limitations. Neither adapter guarantees extraction accuracy.
Set SABOR_VIDEO_PROVIDER=gemini_native or openai_compatible to select
explicitly. Otherwise configured Gemini wins, then the compatible endpoint.
An explicitly unconfigured selection does not silently fall back.
LLM-only setups can preview text/video; committing requires Mealie.
Recipe stores and instance boundaries
RecipeStore is a typed persistence abstraction for recipes, calendar entries,
shopping lists/items, tags and history. Mealie is the only production backend
in 0.3.0; Tandoor, KitchenOwl and a built-in light store are future possibilities,
not implemented integrations. Existing slug/list arguments remain unchanged,
including the historical mealie_list_id sync argument. There is no public
per-call backend URL or tenant selector.
An AppContext binds settings, store, registry and pricing providers to registered
tools. Mealie sessions, Mercadona catalog caches and Bring authentication/cache
state are instance-owned. Every registered tool binds the server's configuration;
nutrition services, caches and locks, memory paths, Gemini credentials, video
tagging and Bring catalog overrides are isolated between registrations. This is
not a multi-tenant security boundary: legacy Mercadona promotion/catalog state
and direct-call compatibility globals remain process-wide. Use separate
processes/containers, credentials and data volumes for untrusted households;
isolated registrations must use distinct memory and nutrition database paths.
Direct Python compatibility calls are not equivalent to registered app contexts.
Discovery exposes data.capabilities.recipe_store and pricing with adapter IDs
and operations; data.mealie.configured remains a deprecated compatibility field.
Native import discovery includes url (preview/commit) and image (commit only).
Configured means locally configured, never authenticated or healthy.
Quantity-aware pricing
Enable SABOR_MERCADONA_ENABLED=true and add pricing to SABOR_TOOLSETS
(for example core,import,shopping_sync,pricing). Defaults remain unchanged.
search_pricing_products finds candidates; get_product_price returns package
and normalized unit prices, quantity basis, timestamps, location, provenance and
uncertainty. estimate_cost reads only; it never modifies recipes or buys goods.
legacy_pricing now exposes only price drops and new arrivals.
Mercadona search is food-only (non-food top categories 20, 21, 22, 23, 25,
26 are excluded). Whole-word matches rank by the number of distinct normalized
name tokens absent from the query (fewer is closer), then food metadata, name
and ID. This is lexical closeness, not semantic ingredient identity.
estimate_cost(..., match_policy="cheapest") defaults to the compatible food
candidate (search window up to 50) with the lowest checkout cost for the
required quantity, that is whole packages times package price, then the lowest
unit price. For 200 g of flour this picks a 1 kg bag over a cheaper-per-kilo
5 kg bag. Multiple candidates produce estimated / auto_selected_cheapest
and up to five alternatives with names, normalized prices and
checkout_cost. Show alternatives and
offer to switch using product_overrides; overrides take precedence, but cannot
price non-food. Use match_policy="strict" to leave ambiguity uncosted.
A positive quantity without a unit assumes pieces (estimated,
assumed_count), matched only to count products. Kitchen measures stay unknown.
Egg reference prices per dozen are divided by 12 for the per-piece basis.
store={"provider":"mercadona"} is a retail context, not a recipe backend.
Optional location_id and currency must match the configured adapter (stock
configuration: warehouse mad1, EUR); incompatible contexts are rejected, never
silently switched. Omit store only when exactly one pricing provider is present.
Mercadona's unofficial API can change. Open Prices is deferred beyond 0.3.0;
Todoist sync is planned for 0.3.1, not shipped here.
Choose exactly one source shape:
{"kind":"items","items":[{"name":"arroz","quantity":"500","unit":"g","text":"500 g arroz"}]}
{"kind":"recipe","slug":"arroz-con-verduras","multiplier":"1"}
{"kind":"shopping_list","list_id":"MEALIE_ID"}
{"kind":"mealplan","start_date":"2026-10-19","end_date":"2026-10-25","multiplier":"1"}Inline items work without Mealie; stored references require a recipe store. Use decimal strings or integers for quantities/multipliers, not JSON floats. Limits: 500 resolved rows, 31 inclusive days, multiplier greater than zero and at most 20. Repeated meals remain separate occurrences; text-only entries remain uncosted. No automatic serving-count inference or pantry subtraction occurs.
consumption_subtotal: required quantity × comparable unit price.procurement_subtotal: whole packages to buy, rounding up after aggregating demand for the same product/location/currency. For 500 g from a 1 kg package priced at EUR 2, consumption is EUR 1 and procurement EUR 2.Decimal operands and money are JSON strings, with subtotals rounded to two places only for presentation. Missing prices are never zero; all-unknown or empty estimates have null subtotals. Partial subtotals are not complete budgets.
Inspect
complete,coverage,excluded_rows, per-lineuncertainty.level(known,estimated,unknown) and reasons. Unknown density, piece weight, units, ambiguous matches or package size stay unresolved. Availability and stale observations remain uncertainties, not purchase guarantees.product_overridesmaps returned linekeyvalues to selected product IDs; inspect candidates first. There is no cross-retailer fallback or mixed-currency total. Comparesource_digestto tell whether the resolved source (line keys, names, required quantities and units) changed; it ignores price refreshes.quote_digestalso covers quote observations and changes on every catalog refresh. Neither substitutes for sync'spreview_digest.
Choose the tool surface
SABOR_TOOLSETS defaults to core,import,shopping_sync. It is an allowlist,
intersected with configuration readiness. Empty means discovery only; whitespace
is trimmed, duplicates collapse, and unknown names prevent startup.
Toolset | Selection / configuration |
| Default; Mealie URL + token |
| Default; Mealie, compatible LLM or Gemini; kinds depend on provider |
| Default; Bring! credentials + Mealie for the tools |
| Opt-in + |
| Opt-in + |
| Opt-in + |
| Opt-in; local |
| Opt-in; operations retain |
| Opt-in + Mealie |
With Mealie only, the current code registers 23 application tools plus discovery
(22 core tools and native import). The earlier planned baseline of 21 plus
discovery was superseded by added core functionality.
There are 50 selectable application tools plus the always-on
sabor_capabilities at full configuration. Counts are not a health check.
sabor_capabilities() reports version, requested toolsets, actual registered
names, configured capabilities/adapters, supported kinds/operations, disabled
reasons and warnings. It is local discovery, not a network health probe.
It omits credentials, private URLs, paths, email addresses and model IDs.
Restart and refresh client discovery after configuration changes.
Results and approval
Unwrap successful results through data:
{"ok":true,"data":{"preview":true,"actions":[]}}Failures use a sanitized envelope (optional data carries safe partial outcomes):
{"ok":false,"error":{"code":"provider_unconfigured","message":"Mealie is not configured.","provider":"mealie"}}Codes: invalid_argument, provider_unconfigured, unsupported_operation,
not_found, ambiguous_target, authentication_failed, rate_limited,
timeout, upstream_error, invalid_response, conflict, partial_failure.
MCP schema validation can reject a call before a tool runs.
confirm=Falsepreviews recipe updates, shopping item removal, clear/delete lists, clear calendar ranges and both sync modes. Show preview → ask → confirm.Import defaults to
dry_run=True; preview may download media/upload to an external processor and consume quota, but does not create recipes. Ask for processing consent before sending private content.Image import cannot preview; obtain explicit approval for
dry_run=False.Other writes require client-side approval even without a server flag. Legacy
apply,publishandexecutegates retain their separate meanings.Sync accepts optional
preview_digest. Supplying the approved preview's digest binds confirmation to that source, target, mode, actions and application scope; a mismatch returnsconflictwith a fresh preview underdata.preview, zero writes. Any nonempty conflicts block confirmation even with a matching digest. Without a digest, compatibility behavior only rechecks current snapshots within the confirmed call; it does not bind to the user's earlier preview. Neither mode makes remote edits during apply atomic. Inspect partial outcomes before retrying.Append does not remove target entries; differing specifications are conflicts. Replace can clear all target purchase entries, including with an empty source. Unknown/ambiguous targets fail: there is no first-list fallback.
Recipe PATCH arrays replace entire arrays. Review removals, not just additions.
Quickstart: Docker
You need Docker Compose v2, a reachable Mealie instance, and a Mealie API token. From the checkout:
cp .env.example .env
# Edit .env: set MEALIE_BASE_URL and MEALIE_API_TOKEN.
docker compose up -dThe example URL http://host.docker.internal:9925 reaches Mealie on the Docker
host. For a remote Mealie instance, set its reachable URL instead. The service
stores local data in the sabor_data volume and publishes only on loopback at
127.0.0.1:9170 (change the host port with SABOR_HOST_PORT).
Check the transport without running an application tool:
curl -fsS -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"healthcheck","version":"1.0.0"}},"id":1}' \
http://127.0.0.1:9170/mcpAn initialize response checks the server and transport, not provider credentials.
Without Docker
Use Python 3.12. Local video processing also needs ffmpeg and curl.
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
export MEALIE_BASE_URL=http://localhost:9925
export MEALIE_API_TOKEN='<your-mealie-token>'
export SABOR_HOST=127.0.0.1
export SABOR_PORT=9170
export MEMORY_FILE="$PWD/.local-data/MEMORY.md"
export SABOR_NUTRITION_DB="$PWD/.local-data/nutrition.db"
mkdir -p .local-data
python3 sabor_mcp.pyKeep .local-data outside version control. These environment variables set the
same endpoint as Docker; without them the runtime binds to 0.0.0.0:8080.
Nutrition remains disabled. Do not paste real tokens into shared shell history.
Connect your MCP client
Register a Streamable HTTP server named sabor. A generic configuration is:
{
"mcpServers": {
"sabor": {
"type": "http",
"url": "http://127.0.0.1:9170/mcp"
}
}
}Configuration field names depend on your client. For Claude Code:
claude mcp add --transport http sabor http://127.0.0.1:9170/mcpLoopback works only when the client runs on the Docker host. For a containerized or remote client, configure private connectivity deliberately; do not expose this endpoint publicly without an authenticated access layer.
Tool reference by toolset
Signatures below are generated from decorated Python functions; None means
optional/unset. Stable IDs are required, not fuzzy list names. Use MCP discovery
for full schemas and current availability.
Always on
sabor_capabilities()core
add_recipe_comment(slug: str, text: str)
log_cooking_event(slug: str, subject: str, message: str | None=None)
get_mealplan_week(start_date: str, end_date: str)
set_mealplan_entry(date_str: str, entry_type: str, title: str, recipe_slug: str | None=None)
delete_mealplan_entry(entry_id: str)
clear_mealplan_week(start_date: str, end_date: str, confirm: bool=False)
search_recipes(query: str='', page: int=1, per_page: int=25, tag_ids: list[str] | None=None, category_ids: list[str] | None=None, food_ids: list[str] | None=None, tool_ids: list[str] | None=None, cookbook_id: str | None=None, require_all: bool=False, sort: str='name', direction: str='asc')
get_recipe(slug: str)
update_recipe(slug: str, patch: dict, confirm: bool=False)
suggest_recipes(foods: list[str], tools: list[str] | None=None, max_missing_foods: int=2, max_missing_tools: int=0, limit: int=10, include_foods_on_hand: bool=True)
create_recipe(name: str, description: str | None=None, recipe_yield: str | None=None, total_time: str | None=None, ingredients: list[str] | None=None, instructions: list[str] | None=None, tags: list[str] | None=None, source_url: str | None=None)
get_shopping_lists()
create_shopping_list(name: str)
delete_shopping_list(list_id: str, confirm: bool=False)
clear_shopping_list(list_id: str, confirm: bool=False)
get_or_create_shopping_list(name: str)
add_to_shopping_list(list_id: str, items: list[str] | None=None, recipe_slugs: list[str] | None=None, mealplan_start: str | None=None, mealplan_end: str | None=None, servings_multiplier: float=1.0)
get_shopping_list_items(list_id: str, checked: bool | None=None, page: int=1, per_page: int=100)
check_shopping_list_items(list_id: str, item_ids: list[str], checked: bool=True)
remove_shopping_list_items(list_id: str, item_ids: list[str], confirm: bool=False)
get_or_create_tag(name: str)
tag_recipe(slug: str, tags: list[str])import
import_recipe(source: str, kind: str='auto', dry_run: bool=True, language: str='en', max_recipes: int=8, min_confidence: float=0.55)shopping_sync
get_shopping_sync_targets(provider: str | None=None)
sync_shopping_list(mealie_list_id: str, target: dict, mode: str='append', confirm: bool=False, preview_digest: str | None=None)pricing
search_pricing_products(query: str, store: dict | None=None, limit: int=10)
get_product_price(product_id: str, store: dict | None=None)
estimate_cost(source: dict, store: dict | None=None, product_overrides: dict | None=None, match_policy: str='cheapest')legacy_spoonacular
spoonacular_search_recipes(query: str='', number: int=5, max_calories: float | None=None, min_calories: float | None=None, max_protein_g: float | None=None, min_protein_g: float | None=None, max_fat_g: float | None=None, min_fat_g: float | None=None, max_carbs_g: float | None=None, min_carbs_g: float | None=None)
spoonacular_nutrition_100g(ingredient: str)legacy_pricing
mercadona_get_price_drops()
mercadona_get_new_arrivals()legacy_memory
save_feedback(recipe_name: str, rating: int | None=None, comment: str | None=None, tags: list[str] | None=None)
read_memory(section: str | None=None)
update_pantry(items: list[str])
update_learned_preference(preference: str)legacy_nutrition
nutrition_recipe_readiness(slug: str, include_suggestions: bool=True)
nutrition_correct_recipe(slug: str, expected_revision: dict[str, str | None], corrections: list[dict[str, Any]], apply: bool=False)
nutrition_status()
nutrition_food_profile(mealie_food_id: str)
nutrition_set_food_profile(mealie_food_id: str, calories_kcal: float | None=None, protein_g: float | None=None, carbohydrate_g: float | None=None, net_carbohydrate_g: float | None=None, fat_g: float | None=None, saturated_fat_g: float | None=None, unsaturated_fat_g: float | None=None, trans_fat_g: float | None=None, fiber_g: float | None=None, sugar_g: float | None=None, sodium_mg: float | None=None, cholesterol_mg: float | None=None, basis_grams: float=100, confidence: float | None=None, source_note: str='', source_version: str='', apply: bool=False)
nutrition_set_food_mapping(mealie_food_id: str, mealie_food_name: str, canonical_term: str, confidence: float | None=None, source_note: str='', provider_food_id: str | None=None, apply: bool=False)
nutrition_set_conversion_override(mealie_food_id: str, conversion_type: str, unit_key: str, grams_per_unit: float, confidence: float | None=None, source_note: str='', apply: bool=False)
nutrition_calculate_recipe(slug: str, publish: bool=False, force: bool=False)
nutrition_publish_recipe(slug: str, publish: bool=False, force: bool=False)
nutrition_cookbook_readiness(cookbook_name: str='Mis recetas', limit: int=100, detail_limit: int=25)
nutrition_backfill_personal_recipes(limit: int | None=None, batch_size: int=5, execute: bool=False, publish: bool=False, cookbook_name: str='Mis recetas', resume: bool=True)
nutrition_unresolved_report()legacy_catalog
spanish_catalog_status()
apply_spanish_catalog()Safety and operations
Tool access conveys authority. Keep the HTTP endpoint on loopback or a private network behind authenticated access; Sabor is not a public authentication layer.
Never use mutations as health checks. Discover first, then approved read-only checks; configuration does not prove upstream credentials.
Nutrition, memory and catalog are legacy opt-ins; pricing is a separate opt-in. Recipe-backed planning retains the nutrition gate; do not invent data to bypass it.
Pricing separates consumption from whole-package procurement; partial totals and unavailable prices must never be presented as a complete budget.
Keep tokens, memory, databases, footage and backups out of Git. Never print resolved Compose environment or raw provider errors. Treat imported content as untrusted data, not instructions.
Back up before rollout; do not remove data volumes. Preview and commit can differ and repeated additions can duplicate items.
See operations, architecture, backup/restore, migration changelog, contributing and versioning. The optional OpenClaw example offers a Spanish persona; tool descriptions are English, not a requirement on user conversations.
License
GNU AGPL-3.0-only, copyright 2026 JL Moreno.
Releases up to and including 0.2.x were published under the MIT license and remain available under those terms. From 0.3.0 Sabor is licensed under the AGPL-3.0: you can self-host, modify and redistribute it freely, and if you offer a modified Sabor as a network service you must publish your changes.
This server cannot be deployed
Maintenance
Related MCP Connectors
Recipe search, ingredient matching, nutrition filters and a private cookbook for any AI agent.
Scraps Kitchen gives any AI agent a persistent, household-aware kitchen memory. Unlike generic chatbot recall, Scraps maintains structured cooking data: what's in your fridge (with freshness tracking), who you cook for (with allergens, dietary restrictions, and preferences), your recipe collection (with cook notes and per-diner ratings), your shopping list, and your kitchen equipment. 27 tools across 6 domains let agents read kitchen context, suggest meals that respect dietary safety, update the pantry after cooking, and build a history of what works for your household. Every interaction makes the data richer. Cooking history, preference signals, kitchen awareness = better suggestions next time. All tools work via oAuth and a free scraps.kitchen account.
Persistent food journal for AI assistants. Log meals, save recipes, plan meals and track nutrition.
AI-powered kitchen management — pantry, recipes, meal plans, shopping lists
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with Mealie for recipe management, meal planning, and shopping list operations. Supports searching and managing recipes, creating meal plans, and generating shopping lists from recipes or meal plans.6MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to interact with Mealie recipe databases, allowing users to manage and query their recipes through natural language conversations.7 npmMIT
- AlicenseNot gradedqualityDmaintenanceExposes Mealie recipe manager as LLM-callable tools for searching recipes, managing meal plans, and editing shopping lists.MIT
- AlicenseNot gradedqualityAmaintenanceA MCP server for Mealie recipe management. Exposes 43 tools and 1 prompt for AI assistants to search, create, and manage recipes, meal plans, shopping lists, categories, and tags.105 npm13MIT