MCP Server
KitchenOwl includes a Model Context Protocol (MCP) server. It lets MCP clients and AI assistants (Claude, ChatGPT, Cursor, Open WebUI, …) read from and write to your KitchenOwl instance: shopping lists, items, recipes, tags, the meal plan and expenses.
The MCP endpoint lives next to the REST API and uses the same accounts: everything an MCP client does is limited to the households the authenticated user belongs to.
Enabling the MCP server
The MCP endpoint is disabled by default. Set KITCHENOWL_MCP_ENABLED=true on the backend container:
services:
back:
image: tombursch/kitchenowl-backend:latest
environment:
- JWT_SECRET_KEY=PLEASE_CHANGE_ME
- KITCHENOWL_MCP_ENABLED=true
The server is then reachable at https://<your-kitchenowl-instance>/mcp. If you run the standard setup with the tombursch/kitchenowl-web frontend container, /mcp is already proxied to the backend with buffering disabled, so no reverse-proxy changes are needed. When you host the backend behind your own proxy, make sure streaming responses are not buffered (see Reverse Proxy).
Authentication
The MCP server authenticates requests with a regular KitchenOwl JWT access token, exactly like the REST API. Obtain a token by logging in:
curl -s -X POST https://your-kitchenowl.example.org/api/auth \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secret"}' | jq -r .access_token
and send it as a bearer token with every MCP request:
Authorization: Bearer <access_token>
Warning
The token grants full write access to all your households. Only give it to clients you trust, and log out the session (or rotate JWT_SECRET_KEY) if you no longer need it. Access tokens expire together with the refresh token lifetime (JWT_REFRESH_TOKEN_EXPIRES, default 30 days).
Transports & protocol versions
Two HTTP transports are implemented:
- Streamable HTTP (recommended): send JSON-RPC requests to
POST /mcp. The endpoint is stateless, so it works reliably behind load-balanced or multi-worker setups.GETandDELETEon/mcpreturn405, which the spec allows. - HTTP+SSE (legacy): open
GET /mcp/sseto receive a sessionendpointevent, then send JSON-RPC requests toPOST /mcp/messages?session_id=…; replies arrive over the SSE stream (with keep-alive comments every 15 seconds). Sessions live in a single worker process, so prefer the streamable transport wherever possible.
Supported protocol versions: 2025-06-18, 2025-03-26 and 2024-11-05. An initialize request with a different (or missing) version is answered with the latest supported version.
Trying it out with curl
Initialize
TOKEN=$(curl -s -X POST https://your-kitchenowl.example.org/api/auth \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secret"}' | jq -r .access_token)
curl -s -X POST https://your-kitchenowl.example.org/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "0.0.1" }
}
}' | jq
List tools
curl -s -X POST https://your-kitchenowl.example.org/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}' | jq
Call a tool
# Nearly every tool is household-scoped, so start by listing households
curl -s -X POST https://your-kitchenowl.example.org/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": { "name": "list_households", "arguments": {} }
}' | jq
Connecting an MCP client
Clients that speak MCP over HTTP (Open WebUI, Continue, …) can be pointed directly at https://<your-kitchenowl-instance>/mcp with the Authorization: Bearer <access_token> header.
Clients that only support stdio servers (such as Claude Desktop) can use mcp-remote as a bridge:
{
"mcpServers": {
"kitchenowl": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://your-kitchenowl.example.org/mcp",
"--header",
"Authorization: Bearer <access_token>"
]
}
}
}
Implemented tools
All tools return JSON, both as readable text content and as structuredContent. Required arguments are listed first; optional ones in parentheses.
Households
| Tool | Arguments | Description |
|---|---|---|
list_households |
– | List all households the authenticated user is a member of. Start here: the returned household_id is needed by almost every other tool. |
Shopping lists
| Tool | Arguments | Description |
|---|---|---|
list_shoppinglists |
household_id |
List all shopping lists of a household. |
list_shoppinglist_items |
list_id |
List the items currently on a shopping list. |
create_shoppinglist |
household_id, name |
Create a new shopping list. |
delete_shoppinglist |
list_id |
Delete a shopping list. The default list cannot be deleted. |
add_item_by_name |
list_id, name (description) |
Add an item to a shopping list by name, creating the item if it does not exist yet. |
remove_item_from_list |
list_id (item_id or name) |
Remove an item from a shopping list by id or name. |
Items
| Tool | Arguments | Description |
|---|---|---|
list_items |
household_id (search) |
List household items, optionally filtered by a name search. Returns at most 100 items. |
Recipes
| Tool | Arguments | Description |
|---|---|---|
list_recipes |
household_id (offset, limit, additional_fields) |
List recipes as paginated id/name references. See Behavior notes. |
search_recipes |
household_id, query (offset, limit, additional_fields) |
Search recipe names (case-insensitive substring match), paginated like list_recipes. |
get_recipe |
recipe_id |
Get one full recipe, including description, ingredients and tags. |
create_recipe |
household_id, name (description, time, cook_time, prep_time, yields, source, visibility, items, tags) |
Create a recipe. items accepts plain names or {name, description, optional} objects; tags a list of tag names. Missing items and tags are created automatically. |
update_recipe |
recipe_id (name, description, time, cook_time, prep_time, yields, source, visibility) |
Update the given fields of a recipe. Times are in minutes, visibility is 0 (private), 1 (link) or 2 (public). |
delete_recipe |
recipe_id |
Delete a recipe. |
add_recipe_item |
recipe_id, name (description, optional) |
Add (or update) an ingredient on a recipe, creating the item if needed. |
remove_recipe_item |
recipe_id, item_id |
Remove an ingredient from a recipe. |
add_recipe_tag |
recipe_id, name |
Add a tag to a recipe, creating the tag if needed. |
remove_recipe_tag |
recipe_id (tag_id or name) |
Remove a tag from a recipe. |
scrape_recipe |
household_id, url |
Scrape a recipe from a website and return it (without saving). Only supported websites work; anything else returns an "Unsupported website" error. |
Tags
| Tool | Arguments | Description |
|---|---|---|
list_tags |
household_id |
List all tags of a household. |
create_tag |
household_id, name |
Create a tag (returns the existing tag if the name is already used). |
Meal plan
| Tool | Arguments | Description |
|---|---|---|
list_planner |
household_id |
List all meal plan entries of a household. |
add_planner_entry |
household_id, recipe_id, cooking_date (yields) |
Plan a recipe for a day. cooking_date is an ISO 8601 datetime, e.g. 2026-11-08T00:00:00Z. |
remove_planner_entry |
household_id, recipe_id, cooking_date |
Remove a meal plan entry matching recipe and date. |
Expenses
| Tool | Arguments | Description |
|---|---|---|
list_expenses |
household_id (search) |
List the most recent expenses (up to 50), optionally filtered by name. |
create_expense |
household_id, name, amount (description, date) |
Record an expense paid by the authenticated user. date is an ISO 8601 datetime and defaults to now. |
delete_expense |
expense_id |
Delete an expense. |
Behavior notes
- Household scoping: every mutation and read is checked against the households of the authenticated user. Requests for households you do not belong to fail with a not-found error, as do resources of other households.
- Recipe discovery:
list_recipesandsearch_recipesreturn onlyidandnameby default to keep responses small. Passadditional_fieldsto request more data in bulk:description,photo,photo_hash,time,cook_time,prep_time,yields,source,visibility,created_at,updated_at,planned,planned_days,planned_cooking_dates,itemsandtags. Pagination usesoffset/limit(default 50, maximum 100); the response containstotalandnext_offset(nullwhen the last page was returned). Useget_recipefor one complete recipe. - Idempotency: adding an item, tag or planner entry that already exists does not duplicate it; the existing entry is kept or updated.
- Errors are reported as JSON-RPC errors (invalid arguments, unknown tool) or as a tool result with a reason, e.g.
{ "removed": false, "reason": "not_found" }or{ "deleted": false, "reason": "default_list" }.