Sabor
Sabor is a kitchen-intelligence MCP server for finding, importing and managing recipes, planning meals and building shopping lists (backed by SQLite or Mealie).
Discover capabilities:
sabor_capabilitiesreports the active toolsets and actually registered tools.Search and read recipes:
search_recipes(query, tags, categories, foods, tools, cookbooks, sorting/pagination) andget_recipefor structured ingredients, instructions and times.Suggest meals:
suggest_recipesproposes recipes from foods/tools you have, tolerating a few missing items.Create and edit recipes:
create_recipefrom structured fields,update_recipewith preview/confirm (arrays replace whole lists), plustag_recipeandget_or_create_tag.Import recipes:
import_recipewith a dry-run preview for URLs (and video, returning a media bundle rather than writing).Meal planning:
get_mealplan_week,set_mealplan_entry,delete_mealplan_entry, andclear_mealplan_weekwith confirmation.Shopping lists: list, create, get-or-create, add items/recipes/mealplan ranges with a servings multiplier, read items, check/uncheck, and remove items or clear/delete lists (all destructive actions are preview-then-confirm).
Journalling:
add_recipe_commentfor ratings/notes andlog_cooking_eventfor a recipe's cooking history.
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., "@Sabor Plan 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 with built-in SQLite or optional Mealie/Postgres. 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. The selected recipe store is the source of truth for cooking data; core workflows work without Mealie.
Brand guide: docs/marca/.
Optional non-secret config.toml settings use environment > file > defaults.
Set SABOR_CONFIG to select a file; see configuration keys and OS paths.
Secrets may use environment variables or setup-managed keyring/file references;
sabor_capabilities reports data.config_source. Run sabor-mcp doctor --json
to check store health, or sabor-mcp config show --json for masked values and
origins. See terminal diagnostics.
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 store shopping 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
Sabor solo vs Sabor + Mealie
Capability | Sabor solo | Sabor + Mealie |
Recipes, meal plans, shopping lists | Built-in SQLite; no account or token | Mealie URL + API token |
URL import | Offline recipe-scrapers / JSON-LD | Same extractor; saved through Mealie's CRUD API |
Text / image | The client agent extracts the recipe and calls | Same |
Video URL or local video file |
| Same |
Bring! sync / Mercadona estimates | Optional credentials / pricing opt-in | Same optional integrations |
Spanish catalog / legacy nutrition | Not supported | Mealie adapter required |
Cookbooks | Not supported | Mealie cookbooks ( |
Web UI to browse and edit recipes | None: chat only | Mealie's web interface |
sabor_capabilities reports Mealie-only toolsets that are unavailable in the
current instance under data.mealie_only, each with a reason
(requires_mealie_store for legacy_nutrition, requires_mealie for
legacy_catalog) and a short detail. Already running Mealie? Nothing to
migrate: keep MEALIE_BASE_URL and MEALIE_API_TOKEN and Sabor keeps using it.
Step-by-step clean installs for both paths: docs/install-beta.md.
Bootstrap the Spanish Mealie catalog outside the agent tool list:
sabor-mcp catalog status
sabor-mcp catalog apply
# Equivalent module entrypoints:
python -m sabor catalog status
python -m sabor catalog applyBoth require MEALIE_BASE_URL and MEALIE_API_TOKEN, independent of
SABOR_TOOLSETS. Status is a read-only dry-run; apply creates only missing
curated units and audits foods. Neither updates, merges, deletes nor runs seeders.
Results are indented UTF-8 JSON; exit codes: 0 when ok is true, 1 for
configuration/reconciliation failure, 2 for invalid arguments. Review nested
errors and conflicts even when ok is true (legacy semantics).
Capabilities and providers
Capability | Provider / requirements | Limits |
Core | Built-in SQLite, or Mealie URL + token | Recipes, suggestions, planning, shopping items and history; API baseline Mealie 3.22.0 |
Import: URL | Offline recipe-scrapers + schema.org JSON-LD for every store; Mealie recipes are created through its CRUD API, never its scrapers | Read-only preview; pages without a recipe return |
Import: text, image | None: Sabor contains no AI provider |
|
Import: video URL | A recipe store; | Media bundle for the agent: metadata, platform subtitles and 8 frames as images; writes no recipe (on stdio it leaves audio and frame files in |
Shopping sync | Bring! email + password and a recipe store | Only unchecked source items; does not check off Mealie items |
Pricing |
| Read-only quantity-aware estimates; Mercadona catalog or Open Prices observations |
Legacy | Explicit toolset selection plus provider configuration | Promotions, memory, nutrition and Spanish catalog |
Since 0.4.0 Sabor contains no AI provider client (#102): the client agent reads
text, images and video itself and saves the recipe with create_recipe. For a video
URL, import_recipe hands it the video's title, uploader, duration, date, platform
and description, the platform subtitles (original language first) and 8 frames of
640 px at most, sent as MCP images; without ffmpeg it sends the rest and a
limitations entry. It never writes a recipe, so dry_run has no effect. The
SABOR_LLM_*, GEMINI_API_KEY, VIDEO_RECIPE_GEMINI_MODEL and SABOR_VIDEO_PROVIDER
settings were removed: an old configuration keeps starting, they are ignored, and
sabor-mcp doctor warns once naming them.
Recipe stores and instance boundaries
RecipeStore is a typed persistence abstraction for recipes, calendar entries,
shopping lists/items, tags and history. Without MEALIE_BASE_URL, SQLite is
selected automatically at <data dir>/sabor.db; set SABOR_STORE_DB to change the
path. With a non-empty Mealie URL, Mealie remains selected (and requires its token).
Explicit SABOR_STORE=sqlite, SABOR_STORE=mealie or SABOR_STORE=postgres overrides that selection;
invalid values fail startup. SQLite construction and registration perform no
filesystem I/O. Runtime startup initializes the database before serving requests;
direct store use initializes on first operation. Missing parent directories are
created, and opening failures never silently fall back. Back up its data volume.
URL import works with SQLite and needs no AI. The Spanish catalog and legacy
nutrition still require Mealie.
Tandoor and KitchenOwl are 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.
For generic Postgres (including Supabase and Neon), install the optional extra
with pip install 'sabor-mcp[postgres]' or run
uvx --python ">=3.12" --from 'sabor-mcp[postgres]' sabor-mcp. Select SABOR_STORE=postgres and
store the secret SABOR_DATABASE_URL through the environment or
sabor-mcp setup secret database_url. The adapter uses the private sabor schema,
requires no extensions and enables RLS without public policies. SQLite and Mealie
do not require the extra. See Postgres / Supabase operations
for role permissions, Supabase session-pooler settings and backups, and
docs/supabase-agentes.md to set up Supabase with an agent
(sabor-mcp setup supabase --create-role).
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 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 recipe-store, 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.
Import discovery lists the sabor importer with url (preview/commit) and video (bundle).
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). Matching is by whole words, tolerating one trailing s/es
on either side (zanahoria finds Zanahorias; sal never finds Salsa).
A number that is part of the name (4 quesos, not an amount such as 500 g,
2 huevos or a fraction such as 1/2 kg) ranks products carrying it first, so they stay inside the 50-result
window. A written numeral from two up counts as its digits (cuatro quesos as
4 quesos); an article (un queso) is always an amount. Free-text shopping items
typed in a built-in store are matched on their original text, so 4 quesos keeps its
number; items from Mealie keep their previous selection (Beta A). The number must
modify the same word: Pizza 4 quesos carries 4 quesos, Pizza 3 quesos pack 4 unidades
does not, and estimate_cost applies the same rule. Then matches rank by unrequested processed or compound words (croquetas,
chicles, salsa, molido, a la jardinera, condensed milk...), then by the
number of normalized name words absent from the query (fewer is closer), food
metadata, name and ID. Requested modifiers are not penalized (croquetas de merluza finds croquetas), and entera, desnatada and semidesnatada stay neutral.
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. A basic ingredient ignores processed or compound candidates
when an unprocessed one matches (merluza picks fillets over cheaper croquetas,
jengibre fresh ginger over chewing gum); they are used only when nothing else
matches. Show alternatives and
offer to switch using product_overrides; overrides take precedence, but cannot
price non-food. With at least two non-overridden lines sharing normalized name
words and base unit, known quantities are also evaluated together. A common
product replaces per-line choices only when its combined checkout cost is
strictly cheaper than the original grouped procurement; ties keep the original
choices, and basket procurement never increases. For 300 g + 300 g arroz,
500 g at EUR 0.60 loses to 1 kg at EUR 0.90 (EUR 1.20 → 0.90). For 200 g +
900 g with 1 kg at EUR 1.00, mixed choices stay (EUR 1.60, not EUR 1.80).
Changed lines add auto_selected_cheapest_group and grouped_selection evidence;
their alternatives show checkout costs for the summed quantity (up to five).
Use match_policy="strict" to leave ambiguity uncosted; strict is not grouped.
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. Todoist sync is planned, not shipped here.
Alternatively enable SABOR_OPEN_PRICES_ENABLED=true (default false) and the
pricing toolset. Select store={"provider":"open_prices","location_id":"ES","currency":"EUR"}
with estimate_cost or get_product_price; product IDs are barcodes. Country
is an ISO alpha-2 code (default ES), currency defaults to EUR. Open Prices is
universal crowd observations, not a current catalog: Spanish coverage is sparse
and old. The latest matching country observation wins (ties: latest ID), never
the cheapest. Missing locations/country observations return not_found, not zero.
Search returns up to ten products with observations; search_pricing_products
leaves out those without a quote in the requested country instead of failing the
whole search, and tries to fill the requested limit with the next quoted candidates
within the window the provider returns (Open Prices examines at most ten), so fewer
results can come back (get_product_price on such a product still returns not_found).
Its name filter is a substring match, so a one-word plural query that returns fewer
than ten also searches its singular to fill the remaining room (guisantes also
finds Guisante fino); original results always come first, and multi-word queries
are sent as given.
Quotes include observation date, public store/city, unknown availability and
observation always; stale_observation means older than 180 days, discounted
marks promotions, single_observation means only one matching observation in the
fetched window, and unknown_unit leaves package/unit pricing unknown.
Responses are cached per instance for ten minutes (up to 128 requests); prices
inspect only the latest 50 observations, so country coverage can be incomplete.
Attribution: Open Prices (Open Food Facts), ODbL, https://prices.openfoodfacts.org/.
Do not combine with non-free data into a redistributed database. Contributor
identifiers, proofs and coordinates are never included in quotes or errors.
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; built-in SQLite or Mealie URL + token |
| Default; a recipe store; recipe web pages ( |
| Default; Bring! credentials + recipe store 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 import_recipe). The earlier planned baseline of 21 plus
discovery was superseded by added core functionality.
There are 52 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":"Recipe store 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 fetches the recipe page but does not create recipes. Sabor itself calls no AI provider. With Mealie, a committed import runs Mealie's ingredient parser (SABOR_INGREDIENT_PARSER, defaultnlp, local); onlyopenaisends ingredient text to Mealie's configured AI provider. Sabor never uses Mealie's scrapers, whose fallback is Mealie's AI provider.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.
Install
1. With your AI (recommended)
Paste this into your coding agent (Claude Code, Cursor, Codex, Gemini CLI, Cline, OpenClaw…):
Install and configure Sabor by following
https://github.com/JlMoreno00/sabor/blob/main/llms-install.mdIn Cursor, open this prompt in the chat
(it only fills the chat; nothing runs until you send it). The agent asks a few
questions one at a time, configures Sabor with sabor-mcp setup in a terminal and
verifies configuration and store health with sabor-mcp doctor --json. This does
not check MCP transport or provider credentials; complete verification with
sabor_capabilities and the real round trip in llms-install.md. Secrets are typed by you in a terminal
(sabor-mcp setup secret <name>), never in the chat. See llms-install.md.
2. One click (local SQLite, no questions)
These links register uvx --python ">=3.12" sabor-mcp and work after the first PyPI release;
until then use the uvx --from git+https://github.com/JlMoreno00/sabor sabor-mcp
form from Connect your MCP client.
Or from a terminal:
claude mcp add --scope user sabor -- uvx --python ">=3.12" sabor-mcp
codex mcp add sabor -- uvx --python ">=3.12" sabor-mcp
gemini mcp add --scope user sabor uvx --python ">=3.12" sabor-mcp
code --add-mcp '{"name":"sabor","command":"uvx","args":["--python",">=3.12","sabor-mcp"]}'Without a config.toml, Sabor starts with local SQLite in the OS data directory;
run sabor-mcp setup later to add Mealie, prices or Bring!.
3. Docker or Mealie (advanced)
See Quickstart: Docker — Sabor + Mealie and docs/install-beta.md.
Quickstart: Sabor solo
With Python 3.12+ and uv, the stdio install is:
uvx --python ">=3.12" sabor-mcpOnly after the first PyPI release. Until then, use
uvx --from git+https://github.com/JlMoreno00/sabor sabor-mcp, or build a
local wheel with uv build and run
uvx --from dist/sabor_mcp-0.4.0-py3-none-any.whl sabor-mcp.
No Mealie configuration is needed. The console command defaults to stdio;
sabor-mcp --transport http instead serves Streamable HTTP on loopback
(127.0.0.1:8080/mcp) unless you set SABOR_HOST explicitly.
For standalone Docker (HTTP), only after the first GHCR release:
docker run -p 127.0.0.1:9170:8080 -v sabor-data:/data ghcr.io/jlmoreno00/sabor:0.4.0Until then build locally with docker build -t sabor:local . and use
sabor:local in that command. No Mealie variables are needed.
Connect to http://127.0.0.1:9170/mcp.
Data lives in SABOR_DATA_DIR when set, otherwise an existing /data
directory (Docker), otherwise the OS user data directory:
$XDG_DATA_HOME/sabor or ~/.local/share/sabor on Linux,
~/Library/Application Support/sabor on macOS, %LOCALAPPDATA%\\sabor on Windows.
SQLite, memory, nutrition and catalog cache paths derive from it; explicit
per-file overrides still win. Persist the Docker volume and back up local data.
Video frames in the agent media bundle need system ffmpeg / ffprobe (included
in Docker); install ffmpeg separately for uvx. Without them import_recipe still
returns the video's metadata and subtitles, with a no_frames limitation.
Quickstart: Docker — Sabor + Mealie
You need Docker with Compose v2. Nothing else: the mealie profile also starts
a Mealie for Sabor (pinned to the tested Mealie 3.22.0).
Configure. From the checkout run
cp .env.example .envand setMEALIE_BASE_URL=http://mealie:9000in.env.Start and connect Mealie. Run
docker compose --profile mealie up -d, open Mealie athttp://localhost:9925, sign in as the administrator and create an API token (profile > API Tokens > Generate). Paste it intoMEALIE_API_TOKENin.env.Apply and connect your agent. Run
docker compose --profile mealie up -d --force-recreate saborand registerhttp://127.0.0.1:9170/mcpin your MCP client (Connect your MCP client).
Add COMPOSE_PROFILES=mealie to .env to drop --profile mealie from later
commands. Already running Mealie? Leave the profile out and point
MEALIE_BASE_URL at it, for example http://host.docker.internal:9925 for a
Mealie on the Docker host; then docker compose up -d starts only Sabor. Data
persists in the sabor_data and mealie_data volumes, and both services
publish only on loopback (127.0.0.1:9170 and 127.0.0.1:9925; change the
Sabor port with SABOR_HOST_PORT and Mealie's in
docker-compose.override.yml). Sabor is built locally until a release image
is published to ghcr.io/jlmoreno00/sabor.
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. For videos
without subtitles, the optional sabor-mcp[transcribe] extra transcribes the audio
locally on the CPU (faster-whisper; no AI provider, see docs/operations.md).
python3 -m venv .venv
. .venv/bin/activate
# Optional, for videos without subtitles: pip install -e '.[transcribe]'
pip install -r requirements.txt
export SABOR_HOST=127.0.0.1
export SABOR_PORT=9170
export SABOR_DATA_DIR="$PWD/.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
For stdio, let the MCP client launch uvx (after the first PyPI release):
{
"mcpServers": {
"sabor": {
"command": "uvx",
"args": ["--python", ">=3.12", "sabor-mcp", "--transport", "stdio"]
}
}
}Before that release use
"args": ["--from", "git+https://github.com/JlMoreno00/sabor", "sabor-mcp", "--transport", "stdio"].
Keep stdout reserved for the protocol; logs go to stderr.
For Docker or a running HTTP process, 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
search_recipes accepts sort="name" (default), created_at, updated_at,
rating, last_made, or random, with direction="asc" or "desc".
Random ignores direction and uses the UTC date (YYYY-MM-DD) as its daily
seed, keeping pagination stable within the day for unchanged recipes/filters.
Unrated and never-made recipes sort last in either direction. SQLite has no
ratings: sort="rating" returns unsupported_operation, not a substitute order.
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)Pantry-aware estimates
The instance preferences default-profile pantry list is read
under the preferences lock, even when the preferences toolset is disabled.
Missing files mean an empty pantry. A corrupt file makes exclude_pantry: true
estimates fail with a sanitized invalid_response; estimates without it never
read preferences. Shopping additions are not blocked by a corrupt file (see
pantry_warning below). No pantry data is changed here.
Every estimate_cost source kind accepts optional exclude_pantry: bool:
estimate_cost(source={"kind":"recipe","slug":"rice","exclude_pantry":True}).
Absent/false leaves the full response unchanged and does not read preferences.
True skips pricing matched lines, keeps them in lines and excluded_rows with
uncertainty.reasons: ["in_pantry"], counts them in coverage excluded, and
adds pantry: {"applied": true, "matched": N}. Subtotals exclude those lines;
if nothing is costed, subtotals remain null, not zero.
Matching uses structured food names or ingredient_name on free text, then
casefolding, accent removal and collapsed whitespace. ingredient_name strips a
leading amount, a Spanish or English unit word (300 g de arroz and 300 grams of rice give arroz and rice) and the connector de/of. English unit words apply
with every store, Mealie included; Spanish lines are unchanged. Whole names must equal,
or equal after stripping one trailing s or es on either side. No fuzzy or
substring matching: huevo matches huevos, but sal never matches salsa.
This is lexical matching, not quantity-aware stock accounting.
add_to_shopping_list always adds every requested line. With a nonempty pantry,
its success data additionally contains pantry_matches, for example
[{"name":"huevos","pantry_entry":"huevo"}] (or [] if none match).
The field is absent for an empty/missing pantry. Pantry hints never block the
addition: if preferences are unreadable or matching fails, the lines are still
added and the response carries pantry_warning instead. SQLite, Mealie and Fake use
source lines: inline item text, or each occurrence's recipe ingredients read
before writing. IDs are deliberately omitted because stores can merge lines;
reread get_shopping_list_items to identify IDs, then ask for approval and use
remove_shopping_list_items if desired. No automatic removal or skipping occurs.
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()preferences
Opt in with SABOR_TOOLSETS=core,import,shopping_sync,preferences; defaults
remain unchanged. These tools need no provider. Preferences are per instance /
household, not authenticated users; only the reserved default profile is used.
Both are provisional choices pending maintainer confirmation: the toolset stays
opt-in, and per-user profiles wait for authenticated users (#28).
Preview changes first, then repeat with confirm=True after approval.
get_preferences()
update_preferences(changes: dict, confirm: bool=False)changes accepts set (replace fields), add and remove (list fields only).
Fields: diet, dislikes, pantry, notes are lists of up to 200 strings;
strings are trimmed, non-empty, at most 200 characters and deduplicated ignoring
case. household_size is null or an integer from 1 to 20. Operations run in
set → add → remove order. Unknown keys or invalid values write nothing.
Responses include preferences, updated_at and source (defaults, file, or postgres);
updates also include a before/after field diff and preview flag. Preview does
not change the timestamp. Pantry is also available to pricing and list building
without enabling this toolset; see pantry-aware estimates.
legacy_memory
Deprecated in favor of preferences. These tools still use MEMORY.md unchanged,
including save_feedback; ratings/favorites/comments are not redirected.
See migration instructions.
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
Deprecated: prefer the CLI; the MCP tools will be retired in a future major version. Their signatures and behavior remain unchanged for compatibility.
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.
Configure locally with sabor-mcp setup (English/español), or use
sabor-mcp setup --non-interactive --json. See Setup.
Available Tools
24 toolsadd_recipe_commentC
Anade un comentario a una receta en Mealie (rating, notas, feedback).
Inputs: slug (required), text (required). Returns: object
add_to_shopping_listB
Add exactly one source to a list. The client must get user approval before calling.
Items remain unparsed notes; recipe scaling uses the explicit servings multiplier.
Inputs: list_id (required), items, mealplan_end, recipe_slugs, +2 more.
check_shopping_list_itemsC
Set checked state only after verifying every requested item belongs to the list.
Inputs: list_id (required), item_ids (required), checked.
clear_mealplan_weekA
Preview entries removed in a date range; pass confirm=True to clear them.
Inputs: end_date (required), start_date (required), confirm. Returns: object
clear_shopping_listA
Preview removal of all shopping items; pass confirm=True to clear the list.
Inputs: list_id (required), confirm. Returns: object
create_recipeA
Create a recipe in Mealie using structured fields, not URL import.
Inputs: name (required), tags, source_url, total_time, +4 more. Returns: object
create_shopping_listC
Crea una nueva lista de compra vacia con el nombre indicado.
Inputs: name (required). Returns: object
delete_mealplan_entryC
Elimina una entrada del plan de comidas por su ID.
Inputs: entry_id (required). Returns: object
delete_shopping_listA
Preview deletion of a shopping list; pass confirm=True to delete it permanently.
Inputs: list_id (required), confirm. Returns: object
get_mealplan_weekB
Obtiene el plan de comidas para un rango de fechas (YYYY-MM-DD).
Inputs: end_date (required), start_date (required). Returns: object
get_or_create_shopping_listA
Busca una lista de compra por nombre; si no existe, la crea. Devuelve siempre el ID.
Inputs: name (required). Returns: object
get_or_create_tagA
Busca un tag por nombre (case-insensitive); si no existe, lo crea.
Inputs: name (required). Returns: object
get_recipeB
Read structured ingredients, instructions, taxonomy and times; unknowns stay null.
Inputs: slug (required). Returns: object
get_shopping_list_itemsB
Read one list's items, optionally filter checked state, then paginate locally.
Inputs: list_id (required), page, checked, per_page.
get_shopping_listsA
Devuelve todas las listas de compra del hogar con sus IDs y nombres.
Inputs: no named parameters. Returns: object
import_recipeB
Preview or import recipes. Video previews may download media and incur LLM costs.
Image creation has no read-only preview. Local media paths are unsupported.
Inputs: source (required), kind, dry_run, language, +2 more.
log_cooking_eventC
Registra un evento en el timeline de una receta (cooking log / historial).
Inputs: slug (required), subject (required), message. Returns: object
remove_shopping_list_itemsA
Preview item removal; confirm=True deletes exactly the verified item IDs.
Inputs: list_id (required), item_ids (required), confirm.
sabor_capabilitiesA
Describe configured toolsets and the actual registered tool surface.
Inputs: no named parameters.
search_recipesC
Search recipes by stable filter IDs with upstream pagination; sort by name.
Inputs: page, sort, query, tag_ids, +7 more. Returns: object
set_mealplan_entryC
Anade una entrada al calendario de Mealie. entry_type: lunch|dinner|breakfast|side.
Inputs: title (required), date_str (required), entry_type (required), recipe_slug. Returns: object
suggest_recipesC
Suggest recipes from food/tool IDs, preserving missing foods and tools.
Inputs: foods (required), limit, tools, max_missing_foods, +2 more. Returns: object
tag_recipeB
Asigna etiquetas a una receta. Conserva tags existentes y anade los nuevos.
Inputs: slug (required), tags (required). Returns: object
update_recipeC
Preview or PATCH selected fields. Arrays replace whole lists, not individual entries.
Inputs: slug (required), patch (required), confirm. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 24 tools
Tools are grouped by resource (recipes, shopping lists, meal plans, tags), and most actions are distinct. However import_recipe vs create_recipe and create_shopping_list vs get_or_create_shopping_list can be confused, and delete/clear/remove shopping-list tools require careful selection.
Almost all tools use snake_case with verb_noun or verb_preposition_noun patterns. Outliers like sabor_capabilities and compound get_or_create_* names slightly break the pattern but remain readable and consistent in casing.
24 tools is heavy for a single MCP server; the broad domain justifies many but still sits in the borderline 16-25 range. Some meta or compound tools could potentially be consolidated.
Core workflows for recipes, shopping lists, and meal plans are covered with preview/confirm patterns. But there is no delete_recipe, no read/delete for recipe comments or cooking events, and no list/delete tags, leaving notable lifecycle gaps.
Maintenance
Related MCP Connectors
Recipe search, ingredient matching, nutrition filters and a private cookbook for any AI agent.
Household meal planning: recipes, weekly meal plans, and grocery lists for your 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.
Food list analyzer and cited food answers for agents. Also recipes, meal plans and grocery 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.7MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with Mealie recipe databases, allowing users to manage and query their recipes through natural language conversations.17 npmMIT
- AlicenseNot gradedqualityDmaintenanceExposes Mealie recipe manager as LLM-callable tools for searching recipes, managing meal plans, and editing shopping lists.MIT
- AlicenseAqualityCmaintenanceEnables AI assistants to manage recipes, meal plans, and shopping lists in Mealie through natural language, supporting CRUD operations and URL-based recipe imports.27480 npm8MIT