Ask an agent how many calories are in a Luckin Coconut Latte and you'll get an answer.

You just have no way to check it. The number might be right, or it might be a plausible-looking figure invented on the spot — either way it arrives in exactly the same confident tone.

That's not a model problem. It's a data problem. Ask any agent about Western chain food and it does fine: OpenFoodFacts, USDA, MyFitnessPal dumps are everywhere. Ask about Chinese chains — Luckin, Mixue, Heytea, Haidilao, KFC China, McDonald's China — and there is essentially no structured nutrition data an agent can call. The numbers exist, but they're scattered across brand mini-programs, PDF menu boards, and marketing pages, in Chinese, with no machine-readable field for "per 100ml" vs "per cup".

So I built foodcalorie-mcp — an MCP server that exposes Chinese food and chain-restaurant nutrition as six tools any agent can call.

This post is about the three design decisions that mattered, and the one that turned out to be uncomfortable.

The tool surface

Six tools, shaped around what an agent actually asks rather than around my database schema:

Tool What it answers
search_food_by_name "Luckin coconut latte" → matched items with brand inference
get_daily_nutrition_summary "I had these 5 things today" → totals + % of daily reference values
compare_foods side-by-side macro comparison across items
get_health_advice per-item guidance based on the macros present
browse_by_category explore 18 categories (drinks, fast food, snacks, fruit, dairy…)
search_food_by_barcode lookup by code — see the caveat below

Config is five lines:

{ "mcpServers": { "foodcalorie": { "command": "python", "args": ["-m", "foodcalorie_mcp.server"] } } } 

Or uvx --from foodcalorie-mcp foodcalorie-mcp, or a .mcpb bundle for one-click install.

Decision 1: open core, loaded at runtime, zero code changes

The free build ships 103 general Chinese food items compiled into the package. Brand-menu data is not in the package — it's a JSON file pointed to by an environment variable:

export FOODCALORIE_EXTRA_DATA=/path/to/pro_foods.json 

On startup the server merges the two sets. Verified: 103 + 116 = 219 records, no ID collisions, no duplicated names, name search still returns the right thing across both halves.

I picked this over "free tier hits my API" because a nutrition lookup is a pure function of static data. Making it a network call would add latency, a privacy surface, and an uptime dependency for something that changes a few times a year. The trade-off is obvious and I'll state it plainly: with a local JSON file, there is no server-side enforcement of a licence. What you pay for is the data and the sourcing work, not a gate.

Decision 2: field coverage is published, not hidden

Every nutrition dataset has holes. The question is whether the vendor tells you where they are.

So the Pro dataset ships with its own coverage audit, and the README repeats it:

  • calories: 116 / 116
  • protein: 59 / 116
  • fat: 61 / 116
  • carbohydrate: 54 / 116
  • sugar: 15 / 116
  • sodium: 4 / 116
  • caffeine: 21 / 116
  • allergens: 0 / 116

That last line is the one I get asked about. Chinese chains publish macros much more often than allergens, and an allergen field that is 60% missing is worse than no field at all — an agent will treat a missing value as "contains none". So the schema has the field, the data leaves it empty, and the tool description tells the model not to infer it.

Same reasoning for barcodes. search_food_by_barcode is a real lookup against the dataset keys, but the free package's keys are sample codes, not real EANs. The README says so; the tool docstring says so. If you want to search, use name search.

I'd rather ship a capability with a visible asterisk than a capability that quietly lies.

Decision 3: every Pro row carries its source

Pro records include the URL the number came from — brand nutrition pages, official menu PDFs, mini-program screens. Two reasons: buyers can audit a row they doubt, and I can't quietly drift into "estimated" values without noticing.

Chain menus change constantly and portion sizes vary (medium vs large, hot vs iced, China-only SKUs). Maintaining this is the actual work. 116 rows across 14 brands is not a lot of rows; it's a lot of verification.

What shipping to the official SDKs taught me

Before selling data, I wanted some technical credit, so I sent small pull requests to the official MCP SDKs: a typing.GenericAlias detection fix in python-sdk, and a listChanged capability fix in typescript-sdk.

One of them didn't go the way I wanted. I opened a PR arguing that a request without an Authorization header should return a 401 with a proper Bearer challenge per RFC 6750 §3.1, rather than an error="invalid_token" body. Technically correct. It was closed without merge, because the maintainers were prioritising other things.

The lesson wasn't about HTTP semantics. It was: a contribution is only worth what the maintainer's roadmap says it's worth. I had optimised for being right instead of being useful. That reframing changed how I built the tool afterwards — every tool in the server exists because an agent needs it mid-conversation, not because it was elegant to implement.

Try it without installing anything

There's a web lookup page — the full free 103 items, no login, no install:

foodcalorie lookup →

Search "KFC" and you'll see exactly what the free/Pro split looks like: one sample row in the free set, 24 rows in Pro. That's a more honest demo than a comparison table.

The free package is genuinely usable on its own — 103 items is enough to wire up an agent and see whether the tool-call shape suits you.

On pricing

The brand-menu dataset is a one-time purchase, not a subscription: $6.99 internationally via Agensi, ¥29 early-bird domestically (Alipay/WeChat — China-only payment rails, which is its own whole story).

No DRM, no phone-home, no expiry. If that seems underpriced for 116 verified rows with sources: it probably is, and it's a deliberate early-stage choice while I find out whether anyone actually wants this.

The uncomfortable part

Trust is harder to accumulate than features.

I can ship a better tool surface in a weekend. I cannot ship "this person's data is correct" in a weekend — that has to be earned row by row, source by source, and it can be lost in one bad number. Which is why the coverage table above is published, why the allergen field is empty rather than guessed, and why the barcode tool says what it doesn't do.

If you're building an agent that touches food, diet, fitness, or ordering, the free package costs you one pip install. If it saves you from one hallucinated calorie count, it paid for itself.

— an independent developer, currently maintaining 116 food rows and a growing pile of source PDFs