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

Ask Claude (or any MCP client) what is in Swedish food, using the Swedish Food Agency's official nutrition database.

This is an MCP server for the [Livsmedelsdatabasen API](https://www.livsmedelsverket.se/om-oss/psidata/livsmedelsdatabasen) from Livsmedelsverket. It covers about 2,600 foods and dishes with 50+ nutrients each. You can search foods by name, read nutrient values, compare foods and work out the nutrition of a recipe per serving.

**Unofficial.** This project is not made by, affiliated with or endorsed by Livsmedelsverket. The data is theirs, published under CC BY 4.0.

## Tools

| Tool | What it does |
| --- | --- |
| `search_foods` | Find foods by name (English or Swedish), filter to lab-analysed foods or calculated dishes, paginate with a cursor. |
| `get_food` | One food: type, cooking method, a nutrition summary per 100 g, and optionally classifications, recipe ingredients and raw materials. |
| `get_nutrients` | All ~60 nutrient values for a food, or only the ones you ask for by code (`VITD`) or name (`iron`). |
| `compare_foods` | 2 to 6 foods side by side on the same nutrients. |
| `calculate_recipe_nutrition` | Total, per serving and per 100 g nutrition for a list of foods and gram amounts. |

There is also one prompt, `nutrition_label`, which turns a recipe in plain text into an EU-style nutrition declaration by chaining the tools above.

All tools are read-only. No account or API key is needed.

## Install

Requires Node.js 20 or newer.

### Claude Code

```sh
claude mcp add livsmedel -- npx -y @rickardlind/livsmedel-mcp
```

### Claude Desktop

Add this to `claude_desktop_config.json` (Settings > Developer > Edit Config) and restart Claude Desktop:

```json
{
  "mcpServers": {
    "livsmedel": {
      "command": "npx",
      "args": ["-y", "@rickardlind/livsmedel-mcp"],
      "env": {
        "LIVSMEDEL_LANGUAGE": "en"
      }
    }
  }
}
```

### Cursor

Add this to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in a project:

```json
{
  "mcpServers": {
    "livsmedel": {
      "command": "npx",
      "args": ["-y", "@rickardlind/livsmedel-mcp"]
    }
  }
}
```

### From source

```sh
git clone https://github.com/Rapitzo/livsmedel-mcp.git
cd livsmedel-mcp
npm install
npm run build
node dist/index.js   # speaks MCP over stdio
```

Point your client at `node /absolute/path/to/livsmedel-mcp/dist/index.js` instead of `npx`.

## Configuration

Everything is optional. Empty values count as unset.

| Variable | Default | Purpose |
| --- | --- | --- |
| `LIVSMEDEL_LANGUAGE` | `en` | Default language for names: `en` or `sv`. Each tool call can override it. |
| `LIVSMEDEL_BASE_URL` | `https://dataportal.livsmedelsverket.se/livsmedel/api/v1` | Point at a proxy, a mirror or a mock server. |
| `LIVSMEDEL_API_KEY` | unset | Sent on every request if set. The public API does not need one. |
| `LIVSMEDEL_API_KEY_HEADER` | `x-api-key` | Header for the key. If set to `Authorization`, the key is sent as `Bearer <key>`. |
| `LIVSMEDEL_TIMEOUT_MS` | `15000` | Per-request timeout. |
| `LIVSMEDEL_MAX_RETRIES` | `3` | Retries for 408, 425, 429, 5xx (except 501), timeouts and network errors. |
| `LIVSMEDEL_CACHE_TTL_SECONDS` | `21600` | How long responses are cached in memory. `0` turns caching off. |
| `LIVSMEDEL_DEBUG` | unset | `1` logs each request (method, path, status, timing) to stderr. Keys are never logged. |

The public Livsmedelsverket API is open, so the key settings do nothing against it. They are there because most real APIs need auth, and this is the pattern I use for those: the key comes from the environment, goes into one header, and never appears in logs or tool output. If you run the server behind a gateway that wants a key, this is how you pass it.

## Example prompts

- "How much vitamin D is in fortified oat drink compared with regular milk?"
- "I make pancakes with 180 g wheat flour, 6 dl milk, 3 eggs and 30 g butter. Four servings. What is the nutrition per serving?"
- "Which Swedish sausages in the database have the least salt? Start with falukorv."
- "Use the nutrition_label prompt for my kanelbulle recipe."

## Example output

This is the real response to the pancake recipe above (flour 1941, milk 123, egg 1225, butter 29), recorded against the live API. Three of the nine nutrients are shown here. The full transcript of every tool, including two error cases, is in [docs/example-session.md](docs/example-session.md).

```json
{
  "total_weight_g": 960,
  "servings": 4,
  "nutrients": [
    { "code": "ENERC_KCAL", "name": "Energy (kcal)", "unit": "kcal", "total": 1427.4, "per_serving": 356.85, "per_100g": 148.69 },
    { "code": "PROT", "name": "Protein", "unit": "g", "total": 55.23, "per_serving": 13.81, "per_100g": 5.75 },
    { "code": "NACL", "name": "Salt, NaCl", "unit": "g", "total": 1.41, "per_serving": 0.35, "per_100g": 0.15 }
  ],
  "note": "Sums stored per-100 g values. Water loss and nutrient losses from cooking are not modelled.",
  "source": "Livsmedelsverket, Livsmedelsdatabasen (CC BY 4.0)"
}
```

## Design notes

### Tool boundaries

The API has one list endpoint and five per-food endpoints (food, nutrients, classifications, ingredients, raw materials). Mapping those one to one would give the model six tools, and it would still have to make several calls to answer a normal question. The tools here follow the questions people ask instead: find a food, look at one food, look at its nutrients, compare a few, add up a recipe. Classifications, ingredients and raw materials are opt-in sections of `get_food` because they are useful but rarely needed, and leaving them out by default keeps responses small.

### Search and pagination

The API cannot search by name. It only pages through the full list with `offset` and `limit`. So on the first search the server walks the whole list in pages of 500 (six requests, about a second), caches it per language, and searches in memory after that. Every word in the query has to appear in the name. Exact and start-of-word matches rank first, and shorter names beat long composite dishes. Results come back in pages with an opaque `next_cursor`. The cursor is tied to the query, type and language it came from, so a cursor from a different search gets a clear error instead of the wrong page.

### Compact output

Responses are small JSON objects with English keys, whatever the display language. Energy appears twice in the source data under the same code (`ENERC`, once in kJ and once in kcal), so the server splits it into `ENERC_KJ` and `ENERC_KCAL`. Nutrient filters take codes or name fragments, and anything that did not match is listed under `unmatched`, so the model can tell "no such nutrient" apart from "value is zero".

### Retries and rate limits

The HTTP client retries 408, 425, 429 and 5xx responses (except 501), timeouts and network failures. It uses exponential backoff with full jitter: a random wait of up to 250 ms, then up to 500 ms, then up to 1 s, never more than 10 s. A `Retry-After` header wins over the computed backoff. Other 4xx responses fail at once. Identical requests that run at the same time share one in-flight promise, and failures are never cached.

### Errors

Failures come back as tool results with `isError: true` and a small JSON body such as `{"error": "not_found", "message": "... Use search_foods to find valid numbers."}`. The codes are `not_found`, `bad_request`, `unauthorized`, `rate_limited`, `upstream`, `timeout`, `network`, `invalid_response` and `invalid_input`. I return them as tool results rather than protocol errors because the MCP spec says tool failures should be visible to the model so it can recover, for example by searching again. Schema violations are rejected by the SDK before any HTTP call is made.

### Logging

stdout carries the MCP protocol, so the server writes nothing there. With `LIVSMEDEL_DEBUG=1` it logs one line per request to stderr. The API key travels in a header and is never part of a logged URL.

## Development

```sh
npm install
npm run typecheck
npm test                 # unit tests against recorded fixtures, no network
npm run test:live        # opt-in smoke test against the real API
npm run session          # build, then call every tool over stdio against the live API
node scripts/record-fixtures.mjs   # refresh test/fixtures from the live API
```

The unit tests use responses recorded from the real API (`test/fixtures/livsmedel-en.json`) behind a small fake that implements the same paging. They cover retries, `Retry-After`, error mapping, auth headers, log redaction, pagination, caching, cursors and every tool through an in-memory MCP client.

## Limitations

- stdio transport only. There is no hosted HTTP endpoint.
- Search matches on names only. It does not know that "mjölk" and "milk" are the same food, so search in the language you set.
- `calculate_recipe_nutrition` adds up stored values. It does not model water loss or nutrient loss from cooking. For cooked dishes, use a calculated dish from the database where one exists, since those already include cooking factors.
- The database describes generic foods, not branded products.
- The catalogue is cached for 6 hours by default, so database updates show up after the cache expires or the server restarts.

## License

MIT, see [LICENSE](LICENSE). Data from Livsmedelsverket's Livsmedelsdatabasen, licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Credit Livsmedelsverket when you publish values from it.

Built by Rickard Lindbom · Lindforge Digital Studio · https://lindforge.dev

TDQS

A4.4/5.0

Scored across 5 tools

Disambiguation5/5

Each tool targets a distinct operation: searching, retrieving a basic record, retrieving detailed nutrients, comparing foods, and calculating recipe nutrition. The only mild overlap is between get_food's summary and get_nutrients' detailed values, but their scopes are clear.

Naming Consistency5/5

All tools follow a consistent verb_noun snake_case pattern: search_foods, get_food, get_nutrients, compare_foods, calculate_recipe_nutrition. The singular/plural objects are semantically appropriate.

Tool Count5/5

Five tools is well-scoped for a food composition database MCP. The set covers discovery, detailed lookup, comparison, and recipe calculation without unnecessary extras.

Completeness5/5

For a read-only database, the surface is complete: search finds foods, get_food gives the record, get_nutrients gives full nutrient detail, compare_foods supports side-by-side analysis, and calculate_recipe_nutrition handles meal-level use cases. The documented limitation about cooking losses is reasonable rather than a gap.

Maintenance

ActivityMaintained
ResponsivenessNo issues