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
访问服务器

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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选