@unclick/openfoodfacts-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Apache License 2.0
2
+
3
+ Copyright (c) 2026 UnClick / malamutemayhem
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Open Food Facts MCP by UnClick
2
+
3
+ Search food products, ingredients, nutrition, brands, and categories. No API key.
4
+
5
+ > By UnClick. 180+ tools plus persistent agent memory in one install: https://unclick.world
6
+
7
+ ## Install
8
+
9
+ Installs straight from GitHub, no npm account needed.
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "openfoodfacts": {
15
+ "command": "npx",
16
+ "args": ["-y", "https://github.com/malamutemayhem/unclick/releases/download/standalone-mcps-latest/openfoodfacts.tgz"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## Tools
23
+
24
+ - `food_search`
25
+ - `food_get_product`
26
+ - `food_by_category`
27
+
28
+ ## Want the rest?
29
+
30
+ This is one connector. [UnClick](https://unclick.world) bundles 180+ tools plus
31
+ persistent cross-session agent memory in a single install.
32
+
33
+ ## License
34
+
35
+ Apache-2.0
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // Open Food Facts MCP. Standalone MCP server by UnClick.
3
+ // By UnClick. 180+ tools plus persistent agent memory in one install: https://unclick.world
4
+ //
5
+ // Generated from the UnClick connector by scripts/generate-standalone-mcp.mjs.
6
+ // Edit the connector in the UnClick monorepo, not here.
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
10
+ import { searchFoodProducts, getFoodProduct, getFoodByCategory, } from "./openfoodfacts-tool.js";
11
+ const TOOLS = [
12
+ {
13
+ name: "food_search",
14
+ description: "Search for food products on Open Food Facts.",
15
+ inputSchema: {
16
+ type: "object",
17
+ additionalProperties: false,
18
+ properties: {
19
+ query: { type: "string" },
20
+ page: { type: "number" },
21
+ page_size: { type: "number" },
22
+ },
23
+ required: ["query"],
24
+ },
25
+ },
26
+ {
27
+ name: "food_get_product",
28
+ description: "Get a food product from Open Food Facts by barcode.",
29
+ inputSchema: {
30
+ type: "object",
31
+ additionalProperties: false,
32
+ properties: {
33
+ barcode: { type: "string" },
34
+ },
35
+ required: ["barcode"],
36
+ },
37
+ },
38
+ {
39
+ name: "food_by_category",
40
+ description: "Get food products by category from Open Food Facts.",
41
+ inputSchema: {
42
+ type: "object",
43
+ additionalProperties: false,
44
+ properties: {
45
+ category: { type: "string" },
46
+ page: { type: "number" },
47
+ },
48
+ required: ["category"],
49
+ },
50
+ }
51
+ ];
52
+ const HANDLERS = {
53
+ food_search: (args) => searchFoodProducts(args),
54
+ food_get_product: (args) => getFoodProduct(args),
55
+ food_by_category: (args) => getFoodByCategory(args),
56
+ };
57
+ const server = new Server({ name: "io.github.malamutemayhem/openfoodfacts", version: "0.1.0" }, { capabilities: { tools: {} } });
58
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
59
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
60
+ const handler = HANDLERS[req.params.name];
61
+ if (!handler) {
62
+ return { content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }], isError: true };
63
+ }
64
+ try {
65
+ const result = await handler((req.params.arguments ?? {}));
66
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
67
+ }
68
+ catch (err) {
69
+ const message = err instanceof Error ? err.message : String(err);
70
+ return { content: [{ type: "text", text: message }], isError: true };
71
+ }
72
+ });
73
+ async function main() {
74
+ const transport = new StdioServerTransport();
75
+ await server.connect(transport);
76
+ }
77
+ main().catch((err) => {
78
+ process.stderr.write(`[openfoodfacts-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
79
+ process.exit(1);
80
+ });
@@ -0,0 +1,105 @@
1
+ // ─── Open Food Facts Product Database ────────────────────────────────────────
2
+ // Free, open-source food product database - no auth required.
3
+ // Docs: https://wiki.openfoodfacts.org/API
4
+ const OFF_BASE = "https://world.openfoodfacts.org";
5
+ // ─── API helper ──────────────────────────────────────────────────────────────
6
+ async function offFetch(url) {
7
+ const res = await fetch(url, {
8
+ headers: { "User-Agent": "UnClick-MCP/1.0 (mcp@unclick.io)" },
9
+ });
10
+ if (!res.ok) {
11
+ throw new Error(`Open Food Facts API HTTP ${res.status}: ${url}`);
12
+ }
13
+ return res.json();
14
+ }
15
+ function mapProduct(p) {
16
+ return {
17
+ code: p.code ?? p._id,
18
+ product_name: p.product_name ?? p.product_name_en ?? null,
19
+ brands: p.brands ?? null,
20
+ categories: p.categories ?? null,
21
+ quantity: p.quantity ?? null,
22
+ image_url: p.image_url ?? null,
23
+ nutriscore_grade: p.nutriscore_grade ?? null,
24
+ ecoscore_grade: p.ecoscore_grade ?? null,
25
+ nova_group: p.nova_group ?? null,
26
+ ingredients_text: p.ingredients_text ?? null,
27
+ allergens: p.allergens ?? null,
28
+ labels: p.labels ?? null,
29
+ countries: p.countries ?? null,
30
+ nutriments: p.nutriments
31
+ ? {
32
+ energy_kcal_100g: p.nutriments["energy-kcal_100g"] ?? null,
33
+ fat_100g: p.nutriments["fat_100g"] ?? null,
34
+ saturated_fat_100g: p.nutriments["saturated-fat_100g"] ?? null,
35
+ carbohydrates_100g: p.nutriments["carbohydrates_100g"] ?? null,
36
+ sugars_100g: p.nutriments["sugars_100g"] ?? null,
37
+ fiber_100g: p.nutriments["fiber_100g"] ?? null,
38
+ proteins_100g: p.nutriments["proteins_100g"] ?? null,
39
+ salt_100g: p.nutriments["salt_100g"] ?? null,
40
+ }
41
+ : null,
42
+ };
43
+ }
44
+ // ─── Operations ──────────────────────────────────────────────────────────────
45
+ export async function searchFoodProducts(args) {
46
+ const query = String(args.query ?? args.search_terms ?? "").trim();
47
+ if (!query)
48
+ throw new Error("query is required.");
49
+ const page = Math.max(1, Number(args.page ?? 1));
50
+ const pageSize = Math.min(50, Math.max(1, Number(args.page_size ?? 10)));
51
+ const url = new URL(`${OFF_BASE}/cgi/search.pl`);
52
+ url.searchParams.set("search_terms", query);
53
+ url.searchParams.set("json", "1");
54
+ url.searchParams.set("page", String(page));
55
+ url.searchParams.set("page_size", String(pageSize));
56
+ url.searchParams.set("fields", "code,product_name,brands,categories,quantity,image_url,nutriscore_grade,ecoscore_grade,nova_group,nutriments");
57
+ const data = await offFetch(url.toString());
58
+ return {
59
+ query,
60
+ count: data.count,
61
+ page,
62
+ page_size: pageSize,
63
+ products: (data.products ?? []).map(mapProduct),
64
+ };
65
+ }
66
+ export async function getFoodProduct(args) {
67
+ const barcode = String(args.barcode ?? "").trim().replace(/[^0-9]/g, "");
68
+ if (!barcode)
69
+ throw new Error("barcode is required (numeric EAN/UPC code).");
70
+ const data = await offFetch(`${OFF_BASE}/api/v2/product/${barcode}.json`);
71
+ if (data.status !== 1 || !data.product) {
72
+ return { error: `Product with barcode ${barcode} not found.` };
73
+ }
74
+ return mapProduct({ ...data.product, code: barcode });
75
+ }
76
+ export async function getFoodByCategory(args) {
77
+ const category = String(args.category ?? "").trim().toLowerCase().replace(/\s+/g, "-");
78
+ if (!category)
79
+ throw new Error("category is required (e.g. 'biscuits', 'dairy').");
80
+ const page = Math.max(1, Number(args.page ?? 1));
81
+ const pageSize = Math.min(50, Math.max(1, Number(args.page_size ?? 10)));
82
+ const url = new URL(`${OFF_BASE}/category/${category}.json`);
83
+ url.searchParams.set("page", String(page));
84
+ url.searchParams.set("page_size", String(pageSize));
85
+ const data = await offFetch(url.toString());
86
+ return {
87
+ category,
88
+ count: data.count ?? 0,
89
+ page,
90
+ products: (data.products ?? []).slice(0, pageSize).map(mapProduct),
91
+ };
92
+ }
93
+ // ─── Public dispatcher ────────────────────────────────────────────────────────
94
+ export async function openFoodFactsAction(action, args) {
95
+ switch (action) {
96
+ case "search_food_products": return searchFoodProducts(args);
97
+ case "get_food_product": return getFoodProduct(args);
98
+ case "get_food_by_category": return getFoodByCategory(args);
99
+ default:
100
+ return {
101
+ error: `Unknown Open Food Facts action: "${action}". ` +
102
+ "Valid: search_food_products, get_food_product, get_food_by_category.",
103
+ };
104
+ }
105
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@unclick/openfoodfacts-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.malamutemayhem/openfoodfacts",
5
+ "description": "Search food products, ingredients, nutrition, brands, and categories. No API key. By UnClick (https://unclick.world).",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "unclick",
10
+ "food",
11
+ "nutrition",
12
+ "open-food-facts"
13
+ ],
14
+ "author": "UnClick (https://unclick.world)",
15
+ "type": "module",
16
+ "bin": {
17
+ "openfoodfacts-mcp": "./dist/index.js"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "server.json"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "start": "node dist/index.js",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.15.1"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.6.0",
35
+ "@types/node": "^22.0.0"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/malamutemayhem/unclick.git",
41
+ "directory": "packages/standalone/openfoodfacts-mcp"
42
+ },
43
+ "homepage": "https://unclick.world"
44
+ }
package/server.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.malamutemayhem/openfoodfacts",
4
+ "title": "Open Food Facts MCP by UnClick",
5
+ "description": "Search food products, ingredients, nutrition, brands, and categories. No API key. By UnClick.",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://unclick.world",
8
+ "icons": [
9
+ {
10
+ "src": "https://unclick.world/favicon.png",
11
+ "mimeType": "image/png",
12
+ "sizes": [
13
+ "512x512"
14
+ ]
15
+ }
16
+ ],
17
+ "repository": {
18
+ "url": "https://github.com/malamutemayhem/unclick.git",
19
+ "source": "github",
20
+ "subfolder": "packages/standalone/openfoodfacts-mcp"
21
+ },
22
+ "packages": [
23
+ {
24
+ "registryType": "npm",
25
+ "identifier": "@unclick/openfoodfacts-mcp",
26
+ "version": "0.1.0",
27
+ "runtimeHint": "npx",
28
+ "transport": {
29
+ "type": "stdio"
30
+ }
31
+ }
32
+ ]
33
+ }