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

A stdio MCP server that drives your own [balance.it](https://balance.it) account
through its recipe autobalancer.

Balance IT formulates home-prepared canine and feline diets. This server acts as
your signed-in account. It exposes the calorie calculator, the food catalogue,
and the recipe builder as MCP tools.

Use your own account credentials. Read and respect the Balance IT terms of
service before you automate anything against the site. This project has no
affiliation with Balance IT.

## What it does

The server exposes six tools over the Model Context Protocol.

| Tool | Kind | Latency | Purpose |
| --- | --- | --- | --- |
| `calculate_calories` | HTTP | under 500 ms | Return the daily maintenance calories for a pet. |
| `search_foods` | HTTP | under 500 ms | Search the curated `m*` and USDA `n*` food entries. |
| `list_catalogue` | Browser | 5 to 8 s, then cached 7 days | Return the full ingredient catalogue for one species and life stage. |
| `generate_recipe` | Browser | 45 to 90 s | Fill the pet profile, choose ingredients, and wait for the built options. |
| `get_recipe` | Browser | 5 to 10 s | Read the macros and the gram weights for one recipe option. |
| `session_status` | Local | instant | Report whether a usable session is on disk. |

Every tool returns structured JSON. Two of the tools call the site JSON endpoints
directly. The other three drive a Chrome browser through Playwright. The
`/recipes` page and the build flow render only inside a logged-in browser
session.

A dead session produces a distinct `session_expired` error with a remedy hint.
The site answers an unauthenticated request with HTTP 200 and an empty array. A
caller cannot otherwise tell a dead session from an empty result.

## Requirements

- Node.js 20 or later.
- Google Chrome. Playwright launches it through `channel: 'chrome'`.
- A Balance IT account of your own.
- A logged-in balance.it browser session, exported as a Playwright storageState
  file.

## Install

```bash
npm install
npm run build
```

## Configuration

### Session state

The server reads its session from `~/.config/balanceit/state.json`. It creates
that file with mode 0600, inside a directory with mode 0700.

Export a session yourself. Sign in to balance.it in a Playwright Chromium
context. Then write `context.storageState({ path })` to the configuration path
above. The export must hold the `_balance_session` and `aws-waf-token` cookies on
the `balance.it` domain.

Set `BALANCEIT_STATE_SEED` to seed the session from an export that sits
elsewhere. On first run the server copies that file to the configuration path,
and it applies mode 0600. The server ignores the variable once the configuration
file exists.

Re-export the session when a tool returns `session_expired`. The session cookies
expire after a few days.

### Environment variables

| Variable | Purpose |
| --- | --- |
| `BALANCEIT_EMAIL` | Your balance.it account email. |
| `BALANCEIT_PASSWORD` | Your balance.it account password. |
| `BALANCEIT_STATE_SEED` | Path to a storageState export used to seed the session. |

The server reads the credentials from the environment only. It never writes them
to disk. It never logs them. It never puts them in an error message.

The credentials are documented for a future automatic sign-in. The current code
does not use them, because it authenticates from the exported session.

### MCP client

Add the server to your MCP client configuration. Replace
`/path/to/balanceit-mcp` with the path to your own clone.

```json
{
  "mcpServers": {
    "balanceit": {
      "command": "node",
      "args": ["/path/to/balanceit-mcp/dist/index.js"]
    }
  }
}
```

## Usage

Run the browser tools from a foreground shell. Playwright with
`channel: 'chrome'` does not launch Chrome from a backgrounded shell on macOS.
The Node process then stays alive, logs nothing, and looks like a hang.

### generate_recipe

`generate_recipe` takes a pet profile and a list of `food_id` values. It returns
one option list, plus three fields for verification.

- `submitted_food_ids` lists the identifiers read from the hidden form inputs at
  submit time. This is what balance.it received.
- `typeahead_added` lists the subset that the typeahead added rather than a chip
  click.
- `missing_from_first_option` lists each requested ingredient that the first
  built option does not appear to contain. An empty list is the normal case. A
  non-empty list means that the site optimizer substituted or skipped the
  ingredient on that option. Call `get_recipe` on each option when you need
  per-option certainty.

The tool rejects a bad `food_id` before it opens a browser. A malformed
identifier fails immediately. A well-formed identifier that `search_foods` does
not return fails after the resolve pass.

## Contract notes

These details describe how the site behaves. Keep them so that later maintenance
does not have to rediscover them.

- The `/recipes` page returns an empty body to a plain fetch. Use the browser
  tools for any DOM content.
- The two JSON endpoints are
  `/r/api/v1/pets/vet_approvals/auto_calculate_calories` and
  `/r/api/v1/foods/search/<QUERY>`. Both work with a plain fetch, the persisted
  cookie header, a normal desktop user agent, and `X-Requested-With:
  XMLHttpRequest`.
- `foods/search` takes the query as a path segment. The `?q=` form returns 404.
- On tab 2 of `/recipes`, the ingredient chips sit inside seven collapsed
  accordions. Each chip is present in the DOM but not visible. Playwright times
  out on a click against a hidden chip. Click the accordion header
  (`.ingredients-box > a`) first.
- The one-pot toggle `#one_pot_cooking` is hidden by CSS. Toggle the visible
  `label[for=one_pot_cooking]` instead.
- A food that has no chip is added through the tab-2 typeahead
  (`input.food-typeahead.tt-input`). This is the common case when
  `one_pot_cooking` is true.
- Each typeahead suggestion carries a class of the form
  `li-<category>-<food_id>`, such as `li-protein-n13326`. Match that class to
  find the exact suggestion, then click it with the mouse.
- A `.tt-suggestion` click reports success without registering the food when it
  targets the wrong suggestion. Do not click "the first suggestion". Match the
  `li-<category>-<food_id>` class.
- Every registered selection appears as a hidden
  `<input name="<category>_foods[]" value="<food_id>">`. The pre-submit
  invariant is that every requested identifier is present as such an input.
- Typing a saved pet name by hand reads as an edit to that pet record. The site
  then raises an update gate. Select the pet from the `#pet_name` typeahead
  instead, which sets the hidden `pet_id` and raises no gate.
- A recipe build is asynchronous. After the submit navigation lands on
  `/recipes/<recipe_id>`, poll until the option cards render or the "N of N
  complete" status reaches parity.

## Repository layout

```
src/
  index.ts    MCP server entry and tool registration
  session.ts  storageState persistence and Cookie header derivation
  http.ts     the two pure-fetch tools, SessionError, and UpstreamError
  browser.ts  Playwright flows for catalogue, generate, and detail scrape
scripts/
  smoke.ts    live check against the site
```

## Tests

This repository has no offline test suite. `scripts/smoke.ts` runs against the
live site, and it needs a valid session.

```bash
npm run smoke
```

The smoke run calls `calculate_calories` for a spayed female and an intact
female. It asserts that each result falls inside the reported calorie bracket. It
checks that `search_foods('chicken')` returns `m4`. It then builds a recipe. It
repeats the build for the same pet name, which exercises the saved-pet gate. It
reads the first option back. It asserts that a malformed `food_id` is rejected.

The reference profile for the run is an adult dog of 22 kg.

Set `SMOKE_SKIP_RECIPE=1` to skip the slow browser leg.

```bash
SMOKE_SKIP_RECIPE=1 npm run smoke
```

## Development

```bash
npm run dev     # run the server from source through tsx
npm run build   # compile TypeScript into dist/
npm start       # run the compiled server
```

## Licence

Licensed under the PolyForm Noncommercial License 1.0.0. Copyright 2026
Seraphine Renard.

TDQS

A3.9/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a distinct resource and action: loading a recipe, calculating calories, searching foods, listing the catalogue, generating a recipe, and checking session state. Even though get_recipe and generate_recipe both involve recipes, the load-vs-create distinction is clear from the descriptions.

Naming Consistency4/5

Most tools follow a clear verb_noun pattern (get_recipe, calculate_calories, search_foods, list_catalogue, generate_recipe). session_status is a small deviation that reads as a state check rather than an action, but it's still understandable and consistent in style.

Tool Count5/5

Six tools is a well-scoped set for a Balance It nutrition MCP. Each tool covers a core part of the workflow (estimating calories, finding ingredients, generating recipes, loading results) without being bloated or too thin.

Completeness4/5

The core lifecycle is covered: search/select ingredients, generate recipes, retrieve a specific recipe, and estimate calories. Minor gaps exist, such as no explicit recipe listing or update/delete, but these are not essential for the domain and the workflow is functional.

Maintenance

ActivityInactive
ResponsivenessNo issues