Skip to main content
Glama

Open Food Facts MCP Server

by caleb-conner
client.ts3.89 kB
import axios, { AxiosInstance } from 'axios'; import { OpenFoodFactsConfig, ProductResponse, SearchResponse, ProductResponseSchema, SearchResponseSchema } from './types.js'; export class OpenFoodFactsClient { private client: AxiosInstance; private config: OpenFoodFactsConfig; private requestCounts: Map<string, { count: number; resetTime: number }> = new Map(); constructor(config: OpenFoodFactsConfig) { this.config = config; this.client = axios.create({ baseURL: config.baseUrl, headers: { 'User-Agent': config.userAgent, 'Accept': 'application/json', }, timeout: 30000, }); } private async checkRateLimit(endpoint: 'products' | 'search' | 'facets'): Promise<void> { const now = Date.now(); const key = endpoint; const limit = this.config.rateLimits[endpoint]; let tracker = this.requestCounts.get(key); if (!tracker || now > tracker.resetTime) { tracker = { count: 0, resetTime: now + 60000 }; // Reset every minute this.requestCounts.set(key, tracker); } if (tracker.count >= limit) { const waitTime = tracker.resetTime - now; if (waitTime > 0) { throw new Error(`Rate limit exceeded for ${endpoint}. Wait ${Math.ceil(waitTime / 1000)} seconds.`); } } tracker.count++; } async getProduct(barcode: string): Promise<ProductResponse> { await this.checkRateLimit('products'); try { const response = await this.client.get(`/api/v2/product/${barcode}`); return ProductResponseSchema.parse(response.data); } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Failed to fetch product ${barcode}: ${error.response?.status} ${error.message}`); } throw error; } } async searchProducts(params: { search?: string; categories?: string; brands?: string; countries?: string; page?: number; page_size?: number; sort_by?: string; nutrition_grades?: string; nova_groups?: string; } = {}): Promise<SearchResponse> { await this.checkRateLimit('search'); const searchParams = new URLSearchParams(); if (params.search) searchParams.append('search_terms', params.search); if (params.categories) searchParams.append('categories_tags', params.categories); if (params.brands) searchParams.append('brands_tags', params.brands); if (params.countries) searchParams.append('countries_tags', params.countries); if (params.nutrition_grades) searchParams.append('nutrition_grades_tags', params.nutrition_grades); if (params.nova_groups) searchParams.append('nova_groups_tags', params.nova_groups); if (params.sort_by) searchParams.append('sort_by', params.sort_by); searchParams.append('page', String(params.page || 1)); searchParams.append('page_size', String(Math.min(params.page_size || 20, 100))); searchParams.append('json', '1'); try { const response = await this.client.get(`/cgi/search.pl?${searchParams.toString()}`); return SearchResponseSchema.parse(response.data); } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Search failed: ${error.response?.status} ${error.message}`); } throw error; } } async getProductsByBarcodes(barcodes: string[]): Promise<ProductResponse[]> { const results: ProductResponse[] = []; for (const barcode of barcodes) { try { const product = await this.getProduct(barcode); results.push(product); // Small delay to respect rate limits await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { results.push({ status: 0, status_verbose: `Error fetching ${barcode}: ${error instanceof Error ? error.message : 'Unknown error'}`, }); } } return results; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/caleb-conner/open-food-facts-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server