nutritionix-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Nutritionix MCP
2
+
3
+ MCP server for Nutritionix: food search, nutrients. Needs free app keys.
4
+
5
+ Pairs well with `spoonacular-mcp`, `themealdb-mcp`, and `open-food-facts-mcp`.
6
+
7
+ ## Setup
8
+
9
+ Get keys at https://developer.nutritionix.com/ (free), then:
10
+
11
+ ```bash
12
+ export NUTRITIONIX_APP_ID=your_id
13
+ export NUTRITIONIX_API_KEY=your_key
14
+ npm install
15
+ npm run build
16
+ node dist/index.js
17
+ ```
18
+
19
+ The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
20
+
21
+ ## Tools at a glance
22
+
23
+ - `search_foods`: Calories and macros.
24
+ - `analyze_meal`: Nutrients for free-text meals with totals.
25
+
26
+ ## Limits and privacy
27
+
28
+ This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. Keys stay local and go only to Nutritionix. Read-only.
package/dist/api.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare class NutritionixError extends Error {
2
+ }
3
+ export interface Food {
4
+ name: string;
5
+ calories?: number;
6
+ protein?: number;
7
+ fat?: number;
8
+ carbs?: number;
9
+ serving?: string;
10
+ }
11
+ export declare function searchFoods(query: string, limit?: number): Promise<Food[]>;
12
+ export declare function analyzeMeal(text: string): Promise<Food[]>;
13
+ export declare function formatFood(f: Food, index?: number): string;
package/dist/api.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Nutritionix API v2 client. Needs NUTRITIONIX_APP_ID + NUTRITIONIX_API_KEY
3
+ * (free at https://developer.nutritionix.com/).
4
+ * Docs: https://developer.nutritionix.com/docs/v2
5
+ */
6
+ const BASE = "https://trackapi.nutritionix.com/v2";
7
+ export class NutritionixError extends Error {
8
+ }
9
+ function headers(extra = {}) {
10
+ const id = process.env.NUTRITIONIX_APP_ID;
11
+ const key = process.env.NUTRITIONIX_API_KEY;
12
+ if (!id || !key)
13
+ throw new NutritionixError("Set NUTRITIONIX_APP_ID and NUTRITIONIX_API_KEY first (free at developer.nutritionix.com).");
14
+ return { "User-Agent": "nutritionix-mcp/1.0", Accept: "application/json", "x-app-id": id, "x-app-key": key, ...extra };
15
+ }
16
+ async function getJson(path) {
17
+ const res = await fetch(`${BASE}${path}`, {
18
+ headers: headers(),
19
+ signal: AbortSignal.timeout(20000),
20
+ });
21
+ if (res.status === 401 || res.status === 403)
22
+ throw new NutritionixError("Nutritionix refused (401/403). Check app id and key.");
23
+ if (res.status === 404)
24
+ throw new NutritionixError("Not found.");
25
+ if (!res.ok)
26
+ throw new NutritionixError(`Nutritionix error ${res.status}`);
27
+ return (await res.json());
28
+ }
29
+ async function postJson(path, body) {
30
+ const res = await fetch(`${BASE}${path}`, {
31
+ method: "POST",
32
+ headers: headers({ "Content-Type": "application/json" }),
33
+ body: JSON.stringify(body),
34
+ signal: AbortSignal.timeout(20000),
35
+ });
36
+ if (res.status === 401 || res.status === 403)
37
+ throw new NutritionixError("Nutritionix refused (401/403). Check app id and key.");
38
+ if (!res.ok)
39
+ throw new NutritionixError(`Nutritionix error ${res.status}`);
40
+ return (await res.json());
41
+ }
42
+ export async function searchFoods(query, limit = 5) {
43
+ if (!query.trim())
44
+ throw new NutritionixError("Query is empty.");
45
+ const data = await getJson(`/search/instant?query=${encodeURIComponent(query.trim())}`);
46
+ const branded = Array.isArray(data.branded) ? data.branded : [];
47
+ const common = Array.isArray(data.common) ? data.common : [];
48
+ const out = [];
49
+ for (const f of common.slice(0, limit))
50
+ out.push({ name: String(f.food_name ?? "?") });
51
+ for (const f of branded.slice(0, Math.max(limit - out.length, 0))) {
52
+ out.push({
53
+ name: String(f.food_name ?? "?"),
54
+ calories: f.nf_calories,
55
+ protein: f.nf_protein,
56
+ fat: f.nf_total_fat,
57
+ carbs: f.nf_total_carbohydrate,
58
+ serving: f.serving_unit ? `${f.serving_qty ?? 1} ${f.serving_unit}` : undefined,
59
+ });
60
+ }
61
+ return out.slice(0, limit);
62
+ }
63
+ export async function analyzeMeal(text) {
64
+ if (!text.trim())
65
+ throw new NutritionixError("Text is empty.");
66
+ const data = await postJson("/natural/nutrients", { query: text.trim() });
67
+ const rows = Array.isArray(data.foods) ? data.foods : [];
68
+ return rows.slice(0, 10).map((f) => ({
69
+ name: String(f.food_name ?? "?"),
70
+ calories: f.nf_calories,
71
+ protein: f.nf_protein,
72
+ fat: f.nf_total_fat,
73
+ carbs: f.nf_total_carbohydrate,
74
+ serving: f.serving_unit ? `${f.serving_qty ?? 1} ${f.serving_unit}` : undefined,
75
+ }));
76
+ }
77
+ export function formatFood(f, index) {
78
+ const prefix = index !== undefined ? `${index + 1}. ` : "";
79
+ const macros = [f.calories !== undefined ? `${f.calories} kcal` : "", f.protein !== undefined ? `P ${f.protein}g` : "", f.fat !== undefined ? `F ${f.fat}g` : "", f.carbs !== undefined ? `C ${f.carbs}g` : ""].filter(Boolean).join(" · ");
80
+ return `${prefix}${f.name}${macros ? ` — ${macros}` : ""}${f.serving ? ` (${f.serving})` : ""}`;
81
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ async function main() {
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ console.error("MCP server running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,58 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { analyzeMeal, formatFood, NutritionixError, searchFoods } from "./api.js";
4
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ export function createServer() {
8
+ const server = new McpServer({
9
+ name: "nutritionix-mcp",
10
+ version: "1.0.0",
11
+ });
12
+ server.registerTool("search_foods", {
13
+ title: "Search foods",
14
+ description: "Foods by name with calories and macros where known.",
15
+ inputSchema: z.object({
16
+ query: z.string().describe("Food, e.g. 'banana', 'big mac'"),
17
+ limit: z.number().int().min(1).max(20).default(5),
18
+ }),
19
+ annotations: READ_ONLY,
20
+ }, async ({ query, limit }) => {
21
+ try {
22
+ const rows = await searchFoods(query, limit);
23
+ if (rows.length === 0)
24
+ return text(`No foods for "${query}".`);
25
+ return text(rows.map((f, i) => formatFood(f, i)).join("\n"));
26
+ }
27
+ catch (e) {
28
+ return textError(errorMessage(e));
29
+ }
30
+ });
31
+ server.registerTool("analyze_meal", {
32
+ title: "Analyze meal",
33
+ description: "Nutrients for free-text meal descriptions like '2 eggs and toast'.",
34
+ inputSchema: z.object({
35
+ text: z.string().describe("Meal description"),
36
+ }),
37
+ annotations: READ_ONLY,
38
+ }, async ({ text: meal }) => {
39
+ try {
40
+ const rows = await analyzeMeal(meal);
41
+ if (rows.length === 0)
42
+ return text("Nothing recognized.");
43
+ const total = rows.reduce((n, f) => n + (f.calories ?? 0), 0);
44
+ return text(`${rows.map((f, i) => formatFood(f, i)).join("\n")}\n\nTotal: ${Math.round(total)} kcal`);
45
+ }
46
+ catch (e) {
47
+ return textError(errorMessage(e));
48
+ }
49
+ });
50
+ return server;
51
+ }
52
+ function errorMessage(e) {
53
+ if (e instanceof NutritionixError)
54
+ return `Error: ${e.message}`;
55
+ if (e instanceof Error)
56
+ return `Error: ${e.message}`;
57
+ return `Error: ${String(e)}`;
58
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "nutritionix-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Nutritionix: food search, nutrients. Needs free app keys.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/nutritionix-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrfentmen/awesome-mcps.git"
10
+ },
11
+ "bin": {
12
+ "nutritionix-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "start": "node dist/index.js",
21
+ "dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
22
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "nutritionix",
27
+ "nutrition",
28
+ "api"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.0.4",
33
+ "zod": "^3.23.8"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.6.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ }
42
+ }