Skip to main content
Glama
domdomegg

openfoodfacts-mcp

search_products_lucene

Search Open Food Facts products using Lucene query syntax for advanced filtering, boolean logic, and negation capabilities.

Instructions

Search Open Food Facts using the Search-a-licious Elasticsearch backend. Powered by Lucene query syntax with full boolean logic and negation support.

Use this instead of search_products_standard when you need:

  • Negation queries: find gluten-free cereals with allergens_tags_without="en:gluten"

  • Filter-only browsing: categories_tags without any text query (standard API times out on this)

  • Combined text + filter with relevance scoring: text matches are ranked by relevance within filter results

  • Boolean logic in raw Lucene: brands:"kellogg*" OR brands:"nestle"

Trade-offs vs search_products_standard:

  • Counts are approximate (capped at 10,000 for large result sets)

  • Brand tag matching may be narrower (less normalization than standard)

  • Data has a short sync delay (hours) from the primary database

  • popularity sort uses scan counts rather than the standard popularity algorithm

Response format matches search_products_standard: { count, page, page_size, page_count, products: [...] }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryNoFree-text search terms. Combined with any filter params using AND logic. Omit to browse by filters alone (unlike search_products_standard, filter-only queries work here without timeouts).
categories_tagsNoFilter by category tag (e.g. "en:breakfast-cereals"). Added as categories_tags:"value" in the Lucene query.
brands_tagsNoFilter by brand tag (e.g. "nutella"). Added as brands_tags:"value" in the Lucene query.
nutrition_grades_tagsNoFilter by Nutri-Score grade (a, b, c, d, e). Added as nutriscore_grade:"value".
labels_tagsNoFilter by label tag (e.g. "en:organic", "en:fair-trade"). Added as labels_tags:"value".
countries_tagsNoFilter by country tag (e.g. "en:united-kingdom", "en:france"). Added as countries_tags:"value".
allergens_tags_withoutNoEXCLUDE products containing this allergen (e.g. "en:gluten", "en:milk"). This is negation — a capability unique to this tool. Added as -allergens_tags:"value". Use for allergen-free searches.
lucene_queryNoRaw Lucene query string for full control. If provided, all other filter params are ignored. Supports field:value, negation (-field:value), quoted phrases, wildcards. Examples: 'categories_tags:"en:beverages" nutriscore_grade:a -allergens_tags:"en:gluten"', 'brands:"kellogg*"'
sort_byNoSort order. Note: uses different underlying fields than search_products_standard.
sort_descendingNoSort in descending order (default: true). Set false for ascending (e.g. lowest nutriscore_score first).
pageNoPage number (default: 1)
page_sizeNoResults per page (default: 24, max: 100)
fieldsNoFields to return per product. Defaults to: code, product_name, brands, categories, nutriscore_grade, nova_group, image_url, quantity

