Skip to main content
Glama
README.md
# mixit-mcp

An [MCP](https://modelcontextprotocol.io) server for [Mixit.cz](https://www.mixit.cz), built with [xmcp](https://xmcp.dev) (TypeScript).
It exposes the ingredients of the two "build your own" mixers:

- **Müsli & granola** – <https://www.mixit.cz/namixuj-si/musli>
- **Porridge (kaše)** – <https://www.mixit.cz/namixuj-si/kase>

For every ingredient you get its description, composition (složení), allergens, price, portion weight, properties (gluten-free, vegan, …) and nutrition values, grouped by category.

The server runs over **stdio** and **HTTP** (Streamable HTTP) and is shipped as a single Docker image that supports both.

## Tools

| Tool                       | Description                              |
| -------------------------- | ---------------------------------------- |
| `get-musli-ingredients`    | Ingredients of the müsli & granola mixer |
| `get-porridge-ingredients` | Ingredients of the porridge (kaše) mixer |

Both tools take the same optional parameters:

| Parameter        | Type       | Description                                                                                                          |
| ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------------- |
| `categories`     | `string[]` | Categories to return. Omit for all. See the table below.                                                             |
| `includeDetails` | `boolean`  | Include composition, legislative name and allergens (default `true`). Set to `false` for a faster, lighter response. |

### Categories

| Key              | Müsli tab on the site  | Porridge tab on the site |
| ---------------- | ---------------------- | ------------------------ |
| `base`           | Müsli základ (18)      | Základ kaše (8)          |
| `base_extension` | Rozšíření základu (16) | Rozšíření základu (16)   |
| `crunchy_fruit`  | Křupavé ovoce (15)     | Křupavé ovoce (18)       |
| `dried_fruit`    | Sušené ovoce (8)       | Sušené ovoce (8)         |
| `nuts`           | Ořechy (13)            | Ořechy (14)              |
| `choco_and_more` | Čoko & spol. (28)      | Čoko & spol. (26)        |

The counts are the ones shown on the website at the time of writing. They can change when Mixit changes its offer.

### Response

The response is returned as `structuredContent` (and as JSON text for clients that do not support structured content):

```jsonc
{
  "kind": "musli",
  "title": "Müsli a granola",
  "source": "https://www.mixit.cz/namixuj-si/musli",
  "totalCount": 98,
  "notes": ["..."],
  "categories": [
    {
      "key": "base",
      "id": "musli_base",
      "label": "Müsli základ",
      "count": 18,
      "ingredients": [
        {
          "id": 31,
          "name": "Ovesná klasika Dr. Birchera",
          "permalink": "ovesna-klasika-dr-birchera",
          "description": "Švýcarský lékař Maxmilian Bircher-Benner dal světu müsli. ...",
          "isBase": true,
          "available": true,
          "portionWeightG": 625,
          "price": 69,
          "priceWithoutDiscount": 69,
          "hasDiscount": false,
          "pricePer100g": 11.04,
          "currency": "Kč",
          "nutritionPer100g": {
            "energyKj": 1539,
            "energyKcal": 368,
            "fatG": 7,
            "saturatedFatG": 1.2,
            "carbohydratesG": 65,
            "sugarsG": 0.5,
            "fibreG": 11,
            "proteinsG": 16,
            "saltG": 0,
          },
          "properties": ["Bez přídavku cukru", "Vhodné pro vegany", "Neobsahuje laktózu"],
          "imageUrl": "https://img.mix.it/ingredient/.../jpeg_lg-....jpg",
          "legislativeName": "Směs ovesných vloček",
          "composition": "Ovesná klasika Dr. Birchera (**ovesné** vločky 60 %, **ovesné** lupínky 40 %)",
          "containsAllergens": ["ovesné"],
          "allergens": "Může obsahovat stopy mléka, skořápkových plodů, arašídů a sóji.",
        },
      ],
    },
  ],
}
```

Notes on the data:

- `nutritionPer100g` is per 100 g of the ingredient. `energyKcal` is computed from `energyKj` (kJ / 4.185), the same way the website does it.
- `price` is the price of one portion (`portionWeightG` grams) in `currency`. Bases are sold as a whole pack (the mixer allows exactly one base), other ingredients are sold in small portions (e.g. 40 g).
- In `composition`, allergens are marked with `**bold**` (Mixit highlights them with `<em>`). `containsAllergens` is the de-duplicated list of those highlighted words (e.g. `["mléko", "kešu"]`); it is not an official EU-14 allergen classification, just what Mixit highlights.
- `allergens` is the "may contain traces of" statement. It can be `null` even when `containsAllergens` is not empty, so always check both.
- `composition`, `containsAllergens`, `allergens` and `legislativeName` are only present when `includeDetails` is not `false`.
- Text fields are converted from HTML to plain text.

## Run with Docker

The image contains both transports. The first argument selects which one is started (`http` is the default).

```bash
docker build -t mixit-mcp .
```

### HTTP

```bash
docker run --rm -p 3001:3001 mixit-mcp          # same as: docker run --rm -p 3001:3001 mixit-mcp http
```

The MCP endpoint is then `http://localhost:3001/mcp`. To use a different host port, change the left side of the mapping (e.g. `-p 8080:3001`).

Or with Docker Compose:

```bash
docker compose up -d
```

### stdio

stdio needs an interactive container (`-i`), no port mapping and no TTY:

```bash
docker run -i --rm mixit-mcp stdio
```

## Connect a client

### stdio (Docker)

Claude Desktop, Cursor and similar clients (`mcpServers` config):

```json
{
  "mcpServers": {
    "mixit": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "mixit-mcp", "stdio"]
    }
  }
}
```

### HTTP

Clients with native Streamable HTTP support:

```json
{
  "mcpServers": {
    "mixit": {
      "url": "http://localhost:3001/mcp"
    }
  }
}
```

For clients that only speak stdio you can bridge the HTTP server with `npx mcp-remote http://localhost:3001/mcp`.

### Quick check with curl

```bash
curl -s -X POST http://localhost:3001/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get-porridge-ingredients","arguments":{"categories":["base"]}}}'
```

You can also explore the server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): `npx @modelcontextprotocol/inspector`.

