Skip to main content
Glama
lukegskw

kitchenowl-insights-mcp

README.md
# KitchenOwl Insights MCP

[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![CI](https://github.com/lukegskw/kitchenowl-insights-mcp/actions/workflows/container.yml/badge.svg)](https://github.com/lukegskw/kitchenowl-insights-mcp/actions/workflows/container.yml)
[![Container](https://img.shields.io/badge/GHCR-amd64%20%7C%20arm64-2496ED?logo=docker&logoColor=white)](https://github.com/lukegskw/kitchenowl-insights-mcp/pkgs/container/kitchenowl-insights-mcp)

**KitchenOwl Insights MCP** is a standalone, read-only
[Model Context Protocol](https://modelcontextprotocol.io/) server that estimates which
ingredients may still be available in a KitchenOwl household and ranks recipes already
saved in KitchenOwl.

It uses strict validation, stdio and Streamable HTTP transports, a hardened container,
and automated GHCR publication. It runs alongside KitchenOwl without modifying or
forking it, and every estimate includes its supporting evidence.

## Navigation

- [Use this server](#use-this-server)
- [About](#about)
- [Features](#features)
- [MCP tools](#mcp-tools)
- [How estimates work](#how-estimates-work)
- [Tech stack](#tech-stack)
- [Installation](#installation)
- [Configuration](#configuration)
- [MCP client setup](#mcp-client-setup)
- [Architecture and development](#architecture-and-development)
- [Verification](#verification)
- [Limitations](#limitations)
- [Contributing](#contributing)
- [License](#license)

## Use this server

Use the published container image for a standard HTTP deployment, or clone the
repository for local stdio use and development. Both workflows are documented in
[Installation](#installation).

Fork this repository when you want to propose changes through a pull request. See
[Contributing](#contributing) before submitting changes.

If this server is useful to you, consider giving the repository a star. It helps other
KitchenOwl users discover the integration.

## About

KitchenOwl records when items are added to and removed from shopping lists, but it does
not maintain a confirmed household inventory. This server uses that history to produce
evidence-based estimates without writing back to KitchenOwl.

The TypeScript foundation is based on the
[MCP TypeScript Starter](https://github.com/lukegskw/mcp-typescript-starter), adapted
with KitchenOwl-specific database access, domain logic, tools, and tests.

The default stdio transport is intended for local clients that launch the server as a
child process. Streamable HTTP is stateless and creates a fresh MCP server for each
request while sharing the read-only application context, so it can be replicated
without MCP session storage.

The server reads one existing SQLite or PostgreSQL KitchenOwl database. It does not
perform migrations, persist its own data, modify KitchenOwl records, provide telemetry,
or authenticate HTTP callers.

## Features

- Registers tools with strict Zod input and output schemas.
- Returns both human-readable content and typed structured content.
- Includes accurate read-only MCP safety annotations.
- Supports stdio and stateless Streamable HTTP.
- Connects through read-only SQLite or PostgreSQL sessions.
- Validates the required KitchenOwl schema before accepting MCP traffic.
- Reconstructs item-consumption cycles from shopping-list history.
- Reports probability, confidence, evidence, and warnings for every estimate.
- Ranks recipes already saved in the selected KitchenOwl household.
- Associates recipes and inventory by KitchenOwl item ID rather than item name.
- Uses Hono with Host and Origin validation against DNS rebinding.
- Limits tool inputs and HTTP request bodies.
- Keeps stdout exclusive to MCP protocol messages in stdio mode.
- Handles SIGINT and SIGTERM with idempotent graceful shutdown.
- Runs as a non-root container with read-only-root-filesystem support.
- Tests configuration, database behavior, MCP behavior, Hono routes, and real traffic.
- Publishes multi-architecture images only after quality checks pass.

## MCP tools

### `get_inventory_estimate`

Returns estimated inventory states for a household, optionally filtered by shopping
list or item IDs.

Example input:

```json
{
  "household_id": 1,
  "list_id": 1,
  "item_ids": [225, 469],
  "include_unknown": true
}
```

Example structured output:

```json
{
  "household_id": 1,
  "as_of": "2026-07-19T18:00:00Z",
  "items": [
    {
      "item_id": 225,
      "name": "Eggs",
      "state": "uncertain",
      "availability_probability": 0.5,
      "confidence": "low",
      "typical_duration_days": 10,
      "completed_cycles": 2,
      "currently_on_list": false,
      "last_dropped_at": "2026-07-15T12:00:00Z",
      "evidence": [
        {
          "kind": "legacy_drop",
          "occurred_at": "2026-07-15T12:00:00Z",
          "interpretation": "Removed from the shopping list; possible purchase"
        }
      ],
      "warnings": [
        "KitchenOwl does not distinguish a purchase from deletion in legacy history"
      ]
    }
  ]
}
```

Possible states are `probably_available`, `uncertain`, `probably_missing`, and
`unknown`.

### `recommend_available_recipes`

Ranks recipes saved in a household according to estimated ingredient availability.

Example input:

```json
{
  "household_id": 1,
  "list_id": 1,
  "top_k": 5,
  "include_optional": false
}
```

Example structured output:

```json
{
  "household_id": 1,
  "as_of": "2026-07-19T18:00:00Z",
  "recommendations": [
    {
      "recipe_id": 100,
      "name": "Omelette",
      "score": 0.75,
      "classification": "possible_with_uncertainty",
      "likely_available": ["Eggs"],
      "uncertain": ["Cheese"],
      "likely_missing": [],
      "unknown": [],
      "ignored_optional": ["Parsley"],
      "evidence": {
        "Eggs": "Removed from the shopping list; possible purchase"
      },
      "warnings": [
        "Estimated availability; it does not represent food safety or expiry."
      ]
    }
  ]
}
```

Possible classifications are `probably_possible`, `possible_with_uncertainty`,
`probably_missing_ingredients`, and `insufficient_data`.

## How estimates work

- `ADDED` means an item entered a shopping list and likely needs replenishment.
- `DROPPED` means it left a shopping list and may have been purchased, deleted, or
  corrected.
- A complete consumption cycle starts at `DROPPED` and ends at the next `ADDED` event
  for the same item.
- Typical duration is the median of complete cycles.
- One or two complete cycles produce low confidence; three or more produce medium
  confidence.
- An item currently on a shopping list is treated as probably missing.

Recipe scores use these weights:

| Ingredient state     | Weight |
| -------------------- | -----: |
| `probably_available` |   1.00 |
| `uncertain`          |   0.50 |
| `unknown`            |   0.25 |
| `probably_missing`   |   0.00 |

Optional ingredients are excluded by default.

## Tech stack

- [Node.js 24+](https://nodejs.org/)
- [TypeScript](https://www.typescriptlang.org/) with strict project rules
- [Model Context Protocol TypeScript SDK 2](https://github.com/modelcontextprotocol/typescript-sdk)
- [Hono](https://hono.dev/)
- [Zod](https://zod.dev/)
- [Kysely](https://kysely.dev/) with `better-sqlite3` and `pg`
- [Vitest](https://vitest.dev/)
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/)

## Installation

### Prerequisites

- A running KitchenOwl installation.
- Read-only access to its SQLite or PostgreSQL database.
- Node.js 24+ and pnpm 11 for local development.
- Docker and Docker Compose for container deployment.

### Docker Compose

The recommended HTTP deployment uses the published multi-architecture image:

```text
ghcr.io/lukegskw/kitchenowl-insights-mcp:latest
```

Download the Compose example and provide the database path and hostname clients will
use:

```sh
curl -O https://raw.githubusercontent.com/lukegskw/kitchenowl-insights-mcp/main/compose.example.yaml
export KITCHENOWL_DATABASE_PATH=/path/to/kitchenowl/database.db
export KITCHENOWL_INSIGHTS_ALLOWED_HOSTS='mcp.example.internal'
docker compose -f compose.example.yaml up -d
```

The Streamable HTTP and health endpoints will be available at:

```text
http://<host>:8099/mcp
http://<host>:8099/healthz
```

The database file must be readable by UID/GID `10001:10001`. To use another identity
that already has read access, set `KITCHENOWL_INSIGHTS_UID_GID`. To publish a different
host port, set `KITCHENOWL_INSIGHTS_PUBLISHED_PORT`; the application still uses port
`8099` inside the container.

The `latest` tag follows the newest successful build from the default branch. Use a
version or immutable `sha-*` tag for controlled deployment and rollback.

### Docker run

```sh
docker run -d \
  --name kitchenowl-insights \
  --restart unless-stopped \
  --read-only \
  --user 10001:10001 \
  --cap-drop ALL \
  --security-opt no-new-privileges:true \
  --tmpfs /tmp:size=16m,mode=1777 \
  -v /path/to/kitchenowl/database.db:/kitchenowl/database.db:ro \
  -e 'KITCHENOWL_INSIGHTS_DATABASE_URL=sqlite+pysqlite:///file:/kitchenowl/database.db?mode=ro&uri=true' \
  -e KITCHENOWL_INSIGHTS_TRANSPORT=streamable-http \
  -e KITCHENOWL_INSIGHTS_HOST=0.0.0.0 \
  -e KITCHENOWL_INSIGHTS_ALLOWED_HOSTS=127.0.0.1,localhost,mcp.example.internal \
  -p 8099:8099 \
  ghcr.io/lukegskw/kitchenowl-insights-mcp:latest
```

### Build the container from source

```sh
git clone https://github.com/lukegskw/kitchenowl-insights-mcp.git
cd kitchenowl-insights-mcp
docker buildx build --load -t kitchenowl-insights-mcp:local .
```

### Local Node.js installation

```sh
git clone https://github.com/lukegskw/kitchenowl-insights-mcp.git
cd kitchenowl-insights-mcp
pnpm install --frozen-lockfile
pnpm build
export KITCHENOWL_INSIGHTS_DATABASE_URL='sqlite+pysqlite:///file:/path/to/database.db?mode=ro&uri=true'
pnpm start -- --transport stdio
```

For local Streamable HTTP development:

```sh
KITCHENOWL_INSIGHTS_DATABASE_URL='sqlite:////path/to/database.db' \
KITCHENOWL_INSIGHTS_TRANSPORT=streamable-http \
KITCHENOWL_INSIGHTS_HOST=127.0.0.1 \
pnpm dev
```

### PostgreSQL

Use a dedicated PostgreSQL role with only `CONNECT`, schema `USAGE`, and table `SELECT`
permissions, and set `default_transaction_read_only=on`. Both native and existing
SQLAlchemy-style URLs are accepted:

```text
postgresql://<user>:<password>@<host>:5432/<database>
postgresql+psycopg://<user>:<password>@<host>:5432/<database>
```

Do not reuse an administrative KitchenOwl credential. The standard image contains both
database drivers; no alternative image is required.

## Configuration

All settings use the `KITCHENOWL_INSIGHTS_` prefix. A local `.env` file is loaded when
present.

| Variable                                 | Required      | Default   | Description                                      |
| ---------------------------------------- | ------------- | --------- | ------------------------------------------------ |
| `KITCHENOWL_INSIGHTS_DATABASE_URL`       | Yes           | None      | SQLite or PostgreSQL database URL.               |
| `KITCHENOWL_INSIGHTS_TRANSPORT`          | No            | `stdio`   | `stdio` or `streamable-http`.                    |
| `KITCHENOWL_INSIGHTS_HOST`               | No            | `0.0.0.0` | HTTP bind address.                               |
| `KITCHENOWL_INSIGHTS_PORT`               | No            | `8099`    | HTTP listening port.                             |
| `KITCHENOWL_INSIGHTS_ALLOWED_HOSTS`      | External HTTP | None      | Comma-separated Host and Origin hostname list.   |
| `KITCHENOWL_INSIGHTS_LOG_LEVEL`          | No            | `INFO`    | Application log-level contract.                  |
| `KITCHENOWL_INSIGHTS_MAX_HISTORY_EVENTS` | No            | `5000`    | Maximum history events loaded per request.       |
| `KITCHENOWL_INSIGHTS_DEFAULT_TOP_K`      | No            | `5`       | Default number of recommendations.               |
| `KITCHENOWL_INSIGHTS_MAX_TOP_K`          | No            | `20`      | Maximum allowed recommendations.                 |
| `KITCHENOWL_INSIGHTS_NOW_OVERRIDE`       | No            | None      | Deterministic clock override intended for tests. |

The `--transport` command-line option overrides `KITCHENOWL_INSIGHTS_TRANSPORT`.
`KITCHENOWL_INSIGHTS_ALLOWED_HOSTS` contains hostnames, not URLs; include every hostname
legitimate clients and health checks use.

The database URL is a secret. Supply it through the deployment platform or environment,
never as an MCP tool argument or committed file.

## MCP client setup

For a client that accepts Streamable HTTP server definitions:

```yaml
mcp_servers:
  kitchenowl_insights:
    url: http://127.0.0.1:8099/mcp
```

For a client that launches a local stdio server:

```json
{
  "mcpServers": {
    "kitchenowl_insights": {
      "command": "node",
      "args": [
        "/absolute/path/to/kitchenowl-insights-mcp/dist/main.js",
        "--transport",
        "stdio"
      ],
      "env": {
        "KITCHENOWL_INSIGHTS_DATABASE_URL": "sqlite:////absolute/path/to/database.db"
      }
    }
  }
}
```

To let a local client launch the container over stdio, use `docker run -i --rm`, mount
the database read-only, supply `KITCHENOWL_INSIGHTS_DATABASE_URL`, and pass `--transport
stdio` after the image name. `-i` is required so the client can exchange MCP messages
through standard input and output.

Client configuration formats differ. Consult the client's documentation for its exact
schema and restart or reload the client after changing its server definition.

## Architecture and development

The main extension points remain intentionally direct:

1. Define domain models and output contracts in [`src/models/index.ts`](src/models/index.ts).
2. Keep database access in [`src/repository/index.ts`](src/repository/index.ts).
3. Coordinate domain behavior in [`src/service/index.ts`](src/service/index.ts).
4. Define strict input and output schemas in [`src/tools/index.ts`](src/tools/index.ts).
5. Register tool behavior through [`src/server.ts`](src/server.ts).
6. Add MCP behavior tests and database integration tests for every visible change.

The application creates one read-only database context and service at startup. Stdio
uses one MCP server for the process; stateless HTTP creates request-scoped MCP servers
that share the application context. Transport modules remain independent from domain
logic, and shutdown closes transports before the database context.

## Verification

Run the complete repository suite:

```sh
pnpm install --frozen-lockfile
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test:unit
pnpm test:integration
pnpm build
```

The PostgreSQL integration test runs when `KITCHENOWL_TEST_POSTGRES_URL` points to a
disposable database named `kitchenowl_insights_test`; CI supplies this automatically.

Verify schema compatibility and write protection against a database:

```sh
pnpm verify:read-only \
  'sqlite+pysqlite:///file:/path/to/database.db?mode=ro&uri=true'
```

For container changes:

```sh
docker buildx build --load -t kitchenowl-insights-mcp:test .
```

Finally, connect an MCP client and confirm that both tools are listed and return text
and structured content. In HTTP mode, confirm `/healthz` reports `{"status":"ok"}`.

## Limitations

- KitchenOwl legacy history does not distinguish purchases from deletions.
- Estimates do not account for free-text quantities, freshness, expiry, or food safety.
- Only recipes already saved in KitchenOwl are ranked.
- Sparse history produces low confidence or unknown states.
- The server fails closed when required KitchenOwl tables or columns are missing.
- A live SQLite database in WAL mode may require access to its sidecar files. Use a
  consistent read-only snapshot if live reads are unstable.
- Streamable HTTP has no authentication. Restrict it to loopback, a trusted LAN, a VPN,
  a private container network, or an authenticated reverse proxy.
- Host and Origin allowlists prevent classes of DNS rebinding attacks but do not
  authenticate callers.
- The HTTP transport is stateless and contains no MCP session storage.
- Rate limiting, tracing, and metrics are not included.

Review [SECURITY.md](SECURITY.md) before exposing the HTTP transport or reporting a
security issue.

## Contributing

Contributions are welcome. Before opening a pull request:

```sh
pnpm install --frozen-lockfile
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm build
docker buildx build --load -t kitchenowl-insights-mcp:test .
```

Changes must preserve strict typing, bounded validation, structured MCP results, stdout
protocol purity, secure HTTP defaults, deterministic tests, household isolation,
database URL redaction, SQLite and PostgreSQL support, layered read-only enforcement,
and documentation for user-visible behavior. Do not add abstractions without a concrete
use case for them.

## License

This repository does not currently declare a software license.