Implementation Reference

  • The actual handler function for the `search_products_lucene` tool which constructs the Lucene query, fetches from the Search-a-licious API, normalizes the response, and returns the result.
    		async (args) => {
    			const luceneQuery = args.lucene_query ?? buildLuceneQuery(args);
    
    			const url = new URL(`${SEARCH_A_LICIOUS_BASE}/search`);
    			url.searchParams.set('q', luceneQuery);
    			url.searchParams.set('page', String(args.page));
    			url.searchParams.set('page_size', String(args.page_size));
    
    			if (args.sort_by) {
    				const mappedField = SORT_FIELD_MAP[args.sort_by] ?? args.sort_by;
    				const prefix = args.sort_descending ? '-' : '';
    				url.searchParams.set('sort_by', `${prefix}${mappedField}`);
    			}
    
    			const fields = args.fields ?? DEFAULT_FIELDS;
    			url.searchParams.set('fields', fields.join(','));
    
    			const response = await fetch(url.toString(), {
    				headers: {
    					'User-Agent': config.userAgent,
    					Accept: 'application/json',
    				},
    			});
    
    			if (!response.ok) {
    				const errorText = await response.text();
    				throw new Error(`Search-a-licious API error: ${response.status} ${response.statusText} - ${errorText}`);
    			}
    
    			const data = await response.json() as Record<string, unknown>;
    			const normalized = normalizeResponse(data);
    
    			return jsonResult(normalized);
    		},
    	);
    }
  • Zod input schema for the `search_products_lucene` tool.
    const inputSchema = strictSchemaWithAliases(
    	{
    		query: z.string().optional().describe('Free-text search terms. Combined with any filter params using AND logic. Omit to browse by filters alone (unlike search_products_standard, filter-only queries work here without timeouts).'),
    		categories_tags: z.string().optional().describe('Filter by category tag (e.g. "en:breakfast-cereals"). Added as categories_tags:"value" in the Lucene query.'),
    		brands_tags: z.string().optional().describe('Filter by brand tag (e.g. "nutella"). Added as brands_tags:"value" in the Lucene query.'),
    		nutrition_grades_tags: z.string().optional().describe('Filter by Nutri-Score grade (a, b, c, d, e). Added as nutriscore_grade:"value".'),
    		labels_tags: z.string().optional().describe('Filter by label tag (e.g. "en:organic", "en:fair-trade"). Added as labels_tags:"value".'),
    		countries_tags: z.string().optional().describe('Filter by country tag (e.g. "en:united-kingdom", "en:france"). Added as countries_tags:"value".'),
    		allergens_tags_without: z.string().optional().describe('EXCLUDE products containing this allergen (e.g. "en:gluten", "en:milk"). This is negation — a capability unique to this tool. Added as -allergens_tags:"value". Use for allergen-free searches.'),
    		lucene_query: z.string().optional().describe('Raw Lucene query string for full control. If provided, all other filter params are ignored. Supports field:value, negation (-field:value), quoted phrases, wildcards. Examples: \'categories_tags:"en:beverages" nutriscore_grade:a -allergens_tags:"en:gluten"\', \'brands:"kellogg*"\''),
    		sort_by: z.enum([
    			'popularity',
    			'product_name',
    			'created_t',
    			'last_modified_t',
    			'nutriscore_score',
    			'ecoscore_score',
    		]).optional().describe('Sort order. Note: uses different underlying fields than search_products_standard.'),
    		sort_descending: z.boolean().optional().default(true).describe('Sort in descending order (default: true). Set false for ascending (e.g. lowest nutriscore_score first).'),
    		page: z.number().int().min(1).default(1).describe('Page number (default: 1)'),
    		page_size: z.number().int().min(1).max(100).default(24).describe('Results per page (default: 24, max: 100)'),
    		fields: z.array(z.string()).optional().describe(`Fields to return per product. Defaults to: ${DEFAULT_FIELDS.join(', ')}`),
    	},
    	{
    		q: 'query',
    		search: 'query',
    	},
    );
  • Registration function for `search_products_lucene` within the MCP server.
    export function registerSearchProductsLucene(server: McpServer, config: Config): void {
    	server.registerTool(
    		'search_products_lucene',
    		{
    			title: 'Search products (Lucene)',
    			description: `Search Open Food Facts using the Search-a-licious Elasticsearch backend. Powered by Lucene query syntax with full boolean logic and negation support.
    
    Use this instead of search_products_standard when you need:
    - Negation queries: find gluten-free cereals with allergens_tags_without="en:gluten"
    - Filter-only browsing: categories_tags without any text query (standard API times out on this)
    - Combined text + filter with relevance scoring: text matches are ranked by relevance within filter results
    - Boolean logic in raw Lucene: brands:"kellogg*" OR brands:"nestle"
    
    Trade-offs vs search_products_standard:
    - Counts are approximate (capped at 10,000 for large result sets)
    - Brand tag matching may be narrower (less normalization than standard)
    - Data has a short sync delay (hours) from the primary database
    - popularity sort uses scan counts rather than the standard popularity algorithm
    
    Response format matches search_products_standard: { count, page, page_size, page_count, products: [...] }`,
    			inputSchema,
    			annotations: {
    				readOnlyHint: true,
    			},
    		},
    		async (args) => {
    			const luceneQuery = args.lucene_query ?? buildLuceneQuery(args);
    
    			const url = new URL(`${SEARCH_A_LICIOUS_BASE}/search`);
    			url.searchParams.set('q', luceneQuery);
    			url.searchParams.set('page', String(args.page));
    			url.searchParams.set('page_size', String(args.page_size));
    
    			if (args.sort_by) {
    				const mappedField = SORT_FIELD_MAP[args.sort_by] ?? args.sort_by;
    				const prefix = args.sort_descending ? '-' : '';
    				url.searchParams.set('sort_by', `${prefix}${mappedField}`);
    			}
    
    			const fields = args.fields ?? DEFAULT_FIELDS;
    			url.searchParams.set('fields', fields.join(','));
    
    			const response = await fetch(url.toString(), {
    				headers: {
    					'User-Agent': config.userAgent,
    					Accept: 'application/json',
    				},
    			});
    
    			if (!response.ok) {
    				const errorText = await response.text();
    				throw new Error(`Search-a-licious API error: ${response.status} ${response.statusText} - ${errorText}`);
    			}
    
    			const data = await response.json() as Record<string, unknown>;
    			const normalized = normalizeResponse(data);
    
    			return jsonResult(normalized);
    		},
    	);
    }

Latest Blog Posts

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/domdomegg/openfoodfacts-mcp'

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