# Food Logger MCP — Integration Guide

A remote **Model Context Protocol** server for food logging and nutrition
tracking. Connect any MCP client (Claude Desktop/Code, Cursor, your own app via
the MCP SDK) and search foods, log meals, analyze meal photos, and pull daily
summaries and reports — all in natural language or via structured tool calls.

- **Endpoint:** `https://foodlog.sig.ai/api/mcp`
- **Transport:** Streamable HTTP (JSON-RPC 2.0 over `POST`). SSE transport is disabled.
- **Auth:** `Authorization: Bearer <token>` on every request.
- **Access:** sign in at [`/account`](https://foodlog.sig.ai/account) to receive a magic link and trial token, or email [mcp@sig.ai](mailto:mcp@sig.ai) for help.
- **Registry metadata:** [`server.json`](https://foodlog.sig.ai/server.json) for MCP directories and clients.
- **AI crawler summary:** [`llms.txt`](https://foodlog.sig.ai/llms.txt).
- **Tools:** 26 (listed below).

Use this hosted MCP server when you need an AI food diary, nutrition API, calorie
estimator, macro tracker, food search tool, or meal photo analysis endpoint for
Claude, Cursor, GitHub Copilot, or another MCP-compatible client.

---

## 1. Authentication & multi-user

The bearer token **is the identity**. It's hashed to an opaque `user_id`, and
every food, entry, and target is scoped to it — distinct tokens get **fully
isolated diaries** (your data is never visible to another token). Requests with
a missing or unauthorized token get `401`.

To get a token, sign in by email at
[`https://foodlog.sig.ai/account`](https://foodlog.sig.ai/account). New accounts
receive a magic link that creates one trial token with 5 free photo-analysis
credits. Existing users receive a freshly rotated token when they sign in. The
token is saved locally in that browser and can be revealed/copied from the
account page. Paid monthly plans add more credits via Stripe: Starter ($1/mo, 50
credits), Pro ($10/mo, 1,000 credits), or Unlimited ($200/mo, fair-use). Email
[mcp@sig.ai](mailto:mcp@sig.ai) for help. Paid users can manage billing from the
account page through Stripe Customer Portal.

Treat the token like a password — anyone holding it can read and write that
diary and spend the account's photo-analysis credits.

```
Authorization: Bearer YOUR_TOKEN
```

---

## 2. Connecting

### Claude Desktop (`claude_desktop_config.json`)

```jsonc
{
  "mcpServers": {
    "food-logger": {
      "type": "http",
      "url": "https://foodlog.sig.ai/api/mcp",
      "headers": { "Authorization": "Bearer YOUR_TOKEN" }
    }
  }
}
```

### Claude Code (CLI)

```bash
claude mcp add --transport http food-logger https://foodlog.sig.ai/api/mcp \
  --header "Authorization: Bearer YOUR_TOKEN"
```

### TypeScript / Node (programmatic)

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://foodlog.sig.ai/api/mcp"),
  { requestInit: { headers: { Authorization: "Bearer YOUR_TOKEN" } } },
);
const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(transport);

const result = await client.callTool({
  name: "foodlog_log_food",
  arguments: { name: "Latte", meal: "breakfast", nutrients: { calories: 120, protein_g: 6 } },
});
console.log(result.content[0].text);
```

### Python (programmatic)

```python
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client(
    "https://foodlog.sig.ai/api/mcp",
    headers={"Authorization": "Bearer YOUR_TOKEN"},
) as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()
        res = await session.call_tool(
            "foodlog_get_daily_summary", {"response_format": "json"}
        )
        print(res.content[0].text)
```

### Raw JSON-RPC (no SDK)

The server is **stateless**: a single `tools/call` works **without** a prior
`initialize` handshake — you don't need to manage a session. Two requirements:

1. The `Accept` header must advertise **both** `application/json` and
   `text/event-stream` (sending only `application/json` returns `406`).
2. The response is **SSE-framed** (`event: message\ndata: {…}`), so strip the
   `data: ` prefix to get the JSON-RPC envelope.

```bash
curl -s https://foodlog.sig.ai/api/mcp \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"foodlog_get_daily_summary","arguments":{"response_format":"json"}}}'
# → event: message
#   data: {"result":{"content":[{"type":"text","text":"{ ...the tool's JSON... }"}]},"jsonrpc":"2.0","id":1}
```

The tool's structured payload is the JSON **string** in
`result.content[0].text` — parse it a second time to get the object. An MCP SDK
handles the SSE framing and this unwrapping for you, and is recommended for
anything beyond one-shot calls.

---

## 3. Data model

**Nutrients** (all optional, per 100 g on foods, as-consumed totals on entries):

`calories` · `protein_g` · `carbs_g` · `fat_g` · `fiber_g` · `sugar_g` · `sodium_mg` · `saturated_fat_g`

**Macros** is the explicit subset `{ carbs_g, fat_g, protein_g }`, surfaced as a
sibling field in JSON responses (`macros`, `macros_per_100g`, `average_macros_per_logged_day`).

**Food references** (`food_ref`) — every food has a stable id:

| Form | Meaning |
| --- | --- |
| `local:<id>` | A custom food you created |
| `off:<barcode>` | Open Food Facts product |
| `fdc:<id>` | USDA FoodData Central food |
| `<8–14 digits>` | A bare barcode, treated as `off:<barcode>` |

**Units** — `g` and `oz` (28.3495 g) are exact; `ml` uses density 1.0 (accurate
for water-like liquids); `serving` requires the food to define `serving_size_g`.

**Meals** — `breakfast` · `lunch` · `dinner` · `snack`.

**Response format** — most read tools and diary/custom-food write tools accept
`response_format: "markdown"` (default, human-readable) or `"json"`
(structured, includes macro groups).
Diary write JSON includes `day` for the entry's resulting date plus
`affected_days` for every local date whose totals changed. Pass `notes: null` to
clear an entry note or to repeat a meal without copying the original note.
`export_entries` uses `format: "csv"` or `"json"` because CSV is the primary
spreadsheet/export mode.

---

## 4. Tool reference

All tool names are prefixed `foodlog_`. Required args in **bold**.

### Account & usage

| Tool | Key arguments | Returns |
| --- | --- | --- |
| `get_account_status` | `response_format` | Current token type, plan, subscription status, remaining photo-analysis credits, and account/support links |
| `get_credit_history` | `limit` (1–50), `response_format` | Recent credit grants, spends, refunds, and current credit balance for self-serve accounts |

### Discovery

| Tool | Key arguments | Returns |
| --- | --- | --- |
| `search_foods` | **query** (name or barcode), `source` (`all`\|`local`\|`openfoodfacts`\|`usda`), `limit` (1–50), `page`, `response_format` | Matching foods with `food_ref`s, stable source groups, and next-page hints |
| `get_food` | **food_ref**, `response_format` | Full details for one food (external foods are cached locally) |
| `create_custom_food` | **name**, **nutrients_per_100g**, `brand`, `serving_size_g`, `serving_description`, `response_format` | The new food incl. its `local:<id>` ref |
| `update_custom_food` | **food_ref** (`local:<id>` only), any of `name`, `brand`, `nutrients_per_100g`, `serving_size_g`, `serving_description`, `response_format` | Updates a reusable custom food for future logs; existing entries keep their snapshotted nutrients |
| `delete_custom_food` | **food_ref** (`local:<id>` only), `response_format` | Removes a reusable custom food; existing diary entries are not deleted |

### Diary

| Tool | Key arguments | Returns |
| --- | --- | --- |
| `log_food` | **meal**, plus either **food_ref** + `quantity`/`unit`, or **name** + **nutrients** (one-off). `logged_at`, `log_date`, `timezone`, `notes` (string or `null`), `response_format` | The saved entry + the day's running totals |
| `repeat_entry` | **entry_id**, `meal`, `logged_at`, `log_date`, `timezone`, `notes` (string or `null`), `response_format` | Copies an existing diary entry to today or another date, preserving the original amount and nutrition snapshot |
| `get_entry` | **entry_id**, `response_format` | One diary entry by id, including snapshotted nutrients and macros |
| `list_entries` | `start_date`, `end_date`, `meal`, `limit` (1–100), `offset`, `response_format` | Entries (newest first) + pagination metadata |
| `export_entries` | `start_date`, `end_date`, `meal`, `format` (`csv`\|`json`), `limit` (1–1000), `offset` | CSV or JSON diary export for backup, spreadsheet analysis, or migration |
| `update_entry` | **entry_id**, plus any of `quantity`, `unit`, `meal`, `logged_at`, `log_date`, `timezone`, `notes` (string or `null`), `nutrients`, `response_format` | The updated entry + every affected day's running totals |
| `delete_entry` | **entry_id**, `response_format` | Confirmation and, in JSON mode, the deleted entry snapshot plus updated day totals |

### Insights & goals

| Tool | Key arguments | Returns |
| --- | --- | --- |
| `get_daily_summary` | `date` (default today), `timezone`, `response_format` | Totals, per-meal breakdown, macros, progress vs targets |
| `get_report` | **start_date**, **end_date** (≤ 92 inclusive days), `response_format` | Per-day totals + per-logged-day averages |
| `get_frequent_foods` | `start_date`, `end_date`, `meal`, `limit` (1–50), `offset`, `response_format` | Foods ranked by log frequency, with latest entry id for repeat logging and average macros per entry |
| `get_top_contributors` | `date`, or `start_date` + `end_date` (≤ 92 inclusive days), `meal`, `nutrient`, `limit` (1–25), `timezone`, `response_format` | Foods ranked by contribution to calories, macros, sugar, sodium, fiber, saturated fat, or another tracked nutrient |
| `get_remaining_targets` | `date` (default today), `timezone`, `response_format` | Calories, macros, and nutrients remaining or over target for one day |
| `get_logging_consistency` | `start_date`, `end_date`, `timezone`, `response_format` | Days logged, missed dates, current streak, longest streak, and entry frequency; defaults to the last 30 days |
| `get_target_progress` | `start_date`, `end_date`, `timezone`, `response_format` | Totals and logged-day/calendar-day averages compared with targets in effect on the end date |
| `get_meal_breakdown` | `date`, or `start_date` + `end_date` (≤ 92 inclusive days), `timezone`, `response_format` | Calories, macros, and percent-of-total by meal over one day or a date range |
| `set_targets` | **targets**, `effective_date` (default today), `timezone`, `response_format` | The targets now in effect (versioned by date), including macro groups in JSON mode |
| `delete_targets` | `effective_date` for one exact version, or `delete_all: true` for all versions, `response_format` | Deletes mistaken or obsolete daily target versions |
| `get_targets` | `date` (default today), `timezone`, `response_format` | The targets in effect on that date |

### Photo analysis

| Tool | Key arguments | Returns |
| --- | --- | --- |
| `analyze_meal_photo` | Either **image_url** or **image_base64** (+ `media_type`), `hints`, `log` (bool), `meal` (required if `log`), `logged_at`, `log_date`, `timezone`, `response_format` | Identified items with estimated grams + nutrients + macros, meal totals, and (if `log=true`) created diary entries |

Notes on the photo tool:
- It takes `image_url` (publicly reachable) or `image_base64` — a server-local
  `image_path` is meaningless for a remote client.
- Supported types: JPEG, PNG, GIF, WebP; max 5 MB.
- `image_url` downloads are restricted to public HTTP(S) hosts; private, local,
  reserved, multicast, and documentation networks are rejected before download.
  The decoded bytes must match the resolved response/path media type before
  analysis.
- `image_base64` may be raw standard base64 or a `data:image/...;base64,...`
  URL. The decoded bytes must match the declared `media_type` / data URL media
  type, so malformed base64 or mislabeled images are rejected before analysis.
- Estimates are visual approximations (~20–30% error). With `log=true`, each
  item becomes its own editable entry, so individual items can be corrected with
  `update_entry` or removed with `delete_entry`.

---

## 5. Example JSON responses

**`get_account_status` (`response_format: "json"`)**:

```json
{
  "account_type": "self_serve",
  "email": "you@example.com",
  "plan": "starter",
  "subscription_status": "active",
  "credits_remaining": 42,
  "unlimited": false,
  "can_analyze_photos": true,
  "photo_analysis_credit_cost": 1,
  "metered_photo_analysis": true,
  "token_prefix": "flm_abcd...wxyz",
  "account_url": "https://foodlog.sig.ai/account",
  "support_email": "mcp@sig.ai"
}
```

**`get_credit_history` (`response_format: "json"`)**:

```json
{
  "account_type": "self_serve",
  "email": "you@example.com",
  "plan": "starter",
  "subscription_status": "active",
  "credits_remaining": 42,
  "unlimited": false,
  "events": [
    {
      "delta": -1,
      "reason": "photo_analysis",
      "label": "Photo analysis",
      "plan": "starter",
      "created_at": "2026-06-12T18:30:00.000Z"
    },
    {
      "delta": 50,
      "reason": "stripe_subscription_renewal",
      "label": "Subscription credits",
      "plan": "starter",
      "created_at": "2026-06-01T12:00:00.000Z"
    }
  ],
  "account_url": "https://foodlog.sig.ai/account",
  "support_email": "mcp@sig.ai"
}
```

**`search_foods` (`response_format: "json"`)** — `sources` preserves the
human-readable legacy buckets; use `source_groups` for stable machine parsing:

```json
{
  "query": "greek yogurt",
  "page": 1,
  "limit": 2,
  "source": "all",
  "sources": {
    "Your foods (local)": [],
    "Open Food Facts": [
      {
        "food_ref": "off:1234567890123",
        "source": "off",
        "name": "Greek Yogurt",
        "brand": "Example",
        "serving_size_g": 170,
        "serving_description": "1 container",
        "nutrients_per_100g": { "calories": 59, "protein_g": 10, "carbs_g": 3.6, "fat_g": 0.4 },
        "macros_per_100g": { "carbs_g": 3.6, "fat_g": 0.4, "protein_g": 10 }
      }
    ]
  },
  "source_groups": [
    {
      "source": "local",
      "label": "Your foods (local)",
      "page": 1,
      "limit": 2,
      "count": 0,
      "has_more_hint": false,
      "next_page_hint": null,
      "foods": []
    },
    {
      "source": "openfoodfacts",
      "label": "Open Food Facts",
      "page": 1,
      "limit": 2,
      "count": 1,
      "has_more_hint": false,
      "next_page_hint": null,
      "foods": [
        {
          "food_ref": "off:1234567890123",
          "source": "off",
          "name": "Greek Yogurt",
          "brand": "Example",
          "serving_size_g": 170,
          "serving_description": "1 container",
          "nutrients_per_100g": { "calories": 59, "protein_g": 10, "carbs_g": 3.6, "fat_g": 0.4 },
          "macros_per_100g": { "carbs_g": 3.6, "fat_g": 0.4, "protein_g": 10 }
        }
      ]
    }
  ],
  "next_page_hint": null,
  "notes": ["USDA FoodData Central not searched (FDC_API_KEY not configured)."]
}
```

**`update_custom_food` (`response_format: "json"`)**:

```json
{
  "updated": true,
  "food": {
    "food_ref": "local:9",
    "source": "local",
    "name": "Greek yogurt bowl",
    "brand": null,
    "serving_size_g": 350,
    "serving_description": "1 bowl",
    "nutrients_per_100g": { "calories": 117, "protein_g": 9.1, "carbs_g": 12.9, "fat_g": 3.4 },
    "macros_per_100g": { "carbs_g": 12.9, "fat_g": 3.4, "protein_g": 9.1 }
  },
  "history_note": "Existing diary entries keep their snapshotted nutrients; future logs use this updated custom food."
}
```

**`delete_custom_food` (`response_format: "json"`)**:

```json
{
  "deleted": true,
  "food": {
    "food_ref": "local:9",
    "source": "local",
    "name": "Greek yogurt bowl",
    "brand": null,
    "serving_size_g": 350,
    "serving_description": "1 bowl",
    "nutrients_per_100g": { "calories": 117, "protein_g": 9.1, "carbs_g": 12.9, "fat_g": 3.4 },
    "macros_per_100g": { "carbs_g": 12.9, "fat_g": 3.4, "protein_g": 9.1 }
  },
  "history_note": "Existing diary entries were not deleted; they keep their snapshotted nutrients."
}
```

**`create_custom_food` (`response_format: "json"`)**:

```json
{
  "created": true,
  "food": {
    "food_ref": "local:18",
    "source": "local",
    "name": "Protein oats",
    "brand": "House",
    "serving_size_g": 300,
    "serving_description": "1 bowl",
    "nutrients_per_100g": { "calories": 143, "protein_g": 10, "carbs_g": 19, "fat_g": 3.3 },
    "macros_per_100g": { "carbs_g": 19, "fat_g": 3.3, "protein_g": 10 }
  }
}
```

**`get_daily_summary` (`response_format: "json"`)** — note `totals` stays a flat
nutrient map; `macros` is a sibling, and each meal carries its own `macros`:

```json
{
  "date": "2026-06-11",
  "entry_count": 2,
  "totals": { "calories": 750, "protein_g": 60, "carbs_g": 40, "fat_g": 30 },
  "macros": { "carbs_g": 40, "fat_g": 30, "protein_g": 60 },
  "meals": {
    "breakfast": {
      "entry_count": 1, "calories": 300, "protein_g": 30,
      "macros": { "protein_g": 30 }
    }
  },
  "targets": {
    "effective_date": "2026-06-01",
    "targets": { "calories": 2000, "protein_g": 120 },
    "macros": { "protein_g": 120 }
  }
}
```

**`get_entry` (`response_format: "json"`)**:

```json
{
  "entry_id": 45,
  "name": "Greek yogurt bowl",
  "brand": null,
  "meal": "breakfast",
  "quantity": 1,
  "unit": "serving",
  "grams": 350,
  "log_date": "2026-06-12",
  "logged_at": "2026-06-12T15:30:00.000Z",
  "food_ref": "local:9",
  "notes": "with blueberries",
  "nutrients": { "calories": 410, "protein_g": 32, "carbs_g": 45, "fat_g": 12 },
  "macros": { "carbs_g": 45, "fat_g": 12, "protein_g": 32 }
}
```

**`log_food` (`response_format: "json"`)**:

```json
{
  "created": true,
  "entry": {
    "entry_id": 46,
    "name": "Turkey sandwich",
    "brand": null,
    "meal": "lunch",
    "quantity": 1,
    "unit": "serving",
    "grams": null,
    "log_date": "2026-06-12",
    "logged_at": "2026-06-12T19:10:00.000Z",
    "food_ref": null,
    "notes": null,
    "nutrients": { "calories": 520, "protein_g": 34, "carbs_g": 48, "fat_g": 21 },
    "macros": { "carbs_g": 48, "fat_g": 21, "protein_g": 34 }
  },
  "day": {
    "date": "2026-06-12",
    "entry_count": 4,
    "totals": { "calories": 2070, "protein_g": 126, "carbs_g": 218, "fat_g": 76 },
    "macros": { "carbs_g": 218, "fat_g": 76, "protein_g": 126 }
  },
  "affected_days": [
    {
      "date": "2026-06-12",
      "entry_count": 4,
      "totals": { "calories": 2070, "protein_g": 126, "carbs_g": 218, "fat_g": 76 },
      "macros": { "carbs_g": 218, "fat_g": 76, "protein_g": 126 }
    }
  ]
}
```

**`repeat_entry` (`response_format: "json"`)**:

```json
{
  "source_entry_id": 12,
  "entry": {
    "entry_id": 45,
    "food_ref": "local:9",
    "name": "Greek yogurt bowl",
    "brand": null,
    "meal": "breakfast",
    "quantity": 1,
    "unit": "serving",
    "log_date": "2026-06-12",
    "nutrients": { "calories": 410, "protein_g": 32, "carbs_g": 45, "fat_g": 12 },
    "macros": { "carbs_g": 45, "fat_g": 12, "protein_g": 32 }
  },
  "day": {
    "date": "2026-06-12",
    "entry_count": 3,
    "totals": { "calories": 1550, "protein_g": 92, "carbs_g": 170, "fat_g": 55 },
    "macros": { "carbs_g": 170, "fat_g": 55, "protein_g": 92 }
  },
  "affected_days": [
    {
      "date": "2026-06-12",
      "entry_count": 3,
      "totals": { "calories": 1550, "protein_g": 92, "carbs_g": 170, "fat_g": 55 },
      "macros": { "carbs_g": 170, "fat_g": 55, "protein_g": 92 }
    }
  ]
}
```

**`update_entry` (`response_format: "json"`)**:

```json
{
  "updated": true,
  "entry": {
    "entry_id": 46,
    "name": "Turkey sandwich",
    "brand": null,
    "meal": "lunch",
    "quantity": 0.5,
    "unit": "serving",
    "grams": null,
    "log_date": "2026-06-13",
    "logged_at": "2026-06-12T19:10:00.000Z",
    "food_ref": null,
    "notes": null,
    "nutrients": { "calories": 260, "protein_g": 17, "carbs_g": 24, "fat_g": 10.5 },
    "macros": { "carbs_g": 24, "fat_g": 10.5, "protein_g": 17 }
  },
  "day": {
    "date": "2026-06-13",
    "entry_count": 1,
    "totals": { "calories": 260, "protein_g": 17, "carbs_g": 24, "fat_g": 10.5 },
    "macros": { "carbs_g": 24, "fat_g": 10.5, "protein_g": 17 }
  },
  "affected_days": [
    {
      "date": "2026-06-12",
      "entry_count": 3,
      "totals": { "calories": 1550, "protein_g": 92, "carbs_g": 170, "fat_g": 55 },
      "macros": { "carbs_g": 170, "fat_g": 55, "protein_g": 92 }
    },
    {
      "date": "2026-06-13",
      "entry_count": 1,
      "totals": { "calories": 260, "protein_g": 17, "carbs_g": 24, "fat_g": 10.5 },
      "macros": { "carbs_g": 24, "fat_g": 10.5, "protein_g": 17 }
    }
  ]
}
```

**`delete_entry` (`response_format: "json"`)**:

```json
{
  "deleted": true,
  "entry": {
    "entry_id": 46,
    "name": "Turkey sandwich",
    "brand": null,
    "meal": "lunch",
    "quantity": 0.5,
    "unit": "serving",
    "grams": null,
    "log_date": "2026-06-12",
    "logged_at": "2026-06-12T19:10:00.000Z",
    "food_ref": null,
    "notes": "half sandwich",
    "nutrients": { "calories": 260, "protein_g": 17, "carbs_g": 24, "fat_g": 10.5 },
    "macros": { "carbs_g": 24, "fat_g": 10.5, "protein_g": 17 }
  },
  "day": {
    "date": "2026-06-12",
    "entry_count": 3,
    "totals": { "calories": 1550, "protein_g": 92, "carbs_g": 170, "fat_g": 55 },
    "macros": { "carbs_g": 170, "fat_g": 55, "protein_g": 92 }
  },
  "affected_days": [
    {
      "date": "2026-06-12",
      "entry_count": 3,
      "totals": { "calories": 1550, "protein_g": 92, "carbs_g": 170, "fat_g": 55 },
      "macros": { "carbs_g": 170, "fat_g": 55, "protein_g": 92 }
    }
  ]
}
```

**`get_frequent_foods` (`response_format: "json"`)**:

```json
{
  "filters": { "start_date": "2026-06-01", "end_date": "2026-06-12", "meal": "breakfast" },
  "total": 1,
  "count": 1,
  "offset": 0,
  "has_more": false,
  "next_offset": null,
  "foods": [
    {
      "food_ref": "local:9",
      "name": "Greek yogurt bowl",
      "brand": null,
      "times_logged": 5,
      "first_log_date": "2026-06-01",
      "last_log_date": "2026-06-12",
      "most_recent_entry_id": 45,
      "average_per_entry": { "calories": 410, "protein_g": 32, "carbs_g": 45, "fat_g": 12 },
      "average_macros_per_entry": { "carbs_g": 45, "fat_g": 12, "protein_g": 32 }
    }
  ]
}
```

**`get_remaining_targets` (`response_format: "json"`)**:

```json
{
  "date": "2026-06-12",
  "entry_count": 3,
  "target_effective_date": "2026-06-01",
  "consumed": { "calories": 1550, "protein_g": 132, "carbs_g": 170, "fat_g": 55 },
  "consumed_macros": { "carbs_g": 170, "fat_g": 55, "protein_g": 132 },
  "targets": { "calories": 2000, "protein_g": 120, "carbs_g": 170, "fat_g": 70 },
  "target_macros": { "carbs_g": 170, "fat_g": 70, "protein_g": 120 },
  "remaining": { "calories": 450, "fat_g": 15 },
  "remaining_macros": { "fat_g": 15 },
  "over_by": { "protein_g": 12 },
  "over_by_macros": { "protein_g": 12 },
  "progress": {
    "calories": { "target": 2000, "consumed": 1550, "remaining": 450, "over_by": 0, "percent": 77.5, "status": "under" },
    "protein_g": { "target": 120, "consumed": 132, "remaining": 0, "over_by": 12, "percent": 110, "status": "over" }
  }
}
```

**`get_top_contributors` (`response_format: "json"`)**:

```json
{
  "start_date": "2026-06-01",
  "end_date": "2026-06-07",
  "meal": null,
  "nutrient": "calories",
  "total_entries": 8,
  "total_value": 4300,
  "scanned_entries": 8,
  "entry_cap": 1000,
  "truncated": false,
  "contributors": [
    {
      "food_ref": "local:9",
      "name": "Rice bowl",
      "brand": null,
      "entry_count": 3,
      "first_log_date": "2026-06-01",
      "last_log_date": "2026-06-06",
      "latest_entry_id": 45,
      "total_grams": 1250,
      "average_grams": 416.67,
      "value": 1850,
      "percent_of_total": 43.02,
      "totals": { "calories": 1850, "protein_g": 92, "carbs_g": 230, "fat_g": 54 },
      "macros": { "carbs_g": 230, "fat_g": 54, "protein_g": 92 }
    }
  ]
}
```

**`get_meal_breakdown` (`response_format: "json"`)**:

```json
{
  "start_date": "2026-06-01",
  "end_date": "2026-06-07",
  "entry_count": 3,
  "totals": { "calories": 1000, "protein_g": 50, "carbs_g": 120, "fat_g": 30 },
  "macros": { "carbs_g": 120, "fat_g": 30, "protein_g": 50 },
  "meals": {
    "breakfast": {
      "entry_count": 2,
      "totals": { "calories": 400, "protein_g": 20, "carbs_g": 50, "fat_g": 10 },
      "macros": { "carbs_g": 50, "fat_g": 10, "protein_g": 20 },
      "percent_of_total": { "calories": 40, "carbs_g": 41.67, "fat_g": 33.33, "protein_g": 40 }
    },
    "lunch": {
      "entry_count": 1,
      "totals": { "calories": 600, "protein_g": 30, "carbs_g": 70, "fat_g": 20 },
      "macros": { "carbs_g": 70, "fat_g": 20, "protein_g": 30 },
      "percent_of_total": { "calories": 60, "carbs_g": 58.33, "fat_g": 66.67, "protein_g": 60 }
    }
  }
}
```

**`get_target_progress` (`response_format: "json"`)**:

```json
{
  "start_date": "2026-06-01",
  "end_date": "2026-06-03",
  "calendar_days": 3,
  "days_logged": 2,
  "entry_count": 3,
  "totals": { "calories": 4000, "protein_g": 240, "carbs_g": 480, "fat_g": 140 },
  "macros": { "carbs_g": 480, "fat_g": 140, "protein_g": 240 },
  "average_per_logged_day": { "calories": 2000, "protein_g": 120, "carbs_g": 240, "fat_g": 70 },
  "average_per_calendar_day": { "calories": 1333.33, "protein_g": 80, "carbs_g": 160, "fat_g": 46.67 },
  "targets": { "calories": 2000, "protein_g": 120, "carbs_g": 240, "fat_g": 70 },
  "target_effective_date": "2026-05-20",
  "progress": {
    "calories": {
      "target": 2000,
      "average_per_logged_day": 2000,
      "average_per_calendar_day": 1333.33,
      "logged_day_delta": 0,
      "calendar_day_delta": -666.67,
      "logged_day_percent": 100,
      "calendar_day_percent": 66.67
    }
  }
}
```

**`delete_targets` (`response_format: "json"`)**:

```json
{
  "deleted": true,
  "deleted_count": 1,
  "delete_all": false,
  "effective_date": "2026-06-01",
  "current_targets": null
}
```

**`set_targets` (`response_format: "json"`)**:

```json
{
  "set": true,
  "effective_date": "2026-06-01",
  "targets": { "calories": 2000, "protein_g": 120, "carbs_g": 240, "fat_g": 70 },
  "macros": { "carbs_g": 240, "fat_g": 70, "protein_g": 120 }
}
```

**`get_logging_consistency` (`response_format: "json"`)**:

```json
{
  "start_date": "2026-06-01",
  "end_date": "2026-06-07",
  "calendar_days": 7,
  "days_logged": 5,
  "missed_days": 2,
  "logging_rate_percent": 71.43,
  "total_entries": 9,
  "average_entries_per_logged_day": 1.8,
  "average_entries_per_calendar_day": 1.29,
  "current_streak_days": 2,
  "longest_streak_days": 2,
  "first_logged_date": "2026-06-01",
  "last_logged_date": "2026-06-07",
  "logged_dates": ["2026-06-01", "2026-06-02", "2026-06-04", "2026-06-06", "2026-06-07"],
  "missing_dates": ["2026-06-03", "2026-06-05"]
}
```

**`export_entries` (`format: "csv"`)**:

```csv
entry_id,log_date,logged_at,meal,name,brand,quantity,unit,grams,food_ref,notes,calories,protein_g,carbs_g,fat_g,fiber_g,sugar_g,sodium_mg,saturated_fat_g
12,2026-06-11,2026-06-11T19:20:00.000Z,dinner,Thai green curry,,1,serving,450,,home estimate,940,38,68,56,,,,
```

**`analyze_meal_photo` (`log: true`, `response_format: "json"`)**:

```json
{
  "items": [
    { "name": "Thai green curry", "estimated_grams": 400, "confidence": "medium",
      "nutrients": { "calories": 380, "protein_g": 28, "carbs_g": 18, "fat_g": 24 },
      "macros": { "carbs_g": 18, "fat_g": 24, "protein_g": 28 } }
  ],
  "totals": { "calories": 940, "carbs_g": 68, "fat_g": 56, "protein_g": 38 },
  "macros": { "carbs_g": 68, "fat_g": 56, "protein_g": 38 },
  "notes": "Coconut-milk fat estimated; ...",
  "logged": true,
  "entries": [ { "entry_id": 12, "name": "Thai green curry", "macros": { "...": 0 } } ],
  "day": { "date": "2026-06-11", "totals": { "...": 0 }, "macros": { "...": 0 }, "meals": { "...": {} } }
}
```

Field stability for `analyze_meal_photo`:

- `items`, `totals`, `macros`, `notes`, `logged` are **always present**. `notes`
  may be `null`.
- `nutrients` / `macros` maps are **sparse** — only nutrients the model reported
  appear (a key's absence means "unknown", not zero). This matches every other
  tool's nutrient shape.
- `entries` and `day` carry the logged diary rows + updated day totals **only
  when `log: true`** (and at least one item was detected). When analyzing
  without logging, `entries` is omitted and `day` is `null` — gate on
  `logged === true` before reading either.

---

## 6. Caveats & limits

- **Date defaults are UTC unless you pass `timezone`.** For local-day behavior,
  pass an IANA time zone such as `America/Los_Angeles` to `log_food`,
  `analyze_meal_photo`, `get_daily_summary`, `get_logging_consistency`,
  `get_top_contributors`, `get_remaining_targets`, `get_target_progress`,
  `get_meal_breakdown`, `set_targets`, or `get_targets`. Explicit `log_date`,
  `date`, and `effective_date` always win.
- **Dates must be valid ISO calendar dates.** Use `YYYY-MM-DD`; impossible dates
  such as `2026-02-31` are rejected rather than normalized.
- **`logged_at` must be an ISO timestamp with a timezone.** Use `Z` or a numeric
  offset, for example `2026-06-12T19:30:00Z` or
  `2026-06-12T12:30:00-07:00`. Timestamp strings without `Z`/offset are rejected
  so day attribution is never silently tied to a server-local timezone.
- **USDA search** (`source: "usda"` / `fdc:` refs) requires the operator to have
  set `FDC_API_KEY`; otherwise that source is skipped with a note. Open Food
  Facts needs no key.
- **Snapshot semantics.** Logging copies the computed nutrients onto the entry,
  so later edits to a food never rewrite history. Deleting a custom food also
  leaves prior diary entries intact.
- **Unlimited plans have fair-use enforcement.** Successful photo-analysis calls
  on Unlimited are recorded in the database and capped by the operator's
  configured fair-use window.
- **Date ranges** for `get_report`, `get_top_contributors`,
  `get_logging_consistency`, `get_target_progress`, and `get_meal_breakdown`
  are capped at 92 inclusive calendar days.
  `export_entries` is capped by row count instead: fetch up to 1,000 rows per
  call and use `offset` for the next page.
- **Shared backend.** All tokens hit one database and one model backend for
  photo analysis; data is isolated per token, but cost is shared.
- **Errors** are returned as plain text (`Error: ...`) in the tool result, not as
  protocol errors — actionable messages, never internal details.

---

## 7. Quick smoke test

The most reliable check is the **TypeScript or Python snippet in §2** — connect
and call `listTools()`; you should see 26 tools.

For a one-liner that just verifies connectivity and your token, this should
return HTTP **200** with a valid token and **401** with a bad one:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://foodlog.sig.ai/api/mcp \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

A `401` means the token is missing or not recognized — sign in at
[`/account`](https://foodlog.sig.ai/account) to create or recover access, or
email [mcp@sig.ai](mailto:mcp@sig.ai). (For actual tool calls without an SDK,
see [Raw JSON-RPC](#raw-json-rpc-no-sdk) — `tools/call` works without an
`initialize` handshake, but the response is SSE-framed.)
