Skip to main content
Glama

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

  1. Call sabor_capabilities(); use only tools in data.registered_tools.

  2. Find recipes with search_recipes or suggest_recipes; inspect structured ingredients and instructions with get_recipe. Alternatively preview import_recipe(source="https://example.org/recipe", kind="url"), ask the user, then repeat with dry_run=False.

  3. Agree a menu and use set_mealplan_entry for each approved entry.

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

  5. Reread get_shopping_list_items and estimate the actual list with estimate_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.

  6. Discover get_shopping_sync_targets. Preview sync_shopping_list(mealie_list_id="MEALIE_ID", target={"provider":"bring","list_id":"BRING_ID"}). Show additions, removals, conflicts, skipped checked items and preview_digest. Ask; repeat the same call with confirm=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 create_recipe (no AI provider in Sabor)

Same

Video URL or local video file

import_recipe returns a media bundle (metadata, subtitles, 8 frames); the agent extracts the recipe and calls create_recipe. Local files: stdio only, under SABOR_MEDIA_ROOTS (default home). On stdio the audio and frames are also files in .sabor-media/ (removed after 24 h)

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 (search_recipes(cookbook_id=...), nutrition cookbook tools)

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 apply

Both 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 not_found; no AI fallback

Import: text, image

None: Sabor contains no AI provider

import_recipe returns unsupported_operation; the agent reads the source and saves it with create_recipe

Import: video URL

A recipe store; ffmpeg and ffprobe for frames

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 .sabor-media/)

Shopping sync

Bring! email + password and a recipe store

Only unchecked source items; does not check off Mealie items

Pricing

pricing toolset + SABOR_MERCADONA_ENABLED=true or SABOR_OPEN_PRICES_ENABLED=true

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-line uncertainty.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_overrides maps returned line key values to selected product IDs; inspect candidates first. There is no cross-retailer fallback or mixed-currency total. Compare source_digest to tell whether the resolved source (line keys, names, required quantities and units) changed; it ignores price refreshes. quote_digest also covers quote observations and changes on every catalog refresh. Neither substitutes for sync's preview_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

core

Default; built-in SQLite or Mealie URL + token

import

Default; a recipe store; recipe web pages (kind='url') only

shopping_sync

Default; Bring! credentials + recipe store for the tools

pricing

Opt-in + SABOR_MERCADONA_ENABLED=true or SABOR_OPEN_PRICES_ENABLED=true; no Mealie required for inline items

legacy_spoonacular

Opt-in + SPOONACULAR_API_KEY

legacy_pricing

Opt-in + SABOR_MERCADONA_ENABLED=true

legacy_memory

Opt-in; local MEMORY_FILE

legacy_nutrition

Opt-in; operations retain SABOR_NUTRITION_ENABLED and existing gates

legacy_catalog

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=False previews 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, default nlp, local); only openai sends 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, publish and execute gates 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 returns conflict with a fresh preview under data.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

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

In 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-mcp

Only 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.0

Until 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).

  1. Configure. From the checkout run cp .env.example .env and set MEALIE_BASE_URL=http://mealie:9000 in .env.

  2. Start and connect Mealie. Run docker compose --profile mealie up -d, open Mealie at http://localhost:9925, sign in as the administrator and create an API token (profile > API Tokens > Generate). Paste it into MEALIE_API_TOKEN in .env.

  3. Apply and connect your agent. Run docker compose --profile mealie up -d --force-recreate sabor and register http://127.0.0.1:9170/mcp in 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/mcp

An 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.py

Keep .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/mcp

Loopback 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 tools
add_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

    B3.1/5.0

    Scored across 24 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count3/5

    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.

    Completeness3/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables 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.
      7
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables AI assistants to manage recipes, meal plans, and shopping lists in Mealie through natural language, supporting CRUD operations and URL-based recipe imports.
      27
      480 npm
      8
      MIT