Hermes Nutrition Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Hermes Nutrition Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A local-first nutrition tracker exposed as an HTTP MCP server for Hermes. SQLite is the source of truth for foods, recipes, and meal history; the agent does not need to memorize nutrition facts.
The catalog separates a generic food from the products that can satisfy it:
mozzarella, tuna, or pickles.Milbona.Lidl; one product may be linked to multiple retailers.For example, a Mozzarella food type can contain several branded products. If the Milbona product is sold at Lidl and is the default:
mozzarella resolves to the default Milbona product.milbona mozzarella resolves from its brand and food-type relationship.lidl mozzarella resolves from its retailer and food-type relationship.Resolution is conservative. Exact product aliases are checked first, then generic aliases/defaults, then exact relationship phrases. Ambiguous relationship phrases fail instead of selecting an arbitrary product.
Product and food-type notes, aliases, brands, retailers, and names are searchable. Notes are useful for details such as low moisture, only sold at Lidl, or the jar with the green lid.
Use add_food_product for all new packaged foods and ingredients. It requires:
usual_portion_grams;usual_portion_name, such as 1 slice, 1 piece, or usual serving.The older add_food tool remains available for backward compatibility and unusual serving-only data. audit_foods lists migrated or legacy foods that still lack a reliable weight or per-100g facts. Repair those with update_food_product.
Tracked nutrients are calories, protein, carbs, fat, fiber, sugars, saturated fat, and salt. salt_g means grams of salt from the nutrition label, not milligrams of sodium.
Historical entries always keep their stored snapshots when products, defaults, or recipes change later.
http://HOST:8765/mcpGET / and GET /healthIf MCP_TOKEN is set, every HTTP request must include:
Authorization: Bearer <token>PUBLIC_HOSTS is a comma-separated list of hostnames or IP addresses accepted by MCP DNS-rebinding protection. Do not include a scheme or port.
Food products:
add_food_product, update_food_product, get_food, search_foods, list_foodsadd_food, update_food for backward compatibilityadd_alias, delete_food, audit_foodsGeneric food catalog:
add_food_type, update_food_type, get_food_type, search_food_typesadd_food_type_alias, assign_food_to_type, set_default_foodadd_retailer, link_food_retailer, unlink_food_retailer, list_retailersRecipes:
add_recipe, update_recipe, get_recipe, search_recipes, delete_recipeLogging and history:
log_food, log_recipeget_day, get_entries, get_weekly_reportupdate_entry, bulk_update_entries, delete_entry, finalize_daylist_aliases, healthCreate the generic type first:
{
"name": "Mozzarella",
"aliases": ["mozzarella", "mozz"],
"notes": "Generic mozzarella used on pizza"
}Then create a branded product with per-100g nutrition and a usual portion:
{
"name": "Mozzarella",
"brand": "Milbona",
"food_type_alias": "mozzarella",
"kcal_per_100g": 250,
"protein_g_per_100g": 18.5,
"carbs_g_per_100g": 2.0,
"fat_g_per_100g": 19.0,
"fiber_g_per_100g": 0,
"sugars_g_per_100g": 1.0,
"saturated_fat_g_per_100g": 13.0,
"salt_g_per_100g": 0.6,
"usual_portion_grams": 40,
"usual_portion_name": "pizza portion",
"aliases": ["milbona mozzarella"],
"retailers": ["Lidl"],
"make_default": true,
"notes": "Low-moisture bag normally used for pizza"
}After this, all of these log the same product:
{"alias": "mozzarella", "grams": 60}{"alias": "milbona mozzarella", "grams": 60}{"alias": "lidl mozzarella", "grams": 60}Use set_default_food to change which branded product a generic alias resolves to. Existing meal entries are unchanged.
Recipe items may target either a food or another recipe. A nested recipe must have a positive yield_grams, because the parent needs to know what fraction is used. Cycles are rejected.
Example dough recipe:
{
"name": "Pizza dough batch",
"aliases": ["pizza dough"],
"yield_grams": 600,
"ingredients": [
{"alias": "flour", "grams": 400},
{"alias": "olive oil", "grams": 20}
]
}Use 165 g of that recipe inside a pizza:
{
"name": "Tuna pizza",
"aliases": ["tuna pizza"],
"yield_grams": 285,
"ingredients": [
{"recipe_alias": "pizza dough", "grams": 165},
{"alias": "tuna", "grams": 60},
{"alias": "mozzarella", "grams": 60}
]
}Recipe responses include direct_items and fully flattened ingredients. Logs snapshot the flattened components and source paths, so the complete history remains understandable after later recipe edits.
One-off recipe changes are normalized and stored in the entry:
{
"alias": "tuna pizza",
"adjustments": [
{"alias": "mozzarella", "delta_grams": 20}
]
}Each stored adjustment records the original request and before/after/delta quantities and grams. Updating the logged recipe quantity scales those snapshots consistently.
update_entry accepts either quantity or grams for one entry. bulk_update_entries applies several corrections atomically:
{
"date": "2026-07-17",
"updates": [
{"entry_id": 104, "grams": 85},
{"entry_id": 105, "scale_factor": 0.9},
{"entry_id": 107, "quantity": 2, "note": "Corrected from photo"}
]
}Each item may contain only one of quantity, grams, or scale_factor. If any correction is invalid, the entire request is rolled back.
At startup the server checks schema_meta. Schema 1-3 databases are upgraded automatically to schema 4 before MCP tools are registered.
The migration:
/data/backups.PRAGMA quick_check.Foods without a known serving weight are preserved in legacy mode as needs_review; the migration never invents grams or per-100g values. The /health result reports migration status and review counts.
Inside the container:
/data/nutrition.db/data/backups/data/exports/daily/YYYY-MM-DD.md/data/exports/csv/YYYY-MM-DD.csv/data/exports/json/YYYY-MM-DD.jsonCSV and JSON exports include grams, amount source, per-100g nutrition, and persistent recipe adjustments.
py -3.12 -m venv .venv
.\.venv\Scripts\python -m pip install -e ".[test]"
.\.venv\Scripts\python -m pytest
.\.venv\Scripts\python -m app.mainTest the local endpoint:
curl -H "Authorization: Bearer change-me" http://localhost:8765/healthThe included compose file builds locally and persists the database in ./data:
docker compose up -d --build
docker compose logs -f nutrition-mcpFor Unraid, use a host path instead of the relative volume:
services:
nutrition-mcp:
image: ghcr.io/ispas-catalin/nutrition-mcp:0.5.0
container_name: nutrition-mcp
restart: unless-stopped
ports:
- "8765:8765"
environment:
DATA_DIR: /data
TZ: Europe/Bucharest
MCP_TOKEN: "replace-with-a-long-random-token"
PUBLIC_HOSTS: "192.168.1.142,nutrition-mcp"
HOST: 0.0.0.0
PORT: 8765
volumes:
- /mnt/user/appdata/nutrition-mcp:/dataAfter replacing the token:
docker compose pull
docker compose up -d
curl -H "Authorization: Bearer YOUR_REAL_TOKEN" http://192.168.1.142:8765/healthnutrition-mcpghcr.io/ispas-catalin/nutrition-mcp:0.5.0bridge8765 to container 8765 TCP/mnt/user/appdata/nutrition-mcp to /dataTZ=Europe/BucharestMCP_TOKEN=<long random token>PUBLIC_HOSTS=192.168.1.142,nutrition-mcphttp://[IP]:[PORT:8765]/mcp_servers:
nutrition:
url: "http://192.168.1.142:8765/mcp"
headers:
Authorization: "Bearer <REAL_MCP_TOKEN>"Detailed agent behavior and call examples are in HERMES_AGENT_INSTRUCTIONS.md.
8765 on the trusted LAN; do not expose it directly to the internet./data private because it contains meal history and nutrition preferences./mnt/user/appdata/nutrition-mcp before major host changes even though schema migrations create their own database backup.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.