Tandoor Recipes MCP
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| TANDOOR_URL | Yes | Base URL of your Tandoor instance. | |
| TANDOOR_TOKEN | No | Tandoor API token with write access. Required except in remote mode, where it must not be set. | |
| TANDOOR_MCP_AI | No | Tool section: ai. Takes off or on. | off |
| TANDOOR_MCP_LOG | No | Stderr trace mode: request, response, error, all, or a comma list. Bearer token is redacted. | |
| TANDOOR_MCP_ADMIN | No | Tool section: admin. Takes off or on. | off |
| TANDOOR_MCP_FOODS | No | Tool section: foods. Takes off, simple, or full. | simple |
| TANDOOR_MCP_COOKLOG | No | Tool section: cooklog. Takes off or on. | off |
| TANDOOR_MCP_EXCLUDE | No | Comma-separated denylist, e.g. merge_*,delete_* to hide destructive tools. | |
| TANDOOR_MCP_RECIPES | No | Tool section: recipes. Takes off, simple, or full. | simple |
| TANDOOR_MCP_DATA_DIR | No | Folder for oauth.json. Must be an absolute path. | |
| TANDOOR_MCP_SHOPPING | No | Tool section: shopping. Takes off, simple, or full. | simple |
| TANDOOR_MCP_HTTP_HOST | No | Address the server listens on. Defaults to 0.0.0.0 in the Docker image. | 127.0.0.1 |
| TANDOOR_MCP_HTTP_PATH | No | MCP endpoint path. | /mcp |
| TANDOOR_MCP_HTTP_PORT | No | HTTP-mode port. | 3737 |
| TANDOOR_MCP_INVENTORY | No | Tool section: inventory. Takes off, simple, or full. | simple |
| TANDOOR_MCP_MEAL_PLAN | No | Tool section: meal-plan. Takes off, simple, or full. | simple |
| TANDOOR_MCP_TRANSPORT | No | Transport mode. Use stdio (default) or http. Remote mode requires http. | stdio |
| TANDOOR_MCP_HTTP_TOKEN | No | HTTP-mode bearer token. If unset, HTTP transport runs without auth. Not allowed in remote mode. | |
| TANDOOR_MCP_PUBLIC_URL | No | The public HTTPS URL clients connect to, such as https://mcp.example.com/mcp. Required in remote mode. | |
| TANDOOR_MCP_SECRET_KEY | No | Exactly 64 hex characters (32 bytes), generated with openssl rand -hex 32. Required in remote mode. | |
| TANDOOR_MCP_TRUST_PROXY | No | Set when a reverse proxy or tunnel sits in front. Use a hop count such as 1, or comma-separated IP addresses and CIDR ranges. | |
| TANDOOR_MCP_INCLUDE_ONLY | No | Comma-separated allowlist (glob * supported), e.g. list_*,get_*,create_meal_plan. | |
| TANDOOR_MCP_RECIPE_BOOKS | No | Tool section: recipe-books. Takes off or on. | off |
| TANDOOR_MCP_STASH_TTL_MS | No | Per-entry TTL in milliseconds (default 10 minutes). | 600000 |
| TANDOOR_MCP_DOWNLOAD_ROOT | No | Existing folder that save: true writes downloads into. Stdio mode only. | |
| TANDOOR_MCP_HTTP_DRAIN_MS | No | On SIGTERM or SIGINT, how long the server waits for requests in progress. | 10000 |
| TANDOOR_MCP_IMPORT_EXPORT | No | Tool section: import-export. Takes off or on. | off |
| TANDOOR_MCP_STASH_ENABLED | No | Park large tool results in an in-memory cache and return a stash summary. Set 0 to disable. Auto-disabled in HTTP mode. | 1 |
| TANDOOR_MCP_HTTP_MAX_CONNS | No | The most open connections the server accepts. | 128 |
| TANDOOR_MCP_HOUSEHOLD_SPACE | No | Tool section: household-space. Takes off or on. | off |
| TANDOOR_MCP_STASH_MAX_BYTES | No | Total-bytes cap on the stash; LRU eviction kicks in past this budget. | 32000000 |
| TANDOOR_MCP_STASH_THRESHOLD | No | Stash payloads strictly larger than this many bytes. | 25000 |
| TANDOOR_MCP_STASH_MAX_ENTRIES | No | LRU cap on stashed handles. | 32 |
| TANDOOR_MCP_TANDOOR_PUBLIC_URL | No | The address users open Tandoor at, such as https://recipes.example.com. Used in remote mode when TANDOOR_URL is an internal address. | |
| TANDOOR_MCP_HTTP_MAX_BODY_BYTES | No | Larger request bodies get a 413. | 5242880 |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
NOTE: the Tandoor version could not be verified at startup. If write tools fail with serializer errors, the instance may be pre-2.x (unsupported).
Tandoor Recipes MCP server, full access to recipes, meal plans, shopping lists, ingredients, cook logs, nutrition, and AI-assisted imports.
Where to start:
Current state (read-only):
tandoor://meal-plan/this-week,tandoor://pantry/on-hand,tandoor://shopping-list/active,tandoor://meal-types.Common workflows: use the
plan_week,grocery_list_for_plan,what_can_i_make_tonight, orimport_and_planprompts.Recipe search: use
search_recipeswith food/keyword names (it resolves IDs for you). Fall back tolist_recipesfor the full filter surface.Write tools return a slim JSON shape by default. Pass
format: "full"for the raw Tandoor API response when you need substitutes, image URLs, nutrition objects, etc.
Large results: tool responses bigger than ~25000 bytes come back as
{stashed:true, handle, shape, sample_filters} instead of the full payload. Call
jq_query with that handle and a jq filter to extract the parts you need, do
NOT re-run the original tool. Example: jq_query({handle, filter: ".results | map({id, name})"}).
Operators can set TANDOOR_MCP_STASH_ENABLED=0 to disable this behavior.
Pass ids (food_id, unit_id, ...), not objects; the tools build the request shape.
Tool sections: recipes=simple, meal-plan=simple, shopping=simple, inventory=simple, foods=simple, cooklog=off, recipe-books=off, ai=off, import-export=off, household-space=off, admin=off. Call list_tool_groups to see them.
Transport: stdio. auth-required: no
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
| prompts | {
"listChanged": true
} |
| resources | {
"listChanged": true
} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| list_tool_groupsA | List every tool section with its current mode and the modes it accepts. Tiered sections (off / simple / full) and on/off sections can be widened with enable_tool_group and turned off with disable_tool_group. The "always" section is permanent. When a section changes, its tools appear in or leave tools/list and the SDK emits tools/list_changed so the client refreshes mid-conversation. Use this first to discover what is available before reaching for enable_tool_group. |
| enable_tool_groupA | Widen a section so its tools become visible in tools/list. For a tiered section, tier "simple" or "full" (default full) picks how much to show; on/off sections take no tier. Enabling never narrows a section: asking for simple on a section already at full leaves it at full. The SDK emits tools/list_changed automatically; the client refreshes its tool list mid-conversation. Be sparing: every enabled tool adds its schema to your context budget. Run list_tool_groups first to see what is available. |
| disable_tool_groupA | Turn a section off so all its tools disappear from tools/list. The SDK emits tools/list_changed automatically. The "always" section cannot be disabled. Use this to free context budget once a workflow no longer needs a section. |
| list_recipesA | List/search recipes with extensive filtering. Default returns slim results (id, name, description, servings, rating, keywords). Pass format="full" for raw API response. sort_order options: score,-score,name,-name,lastcooked,-lastcooked,rating,-rating,times_cooked,-times_cooked,created_at,-created_at,lastviewed,-lastviewed. If filtering by food/keyword/book NAMES (not ids), prefer search_recipes — it resolves names to ids for you and reports unresolved names in _meta.unresolved so you know what was dropped. If this returns an empty result set, recommend: broaden the query, drop restrictive filters, or try search_recipes if you were passing names. |
| get_recipeA | Get recipe by ID. Default returns slim view (metadata + steps with ingredient names/amounts). Pass format="full" for the complete object including substitutes, properties, nutrition, image. |
| create_recipeC | Create recipe. Required: name, steps[{instruction, ingredients[{food, amount, unit?}]}]. Optional: description, servings, working_time, waiting_time, keywords[] |
| update_recipeB | Update recipe metadata and content. Only provide fields you want to update. All fields optional except id. |
| import_recipe_from_urlA | Import a recipe from a URL. Tries Tandoor's built-in scraper first, then fetches the page and extracts schema.org JSON-LD as a real fallback (works when Tandoor can't reach the URL or doesn't support the site). On total failure, throws with an attempts log — pass create_stub_on_failure=true to write an empty placeholder recipe instead. If all stages fail, recommend: (1) retry with create_stub_on_failure=true to capture the URL as a stub you can fill in later; (2) if the page is an image/PDF, use import_recipe_from_image (AI provider required); (3) if the URL is paywalled or JS-rendered, fall back to manual create_recipe with fields you can see. |
| related_recipesA | List recipes related to this one (Tandoor similarity). Slim by default (id + name); pass format="full" for raw API response. |
| add_recipe_to_shopping_listA | Add a recipe's ingredients to the shopping list at N servings. Omit ingredients[] to add all. Set servings=0 with list_recipe (id) to delete that shopping-list-recipe. |
| search_recipesA | High-level recipe search. Accepts food/keyword/book names (not IDs) and resolves them internally — typically collapses 3-4 round-trips into 1. Example: {foods: ["chicken", "broccoli"], exclude_foods: ["peanuts"], keywords: ["weeknight"]}. For complex ID-based filtering, fall back to list_recipes. |
| list_recipes_flatA | Lighter alternative to list_recipes; returns minimal recipe metadata suitable for large browse queries. Use list_recipes if you need keywords/foods/steps nested. |
| list_meal_plansC | List meal plans. Optional: from_date, to_date (YYYY-MM-DD), page, page_size |
| get_meal_planC | Get meal plan by ID |
| create_meal_planC | Create meal plan. Required: from_date (ISO), meal_type_id, servings. Optional: recipe_id, title, note, addshopping |
| update_meal_planC | Update meal plan. Required: id. All other fields optional |
| delete_meal_planC | Delete meal plan by ID |
| list_meal_typesA | List available meal types (breakfast, lunch, dinner, etc.) |
| parse_ingredientA | Parse a free-form ingredient string (e.g. "2 cups flour") into amount, unit, unit_id, food, food_id, note. Writes to the database: Tandoor creates any Food or Unit row it does not already have. unit and food are the matched row names, so "cups" can come back as "cup". With no unit, unit is "" and unit_id is omitted. food_id and unit_id pass straight to create_ingredient. Blank text returns Tandoor's 400. Tandoor's parser is heuristic and can misfire on compound descriptions, brand names, or non-English units. |
| list_shopping_listsB | List the named shopping lists of this space (for example one per store), as {id, name, description, color}. Every member of the space sees the same lists. Optional: page, page_size. |
| list_shopping_entriesA | List shopping list entries (auto-filtered to unchecked + recent by Tandoor). Optional: mealplan, updated_after (ISO), page, page_size, format. Entries cannot be filtered by shopping list; use the shopping_lists field of the results. |
| get_shopping_entryC | Get a shopping list entry by ID. |
| create_shopping_entryA | Create a shopping list entry. Required: amount, food_id. Optional: unit_id, checked, list_recipe, ingredient, order, delay_until, mealplan_id (links to existing/new ShoppingListRecipe for that mealplan), shopping_list_ids. shopping_list_ids (ids from list_shopping_lists) puts the entry on exactly those lists; the food's lists are not added. When shopping_list_ids is omitted, Tandoor copies the food's lists onto the new entry if the user preference shopping_update_food_lists is on. With [], the tool clears any lists Tandoor copied, so the entry is on no list. add_recipe_to_shopping_list always copies each food's lists; food_shopping_update never does. Changing a food's lists later does not change existing entries. Every list id is checked against list_shopping_lists first; unknown or repeated ids are rejected and nothing is written. If the lists after the write differ from the request, the tool errors and names the missing and the extra lists. |
| update_shopping_entryA | Update a shopping entry (PATCH). Required: id. Other fields optional. When checked is omitted, the tool reads the entry and sends its current checked value again, so a checked entry keeps its completed_at. A check made elsewhere between that read and the write is undone (last write wins). shopping_list_ids puts the entry on exactly those lists; [] clears them; omit it to keep them. Every list id is checked against list_shopping_lists first; unknown or repeated ids are rejected and nothing is written. If the lists after the write differ from the request, the tool errors and names the missing and the extra lists. |
| delete_shopping_entryC | Delete a shopping entry by ID. |
| bulk_check_shopping_entriesB | Bulk check or uncheck many shopping entries by ID list. |
| bulk_update_shopping_entriesA | Update many shopping entries in one call. Required: ids (entry ids; ids that do not exist or are not visible to you are skipped). Give checked, a list change, or both. checked: true checks every entry and sets completed_at to now, also on entries already checked; without checked, checked and completed_at stay as they are. List changes do not update the entries' updated_at. List changes: shopping_lists_add and shopping_lists_remove (may be combined), shopping_lists_set (replaces the lists; must not be empty) or shopping_lists_remove_all: true (takes every list off). set and remove_all must be used alone. Empty arrays and shopping_lists_remove_all: false count as no change and are not sent. An id may not appear in both shopping_lists_add and shopping_lists_remove. Every list id is checked against list_shopping_lists first; unknown or repeated ids are rejected and nothing is written. |
| list_foodsC | List foods. Optional: query (case-insensitive name match), root (parent id, 0 for top-level), tree (id), random, limit, page, page_size. |
| get_foodC | Get food by ID. |
| create_foodA | Create food. Required: name. Optional: plural_name, description, food_onhand, fdc_id, supermarket_category_id, shopping_list_ids (ids from list_shopping_lists). If a food with this name exists, Tandoor returns it instead of creating one. With shopping_list_ids (including []), the tool looks up the name first and, if a food with that name exists (ignoring case and surrounding spaces), refuses and changes nothing; use update_food. Tandoor also returns an existing food whose plural name matches; the tool detects that from the create reply and refuses the same way, setting no lists. With non-empty shopping_list_ids, the tool creates the food first and then sets its lists, because Tandoor cannot set lists while creating a food. With any shopping_list_ids (including []), if a step after the create fails, the error gives the new food's id and says not to retry. Changing a food's lists does not change its existing shopping entries. add_recipe_to_shopping_list copies the food's lists onto new entries. create_shopping_entry without shopping_list_ids copies them when the user preference shopping_update_food_lists is on. bulk_create_shopping_list_recipe_entries without shopping_list_ids copies each food's lists whatever the user preference. Every list id is checked against list_shopping_lists first; unknown or repeated ids are rejected and nothing is written. If the lists after the write differ from the request, the tool errors and names the missing and the extra lists. |
| update_foodA | Update food (PATCH). Required: id. Optional properties[] {property_type_id, property_amount} sets one value per property type (amount null = unknown), updating the existing value of that type in place and deleting any duplicate values of that type; remove_property_type_ids[] deletes values by type. Other existing property values are kept. A type may appear once across both lists. properties_food_amount and properties_food_unit_id set the food amount the property values refer to (e.g. 100 g); properties_food_unit_id null clears the unit. Deleting a property row removes it from every food or recipe it is linked to. If a row delete fails, the food is already updated and the tool returns an error naming those rows. shopping_list_ids replaces the food's lists; [] clears them; omit it to keep them. Changing a food's lists does not change its existing shopping entries. add_recipe_to_shopping_list copies the food's lists onto new entries. create_shopping_entry without shopping_list_ids copies them when the user preference shopping_update_food_lists is on. bulk_create_shopping_list_recipe_entries without shopping_list_ids copies each food's lists whatever the user preference. Every list id is checked against list_shopping_lists first; unknown or repeated ids are rejected and nothing is written. If the lists after the write differ from the request, the tool errors and names the missing and the extra lists. |
| food_shopping_updateA | Add a food to active shopping lists with optional amount/unit. Pass delete=true to remove this food from all active lists. |
| list_unitsC | List units. Optional: query, limit, page, page_size, random. |
| jq_queryA | Run a jq filter against a previously stashed tool result and return the focused subset. Use this when a prior tool returned |
| jq_stash_statsA | Inspect the in-memory stash (count + total bytes). Diagnostic; not normally needed. |
| get_guard_statsA | Inspect security-guard rejection counters (PathGuardError, SsrfBlockedError) since process start. Useful for dashboards and "is anything probing me?" checks. |
| get_versionA | Report this MCP server's name/version and the Tandoor server version + compatibility status detected at startup (ok | unsupported | unknown). No arguments; answers from cached startup state without a network call. |
| list_keywordsC | List keywords (recipe tags). Optional: query, root, tree, random, limit, page, page_size. |
| get_keywordC | Get keyword by ID. |
| get_meal_typeC | Get a meal type by ID. |
| get_current_spaceC | Get the space currently active for this API token. |
| list_inventory_locationsC | List inventory locations (pantry, fridge, freezer and so on). Optional: page, page_size. |
| list_inventory_entriesA | List inventory entries (what is on hand, where). Optional: location_id, food_id, include_empty (entries with amount 0 or less are hidden unless true), page, page_size. |
| add_inventory_itemA | Add food to inventory. Required: food_id, unit_id, amount (> 0), location_id. Optional: expires (YYYY-MM-DD), sub_location, note, code. Always creates a new entry, even when the same food already has an entry at that location. |
| consume_inventory_itemA | Use up some or all of an inventory entry. Identify it by entry_id, or by food_id with optional location_id (errors listing the candidates when several entries match). Give exactly one of amount (> 0) or all: true. Optional unit_id must match the entry unit; there is no unit conversion. The amount never goes below 0: consuming more than is on hand sets it to 0 and returns a warning. Never deletes the entry; at 0 (including after all: true) it is hidden from list_inventory_entries unless include_empty is true. Not atomic: two concurrent consumes of one entry can lose an update. |
| move_inventory_itemB | Move a whole inventory entry to another location. Required: entry_id, to_location_id. Optional: sub_location (null clears it). |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
| plan_week | Plan dinners for the next 7 days, optionally biased toward on-hand pantry foods, a set of keyword tags, or a saved custom filter. |
| grocery_list_for_plan | Convert this week's meal plan into a shopping list, grouped by supermarket category, at the specified servings. |
| what_can_i_make_tonight | Find recipes that can be made with on-hand pantry foods right now, sorted by rating. |
| import_and_plan | Import a recipe from a URL and immediately schedule it on a meal plan. |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
| meal-plan-this-week | Your meal plan entries for Monday-Sunday of the current week, trimmed to {id, date, meal_type, recipe_name, servings, note}. Auto-refreshes every read. |
| pantry-on-hand | What is available. locations holds inventory entries with amount > 0, grouped by location. is_freezer marks freezers. Each entry has amount, unit, expires, sub_location, and note. on_hand_no_quantity holds foods flagged on hand that have no inventory entry. Shows at most 500 entries and 500 foods. on_hand_no_quantity is exact only when truncated.entries is false. When truncated.entries is true, use list_inventory_entries (inventory section) for the full list of entries. When truncated.on_hand_no_quantity is true, use list_foods for the full list of foods. |
| shopping-list-active | Unchecked shopping list entries (Tandoor filters to recent + unchecked by default). Slim fields: food, unit, amount, list_recipe. |
| meal-types | Available meal types (Breakfast, Lunch, Dinner, etc.) with their IDs for use in create_meal_plan. |
TDQS
Scored across 46 tools
Most tools separate resource and action clearly, and the descriptions distinguish overlapping cases like list_recipes, search_recipes, and list_recipes_flat. Some ambiguity remains around food/list manipulation tools such as create_food, update_food, and food_shopping_update, but the guidance generally resolves it.
Almost all tools use a predictable snake_case verb_noun pattern such as list_meal_plans, get_recipe, create_shopping_entry, and delete_meal_plan. A few names deviate, notably food_shopping_update, related_recipes, and jq_stash_stats, so it is not perfectly uniform.
At 46 tools, the surface is far beyond the typical 3-15 range and includes many domains plus utility/diagnostic and tool-group management tools. Tool groups can reduce context load, but the raw tool count is still too heavy for a well-scoped set.
Core recipe, meal-plan, shopping-entry, and inventory workflows are covered with several create/read/update operations. However, notable gaps remain: there is no delete_recipe or delete_food, shopping-list CRUD is missing, keywords and units are read-only, and inventory lacks update/delete operations.