## Configuration

| Environment variable      | Default                         | Description                                                         |
| ------------------------- | ------------------------------- | ------------------------------------------------------------------- |
| `MIXIT_CACHE_TTL_SECONDS` | `3600`                          | How long ingredient lists and product details are cached in memory. |
| `MIXIT_API_URL`           | `https://admin.mixit.cz/api/v1` | Base URL of the Mixit API.                                          |

The HTTP host/port/endpoint (`0.0.0.0:3001/mcp`) and the server info are set at build time in [`xmcp.config.ts`](xmcp.config.ts). Inside the container the server always listens on port 3001; remap it with `-p`.

## Local development

Requires Node.js 22+.

```bash
npm install
npm run dev          # xmcp dev server with reload
npm run typecheck
npm run lint         # eslint
npm run format       # prettier --write
npm run build        # builds dist/http.js and dist/stdio.js
npm run start:http   # node dist/http.js
npm run start:stdio  # node dist/stdio.js
```

Project layout:

```
src/
  lib/mixit.ts                      API client, caching, HTML cleanup, mapping
  tools/get-musli-ingredients.ts    tool definition (müsli)
  tools/get-porridge-ingredients.ts tool definition (kaše)
xmcp.config.ts                      transports + server info
Dockerfile, docker-entrypoint.sh    image supporting both transports
```

## How it works

The Mixit website is a single page app, so its HTML contains no data. While inspecting it in the browser I found that it talks to a public JSON API (no authentication needed):

| Purpose                                                | Request                                                                                     |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| Ingredients of a mixer, with description and nutrition | `GET https://admin.mixit.cz/api/v1/mix_ingredients?kind={musli\|porridge}&categories=<ids>` |
| Composition, legislative name, allergens               | `GET https://admin.mixit.cz/api/v1/products/{permalink}`                                    |

The `kind` parameter matters: the porridge mixer has a different base and slightly different lists of crunchy fruit, nuts and extras than the müsli mixer.

The server calls the list endpoint once and then fetches the product detail of every ingredient (8 requests in parallel). Results are cached in memory, so repeated calls are fast (first call ≈ 1–2 s, cached ≈ ms). If a product detail fails to load, the ingredient is still returned, with `composition: null`.

This is an unofficial integration that relies on undocumented endpoints, which Mixit may change at any time. Please be considerate and do not use it for high-volume scraping.

## License

The source code of this project is released under the [MIT License](LICENSE). The license covers only the code in this repository. The data returned by the server (ingredient names, descriptions, images, prices, nutrition values, etc.) belongs to Mixit.cz and is not covered by it. This project is not affiliated with or endorsed by Mixit.