tandoor-recipe-mcp
Health Uyari
- License — License: MIT
- No description — Repository has no description
- Active repo — Last push 0 days ago
- Low visibility — Only 7 GitHub stars
Code Gecti
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.

Turn "plan the week and make me a grocery list" into one prompt.
Recipes, meal plans, shopping lists and more for your local or self-hosted Tandoor instance.
Install · Configure · Remote mode · Tool sections · Example conversations · Contributing
Unofficial community project, not affiliated with Tandoor. Tandoor logo © TandoorRecipes, used to show compatibility.
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, AI-powered URL/image imports, invite/access tokens, storages, sync, spaces, exports, imports, and more.
What's new in 3.4
- Remote mode for Claude and ChatGPT. Host the server next to your Tandoor and add it as a custom connector in Claude or ChatGPT. Each person signs in with their own Tandoor API token. See Remote mode.
- Docker image on GHCR. Each release pushes a signed multi-arch image to
ghcr.io/cliftonz/tandoor-recipes-mcp. See Run with Docker. - MCP Registry listing. Stable releases publish
server.jsonto the MCP Registry, with the npm package, the.mcpbfile and the Docker image. - Titles and annotations on every tool. All 307 tools declare a title and the read-only, destructive, idempotent and open-world hints.
- Host and Origin checks, plus rate limits. Remote mode checks the Host and Origin headers and rate limits each visitor on
/mcp. See Remote mode security.
See CHANGELOG.md for earlier releases.
Why this server
- Full Tandoor 2.6 API coverage. 307 typed tools, spanning every domain the Tandoor OpenAPI exposes.
- Resources + prompts, not just tools. Read-only 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. - Per-section tool settings. One env var per section (recipes, meal plan, shopping, and so on) sets it. Tiered sections take
off,simple, orfull. Toggle sections takeofforon. In stdio mode the LLM can turn a hidden section on mid-session withenable_tool_group. See Tool sections. - jq stash for large payloads. Responses larger than 5KB return a stash handle plus a schema summary; the LLM follows up with
jq_queryto project just the fields it needs. - 3-stage URL import with a real fallback. Tries Tandoor's native scraper first, then extracts schema.org JSON-LD from the page directly (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. - Local or remote. Stdio (default, for Claude Desktop / Claude Code), stateless HTTP (for out-of-Claude callers), or remote mode: a self-hosted server with OAuth sign-in that Claude and ChatGPT connect to as a custom connector.
- Destructive-action safety layer. Every tree-shaped delete has a
preview_*sibling that projects the cascade before it runs; every token-mint tool leads with an ESCALATION SURFACE warning.
Install
npm install -g @cliftonz/tandoor-recipes-mcp
Or npx without installing:
npx -y @cliftonz/tandoor-recipes-mcp
Node 24 or later is required (see engines.node in package.json).
Other ways to get it:
- Claude Desktop extension:
tandoor-recipes-mcp.mcpbfrom the latest GitHub release. It needs no Node install. See Claude Desktop, desktop extension. - Docker image:
ghcr.io/cliftonz/tandoor-recipes-mcp, for linux/amd64 and linux/arm64, tagged with each version andlatest. The image starts in remote mode by default. - MCP Registry: listed as
io.github.Cliftonz/tandoor-recipe-mcpin the MCP Registry, with the npm package, the.mcpbfile and the Docker image. - Claude Code plugin and Codex plugin: see Configure.
Configure
- 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-tandoor
From a terminal, the same two steps are:
claude plugin marketplace add Cliftonz/tandoor-recipe-mcp
claude plugin install tandoor-recipes-mcp@cliftonz-tandoor
Claude Code prompts you for TANDOOR_URL and TANDOOR_TOKEN on install and stores the token securely. No mcpServers JSON editing needed.
Claude Desktop, desktop extension
Use the desktop extension for Claude Desktop Chat and Cowork. It bundles the server and runs on Claude Desktop's built-in Node, so you do not need to install Node.
- Download
tandoor-recipes-mcp.mcpbfrom the latest GitHub release. - Double-click the file. Or, in Claude Desktop, open Settings > Extensions > Advanced settings > Install Extension and select the file.
- Enter your Tandoor URL and API token. Claude Desktop stores the token securely.
- Optional: set the section settings you want.
Claude Desktop, plugin marketplace
Claude Desktop can also add the plugin:
- Open Customize > Plugins > Add > Add marketplace > Add from a repository.
- Enter
Cliftonz/tandoor-recipe-mcp. - Open Customize > Plugins > Discover, select the plugin, and select Add.
This path does not give you a working server in Chat or Cowork. Chat and Cowork do not prompt for plugin settings, so the plugin's Tandoor URL and API token settings stay empty. Chat ignores local MCP servers from plugins. Cowork loads them only when the session runs on your computer. Use the desktop extension or the manual config instead. Use the plugin from the Claude Code CLI.
Claude Desktop / Claude Code, manual config
Edit the Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
For Claude Code, put the same mcpServers block in a project .mcp.json, or use claude mcp add.
{
"mcpServers": {
"tandoor": {
"command": "npx",
"args": ["-y", "@cliftonz/tandoor-recipes-mcp"],
"env": {
"TANDOOR_URL": "https://your-tandoor-instance.com",
"TANDOOR_TOKEN": "your-api-token"
}
}
}
}
Quit and reopen Claude Desktop to load the change. This config runs the server with npx, so Node 24 or later must be on your PATH.
Codex
The ChatGPT desktop app, the Codex CLI, and the Codex IDE extension share one config file, ~/.codex/config.toml. Every Codex path below runs the server with npx, so Node 24 or later must be on your PATH.
Codex, settings
- In the app, open Settings > MCP servers > Add server.
- Choose STDIO.
- Set the command to
npx. - Add the arguments
-yand@cliftonz/tandoor-recipes-mcp. - Add the environment variables
TANDOOR_URLandTANDOOR_TOKEN. Add anyTANDOOR_MCP_*section settings you want. - Select Save, then restart the app.
From a terminal, the same setup is:
codex mcp add tandoor \
--env TANDOOR_URL=https://your-tandoor-instance.com \
--env TANDOOR_TOKEN=your-api-token \
-- npx -y @cliftonz/tandoor-recipes-mcp
codex mcp add writes this block to ~/.codex/config.toml. You can also add it to config.toml by hand:
[mcp_servers.tandoor]
command = "npx"
args = ["-y", "@cliftonz/tandoor-recipes-mcp"]
[mcp_servers.tandoor.env]
TANDOOR_URL = "https://your-tandoor-instance.com"
TANDOOR_TOKEN = "your-api-token"
Codex, plugin
- Open the Plugins page and select Add > Add a marketplace.
- Enter the source
Cliftonz/tandoor-recipe-mcp. - Open the Tandoor plugin's details and select the plus button.
- Restart the app.
From a terminal:
codex plugin marketplace add Cliftonz/tandoor-recipe-mcp
codex plugin add tandoor-recipes-mcp@cliftonz-tandoor
Codex does not prompt for settings on install, and you cannot override a plugin's environment. The plugin passes through only TANDOOR_URL and TANDOOR_TOKEN from the app's environment. An app you start from the Dock usually does not see exports in ~/.zshrc. On macOS, set the variables for your login session and then restart the app:
launchctl setenv TANDOOR_URL https://your-tandoor-instance.com
launchctl setenv TANDOOR_TOKEN your-api-token
launchctl setenv does not survive a reboot. It also exposes the token to every app you start in that session.
The Codex plugin does not pass through section settings. To change sections, use saved sections or the settings path.
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_*",
"mcp__tandoor__preview_*"
],
"ask": [
"mcp__tandoor__create_*",
"mcp__tandoor__update_*",
"mcp__tandoor__delete_*",
"mcp__tandoor__merge_*",
"mcp__tandoor__switch_active_space",
"mcp__tandoor__create_access_token",
"mcp__tandoor__create_invite_link",
"mcp__tandoor__get_external_recipe_file_link"
]
}
}
Transport modes
The server ships two transports. Default is stdio; opt in to HTTP with an env var. HTTP also has a remote mode for Claude and ChatGPT custom connectors.
| Mode | When to use | Auth |
|---|---|---|
stdio (default) |
Claude Desktop, Claude Code, any MCP client that spawns the server as a subprocess. | Inherited from parent process. |
http (stateless) |
Out-of-Claude callers, local scripts, other agents on the same host. | Optional bearer via TANDOOR_MCP_HTTP_TOKEN. |
http with TANDOOR_MCP_PUBLIC_URL (remote mode) |
Claude (web, mobile, Desktop, Cowork, Code) and ChatGPT, through your own HTTPS URL. | OAuth sign-in. Each user pastes their own Tandoor API token. |
HTTP mode
TANDOOR_URL=https://tandoor.example.com \
TANDOOR_TOKEN=... \
TANDOOR_MCP_TRANSPORT=http \
TANDOOR_MCP_HTTP_PORT=3737 \
TANDOOR_MCP_HTTP_TOKEN=some-long-random-string \
npx -y @cliftonz/tandoor-recipes-mcp
The server listens on http://127.0.0.1:${TANDOOR_MCP_HTTP_PORT:-3737}/mcp (POST only for JSON-RPC requests). Any other method on that path gets a 405 with Allow: POST and a JSON-RPC error body. Auth is checked first, so a request without a valid token gets a 401 instead. The server binds to 127.0.0.1 unless you set TANDOOR_MCP_HTTP_HOST. To reach it from another host, put a reverse proxy in front of it, or use remote mode.
If TANDOOR_MCP_HTTP_TOKEN is set, every request must include Authorization: Bearer <token>. Missing or wrong tokens get a 401 with WWW-Authenticate: Bearer.
HTTP-mode constraints: the jq stash and dynamic tool gating both assume a single long-lived session. In HTTP mode both are auto-disabled: enable_tool_group and disable_tool_group throw, and every tool response is inlined rather than stashed. The section settings fix the tool list at startup, so set every section you need before you start the server.
Remote mode
Remote mode runs the server on your own host, next to your Tandoor, with a built-in OAuth sign-in. You add it to Claude or ChatGPT as a custom connector, by its HTTPS URL. It works in Claude on the web, mobile, Desktop chat, Cowork and Claude Code, and in ChatGPT. Each person signs in by pasting their own Tandoor API token, so each person acts in Tandoor as themselves.
Why you host it yourself, and why it is not in a connector directory: every Tandoor is a separate, self-hosted server, so there is no single URL a directory listing could point to. A shared hosted copy would also have to hold everyone's Tandoor tokens. In remote mode the tokens stay on your server, encrypted with your key.
How a connection works:
- The app (Claude or ChatGPT) calls your public URL and gets a
401that points to the server's OAuth metadata. - The app identifies itself with its published client document, or registers itself automatically.
- The app opens the sign-in page in your browser. You paste a Tandoor API token and select Approve.
- The server checks the token against Tandoor, stores it encrypted, and gives the app its own tokens. The app never sees your Tandoor token.
Remote mode settings
Remote mode turns on when TANDOOR_MCP_PUBLIC_URL is set. These settings apply to remote mode, and the HTTP ones also apply to plain HTTP mode.
| Variable | Default | Rules |
|---|---|---|
TANDOOR_URL |
none (required) | Your Tandoor. It only has to be reachable from the server, so an internal address such as http://web_recipes:8080 works. The server uses only the scheme, host and port. |
TANDOOR_MCP_TANDOOR_PUBLIC_URL |
TANDOOR_URL |
The address users open Tandoor at, such as https://recipes.example.com. The sign-in page shows it and links to its /settings/api page for creating a token. Set it when TANDOOR_URL is an internal address. The server still calls Tandoor only at TANDOOR_URL. It uses only the scheme, host and port, and adds https:// when the scheme is missing. Ignored outside remote mode. |
TANDOOR_MCP_TRANSPORT |
stdio (http in the Docker image) |
Must be http. The server refuses to start in remote mode otherwise. |
TANDOOR_MCP_PUBLIC_URL |
none | The HTTPS URL clients connect to, such as https://mcp.example.com/mcp. Must use https. Its path must equal TANDOOR_MCP_HTTP_PATH. No query string, fragment, username or password. |
TANDOOR_MCP_SECRET_KEY |
none (required in remote mode) | Exactly 64 hex characters (32 bytes). Generate one with openssl rand -hex 32. It encrypts the stored Tandoor tokens and signs the sign-in form. |
TANDOOR_MCP_DATA_DIR |
$XDG_DATA_HOME/tandoor-mcp, else ~/.local/share/tandoor-mcp (/data in the Docker image) |
The folder for oauth.json. Must be an absolute path; a relative path is ignored. |
TANDOOR_MCP_TRUST_PROXY |
none (no proxy trusted) | Set it when a reverse proxy or tunnel sits in front. A hop count such as 1, or a comma-separated list of IP addresses and CIDR ranges. A /0 range is refused. The server uses it to find each visitor's IP address for rate limits. |
TANDOOR_MCP_HTTP_HOST |
127.0.0.1 (0.0.0.0 in the Docker image) |
The address the server listens on. |
TANDOOR_MCP_HTTP_PORT |
3737 |
The port the server listens on. |
TANDOOR_MCP_HTTP_PATH |
/mcp |
The MCP endpoint path. It cannot be, or start with, /authorize, /token, /register, /revoke, /consent or /.well-known. |
TANDOOR_MCP_HTTP_DRAIN_MS |
10000 |
On SIGTERM or SIGINT, how long the server waits for requests in progress. New requests get a 503 meanwhile. |
TANDOOR_MCP_HTTP_MAX_BODY_BYTES |
5242880 |
Larger request bodies get a 413. |
TANDOOR_MCP_HTTP_MAX_CONNS |
128 |
The most open connections the server accepts. |
Header and request timeouts are fixed at 10 and 30 seconds.
Rules and behavior in remote mode:
TANDOOR_TOKENis refused. The server will not start while it is set. Each user brings their own token.TANDOOR_MCP_HTTP_TOKENis refused. It belongs to the shared-password HTTP mode and cannot be combined with remote mode.- Tool sections apply to every connected user. The section settings are read once at startup, and
enable_tool_groupis off in HTTP mode, so everyone sees the same tools. file_pathuploads are refused. The server's disk is not the user's disk. Usefile_urlinstead. Saving downloads withsave: trueis also stdio only.- A read-only Tandoor token works for reads only. Write tools return Tandoor's error. The connection stays.
- Host and Origin are checked. A request must carry the public URL's host in its
Hostheader, orlocalhost,127.0.0.1or[::1]. Anything else gets a403, logged ashost_rejected. A browserOriginheader on the MCP path must equal the public URL's origin, or the request gets a403, logged asorigin_rejected. - Responses are plain JSON. The server does not hold event streams open, so proxies need no streaming or buffering settings.
GETon the MCP path returns405.
Run with Docker
The image is ghcr.io/cliftonz/tandoor-recipes-mcp. It defaults to TANDOOR_MCP_TRANSPORT=http, TANDOOR_MCP_HTTP_HOST=0.0.0.0 and TANDOOR_MCP_DATA_DIR=/data, and runs as the node user.
Generate a secret key:
openssl rand -hex 32Copy the
tandoor-mcpservice and thetandoor-mcp-datavolume fromdocker-compose.example.ymlinto your Tandoor compose file, so both services share a network.Set the environment:
TANDOOR_URL: your Tandoor service on the internal network. The example useshttp://web_recipes:8080; replaceweb_recipeswith the name of your Tandoor service.TANDOOR_MCP_TANDOOR_PUBLIC_URL: the address users open Tandoor at, such ashttps://recipes.example.com.TANDOOR_MCP_PUBLIC_URL: your public HTTPS URL, ending in/mcp.TANDOOR_MCP_SECRET_KEY: the key from step 1.TANDOOR_MCP_TRUST_PROXY:"1"when one proxy or tunnel sits in front, as in the example.
Start it:
docker compose up -d tandoor-mcp
Notes on the example file:
- The
/datavolume holdsoauth.json. Keep the volume across upgrades, or everyone has to reconnect. - The port is published on
127.0.0.1only (127.0.0.1:3737:3737), so only a proxy or tunnel on the same host can reach it. Remove theports:entry if your proxy reaches the container over the Docker network. - Keep
TANDOOR_MCP_HTTP_HOST=0.0.0.0inside the container. Binding a specific address inside the container is not supported: the image's health check calls127.0.0.1, so the container reports unhealthy. Limit exposure with the port mapping instead. stop_grace_period: 15sis longer than the 10-second drain, so requests in progress can finish ondocker compose down.
The sign-in page shows your Tandoor address and links to its /settings/api page for creating a token. With an internal TANDOOR_URL such as http://web_recipes:8080, set TANDOOR_MCP_TANDOOR_PUBLIC_URL to the address users open Tandoor at, so the link opens from a browser.
Without Docker, run the npm package with the same settings:
TANDOOR_URL=https://tandoor.example.com \
TANDOOR_MCP_TRANSPORT=http \
TANDOOR_MCP_PUBLIC_URL=https://mcp.example.com/mcp \
TANDOOR_MCP_SECRET_KEY=<64 hex characters> \
TANDOOR_MCP_TRUST_PROXY=1 \
npx -y @cliftonz/tandoor-recipes-mcp
Expose it over HTTPS
Claude and ChatGPT need a public HTTPS URL. Put a reverse proxy or a tunnel in front of the server. Whatever you use must:
- Forward the whole host, not only
/mcp. The OAuth routes live at the root. - Pass the original
Hostheader through. - Set
X-Forwarded-For, and you setTANDOOR_MCP_TRUST_PROXYto match.
Caddy
mcp.example.com {
reverse_proxy 127.0.0.1:3737
}
Caddy passes Host through unchanged and sets X-Forwarded-For. Set TANDOOR_MCP_TRUST_PROXY=1. If Caddy runs in the same compose network, use reverse_proxy tandoor-mcp:3737.
nginx
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate and ssl_certificate_key go here
location / {
proxy_pass http://127.0.0.1:3737;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
proxy_set_header Host $host; is required. Without it, nginx sends the upstream name as Host, and the server answers 403 unless that name is localhost or a loopback address. The log line host_rejected includes a hint about this. Responses are plain JSON, so no SSE or buffering settings are needed. Set TANDOOR_MCP_TRUST_PROXY=1.
Cloudflare Tunnel
A tunnel needs no open port. The example compose file has an optional cloudflared service.
- In the Cloudflare dashboard, go to Networking > Tunnels and select Create a tunnel. Copy the tunnel token.
- Uncomment the
cloudflaredservice and setTUNNEL_TOKENto the token. - In the tunnel's Routes tab, select Add route > Published application. Choose your subdomain and domain, and set Service URL to
http://tandoor-mcp:3737. - Leave HTTP Host Header empty, so the server sees your public hostname.
Set TANDOOR_MCP_TRUST_PROXY=1. You can remove the ports: entry from tandoor-mcp.
Tailscale Funnel
On the host where port 3737 is published on 127.0.0.1:
tailscale funnel --bg 3737
The public URL is https://<device-name>.<tailnet-name>.ts.net, so set TANDOOR_MCP_PUBLIC_URL to https://<device-name>.<tailnet-name>.ts.net/mcp. Funnel keeps the Host header and sets X-Forwarded-For. Set TANDOOR_MCP_TRUST_PROXY=1. tailscale funnel reset turns it off.
Access gateways
Warning: If you put an access gateway in front, such as Cloudflare Access, Authelia or another forward-auth proxy, it must let these routes through without its own login. Otherwise Claude and ChatGPT cannot connect.
POST /mcp(yourTANDOOR_MCP_HTTP_PATH)GET /.well-known/oauth-protected-resource/mcpGET /.well-known/oauth-authorization-serverPOST /registerPOST /tokenPOST /revokeGETandPOST /authorizePOST /consent
/authorizeand/consentopen in the user's own browser. The others are called by the app's servers. The simplest rule is to bypass the gateway for/mcp,/register,/token,/revoke,/authorize,/consentand/.well-known/*.
Connect a client
Use your public URL, for example https://mcp.example.com/mcp.
Claude
On a Free, Pro, Max or Ultra plan:
- Go to Customize > Connectors.
- Click Add custom connector.
- Enter the URL.
- If the dialog asks for the OAuth client, choose Use Claude's published identity or Register automatically. The server supports both. Do not enter a client ID or secret.
- Click Add. When you connect, the sign-in page opens in your browser.
On a Team or Enterprise plan, an Owner adds it under Organization settings > Connectors: select Add, then Custom, enter the URL, and click Add. Each member then finds it under Customize > Connectors and clicks Connect.
You can also share a link that opens the dialog with the name and URL filled in. Percent-encode the URL:
https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Tandoor&connectorUrl=https%3A%2F%2Fmcp.example.com%2Fmcp
Owners can use the same parameters on https://claude.ai/admin-settings/connectors. The user still reviews and confirms before anything is added.
Claude Code
claude mcp add --transport http tandoor https://mcp.example.com/mcp
Then run /mcp in Claude Code and follow the sign-in steps in your browser.
ChatGPT
- Go to chatgpt.com/plugins.
- Select the plus button, then Add custom MCP server.
- Enter a name and description.
- Under Connection, enter the URL, including
/mcp. - Configure authentication. The server uses OAuth and supports both ways ChatGPT registers itself: a client ID metadata document and dynamic client registration. Review the risk warning and select I understand and want to continue.
- Select Create as a plugin.
Your ChatGPT account or workspace policies decide whether you can add custom MCP servers.
The sign-in page
The page is titled Connect to Tandoor. It names the app that wants access and the Tandoor URL, and shows where you return after you approve. Paste a Tandoor API token into Tandoor API token and select Approve, or Deny.
Create the token on your Tandoor's API settings page, /settings/api. The token decides what the app can do: a read-write token allows changes, a read-only token allows reads only.
If Tandoor rejects the token, the page says so and you can try again. The form expires after 10 minutes, and each IP address gets 10 attempts a minute.
Operate it
- Back up
oauth.jsonand the secret key separately.oauth.jsonin the data folder holds the registered apps, the connections, the encrypted Tandoor tokens and hashes of the tokens the server issued. Anyone with both the file and the key can read the stored Tandoor tokens. - Revoke everyone: stop the server, delete
oauth.json, and start it again. Every user has to reconnect. - Revoke one user's access: delete their API token in Tandoor. Their tool calls then fail in Tandoor.
- Rotate the secret key: the server refuses to start when the key cannot decrypt
oauth.json(cannot read OAuth store). Rotation means stopping the server, deletingoauth.json, setting the new key and starting again. Every user has to reconnect. - Connections end on their own after 30 days without use, or when Tandoor rejects the stored token with a
401. In the second case the server ends every connection that uses that token, and the tool call returns "Tandoor rejected your token. Reconnect this connector to sign in again." along with a sign-in challenge for the app. App registrations that never finish signing in are removed after 24 hours. - Run one server process per data folder. Each process keeps
oauth.jsonin memory and rewrites it on every change, so two processes overwrite each other. - Health check:
GET /healthzreturns{"status":"ok"}without auth. The Docker image checks it every 30 seconds. - Logs go to standard error (
docker compose logs tandoor-mcp). The startup line shows the public URL,auth=oauth, the Tandoor URL and version, and the sections. Request events look like[tandoor-mcp] http reqId=... level=warn event=host_rejected .... Other events includeorigin_rejected,forwarded_header_untrusted(a proxy sentX-Forwarded-ForbutTANDOOR_MCP_TRUST_PROXYis not set),tandoor_check_failed,cimd_fetch_failed,payload_too_largeandshutdown. PRIVACY.md lists what the logs contain. - Rate limits are per IP address: failed access tokens, sign-in form submissions, and the OAuth routes. Behind a proxy, set
TANDOOR_MCP_TRUST_PROXY, or every visitor shares one limit.
Firewall allowlisting
You can limit who reaches the server-to-server routes (/mcp, /.well-known/*, /register, /token, /revoke):
- Claude: Anthropic's outbound IP ranges, listed at platform.claude.com/docs/en/api/ip-addresses. On 2026-11-07 they were
160.79.104.0/21and153.61.208.0/20. - ChatGPT: OpenAI's ranges for connectors, published at openai.com/chatgpt-connectors.json. The list is long and changes; OpenAI recommends fetching it regularly and updating your allowlist automatically.
Do not limit /authorize and /consent this way. They come from each user's own browser. Claude Code and the MCP Inspector also run on the user's computer, so their requests come from the user's address.
The server itself makes outbound requests to your Tandoor, to the app's client document (such as one on claude.ai or chatgpt.com) when an app uses its published identity, and to web addresses users import from.
Test it
Check the health endpoint and the metadata:
curl https://mcp.example.com/healthz
curl https://mcp.example.com/.well-known/oauth-protected-resource/mcp
Then connect with the MCP Inspector. It runs the sign-in flow in your browser:
npx @modelcontextprotocol/inspector --server-url https://mcp.example.com/mcp --transport http
Remote mode security
- The sign-in routes are reachable from the internet. Anyone who can reach the URL can start signing in, but only a valid Tandoor token completes it.
- The Tandoor token is the real credential. Whoever signs in with a token gets that token's full Tandoor access.
- The secret key protects the stored tokens. The server stores only hashes of the tokens it issues.
- Removing a connection in Claude or ChatGPT does not delete the Tandoor token. Delete it in Tandoor too.
- Tandoor content is untrusted input to the assistant.
SECURITY.md has the full threat model and how to report a problem.
Environment variables
| Var | Required | Default | Purpose |
|---|---|---|---|
TANDOOR_URL |
yes | none | Base URL of your Tandoor instance. |
TANDOOR_TOKEN |
yes, except in remote mode | none | API token with write access. Remote mode refuses to start when it is set. |
TANDOOR_MCP_TRANSPORT |
no | stdio |
stdio or http. |
TANDOOR_MCP_HTTP_PORT |
no | 3737 |
HTTP-mode port. |
TANDOOR_MCP_HTTP_TOKEN |
no | none | HTTP-mode bearer token. If unset, HTTP transport runs without auth. Not allowed in remote mode. |
TANDOOR_MCP_<SECTION> |
no | per section | One variable per tool section, such as TANDOOR_MCP_RECIPES. See Tool sections for every variable, its values, and its default. |
TANDOOR_MCP_INCLUDE_ONLY |
no | none | Comma-separated allowlist (glob * supported). E.g. list_*,get_*,create_meal_plan. |
TANDOOR_MCP_EXCLUDE |
no | none | Comma-separated denylist. E.g. merge_*,delete_* to hide destructive tools. |
TANDOOR_MCP_LOG |
no | none | Stderr trace mode. request, response, error, all, or comma list. Bearer token redacted. |
TANDOOR_MCP_STASH_ENABLED |
no | 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. Auto-disabled in HTTP mode. |
TANDOOR_MCP_STASH_THRESHOLD |
no | 25000 |
Stash payloads strictly larger than this many bytes. |
TANDOOR_MCP_STASH_MAX_ENTRIES |
no | 32 |
LRU cap on stashed handles. |
TANDOOR_MCP_STASH_MAX_BYTES |
no | 32000000 |
Total-bytes cap on the stash; LRU eviction kicks in past this budget. |
TANDOOR_MCP_STASH_TTL_MS |
no | 600000 |
Per-entry TTL in milliseconds (default 10 minutes). |
TANDOOR_MCP_DOWNLOAD_ROOT |
no | none | Existing folder that save: true writes downloads into. Stdio mode only. See Downloading files. |
The other HTTP settings and the remote-mode settings are in Remote mode settings.
INCLUDE_ONLY and EXCLUDE run before the section settings and give finer control ("give me only read tools" or "hide destructive ops from this agent").
Section values can also come from saved sections.
The Claude plugin and the desktop extension pass their settings as CLAUDE_PLUGIN_OPTION_<KEY>, such as CLAUDE_PLUGIN_OPTION_TANDOOR_URL or CLAUDE_PLUGIN_OPTION_RECIPES. A non-empty plugin setting wins over the variable it replaces. An empty one falls back to that variable.
Downloading files
download_recipe_file({recipe_id}) returns the file Tandoor stored for a recipe synced from a Storage backend (Dropbox, Nextcloud, or Local). It is in the recipes section's full tier. download_file({file_id}) returns a user file uploaded to Tandoor. It is in the admin section.
Files up to 5 MiB come back inline, after a text block with {filename, mime, size}. Images (PNG, JPEG, GIF, and WebP) come back as an MCP image block. Other files come back as an embedded resource block. A larger file returns an error that tells the model to retry with save: true.
To save a file to disk instead, set TANDOOR_MCP_DOWNLOAD_ROOT to an existing folder and call the tool with save: true. The tool streams the file into that folder and returns {path, size, mime}. Saving works over stdio only, because over HTTP the folder is on the server machine. The tool removes unsafe characters from the filename. It never overwrites a file: if the name is taken, it adds (2), (3), and so on.
Tandoor compatibility
Minimum: Tandoor server 2.6.0.
The 3.0.0 tools target the endpoints in the vendored Tandoor 2.6.15 OpenAPI spec (see tandoor api specification.yaml, info.version header). Older 2.x instances return 404 on the tools for newer endpoints, such as named shopping lists.
To check your instance version from an LLM session:
> Ask: what version of Tandoor am I connected to?
> Claude calls: get_version
> Returns: { mcp: "3.0.0", protocol: "...", tandoor: "2.6.15" }
The server also probes /api/server-settings/current/ at boot and writes a WARNING to stderr if the connected instance is older than 2.6.0.
Migration from 2.x
3.0.0 removes TANDOOR_MCP_PROFILE and the old tool-group names. The per-section settings in Tool sections replace both.
TANDOOR_MCP_PROFILEis ignored. When it has a value, the server prints one stderr warning.TANDOOR_MCP_PROFILE=full: set every tiered section tofulland every toggle section toon.- Claude Code plugin users: the plugin used to default to
full(every tool). It now starts with the section defaults. To get more tools back, setrecipes,meal_plan,shopping,inventory,foods,cooklog,recipe_books,ai,import_export,household_space, andadminin the plugin settings. - Old group names are gone, with no aliases. Use the section names in
enable_tool_group. The always-on group is nowalways(wascore). The CHANGELOG lists every removed name.
Migration from 1.4.0
- Stdio users: nothing required. No breaking changes, no config edits.
- HTTP users: opt in with
TANDOOR_MCP_TRANSPORT=http. SetTANDOOR_MCP_HTTP_TOKENand expect the jq stash + dynamic gating to be off (both are stdio-session assumptions).
Tool sections
Each section has one env var. The server trims each value and ignores case. An unset, empty, or whitespace-only variable means the saved setting for this TANDOOR_URL if there is one, otherwise the default. If any section variable is invalid, the server stops at startup with one error that names every invalid variable and its valid values.
Tiered sections take off, simple, or full and default to simple. full includes everything in simple.
| Section | Env var | simple |
full adds |
|---|---|---|---|
recipes |
TANDOOR_MCP_RECIPES |
Browse, search, get, related recipes, create, update, import from URL, read keywords. | Delete, steps, ingredients, images, batch update, keyword writes, custom filters, file links, share links. |
meal-plan |
TANDOOR_MCP_MEAL_PLAN |
Meal plan create, read, update, and delete. Read meal types. | Auto-plan, bulk create, meal type writes, iCal export. |
shopping |
TANDOOR_MCP_SHOPPING |
List the named shopping lists. Shopping entry create, read, update, delete, and check. bulk_update_shopping_entries checks entries and adds, removes, sets, or clears their lists in one call. Add a recipe's ingredients or a single food as shopping entries. |
Named shopping list get, create, update, and delete, and their delete previews. Deleting a list removes only its links; the foods, entries, and supermarkets stay. Shopping-list-recipe create, read, update, and delete. Supermarkets, categories, and category relations. |
inventory |
TANDOOR_MCP_INVENTORY |
List locations, list entries, add, consume, and move. add_inventory_item always creates a new entry. consume_inventory_item stops at 0 and warns when you consume more than the entry holds. move_inventory_item moves a whole entry. |
Location create, get, update, and delete. Entry get, update, and delete. The read-only inventory log. Delete previews. Deleting a location also deletes its entries and the log rows of every entry that was ever in it, including entries moved elsewhere since. Deleting an entry deletes its log rows. |
foods |
TANDOOR_MCP_FOODS |
List, get, create, and update foods, including their shopping lists. Parse an ingredient. List units. | Delete, merge, move, FDC lookup and search, batch update (including shopping list changes), unit CRUD, unit merge, food inherit fields, unit conversions, properties, and property types. |
Toggle sections take off or on and default to off.
| Section | Env var | Contents |
|---|---|---|
cooklog |
TANDOOR_MCP_COOKLOG |
Cook-log entries: when and how often a recipe was made. |
recipe-books |
TANDOOR_MCP_RECIPE_BOOKS |
Recipe books (collections) and their entries. |
ai |
TANDOOR_MCP_AI |
AI providers, AI recipe import from images, AI step sort and property estimation, AI logs. |
import-export |
TANDOOR_MCP_IMPORT_EXPORT |
Recipe import and export, import queues, bookmarklet and open-data imports, their logs. |
household-space |
TANDOOR_MCP_HOUSEHOLD_SPACE |
Spaces, memberships, households and household assignment, users, groups, invite links, user preferences. Only the space owner can use the household tools and update_user_space; Tandoor answers space admins with a 403. |
admin |
TANDOOR_MCP_ADMIN |
Server settings, access tokens, storages, syncs, connectors, automations, user files, view logs, search settings, localization. |
A delete preview (preview_*_delete_*) belongs to the same section and tier as its delete tool.
These tools are always on, whatever the section settings: list_tool_groups, enable_tool_group, disable_tool_group, get_version, jq_query, jq_stash_stats, get_guard_stats, and get_current_space. Every tandoor:// resource and every prompt is also always available. list_tool_groups reports them as the always group.
The server applies the settings in this order:
TANDOOR_MCP_INCLUDE_ONLYandTANDOOR_MCP_EXCLUDEfilter the tool list.EXCLUDEis a hard block: nothing can enable an excluded tool.- A section env var that is set and not empty or whitespace-only decides that section.
- For a section whose env var is unset, empty, or whitespace-only, the saved section for this
TANDOOR_URLdecides. - Every other section takes its default.
The result decides which of the remaining tools are visible at startup. An off section is hidden.
In stdio mode the LLM can change sections during a session:
enable_tool_group({group, tier?})widens a section:offtosimpleorfull, orsimpletofull. It never narrows a section. For a tiered section,tierissimpleorfulland defaults tofull. Passingtierfor an on/off section is an error.disable_tool_group({group})sets a section tooff. It cannot disablealways.enable_tool_group({group: "always"})succeeds and changes nothing.
If you see only some tools in Claude Desktop or Codex, ask the model to enable a section, for example "enable the cooklog section". If the new tools do not appear, restart the app. If the change was saved, it applies on the next start.
A section with no registered tools does not count as a section at runtime. That covers any section that INCLUDE_ONLY or EXCLUDE empties. enable_tool_group and disable_tool_group reject it as unknown, and list_tool_groups and the server instructions leave it out.
From an active session, call list_tool_groups for a live listing of sections and their tools.
Saved sections
When enable_tool_group or disable_tool_group changes a section, it saves the change, so the change survives a restart. disable_tool_group saves an explicit "off". The server saves sections per Tandoor instance, keyed by TANDOOR_URL. The key is the URL origin only: the scheme, host, and port, lowercased, with a default port dropped. The server ignores any path, query, or trailing slash, so https://host/tandoor and https://host share one entry. These are the same rules the server applies to TANDOOR_URL, including adding https:// to a URL without a scheme.
The server matches keys in the file by the same rules, so a key you write by hand, such as "https://Tandoor.example.com/tandoor", still matches. A save updates that entry under its key as written.
The file is $XDG_CONFIG_HOME/tandoor-mcp/sections.json. When XDG_CONFIG_HOME is unset or is not an absolute path, the file is ~/.config/tandoor-mcp/sections.json. The path is the same on every OS.
{"https://tandoor.example.com": {"recipes": "full", "meal-plan": "full", "cooklog": "on"}}
Inside each entry, the keys are the section names from the tables above: recipes, meal-plan, shopping, inventory, foods, cooklog, recipe-books, ai, import-export, household-space, and admin. Like the env vars, saved values ignore case and surrounding spaces, so "FULL" works.
- HTTP mode reads saved sections at startup but never changes them.
- A missing file is normal and gives no warning.
- If the file exists but cannot be read, is not valid JSON, has any invalid section value, or has two or more keys that match the same instance, the server ignores the whole file and starts from the env settings. It prints a warning to stderr that names the file.
- If you save a change while the file is invalid, the server replaces the file with a new one that holds only that change.
- If a save succeeds and the section's env var is set, the tool result notes that the env var will override the saved value on restart.
- If saving fails, the change still applies for the current session. The tool result says that the change was not saved.
- The server ignores saved entries for sections that are filtered out or no longer exist, and keeps them in the file.
- The server ignores a key it cannot parse as a URL, and keeps it in the file.
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})
Preview a destructive delete before running it:
You: "Delete the 'Test Keyword' keyword."
Claude:
preview_keyword_delete_protecting({id: <k>})to see what recipes still reference it- If safe:
delete_keyword({id: <k>})- If not safe: reports the reference list back to you before proceeding
Destructive tools
Destructive tools (delete_*, merge_*, switch_active_space, create_access_token, create_invite_link) are irreversible or expose escalation surface. Their descriptions instruct the model to:
- For deletes on tree-shaped resources, call the matching
preview_*_delete_{cascading,nulling,protecting}first. - For merges, verify
numrecipecounts and base-unit compatibility first. - For token / invite creation, warn the user before generating the call. Every such description leads with an explicit ESCALATION SURFACE line.
Operators who want them gone entirely can set TANDOOR_MCP_EXCLUDE="delete_*,merge_*,switch_active_space,create_access_token,create_invite_link". The permission config in Recommended Claude Code permissions makes Claude Code ask before any destructive call.
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 to Sunday meal plan entries, slimmed to {id, date, meal_type, recipe_name, servings, note} |
tandoor://pantry/on-hand |
Inventory entries by location, plus on-hand foods with no entry; at most 500 of each |
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 at most 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 test
Tests 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 |
Contributing
See CONTRIBUTING.md for how pull requests are reviewed, credited, and merged.
Support, security and privacy
- SUPPORT.md: where to ask questions and what to include in a bug report.
- SECURITY.md: supported versions, how to report a vulnerability, and the remote-mode threat model.
- PRIVACY.md: what the server stores, where data goes, and what it logs.
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. - Storage
token/passwordand AI-providerapi_keyare redacted in slim reads.format: "full"still surfaces them for operator inspection. Default (LLM-facing) responses never leak credentials into the model's context. - Keyword payloads are capped at 50 entries and projected to
{id, name}before being forwarded into writes; stops amplification and mass-assignment exposure. - Every mint-a-token / grant-access tool description leads with an ESCALATION SURFACE warning.
create_access_token,update_access_token,create_invite_link,update_invite_link, andauthenticateall warn in-context that the returned token bypasses this MCP's own bearer redaction. - Bearer tokens are redacted from every error message and log line.
- HTTP transport binds to
127.0.0.1by default.TANDOOR_MCP_HTTP_HOSTchanges the address. Each request gets a fresh MCP session, so no session state builds up across requests. - Remote mode uses OAuth with per-user Tandoor tokens. Stored Tandoor tokens are encrypted with
TANDOOR_MCP_SECRET_KEY, issued tokens are stored only as hashes, andHostandOriginheaders are checked. See Remote mode security and SECURITY.md.
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 one exposed to the public internet with anonymous sharing, the hardening above matters.
License
MIT
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi