@cliftonz/tandoor-recipes-mcp
v1.4.0
Published
Model Context Protocol server for Tandoor — recipes, meal plans, shopping lists, ingredients, cook logs, nutrition, AI import, and more.
Downloads
465
Maintainers
Readme
Tandoor Recipes MCP
A Model Context Protocol (MCP) server that gives LLM agents full read/write access to a Tandoor Recipes instance — recipes, meal plans, ingredients, shopping lists, cook logs, nutrition, and AI-powered URL/image imports.
Turn "plan the week and make me a grocery list" into one prompt.
Why this server
- Full Tandoor API coverage — 100+ typed tools spanning recipes, meal plans, ingredients, steps, shopping lists, cook logs, recipe books, foods, units, keywords, supermarket categories, property types / nutrition, custom filters, unit conversions, automations, user files, user prefs, activity logs.
- Resources + Prompts, not just tools — read-only subscribable resources like
tandoor://meal-plan/this-weekandtandoor://pantry/on-hand, plus ready-made prompts (plan_week,grocery_list_for_plan,what_can_i_make_tonight). Most Tandoor MCP forks ship tools only. - Slim-by-default responses — a full recipe GET shrinks from ~20k characters to ~2k by stripping substitute trees, readonly timestamps, image URLs, etc. Pass
format: "full"anywhere for the raw Tandoor response. - 3-stage URL import with a real fallback — tries Tandoor's native scraper first, then extracts schema.org JSON-LD from the page ourselves (works when Tandoor can't reach the URL or doesn't support the site), with an optional stub-on-failure escape hatch.
- AI import from images / PDFs — multipart upload to Tandoor's
ai-import/endpoint, saves the parsed recipe. - Validated inputs — every tool has a Zod input schema; invalid calls are rejected at the MCP boundary before reaching the handler.
- Transport-safe — retries with jittered backoff on 429 / 5xx, redacts your bearer token from error messages, reads response bodies exactly once (no "Body is unusable" footguns).
structuredContent+ text — clients that support structured output skip a JSON re-parse round-trip; older clients still gettextcontent.
Install
Compatibility: this server targets the Tandoor 2.x API. Tandoor 1.x instances are not supported — nested write payloads (recipes, meal plans, shopping entries) use 2.x serializer shapes. The server probes your instance's version at startup and warns on stderr and in the MCP instructions if it detects a pre-2.x Tandoor.
npm install -g @cliftonz/tandoor-recipes-mcpOr npx without installing:
npx -y @cliftonz/tandoor-recipes-mcpConfigure
- In Tandoor: Settings → API Tokens → Create new token.
- Point your MCP client at the server.
Claude Code — one-shot plugin install
/plugin marketplace add Cliftonz/tandoor-recipe-mcp
/plugin install tandoor-recipes-mcp@cliftonz-tandoorClaude Code prompts you for TANDOOR_URL and TANDOOR_TOKEN on install and stores the token securely. No mcpServers JSON editing needed.
Claude Desktop / Claude Code — manual config
{
"mcpServers": {
"tandoor": {
"command": "npx",
"args": ["-y", "@cliftonz/tandoor-recipes-mcp"],
"env": {
"TANDOOR_URL": "https://your-tandoor-instance.com",
"TANDOOR_TOKEN": "your-api-token",
"TANDOOR_MCP_PROFILE": "full"
}
}
}
}Recommended Claude Code permissions
The repo ships a .claude/settings.json that auto-allows read tools and prompts for everything else. Copy these into your own project's .claude/settings.json to reduce permission friction:
{
"permissions": {
"allow": [
"mcp__tandoor__list_*",
"mcp__tandoor__get_*"
],
"ask": [
"mcp__tandoor__create_*",
"mcp__tandoor__update_*",
"mcp__tandoor__delete_*",
"mcp__tandoor__merge_*"
]
}
}Environment variables
| Var | Required | Default | Purpose |
|---|---|---|---|
| TANDOOR_URL | ✅ | — | Base URL of your Tandoor instance |
| TANDOOR_TOKEN | ✅ | — | API token with write access |
| TANDOOR_MCP_PROFILE | — | full | basic exposes core tools only (~40); full includes everything (~100) |
| TANDOOR_MCP_INCLUDE_ONLY | — | — | Comma-separated allowlist (glob * supported). E.g. list_*,get_*,create_meal_plan |
| TANDOOR_MCP_EXCLUDE | — | — | Comma-separated denylist. E.g. merge_*,delete_* to hide destructive tools |
| TANDOOR_MCP_LOG | — | — | Stderr trace mode. request, response, error, all, or comma list. Bearer token redacted. |
| TANDOOR_MCP_STASH_ENABLED | — | 1 | Park large tool results in an in-memory cache and return a {stashed:true, handle, shape} summary; the LLM calls jq_query to extract subsets. Set 0 to disable. |
| TANDOOR_MCP_STASH_THRESHOLD | — | 25000 | Stash payloads strictly larger than this many bytes. |
| TANDOOR_MCP_STASH_MAX_ENTRIES | — | 32 | LRU cap on stashed handles. Oldest entry evicted when exceeded. |
| TANDOOR_MCP_STASH_MAX_BYTES | — | 32000000 | Total-bytes cap on the stash; LRU eviction kicks in past this budget. |
| TANDOOR_MCP_STASH_TTL_MS | — | 600000 | Per-entry TTL in milliseconds (default 10 minutes). |
The basic profile is useful when context size matters — every MCP client loads every tool schema into the model's context on startup. INCLUDE_ONLY and EXCLUDE compose on top of the profile for finer control (e.g. "give me only read tools" or "hide destructive ops from this agent").
Example conversations
Plan the week + generate a grocery list:
You: "Plan dinners for the week biased toward vegetarian recipes I've rated 4+, using what's on-hand where possible. Then give me the shopping list grouped by aisle."
Claude:
- Reads
tandoor://pantry/on-handandtandoor://meal-types- Calls
list_recipes({keywords_or: [<vegetarian>], rating_gte: 4, sort_order: '-rating'})- Calls
create_meal_planfor each day of the week- Calls
add_recipe_to_shopping_listfor each plan- Reads
tandoor://shopping-list/activeand groups by supermarket category
Import a recipe from a URL and schedule it:
You: "Import https://example.com/lasagna and schedule it for Friday dinner, 4 servings."
Claude:
import_recipe_from_url({url: "..."})— JSON-LD extraction handles sites Tandoor doesn't knowcreate_meal_plan({recipe_id, meal_type_id: <Dinner>, from_date: "2026-04-24", servings: 4})
Cook log:
You: "Log that I made the Thai green curry last night — 4 out of 5, kids loved it."
Claude:
create_cook_log({recipe: <id>, rating: 4, comment: "Kids loved it", servings: 4})
Tool catalog
All write-returning tools default to a slim JSON shape. Pass format: "full" for the raw Tandoor response.
Destructive tools (delete_*, merge_*) are irreversible. Their descriptions instruct the model to check usage first (e.g. list_recipes({keywords: [id]}) before delete_keyword) and to prefer the non-destructive alternative where one exists (merge_* for deduplication, update_recipe with private: true instead of delete_recipe). Operators who want them gone entirely can set TANDOOR_MCP_EXCLUDE="delete_*,merge_*"; the permission config above makes Claude Code ask before any destructive call.
Recipes
search_recipes · list_recipes · get_recipe · create_recipe · update_recipe · delete_recipe · import_recipe_from_url · upload_recipe_image · related_recipes · add_recipe_to_shopping_list · recipe_ai_properties · recipe_batch_update
search_recipes is the preferred high-level entry point: it accepts food/keyword/book names (not IDs) and resolves them internally. Use list_recipes only for the raw ID-based filter surface.
list_recipes takes the full Tandoor filter surface: query, sort_order, rating[_gte|_lte], timescooked[_gte|_lte], cookedon_[gte|lte], keywords[_or|_and|_or_not|_and_not][], foods[...], books[...], makenow, new, random, internal, filter, date ranges, pagination.
Meal plans (bulk_create_meal_plans for week-planning)
list_meal_plans · get_meal_plan · create_meal_plan · update_meal_plan · delete_meal_plan · auto_meal_plan · list_meal_types · bulk_create_meal_plans
Ingredients + steps
list_ingredients · get_ingredient · create_ingredient · update_ingredient · delete_ingredient · parse_ingredient
list_steps · get_step · create_step · update_step · delete_step
Shopping lists
list_shopping_entries · get_shopping_entry · create_shopping_entry · update_shopping_entry · delete_shopping_entry · bulk_check_shopping_entries · list_shopping_list_recipes · get_shopping_list_recipe · create_shopping_list_recipe · update_shopping_list_recipe · delete_shopping_list_recipe · bulk_create_shopping_list_recipe_entries
Foods + units + keywords + categories (full profile)
list_foods · get_food · create_food · update_food · delete_food · merge_food · move_food · food_shopping_update · food_fdc_lookup · food_ai_properties · food_batch_update
list_units · get_unit · create_unit · update_unit · delete_unit · merge_unit
list_unit_conversions · get_unit_conversion · create_unit_conversion · update_unit_conversion · delete_unit_conversion
list_keywords · get_keyword · create_keyword · update_keyword · delete_keyword · merge_keyword · move_keyword
list_supermarket_categories · *_supermarket_category_relation
Cook log + recipe books (full profile)
list_cook_logs · get_cook_log · create_cook_log · update_cook_log · delete_cook_log
list_recipe_books · get_recipe_book · create_recipe_book · update_recipe_book · delete_recipe_book · list_recipe_book_entries · get_recipe_book_entry · add_recipe_to_book · remove_recipe_from_book
Nutrition + property types + custom filters (full profile)
list_property_types · get_property_type · create_property_type · update_property_type · delete_property_type
list_properties · get_property · create_property · update_property · delete_property
list_custom_filters · get_custom_filter · create_custom_filter · update_custom_filter · delete_custom_filter
AI imports (full profile)
list_ai_providers · import_recipe_from_image (file path, URL, or text)
Automations, user files, prefs, logs (full profile)
list_automations · *_automation · list_user_files · *_user_file · list_user_preferences · get_user_preference · update_user_preference · get_server_settings · get_share_link · list_view_logs · list_import_logs · list_ai_logs
Utility (both profiles)
get_version · jq_query · jq_stash_stats
get_version reports the MCP server version plus the Tandoor version and compatibility status detected at startup. jq_query extracts subsets from stashed large results (see TANDOOR_MCP_STASH_* above).
Gotchas
FDC lookups rate-limit after 30 requests/hour
food_fdc_lookup delegates to Tandoor, which calls the USDA FoodData Central API. Out of the box Tandoor uses USDA's public DEMO_KEY, capped at 30 requests/hour per IP. Bulk-enriching a grocery list hits that ceiling fast and you'll see 429s on Tandoor's side.
Fix (takes 2 minutes):
Register a free personal key at api.data.gov/signup — no approval needed, instant.
Set it on the Tandoor container, not the MCP server:
# docker-compose.yml — under the tandoor service environment: - FDC_API_KEY=<your-key>Kubernetes / Helm: add
FDC_API_KEYto the Tandoor deployment's env or a secret. Restart the Tandoor pod.The limit jumps to 1,000 requests/hour — plenty for bulk enrichment.
If FDC returns 404 for a food, the fdc_id you passed doesn't exist in USDA's database — try food_ai_properties as a fallback (uses Tandoor's configured AI provider instead of USDA).
Resources
| URI | Purpose |
|---|---|
| tandoor://meal-plan/this-week | Monday–Sunday meal plan entries, slimmed to {id, date, meal_type, recipe_name, servings, note} |
| tandoor://pantry/on-hand | Foods flagged food_onhand=true |
| tandoor://shopping-list/active | Unchecked, recent shopping entries |
| tandoor://meal-types | Breakfast/Lunch/Dinner/etc. with IDs |
Prompts
| Name | Purpose |
|---|---|
| plan_week | Plan 7 days of dinners, optionally pantry-biased and keyword-filtered |
| grocery_list_for_plan | Turn this week's meal plan into a shopping list grouped by aisle |
| what_can_i_make_tonight | Find makeable recipes ≤ N minutes from on-hand foods |
| import_and_plan | Import a URL recipe and schedule it in one shot |
Development
git clone https://github.com/Cliftonz/tandoor-recipe-mcp.git
cd tandoor-recipe-mcp
npm install
npm run build
npm testTests use Vitest and mock fetch — no live Tandoor instance needed for the default run.
End-to-end tests
An opt-in E2E suite exercises the full write path against a real Tandoor instance. Run it only against a throwaway/isolated Tandoor space — the suite creates and deletes real rows.
TANDOOR_URL=https://your-tandoor.example \
TANDOOR_TOKEN=... \
npm run test:e2e| Var | Effect |
|---|---|
| TANDOOR_E2E_KEEP=1 | Skip cleanup; leave created resources for inspection |
| TANDOOR_E2E_SKIP_IMPORT=1 | Skip the URL-import step (offline runners) |
| TANDOOR_E2E_IMPORT_URL=<url> | Override the default recipe URL |
| TANDOOR_E2E_AI_PROVIDER=<id> | Run the AI-import step against this provider |
Security posture
This server treats the configured Tandoor instance as trusted enough to read+write on your behalf — you provide the bearer token. It does NOT treat the instance as fully trusted for content:
- Tool responses from
create_meal_plan/update_meal_planare slimmed — only ids and structural fields flow back to the LLM. User-visible names (recipe.name,meal_type.name, keyword names) are omitted so a hostile or compromised Tandoor instance can't inject model-steering text into Claude's context via these paths. Callget_recipeorlist_meal_typesif you need the human-readable form. - Keyword payloads are capped at 50 entries and projected to
{id, name}before being forwarded into writes — stops amplification and mass-assignment exposure. - Bearer tokens are redacted from every error message and log line.
If your Tandoor deployment is single-tenant and you own every recipe, the posture is conservative for your case. If you share a Tandoor instance with untrusted users (or run a Tandoor instance exposed to the public internet with anonymous sharing), the hardening above matters.
License
MIT
