tandoor-mcp

tandoor-mcp

MCP server for Tandoor Recipes, enabling recipe management (create, edit, search, delete) and entity operations (foods, units, keywords) via natural language from Claude.

Category
Visit Server

README

tandoor-mcp

An MCP server for Tandoor Recipes, built for creating and editing recipes from the Claude app.

Setup

uv sync
cp .env.example .env   # fill in TANDOOR_URL and TANDOOR_TOKEN
uv run tandoor-mcp

The API token comes from Tandoor under Settings → API → Access Tokens.

Docker

export TANDOOR_URL=... TANDOOR_TOKEN=... \
       TANDOOR_AUTH_TOKEN=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
docker compose up -d

Alpine-based, ~136 MB. The container defaults to the HTTP transport bound to 0.0.0.0, since stdio has nothing to talk to inside a container.

Two things bite when connecting to a containerised Tandoor:

  • ALLOWED_HOSTS. Tandoor answers a request whose Host header it does not recognise with a bare Bad Request (400) and no explanation. The hostname in TANDOOR_URL must be listed in Tandoor's ALLOWED_HOSTS.
  • Reachability. Either attach to Tandoor's compose network and use the service name, or reach the host — both need the hostname allowed above.

Transport

TANDOOR_TRANSPORT=stdio (default) for local clients, or http with TANDOOR_HOST / TANDOOR_PORT to expose it over the network. HTTP is served at /mcp, or wherever TANDOOR_PATH points.

TANDOOR_PATH exists for reverse proxies that route by prefix without stripping it — the server then has to answer on the same path the client asks for, e.g. TANDOOR_PATH=/tandoor/mcp behind PathPrefix(/tandoor). Like TANDOOR_HOST and TANDOOR_PORT it describes this server, not the Tandoor instance.

The HTTP transport requires TANDOOR_AUTH_TOKEN; clients send it as Authorization: Bearer <token> and anything else gets a 401. The server refuses to start without it, because an open port hands write access to the recipe database to whoever can reach it. TANDOOR_ALLOW_UNAUTHENTICATED=true waives this deliberately. stdio needs no token — the transport is a pipe to a process the user started.

For Claude Desktop with stdio:

{
  "mcpServers": {
    "tandoor": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/tandoor-mcp", "tandoor-mcp"],
      "env": {
        "TANDOOR_URL": "https://tandoor.example.com",
        "TANDOOR_TOKEN": "tda_..."
      }
    }
  }
}

Tools

Tool Purpose
search_recipes Compact search hits
get_recipe One recipe in full, with all ids
create_recipe Whole recipe incl. steps and ingredients
update_recipe Read-modify-write update
delete_recipe Delete
search_foods / search_units / search_keywords Look up entity ids, many terms per call
create_food / create_unit / create_keyword Add entities deliberately

Layout

src/tandoor_mcp/
├── config.py       # TANDOOR_* settings
├── client.py       # httpx wrapper, DRF errors -> readable messages
├── types.py        # shared JSON alias
├── templating.py   # step-templating reference text (see below)
├── payloads.py     # input -> Tandoor, read-only stripping, response slimming
├── server.py       # assembly; iterates tools.DOMAINS
├── models/         # entities.py, recipes.py — tool input validation
└── tools/          # one module per domain, each exposing register(mcp, client)
    ├── _entity_tools.py   # shared search machinery for the three entity domains
    ├── recipes.py
    ├── foods.py
    ├── units.py
    └── keywords.py

templating.py holds no logic. TEMPLATING_REFERENCE is a plain string that gets interpolated into the description= of create_recipe and update_recipe, so it becomes part of what the model reads when it lists the tools. Editing that one constant changes the guidance everywhere it appears.

The three entity domains differ only in endpoint and wording, so their search tool is built once in _entity_tools.py and registered per domain via EntitySpec and MCPServer.add_tool, which takes the tool name and description as arguments.

Requires fuzzy lookup

Tandoor only uses trigram similarity for food/unit/keyword search when the calling user has lookup enabled in their search preferences (cookbook/views/api.py, FuzzyFilterMixin). Without it the search falls back to a plain substring match, and Kartoffeln then fails to find Kartoffel while happily returning Süßkartoffelnudel.

curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "$TANDOOR_URL/api/search-preference/<user-id>/" -d '{"lookup": true}'

Design notes

Entities are referenced by id, never by name. Tandoor's serializers accept a nested food or unit without an id and create a new one, which is how duplicate ingredients accumulate. EntityRef in models.py makes the id mandatory, so a name-only reference fails validation instead of silently polluting the database. Tandoor additionally requires name next to id on nested entities — id alone is a 400.

No ingredient parser. Tandoor's /api/ingredient-parser/ splits a written line well, but it creates every food and unit it fails to recognise. Measured on 20 realistic German lines it silently added four entities — geriebener Parmesan next to the existing Parmesan, rote as a unit. Since fuzzy search resolves the same lines without writing anything, the parser is not exposed.

Search is batched. One call takes every ingredient of a recipe, one request per term in parallel — 12 terms in ~230 ms against a local instance. Ranking is left entirely to Tandoor; the server adds no matching logic of its own.

Out-of-range template indexes are rejected client-side. Tandoor renders {{ ingredients[9] }} as an empty string with no error, so the mistake would only surface as a missing word in the finished recipe. StepInput validates every index against the step's own ingredient count.

Updates are read-modify-write. Tandoor treats the steps array as the whole truth: omitted steps get deleted, id-less ones get created. update_recipe therefore fetches the recipe, strips the read-only fields, merges the requested changes and writes the complete object back.

Responses are slimmed. A fully expanded recipe carries nutrition, properties, conversions and user objects. payloads.py reduces both search hits and full recipes to what the model can actually act on.

Templating is verifiable. For templated steps the write tools return instruction_rendered: Tandoor's own rendering, collapsed from its Vue markup back to plain text ("200 EL Mehl mit 2 Zehen Knoblauch verrühren"). The syntax reference lives in templating.py, verified against Tandoor 2.6.13.

Development

uv run ruff check .
uv run ruff format .
uv run ty check

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured