Skip to main content
Glama
jsaedtler

cookidoo-mcp

by jsaedtler
README.md
# cookidoo-mcp

An MCP server that bridges LLM sessions (Claude, Gemini, or any MCP client)
to Cookidoo custom recipes on a Thermomix (TM7 by default). Describe a recipe
in a chat, and the server uploads it as a Cookidoo custom recipe with proper
structured Thermomix settings (time, temperature, speed, reverse) so it looks
and behaves like an official recipe on the device.

The server ships its own German authoring guide as an MCP prompt and resource,
so the writing rules for Thermomix steps do not have to live in your prompts.

Disclaimer: Cookidoo/Vorwerk offers no official public API. This project
relies on the reverse-engineered API of
[miaucl/cookidoo-api](https://github.com/miaucl/cookidoo-api) and may break
whenever the Cookidoo backend changes. Use at your own risk.

Prior art this project builds on:

- [miaucl/cookidoo-api](https://github.com/miaucl/cookidoo-api) - the
  unofficial Cookidoo API library used for login and session handling
- [alexandrepa/mcp-cookidoo](https://github.com/alexandrepa/mcp-cookidoo) -
  first Cookidoo MCP server, source of the create/patch upload flow
- [Xdev22/cookidoo-mcp](https://github.com/Xdev22/cookidoo-mcp) - discovered
  the structured TTS and INGREDIENT annotations that make custom recipes look
  like official ones

## Features

- `upload_recipe` - upload a recipe with structured ingredients and steps.
  The LLM supplies semantics only (text, time, temperature, speed, reverse);
  the server renders the German settings notation and computes all Cookidoo
  annotations itself. Optional `image_url`/`image_base64` parameters attach
  a photo in the same call.
- `set_recipe_image` - set or replace the photo of an existing custom
  recipe, from an https URL or base64 data.
- `list_custom_recipes` - list your custom recipes.
- `get_custom_recipe` - fetch one custom recipe by id.
- `delete_custom_recipe` - delete a custom recipe by id.
- `get_recipe_details` - fetch any Cookidoo recipe by id.
- `get_shopping_list`, `add_items_to_shopping_list`,
  `add_recipe_to_shopping_list` - read and fill the shopping list.
- `get_meal_plan`, `add_recipe_to_meal_plan` - read and fill the weekly
  planner.
- MCP prompt and resource with the full German Thermomix authoring guide,
  plus compact server instructions delivered to every client on connect.
- Optional GitHub OAuth with a user allowlist and persistent client
  registrations for safe public exposure.

### Recipe images

Custom recipes can carry a photo. Pass `image_url` or `image_base64`
(exactly one source; base64 works with or without a data URI prefix) to
`upload_recipe`, or call `set_recipe_image` for a recipe that already
exists. JPEG and PNG are supported, from 80x80 pixels (smaller images are
rejected by Cookidoo) up to 10 MB. URL images are downloaded by the server
itself (https only), and Cookidoo re-hosts every image on its own CDN.
Clients should downscale images to about 800px (JPEG, quality around 70)
before embedding them as base64. An image failure does not
abort the recipe upload; the result then contains an `image_warning`
instead.

## Requirements

- Python 3.12 or newer
- [uv](https://docs.astral.sh/uv/)
- A Cookidoo account with an active subscription

## Setup

```bash
uv sync
cp .env.example .env
```

Then edit `.env` and fill in your Cookidoo credentials:

| Variable | Meaning | Default |
| --- | --- | --- |
| `COOKIDOO_EMAIL` | Cookidoo account email | required |
| `COOKIDOO_PASSWORD` | Cookidoo account password | required |
| `COOKIDOO_COUNTRY` | Country code | `de` |
| `COOKIDOO_LANGUAGE` | Language code | `de-DE` |
| `THERMOMIX_MODEL` | Device written to the recipe tools field | `TM7` |

Locale examples:

| Country | `COOKIDOO_COUNTRY` | `COOKIDOO_LANGUAGE` |
| --- | --- | --- |
| Germany | `de` | `de-DE` |
| Austria | `at` | `de-AT` |

## Running

```bash
uv run cookidoo-mcp
```

This starts the server with streamable HTTP transport on
`http://127.0.0.1:8000/mcp`. Host and port are configurable via `MCP_HOST`
and `MCP_PORT`.

For clients that spawn the server as a subprocess, use stdio transport
instead:

```bash
MCP_TRANSPORT=stdio uv run cookidoo-mcp
```

## Client configuration

### Claude Code

```bash
claude mcp add --transport http cookidoo http://localhost:8000/mcp
```

### Claude Desktop

Add the server to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "cookidoo": {
      "type": "http",
      "url": "http://localhost:8000/mcp"
    }
  }
}
```

## Public exposure and OAuth

Connectors on claude.ai (web, mobile app, and the Claude Desktop
connector dialog) are established from Anthropic servers, so LAN-only
deployments are not reachable there. To use the server from those
clients it must be exposed to the internet - and then it MUST be
protected, otherwise anyone could use your Cookidoo account.

The server supports GitHub OAuth via FastMCP:

1. Register an OAuth app at https://github.com/settings/developers.
   Homepage: your `MCP_BASE_URL`; callback URL:
   `<MCP_BASE_URL>/auth/callback`.
2. Set `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `MCP_BASE_URL`,
   `ALLOWED_GITHUB_USERS` (and ideally `MCP_JWT_SIGNING_KEY` plus
   `MCP_STATE_DIR` for persistent client registrations - without it a
   restart disconnects all authorized clients) in the `.env` file and
   restart the container.
3. Make the server publicly reachable (see below).
4. Add the connector with your public `https://.../mcp` URL; Claude
   redirects you through the GitHub login once per client.

Only the GitHub accounts listed in `ALLOWED_GITHUB_USERS` are accepted;
every other login is rejected after authentication. Without the GitHub
variables the server runs unauthenticated as before - keep it LAN-only
in that case.

### Recommended public exposure: Cloudflare Tunnel

The battle-tested setup (and the one this project runs in production) is
a Cloudflare Tunnel in front of a localhost-only server container:

1. Put a domain on Cloudflare (free plan; only the nameservers change,
   the registration stays where it is), create a tunnel under Zero Trust
   -> Networks -> Tunnels, and map a public hostname such as
   `cookidoo.example.com` to `http://localhost:8443`.
2. Run the connector next to the server (same host):

   ```bash
   docker run -d --name cloudflared --network host \
     --restart unless-stopped \
     -e TUNNEL_TOKEN=<your tunnel token> \
     cloudflare/cloudflared:latest tunnel run
   ```

3. Bind the server container to localhost only (the deploy script does
   this), drop the `MCP_SSL_*` variables (Cloudflare terminates TLS with
   auto-renewing certificates) and set `MCP_BASE_URL` and the GitHub app
   URLs to the tunnel hostname.

This needs no router port forwarding and works behind DS-Lite or CGNAT,
because the tunnel connects outbound and Cloudflare provides a
dual-stack edge - important since the claude.ai connector infrastructure
connects over IPv4 only, so an IPv6-only port release is never reached.

Known pitfall: Tailscale Funnel does NOT work as the public endpoint for
claude.ai connectors. The funnel edge intermittently aborts TLS
handshakes, and the multi-request OAuth flow of the connector broker
gives up on the first failure, surfacing as "Couldn't reach the MCP
server" with zero requests in your logs - even though curl and Claude
Code work fine through the same funnel.

## Testing

```bash
uv run pytest
```

Unit and integration tests run offline against mocked HTTP. The end-to-end
tests in `tests/test_e2e.py` talk to the live Cookidoo API and are skipped
unless real credentials are present in `.env`.

## Docker

### Prebuilt image (recommended)

Every push to main builds a multi-arch image (amd64/arm64) via GitHub
Actions and publishes it as
`ghcr.io/jsaedtler/cookidoo-mcp-docker:latest`. On the server you only
need the credentials file and one script:

```bash
sudo mkdir -p /docker/cookidoo-mcp
sudo cp .env.example /docker/cookidoo-mcp/.env   # fill in your credentials
./deploy/update-cookidoo-mcp.sh
```

`deploy/update-cookidoo-mcp.sh` stops and removes the old container,
pulls the latest image and starts it again with `--restart=unless-stopped`.
Re-run it any time to update. Adjust the `ENV_FILE`, `SSL_DIR`,
`STATE_DIR` and `PORT` variables at the top of the script to your setup.

### HTTPS without a tunnel

When you do not use the Cloudflare Tunnel (for example LAN-only use with
Claude Desktop, which requires an https URL), the server can terminate
TLS itself: set `MCP_SSL_CERTFILE` and `MCP_SSL_KEYFILE` and mount a
certificate directory into the container (the deploy script does this
via `SSL_DIR`). Any certificate you already have works, for example a
Let's Encrypt certificate from another service on the same host. The
hostname in the MCP URL must match the certificate. After a certificate
renewal restart the container (`docker restart cookidoo-mcp`) so it
picks up the new files.

### Build locally

```bash
cp .env.example .env   # fill in your Cookidoo credentials
docker compose up -d --build
```

The server then listens on `http://<host>:8000/mcp` (Streamable HTTP) for
clients in your network. To change the port, adjust both sides of the
`ports` mapping in `docker-compose.yml`.

Notes:

- Credentials are injected at runtime via `env_file`; the `.env` file is
  never baked into the image (see `.dockerignore`).
- The container binds `::` (dual-stack). If the Docker daemon runs with
  `ip6tables: true`, published ports are DNATed to the container's IPv6
  address, so a v4-only bind would time out for all IPv6 clients even
  though the port looks open on IPv4.
- The image runs as a non-root user and builds on both amd64 and arm64.
- Typical deployment on a server: clone the repository, create `.env`,
  run `docker compose up -d --build`. The `restart: unless-stopped`
  policy brings the container back after reboots.

TDQS

A3.7/5.0

Scored across 11 tools

Disambiguation5/5

Each tool targets a distinct resource and action: custom recipe CRUD (upload/list/get/delete/set image), official recipe read, shopping list read/add, and meal plan read/add. Even similar tools like add_items_to_shopping_list and add_recipe_to_shopping_list are clearly differentiated by their target (free items vs. recipe ingredients).

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (upload_recipe, list_custom_recipes, get_shopping_list, add_recipe_to_meal_plan). Verbs are consistent (list, get, add, set, delete) and nouns clearly indicate the resource. No mixed naming conventions.

Tool Count5/5

With 11 tools, the server is well-scoped for its purpose. Each tool covers a core aspect of Cookidoo integration—custom recipes, official recipe details, shopping list, and meal plan—without unnecessary duplication or bloat.

Completeness2/5

The set has notable gaps: custom recipes can be created and read but not updated (except image), shopping list items and meal plan entries can be added but not removed, and there is no search or list for official recipes. These missing operations will cause agents to fail when users need to modify or remove data.

Maintenance

ActivityStale
ResponsivenessNo issues