
poket-jony/cookidough-mcp
Developer toolsAn unofficial Model Context Protocol (MCP) server for the Thermomix® Cookidoo® platform
Overview
An unofficial Model Context Protocol (MCP) server for the Thermomix® Cookidoo® platform. Plug it into Claude Desktop, Claude Code or any other MCP-aware client and let your LLM search recipes, manage shopping lists and meal plans, and upload custom recipes. This is an independent, community-built MCP server. It is developed, sponsored, endorsed, or affiliated with Vorwerk, Thermomix®, or Cookidoo®. The name "Thermomix" and "Cookidoo" is used here purely to identify the third-party service this software talks to. See Disclaimer & trademarks for details.
README
Cookidough MCP Server
An unofficial Model Context Protocol (MCP) server for the Thermomix® Cookidoo® platform. Plug it into Claude Desktop, Claude Code or any other MCP-aware client and let your LLM search recipes, manage shopping lists and meal plans, and upload custom recipes.
Unofficial project. This is an independent, community-built MCP server. It is not developed, sponsored, endorsed, or affiliated with Vorwerk, Thermomix®, or Cookidoo®. The name “Thermomix” and “Cookidoo” is used here purely to identify the third-party service this software talks to. See Disclaimer & trademarks for details.
- 42 MCP tools across 7 domains (auth, recipes, collections, shopping, calendar, discovery, interactions), plus 3 MCP resources and 2 prompts
- Dual transport: stdio (default) and streamable HTTP
- Recipe quality gate that blocks low-quality custom recipe uploads
- Guided-cooking annotations (TTS time/speed spans, INGREDIENT spans) — delivered explicitly by the LLM or inferred server-side from plain text
- Web recipe import via
recipe-scrapers(200+ supported sites) - Filtered search of the recipe catalogue (time, difficulty, ingredients, rating, device model, …)
- Ingredient-based recipe suggestions — library-wide or scoped to the user’s own collections
- Recipe interactions: rate, bookmark, personal notes, cooked-history
- Calendar→shopping-list in one call, personalized recommendations, nutrition data, and optional token persistence across restarts
Table of contents
- Features
- Requirements
- Quickstart
- MCP client setup
- Configuration
- Tool reference
- Resources & prompts
- Quality gate
- Guided-cooking annotations
- HTTP transport
- Development
- Architecture
- Troubleshooting
- Credits
- Disclaimer & trademarks
- License
Features
A high-level overview of what the server can do, grouped by domain. The exact tool names live in the Tool reference further below.
Account & authentication
- Lazy login on first tool call — no separate connect step.
- Read the user profile and active subscription, including subscription
level, type, and expiry; optionally include the devices and
accessories linked to the account (
include_devices=true). - Optional token persistence (
COOKIDOUGH_TOKEN_FILE): the OAuth2 tokens survive a server restart and skip the login for thecookidoo-apipaths.
Recipe lookup, creation & import
- Fetch full details of up to 20 catalogue recipes in one call
(
get_recipe_detailswithrecipe_ids, an unknown ID yields an error entry instead of failing the batch) including the grouped cooking steps (instructions, plain text with the appliance icons spelled out), categories, collections and per-serving nutrition values; optionally with the user’s own rating, the community rating and the personal note (include_interactions=true) and every recipe photo in square/portrait/landscape variants (include_images=true). - List, read, delete the authenticated user’s custom recipes.
- Edit an existing custom recipe in place: pass
recipe_idtoupload_custom_recipefor a full-replace update (fetch → edit → resubmit). - Upload a photo for a custom recipe (
set_custom_recipe_image): local file or URL, JPEG/PNG — uploaded via Vorwerk’s signed Cloudinary flow and preserved across later recipe updates. - Clone any catalogue recipe into the user’s custom recipes at a chosen
serving size (
clone_recipe_as_custom). - Build, validate, and upload custom recipes from structured input,
with guided-cooking annotations (
TTS,INGREDIENT, all sevenMODEvariants). - Import a recipe directly from any of 200+ supported recipe sites via
recipe-scrapers, returning the parsed draft + a quality report and uploading when the gate passes. - Filtered search of the recipe catalogue (
search_recipes): total time, difficulty, categories, required/excluded ingredients, minimum rating, portions, device model, accessories, sort order. - Ingredient-based suggestions (
suggest_recipes_from_ingredients) — library-wide via the server-side ingredient filter, or scoped to the user’s collections whencollection_idsis given. - Personalized recommendations (
get_recipe_recommendations): the “For you” feed, or recipes similar to a given one.
Collections (managed + custom)
- Browse and subscribe to / unsubscribe from platform-curated managed collections.
- Full CRUD over custom collections: create, list, delete, add recipes, remove a single recipe.
Shopping list
- Read the full list grouped by source (recipe ingredients vs. free-text items), including the recipes whose ingredients are currently on it.
- Push and pull recipe ingredients for regular and custom recipes
(
add_recipes_to_shopping_list,add_custom_recipes_to_shopping_list, the matchingremove_*variants). - Calendar mode:
add_recipes_to_shopping_listwithfrom_date/to_dateputs every recipe planned in that range (incl. custom recipes, deduplicated) on the list in one call. - Add, remove, rename, and check / uncheck free-text shopping items
(
add_additional_items,rename_additional_items,set_additional_items_ownership). - Check / uncheck recipe-derived ingredient items
(
set_ingredient_items_ownership). - Wipe the whole list in one call.
Calendar / meal plan
- Read the meal plan for any week.
- Schedule and remove regular and custom recipes on a specific date
(
add_recipes_to_calendar,add_custom_recipes_to_calendar,remove_recipe_from_calendar,remove_custom_recipe_from_calendar).
Recipe interactions
- Rate recipes (1-5 stars), bookmark them, keep a personal note, and log
them in the cooking history (catalogue and own recipes) — all through
one write tool (
set_recipe_interactions) with per-action success reporting; read the history back viaget_cooking_history. - Read everything back via
get_recipe_detailswithinclude_interactions=true; list bookmarks vialist_bookmarked_recipes. - These wrap undocumented platform endpoints (not part of
cookidoo-api), verified against the live API; individual failures degrade gracefully instead of failing the call.
Quality, safety & transport
- Recipe quality gate that scores every draft and refuses low-quality custom recipe uploads unless explicitly forced.
- All credentials kept in memory as
SecretStr, with email + token redaction in every log message and upstream error body. - Per-request HTTP timeout; reentrant, lock-protected session lifecycle.
- Dual transport: stdio (default) and streamable HTTP.
Requirements
- Python 3.12 or newer
- A valid Cookidoo® account (
COOKIDOUGH_EMAIL/COOKIDOUGH_PASSWORD) - Optional:
uvfor the recommended client setup, orpipif you prefer
Quickstart
git clone https://github.com/Poket-Jony/cookidough-mcp.git
cd cookidough-mcp
cp .env.example .env # fill in COOKIDOUGH_EMAIL / COOKIDOUGH_PASSWORD
./run.sh
run.sh is idempotent: it detects Python 3.12+, creates .venv/, installs
the project the first time around, loads .env, validates credentials, and
starts the server. Subsequent runs skip the install step and start
immediately. It parses no options of its own — everything is configured
through the COOKIDOUGH_* environment variables.
COOKIDOUGH_MCP_MODE=http ./run.sh # start over HTTP instead of stdio
MCP client setup
Claude Desktop / Claude Code
Two ways to wire it up — pick one and add it to your MCP client config
(claude_desktop_config.json or ~/.claude/mcp.json).
Option A — using run.sh (recommended, no extra tooling):
{
"mcpServers": {
"cookidough": {
"command": "/absolute/path/to/cookidough-mcp/run.sh",
"env": {
"COOKIDOUGH_EMAIL": "[email protected]",
"COOKIDOUGH_PASSWORD": "..."
}
}
}
}
Option B — using uvx (no clone required; works
locally with --from):
{
"mcpServers": {
"cookidough": {
"command": "uvx",
"args": [
"--from",
"/absolute/path/to/cookidough-mcp",
"cookidough-mcp"
],
"env": {
"COOKIDOUGH_EMAIL": "[email protected]",
"COOKIDOUGH_PASSWORD": "...",
"COOKIDOUGH_COUNTRY": "de",
"COOKIDOUGH_LANGUAGE": "de-DE",
"COOKIDOUGH_QUALITY_BAR": "70"
}
}
}
}
Smoke-test with the MCP Inspector
npx @modelcontextprotocol/inspector ./run.sh
The inspector lists every registered tool and lets you call them interactively.
Configuration
The server is configured purely via environment variables (see
.env.example):
| Variable | Required | Default | Description |
|---|---|---|---|
COOKIDOUGH_EMAIL |
yes | - | Account email |
COOKIDOUGH_PASSWORD |
yes | - | Account password (stored in memory as SecretStr) |
COOKIDOUGH_COUNTRY |
no | de |
ISO 3166-1 alpha-2 country code (case-insensitive) |
COOKIDOUGH_LANGUAGE |
no | de |
ISO 639-1 (de, paired with COOKIDOUGH_COUNTRY) or BCP-47 (de-DE); case-normalized to lang-REGION |
COOKIDOUGH_MCP_MODE |
no | stdio |
Transport: stdio or http |
COOKIDOUGH_MCP_HOST |
no | 127.0.0.1 |
Bind host (HTTP only) |
COOKIDOUGH_MCP_PORT |
no | 8765 |
Bind port (HTTP only) |
COOKIDOUGH_QUALITY_BAR |
no | 70 |
Minimum recipe quality score (0-100) for custom uploads |
COOKIDOUGH_TOKEN_FILE |
no | - | Optional path for persisting the OAuth2 tokens across restarts (skips the login while the refresh token is valid) |
A restored token authenticates every call immediately — the library calls as well as search and the interaction endpoints.
Security note on
COOKIDOUGH_TOKEN_FILE: the file holds the OAuth2 access and refresh tokens — anyone who can read it can act as your platform account, and a refresh token outlives a session cookie. The server creates it with0600permissions before writing; keep it outside any repository (the bundled.gitignoreexcludestoken.json/*.token.json) and treat it like a password.
Mainland China
China runs a separate deployment of the platform (cookidoo.com.cn) behind its own
identity provider, with a phone number instead of an email address:
COOKIDOUGH_EMAIL=13800000000 # the account phone number
COOKIDOUGH_COUNTRY=cn
COOKIDOUGH_LANGUAGE=zh-Hans-CN
The server then logs in through the China CIAM and talks to the mainland hosts. Custom-recipe updates use the Chinese web editor’s granular PATCH schema, and recipe images go through Tencent COS plus the platform’s moderation queue instead of Cloudinary. That upload path needs an optional dependency:
pip install 'cookidough-mcp[china]'
Without it every other tool still works; only set_custom_recipe_image
fails, with a message naming the extra.
This path is contributed and has not been verified against a live mainland account by the maintainers. Please report anything that misbehaves.
Tool reference
All tools are registered automatically on server start. Each tool returns
a strongly typed Pydantic DTO (see src/cookidough_mcp/models.py).
Authentication & account
| Tool | Purpose |
|---|---|
get_user_profile |
Return the authenticated user’s profile (also triggers the lazy login on first use); include_devices=true adds linked devices + accessories |
get_subscription |
Return the active subscription, if any |
Recipes
Lookup of any catalogue recipe plus the full custom-recipe workflow (generate → validate → upload, list / delete, scrape from supported sites).
| Tool | Purpose |
|---|---|
get_recipe_details |
Full details of up to 20 catalogue recipes per call (recipe_ids, per-ID error entries), incl. cooking steps, categories, collections and nutrition; include_interactions=true adds own/community rating + personal note, include_images=true adds all recipe photos |
get_custom_recipe_details |
Full details of one of your own custom recipes by ID |
generate_recipe_structure |
Build a validated custom-recipe draft (steps accept plain strings or structured RecipeSteps — see Guided-cooking annotations) |
validate_recipe_quality |
Score a draft against the recipe quality bar without uploading |
upload_custom_recipe |
Upload a draft (rolls back on failure, blocked by Quality gate); pass recipe_id to overwrite an existing custom recipe (full replace) |
list_custom_recipes |
List all custom recipes you own |
delete_custom_recipe |
Delete one of your custom recipes by ID |
clone_recipe_as_custom |
Copy a catalogue recipe into your custom recipes at a chosen serving size |
import_web_recipe |
Scrape a recipe; always returns the draft + quality report, uploads only when the gate passes |
set_custom_recipe_image |
Upload a photo (path or URL, JPEG/PNG, ≥80×80 px, ≤10 MB) for a custom recipe via Vorwerk’s signed Cloudinary flow |
Custom recipe upload talks to the same undocumented /created-recipes/{locale}
endpoint that the official apps use. Recipe photos are uploaded
directly to Vorwerk’s Cloudinary tenant (api-eu.cloudinary.com) after
the platform signs the request — the image bytes leave your machine to that
third-party host, exactly as in the official web app; your platform
credentials are never sent there.
Collections
| Tool | Purpose |
|---|---|
list_managed_collections |
List platform-curated collections you subscribe to (paged: items + total_pages/total_elements) |
add_managed_collection |
Subscribe to a managed collection by ID |
remove_managed_collection |
Unsubscribe from a managed collection |
list_custom_collections |
List your own custom collections (paged, same shape) |
create_custom_collection |
Create a new empty custom collection |
delete_custom_collection |
Delete a custom collection (recipes are kept) |
add_recipes_to_custom_collection |
Add one or more recipes to a custom collection |
remove_recipe_from_custom_collection |
Remove a single recipe from a custom collection |
Shopping list
| Tool | Purpose |
|---|---|
get_shopping_list |
Return all items grouped by source (recipe / additional), plus the recipes currently on the list |
add_recipes_to_shopping_list |
Add all ingredients of one or more recipes — or, with from_date/to_date, of every recipe planned in that calendar range (max 4 weeks, incl. custom recipes) |
remove_recipes_from_shopping_list |
Remove ingredients of given recipes |
add_custom_recipes_to_shopping_list |
Add all ingredients of one or more custom recipes |
remove_custom_recipes_from_shopping_list |
Remove ingredients of given custom recipes |
set_ingredient_items_ownership |
Check or uncheck recipe-derived ingredient items by ID |
add_additional_items |
Add free-text items (not tied to a recipe) |
rename_additional_items |
Rename free-text items in place by ID |
set_additional_items_ownership |
Check or uncheck free-text items by ID |
remove_additional_items |
Remove free-text items by ID |
clear_shopping_list |
Remove every item from the list |
Calendar / meal plan
| Tool | Purpose |
|---|---|
get_calendar_week |
Meal plan for the week containing the given date |
add_recipes_to_calendar |
Schedule one or more recipes on a specific date |
remove_recipe_from_calendar |
Remove a planned recipe from a date |
add_custom_recipes_to_calendar |
Schedule one or more custom recipes on a date |
remove_custom_recipe_from_calendar |
Remove a planned custom recipe from a date |
Discovery (search & suggestions)
| Tool | Purpose |
|---|---|
search_recipes |
Search the recipe catalogue; optional filters: total time, difficulty, categories, required/excluded ingredients, min rating, portions, device model, accessories, sort order |
suggest_recipes_from_ingredients |
Rank recipes by ingredient match — library-wide, or only inside the given collection_ids |
get_recipe_recommendations |
Personalized “For you” feed; with recipe_id, recipes similar to that one |
Interactions (rating, bookmark, note, history)
These wrap undocumented platform endpoints (not exposed by
cookidoo-api), verified against the live API; per-action failures are
reported instead of failing the whole call.
| Tool | Purpose |
|---|---|
set_recipe_interactions |
Rate (1-5), bookmark/unbookmark, set or clear the personal note, and/or log the recipe as cooked (is_custom_recipe=true for own recipes, cooked_at to backdate the entry) — any combination in one call, with per-action status |
list_bookmarked_recipes |
List the recipes saved under “My recipes” |
get_cooking_history |
List the recipes logged as cooked, newest first |
Reading interactions happens through get_recipe_details with
include_interactions=true.
Resources & prompts
Beyond tools, the server exposes MCP resources (read-only context an MCP client can attach without spending a tool call) and prompts (predefined workflows):
| Resource URI | Content |
|---|---|
cookidough://shopping-list |
The current shopping list incl. its recipes (JSON) |
cookidough://calendar/current-week |
The meal plan for the week containing today (JSON) |
cookidough://custom-recipes |
All custom recipes owned by the user (JSON) |
| Prompt | Workflow |
|---|---|
plan_week |
Plan seven dinners (servings, diet, time budget) → confirm → schedule → fill the shopping list |
cook_from_pantry |
Suggest tonight’s recipe from the ingredients on hand and offer to complete the shopping list |
Quality gate
Both upload_custom_recipe and import_web_recipe score every draft against
a small ruleset (time/speed annotations per cooking step, temperature/Varoma
mode, accessory mentions, parallelization hints, ingredient/step linkage).
- Default threshold:
COOKIDOUGH_QUALITY_BAR=70 upload_custom_reciperaisesQualityGateErrorwhen below threshold — the LLM submitted the draft itself, a hard error is the right signal.import_web_recipenever raises on quality; it always returns aWebImportResultwithdraft+qualitypopulated andupload=nullblocked_reasonset when blocked. The caller (typically an LLM) can read the scraped draft, rewrite the steps with guided-cooking annotations (e.g.5 min / 100 °C / speed 3) and resubmit viaupload_custom_recipe— no second scrape needed.
- Pass
force=trueon either tool to upload anyway after the user has explicitly accepted a sub-threshold upload - Call
validate_recipe_qualityfirst to see actionable issues without attempting the upload
Guided-cooking annotations
A RecipeStep carries both human-readable text and an optional list of
typed annotations that turn substrings into interactive guided-cooking
spans in the official app:
All speed values are strings — numeric speeds must be quoted
("4", "6.5", …), and "soft" is the keyword for Sanftrührstufe.
Pydantic rejects unquoted numbers at the boundary.
tools — device compatibility, not accessories
CustomRecipeDraft.tools lists which device generations
the recipe is compatible with. Only three tokens are accepted:
| Value | Meaning |
|---|---|
"TM5" |
Pre-2019 device, no Sanftrührstufe, no browning/steaming/dough/warm_up/blend/turbo/rice_cooker MODE |
"TM6" |
Adds Sanftrührstufe (speed="soft") and the browning, steaming, dough, warm_up, blend, turbo MODEs |
"TM7" |
Adds the rice_cooker MODE |
It is not a list of accessories or in-bowl tools — "Mixtopf",
"Spatel", "Varoma", "Schmetterling", etc. are rejected by Pydantic
with a literal_error. Reference those in the step text instead.
Choose the lowest TM generation that can still run every step, and
list it together with all higher generations (e.g. ["TM7", "TM6"] for a
recipe that uses Sanftrührstufe but no rice_cooker). Default: all
three.
Top-level annotation types
TTS— appliance time + speed instruction the app dispatches.speed:""(e.g."4","6.5") or"soft"for Sanftrührstufetime: integer secondstemperature: optional{ "value": "", "unit": "C" }direction: optional"CW"or"CCW"(counter-clockwise for Linkslauf)
INGREDIENT— Highlights an ingredient reference inside the step text.description: canonical ingredient entry — may differ from the visible span text (e.g. span"1 EL Salz", description"Salz und Pfeffer")
MODE— appliance program. Carries an additional top-level lowercasenamefield; thedatashape depends on the name (see below).
MODE / `` data shapes
browning— Browning program (140-160 °C in 5 °C steps, ≤30 min):{ "time": , "temperature": { "value": "", "unit": "C" }, "power": "Intense"|"Gentle" }steaming— Varoma steaming:{ "time": , "speed": ""|"soft", "direction": "CW"|"CCW", "accessory": "Varoma"|"SimmeringBasket"|"VaromaAndSimmeringBasket" }dough— Kneading:{ "time": }turbo— Pulse mode (sub-secondtimesupported):{ "time": , "pulseCount": }rice_cooker— Empty{}(no parameters)warm_up— Warm-up (time is optional):{ "speed": ""|"soft", "temperature": { "value": "", "unit": "C" }, "time"?: }blend— Blending:{ "speed": "", "time": }
Each annotation pins its span via offset and length, counted in
Python str units over the step text (Unicode code points; this
matches what the platform accepted in our PATCH captures, including
umlauts). A span must not start or end inside a word; the validation
error for such a span names the nearest span of the same length that
fits. Two ways to populate them:
-
Explicit (LLM-supplied) — when
generate_recipe_structureorupload_custom_recipereceives aRecipeStepwithannotations, the spans go upstream unchanged. This is the most precise route and is what the LLM should prefer when it knows the exact substrings.{ "text": "200 g Mehl in den Mixtopf geben, 30 Sek. / Stufe 4 verkneten.", "annotations": [ { "type": "INGREDIENT", "data": { "description": "200 g Mehl" }, "offset": 0, "length": 10 }, { "type": "TTS", "data": { "speed": "4", "time": 30 }, "offset": 33, "length": 17 } ] } -
Inferred (server-side) — when a step is passed as a plain string, or with an omitted or empty
annotationslist (the two are equivalent), the server scans the text on upload:Min./ °C/(Leicht|Intensiv|Gentle|Intense)becomes aMODE/browningspan (temperatures outside the platform’s whitelist of 140-160 °C in 5 °C steps and durations above 30 min are dropped)./Varoma/Stufebecomes aMODE/steamingspan.Min./Teigstufe(andStufe Teig/dough mode/knead mode) becomes aMODE/doughspan.[/][/Linkslauf]/Stufe [/Linkslauf]becomes aTTSspan; the temperature segment is captured when present, and the reverse-blade token may appear either before or after the speed (both orderings are common in real recipes). The reverse-blade token also matchesreverse,counterclockwise,anticlockwiseandsens inverse.[/][/Linkslauf]/Sanftrührstufe(orStufe sanft/speed soft) becomes aTTSspan withspeed="soft".[/]/Anbratstufe[/Linkslauf](TM7 browning-mode stir pattern; theBratstufeshorthand is also accepted) becomes aTTSspan withspeed="anbrat".- Ingredient-list entries are reduced to their head noun: quantity,
unit and parenthetical hints are stripped. Quantities recognised
include integers (
350), decimals (1,5,1.5), ASCII fractions (1/4), Unicode vulgar fractions (½,¼,⅔, …), mixed fractions (1 ½), and ranges (2-3). Units cover the common vocabulary across the platform’s main locales (DE, EN, FR, IT, ES, NL) — includingg/ml/EL/TL/Prise/Bund/Stiel(e)/Stängel/Blätter/Pck./Tasse/Klacks/Block/Kugel/Rispe/Strauß,tbsp/cup/pinch/clove(s)/head(s)/leaves, Frenchcuillère/pincée/gousse(s)/boîte, Italiancucchiaio/spicchio/pizzico/foglia, Spanishcucharada/diente(s)/pizca/manojo, Dutcheetlepel/snufje/teen/takje— so"3 Stängel Petersilie"→ head"Petersilie","½ TL Zucker"→ head"Zucker","1 cuillère de farine"→ head"farine","2 spicchi di aglio"→ head"aglio". Romance/English linker words (de/di/du/of/van/…) right after the unit are also stripped. Trailing comma descriptors are dropped ("100 ml Weißwein, trocken"→"Weißwein"). - The head is matched in the step text with a length-gated pattern:
- ≥ 5 chars: compound-prefix tolerant.
Petersiliematches insidePetersilienblättchen;ZwiebelmatchesZwiebeln. The annotated span covers the whole compound. - 3–4 chars: only known German inflection endings are accepted
(
Salz→Salzen). Compound matching is off to avoidReis→Reisebusfalse positives. - ≤ 2 chars: exact match only. Very short heads (
Ei,Öl) would otherwise collide with function words (Ein,ein).
- ≥ 5 chars: compound-prefix tolerant.
- The leading word boundary is always strict, so a head never matches
as the suffix of a compound —
Öldoes not match insideOlivenöl(different ingredient). - The annotated span also pulls in any quantity (and optional unit)
that immediately precedes the head in the step text — so
"20 g Haselnüsse in den Mixtopf geben"is annotated as"20 g Haselnüsse","1 Prise Salz zugeben"as"1 Prise Salz", and"1 Ei verquirlen"as"1 Ei". When the measurement is not repeated in the step text, only the noun is highlighted. - When the primary head itself ends with a known portion word
(
Spargelstücke=Spargel+stücke,Ziegenkäsescheiben=Ziegenkäse+scheiben), the inferrer also matches the shorter form — so the same ingredient line is found both at"500 g Spargelstücke"and at a later standalone"Spargel". - When the head is multiple whitespace-separated words and the
last word is itself long enough for compound matching (≥ 5 chars),
that last word is added as a secondary head — so
"500 g weißer Spargel"(adj + noun) still matches step text that uses a declined"weißen Spargel"or just"Spargel". The adjective itself is not recovered into the span. descriptioncarries the full canonical ingredient line (not the matched substring) so the platform can resolve quantities.- Not handled (supply explicit annotations for these): reverse plural
(plural ingredient line → singular step), arbitrary compound
splits without a known portion-word suffix (ingredient
Ziegenfrischkäse≠ stepZiegenkäse-Scheiben, ingredientGemüsemaultaschen≠ stepMaultaschen— the middle of a compound cannot be inferred), and umlaut plurals (Apfel→Äpfel).
Explicit annotations replace inference, except for ingredient links: a step without its own
INGREDIENTannotation still gets inferred ingredient links wherever they do not overlap an explicit span. To suppress those, pass at least one explicitINGREDIENTannotation. The remaining MODE types (turbo,rice_cooker,warm_up,blend) currently have no text-pattern detector — the LLM must supply them explicitly.
HTTP transport
For remote clients or web-based MCP integrations:
COOKIDOUGH_MCP_MODE=http \
COOKIDOUGH_MCP_HOST=0.0.0.0 \
COOKIDOUGH_MCP_PORT=8765 \
./run.sh
The server then speaks the MCP streamable-HTTP protocol on the configured host/port.
Development
Manual environment (without run.sh):
git clone https://github.com/Poket-Jony/cookidough-mcp.git
cd cookidough-mcp
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
Testing & quality gates
The canonical way to run the full suite is the bundled check.sh script
— it executes the four gates in order, stops at the first failure, and
prints a single “All gates passed.” summary at the end:
./check.sh # run lint + format-check + mypy + pytest
./check.sh --fix # auto-fix ruff lint + format issues, then re-run gates
./check.sh --help # show usage
Equivalent manual invocation (useful when iterating on a single gate):
ruff check . # lint
ruff format --check . # formatting
mypy # strict type checks over src/ and tests/
pytest # full test suite, coverage gate ≥ 80 %
Targeted test runs:
pytest tests/test_annotations.py # one file
pytest -k ingredient # by keyword
pytest --no-cov -x tests/test_session.py # fast iteration without coverage
./check.sh is also the pre-PR contract: a green run is required before
opening a pull request. Coverage failures (< 80 %) and any lint/type
error block the build.
Project conventions (architecture, invariants, security model) are
documented in AGENTS.md — read it before contributing.
Architecture
src/cookidough_mcp/
├── config.py # Pydantic-settings, env-driven Settings
├── constants.py # Timeouts and other shared constants
├── context.py # AppContext + ToolContext type alias
├── errors.py # Domain exception hierarchy
├── models.py # Pydantic DTOs for every tool I/O
├── annotation_models.py # Guided-cooking annotation DTOs (discriminated union)
├── session.py # Repository facade over cookidoo-api + custom HTTP
├── china_client.py # Platform client for the mainland-China deployment
├── transport.py # Stdio / HTTP transport strategies
├── quality.py # Recipe quality rule strategies
├── annotations.py # Annotation inferrer (text patterns → StepAnnotation)
├── web_import.py # recipe-scrapers adapter → CustomRecipeDraft
├── resources.py # MCP resources + prompts (read-only context, workflows)
├── server.py # MCPServer instance + lifespan
└── tools/ # Thin tool adapters: one module per domain
session.py is the only module that imports from cookidoo-api; every
tool talks to the session through CookidoughSessionProtocol, so swapping
the upstream client only touches one file.
Troubleshooting
Missing required environment variable(s): COOKIDOUGH_EMAIL
Copy .env.example to .env and fill in your credentials, or set the
variables in your MCP client config.
Python 3.12 or newer is required but was not found on PATH
Install Python 3.12+ (macOS: brew install [email protected]; Debian/Ubuntu:
apt install python3.12) and re-run ./run.sh.
HTTP port already in use
Set a different port: COOKIDOUGH_MCP_PORT=9000 ./run.sh.
Custom recipe upload blocked by quality gate
Either improve the draft (add guided-cooking annotations such as
5 min / 100 °C / speed 3 to each step), lower COOKIDOUGH_QUALITY_BAR, or
re-issue the call with force=true after the user accepts the trade-off.
Stdio client sees corrupted JSON-RPC frames
This server keeps stdout clean for MCP traffic — only the wire protocol
goes there, all logs go to stderr. If you wrap run.sh in another script,
make sure that wrapper does not write to stdout either.
ModuleNotFoundError: No module named 'mcp.server.mcpserver' (or
'mcp.server.fastmcp')
Your virtualenv is on the wrong mcp major. This server targets
mcp[cli]>=2,<3 (MCPServer); the 1.x line shipped FastMCP under
mcp.server.fastmcp. Re-run ./run.sh — it reinstalls whenever
pyproject.toml is newer than the install marker. For a manual install:
pip install --upgrade 'mcp[cli]>=2,<3'.
Access token request failed due to bad request, please check your email or refresh token
Vorwerk retired the grant_type=password OAuth flow used by
cookidoo-api ≤ 0.17.0 in May 2026. This project requires `cookidoo-api
=0.18,<0.19
, which ships the browser OAuth2 flow. If you see this error you're on an older version — run./run.sh(the install marker is keyed offpyproject.toml, so editing it forces a reinstall) or, for a manual install,pip install --upgrade ‘cookidoo-api>=0.18,<0.19’`.
The server logs in again on every restart although persistence is configured
COOKIDOUGH_COOKIES_FILE was renamed to COOKIDOUGH_TOKEN_FILE when
cookidoo-api 0.18 switched from cookie to token persistence. Unknown
COOKIDOUGH_* variables are ignored, so the old name disables persistence
silently. Rename the variable; the old cookies.json file holds cookies
that 0.18 cannot use and can be deleted.
AttributeError: 'Cookidoo' object has no attribute 'save_cookies'
cookidoo-api 0.18 replaced cookie persistence with save_token /
load_token. Upgrade the project itself (./run.sh, or
pip install -e .); versions before this fix only work with
cookidoo-api < 0.18.
Credits
Built on top of the unofficial API client and informed by the earlier community MCP servers in this space. Thanks to:
miaucl/cookidoo-apialexandrepa/mcp-cookidooXdev22/cookidoo-mcpdetef10/cookidoo-mcpdanielkliem/mcp-cookidoootisthescribe/cookidoo-mcp
Disclaimer & trademarks
This is an independent, unofficial project maintained by community contributors. It is not developed, sponsored, endorsed, authorised, or in any way affiliated with Vorwerk SE & Co. KG, Vorwerk International AG, Thermomix®, Cookidoo®, or any of their subsidiaries.
Cookidoo® and Thermomix® are registered trademarks of Vorwerk International AG. TM5, TM6, TM7 and related model designations are likewise Vorwerk-owned marks. All other product names, logos and brands referenced in this repository are the property of their respective owners.
Account & terms: Operating this server requires your own credentials. Your use of the service through this server remains subject to Vorwerk’s official Terms of Use and Privacy Policy. This project does not redistribute Cookidoo® content; recipes you fetch are delivered directly from Vorwerk’s servers to your client.
Warranty: The software is provided “as is” without warranty of any kind. See License.
License
Install
uvx --from /absolute/path/to/cookidough-mcp cookidough-mcpConfiguration
{
"mcpServers": {
"cookidough": {
"command": "/absolute/path/to/cookidough-mcp/run.sh",
"env": {
"COOKIDOUGH_EMAIL": "[email protected]",
"COOKIDOUGH_PASSWORD": "..."
}
}
}
}