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
/accountto receive a magic link and trial token, or email mcp@sig.ai for help. - Registry metadata:
server.jsonfor MCP directories and clients. - AI crawler summary:
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. 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 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)
{
"mcpServers": {
"food-logger": {
"type": "http",
"url": "https://foodlog.sig.ai/api/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}
Claude Code (CLI)
claude mcp add --transport http food-logger https://foodlog.sig.ai/api/mcp \
--header "Authorization: Bearer YOUR_TOKEN"
TypeScript / Node (programmatic)
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)
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:
- The
Acceptheader must advertise bothapplication/jsonandtext/event-stream(sending onlyapplication/jsonreturns406). - The response is SSE-framed (
event: message\ndata: {…}), so strip thedata:prefix to get the JSON-RPC envelope.
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_refs, 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) orimage_base64— a server-localimage_pathis meaningless for a remote client. - Supported types: JPEG, PNG, GIF, WebP; max 5 MB.
image_urldownloads 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_base64may be raw standard base64 or adata:image/...;base64,...URL. The decoded bytes must match the declaredmedia_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 withupdate_entryor removed withdelete_entry.
5. Example JSON responses
get_account_status (response_format: "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"):
{
"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:
{
"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"):
{
"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"):
{
"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"):
{
"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:
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"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"):
{
"deleted": true,
"deleted_count": 1,
"delete_all": false,
"effective_date": "2026-06-01",
"current_targets": null
}
set_targets (response_format: "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"):
{
"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"):
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"):
{
"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,loggedare always present.notesmay benull.nutrients/macrosmaps are sparse — only nutrients the model reported appear (a key's absence means "unknown", not zero). This matches every other tool's nutrient shape.entriesanddaycarry the logged diary rows + updated day totals only whenlog: true(and at least one item was detected). When analyzing without logging,entriesis omitted anddayisnull— gate onlogged === truebefore reading either.
6. Caveats & limits
- Date defaults are UTC unless you pass
timezone. For local-day behavior, pass an IANA time zone such asAmerica/Los_Angelestolog_food,analyze_meal_photo,get_daily_summary,get_logging_consistency,get_top_contributors,get_remaining_targets,get_target_progress,get_meal_breakdown,set_targets, orget_targets. Explicitlog_date,date, andeffective_datealways win. - Dates must be valid ISO calendar dates. Use
YYYY-MM-DD; impossible dates such as2026-02-31are rejected rather than normalized. logged_atmust be an ISO timestamp with a timezone. UseZor a numeric offset, for example2026-06-12T19:30:00Zor2026-06-12T12:30:00-07:00. Timestamp strings withoutZ/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 setFDC_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, andget_meal_breakdownare capped at 92 inclusive calendar days.export_entriesis capped by row count instead: fetch up to 1,000 rows per call and useoffsetfor 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:
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 to create or recover access, or
email mcp@sig.ai. (For actual tool calls without an SDK,
see Raw JSON-RPC — tools/call works without an
initialize handshake, but the response is SSE-framed.)