Skip to main content
Glama

Open Food Facts MCP Server

by caleb-conner
server.test.ts19 kB
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { OpenFoodFactsClient } from '../src/client.js'; import { ToolHandlers } from '../src/handlers.js'; import { tools } from '../src/tools.js'; import { mockNutellaProductResponse, mockHealthyYogurtProductResponse, mockSearchResponse, mockBarcodes, } from './fixtures/products.js'; // Mock dependencies jest.mock('../src/client.js'); jest.mock('../src/handlers.js'); describe('MCP Server Integration', () => { let server: Server; let mockClient: jest.Mocked<OpenFoodFactsClient>; let mockHandlers: jest.Mocked<ToolHandlers>; beforeEach(() => { // Create mocked client mockClient = { getProduct: jest.fn(), searchProducts: jest.fn(), getProductsByBarcodes: jest.fn(), } as any; // Create mocked handlers mockHandlers = { handleGetProduct: jest.fn(), handleSearchProducts: jest.fn(), handleAnalyzeProduct: jest.fn(), handleCompareProducts: jest.fn(), handleGetProductSuggestions: jest.fn(), } as any; // Mock constructor (OpenFoodFactsClient as jest.MockedClass<typeof OpenFoodFactsClient>).mockImplementation(() => mockClient); (ToolHandlers as jest.MockedClass<typeof ToolHandlers>).mockImplementation(() => mockHandlers); // Create server instance server = new Server( { name: 'open-food-facts-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); }); afterEach(() => { jest.clearAllMocks(); }); describe('ListTools', () => { it('should return all available tools', async () => { const request = { method: 'tools/list' as const, params: {}, }; const response = await server.handleRequest(ListToolsRequestSchema.parse(request)); expect(response.tools).toHaveLength(5); expect(response.tools).toEqual(tools); // Verify all expected tools are present const toolNames = response.tools.map(tool => tool.name); expect(toolNames).toContain('get_product'); expect(toolNames).toContain('search_products'); expect(toolNames).toContain('analyze_product'); expect(toolNames).toContain('compare_products'); expect(toolNames).toContain('get_product_suggestions'); }); it('should have correct tool schemas', async () => { const request = { method: 'tools/list' as const, params: {}, }; const response = await server.handleRequest(ListToolsRequestSchema.parse(request)); const getProductTool = response.tools.find(tool => tool.name === 'get_product'); expect(getProductTool).toBeDefined(); expect(getProductTool!.inputSchema.properties).toHaveProperty('barcode'); expect(getProductTool!.inputSchema.required).toContain('barcode'); const searchTool = response.tools.find(tool => tool.name === 'search_products'); expect(searchTool).toBeDefined(); expect(searchTool!.inputSchema.properties).toHaveProperty('search'); expect(searchTool!.inputSchema.properties).toHaveProperty('categories'); expect(searchTool!.inputSchema.required).toEqual([]); }); }); describe('CallTool - get_product', () => { beforeEach(() => { // Re-setup server with handlers for each test const handlers = new ToolHandlers(mockClient); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'get_product': return await handlers.handleGetProduct(args.barcode); case 'search_products': return await handlers.handleSearchProducts(args); case 'analyze_product': return await handlers.handleAnalyzeProduct(args.barcode); case 'compare_products': return await handlers.handleCompareProducts(args.barcodes, args.focus); case 'get_product_suggestions': return await handlers.handleGetProductSuggestions(args); default: throw new Error(`Unknown tool: ${name}`); } }); }); it('should handle get_product successfully', async () => { const expectedResponse = { content: [ { type: 'text' as const, text: '**Nutella** product information...', }, ], }; mockHandlers.handleGetProduct.mockResolvedValue(expectedResponse); const request = { method: 'tools/call' as const, params: { name: 'get_product', arguments: { barcode: mockBarcodes.nutella, }, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(mockHandlers.handleGetProduct).toHaveBeenCalledWith(mockBarcodes.nutella); expect(response).toEqual(expectedResponse); }); it('should handle missing barcode parameter', async () => { const request = { method: 'tools/call' as const, params: { name: 'get_product', arguments: {}, // Missing barcode }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); it('should handle invalid barcode type', async () => { const request = { method: 'tools/call' as const, params: { name: 'get_product', arguments: { barcode: 123, // Should be string }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); }); describe('CallTool - search_products', () => { beforeEach(() => { const handlers = new ToolHandlers(mockClient); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'search_products') { return await handlers.handleSearchProducts(args); } throw new Error(`Unknown tool: ${name}`); }); }); it('should handle search_products with all parameters', async () => { const expectedResponse = { content: [ { type: 'text' as const, text: 'Found 150 products...', }, ], }; mockHandlers.handleSearchProducts.mockResolvedValue(expectedResponse); const searchParams = { search: 'chocolate', categories: 'snacks', brands: 'ferrero', countries: 'france', nutrition_grades: 'a,b', nova_groups: '1,2', sort_by: 'popularity', page: 1, page_size: 20, }; const request = { method: 'tools/call' as const, params: { name: 'search_products', arguments: searchParams, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(mockHandlers.handleSearchProducts).toHaveBeenCalledWith(searchParams); expect(response).toEqual(expectedResponse); }); it('should handle search_products with no parameters', async () => { const expectedResponse = { content: [ { type: 'text' as const, text: 'Search results...', }, ], }; mockHandlers.handleSearchProducts.mockResolvedValue(expectedResponse); const request = { method: 'tools/call' as const, params: { name: 'search_products', arguments: {}, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(mockHandlers.handleSearchProducts).toHaveBeenCalledWith({}); expect(response).toEqual(expectedResponse); }); it('should validate sort_by enum values', async () => { const request = { method: 'tools/call' as const, params: { name: 'search_products', arguments: { sort_by: 'invalid_sort', }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); it('should validate page_size limits', async () => { const request = { method: 'tools/call' as const, params: { name: 'search_products', arguments: { page_size: 0, // Below minimum }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); }); describe('CallTool - analyze_product', () => { beforeEach(() => { const handlers = new ToolHandlers(mockClient); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'analyze_product') { return await handlers.handleAnalyzeProduct(args.barcode); } throw new Error(`Unknown tool: ${name}`); }); }); it('should handle analyze_product successfully', async () => { const expectedResponse = { content: [ { type: 'text' as const, text: '**Nutritional Analysis: Nutella**...', }, ], }; mockHandlers.handleAnalyzeProduct.mockResolvedValue(expectedResponse); const request = { method: 'tools/call' as const, params: { name: 'analyze_product', arguments: { barcode: mockBarcodes.nutella, }, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(mockHandlers.handleAnalyzeProduct).toHaveBeenCalledWith(mockBarcodes.nutella); expect(response).toEqual(expectedResponse); }); }); describe('CallTool - compare_products', () => { beforeEach(() => { const handlers = new ToolHandlers(mockClient); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'compare_products') { return await handlers.handleCompareProducts(args.barcodes, args.focus); } throw new Error(`Unknown tool: ${name}`); }); }); it('should handle compare_products successfully', async () => { const expectedResponse = { content: [ { type: 'text' as const, text: '**Product Comparison (nutrition)**...', }, ], }; mockHandlers.handleCompareProducts.mockResolvedValue(expectedResponse); const barcodes = [mockBarcodes.nutella, mockBarcodes.yogurt]; const request = { method: 'tools/call' as const, params: { name: 'compare_products', arguments: { barcodes, focus: 'nutrition', }, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(mockHandlers.handleCompareProducts).toHaveBeenCalledWith(barcodes, 'nutrition'); expect(response).toEqual(expectedResponse); }); it('should validate barcodes array requirements', async () => { // Test minimum items const requestTooFew = { method: 'tools/call' as const, params: { name: 'compare_products', arguments: { barcodes: ['123456789'], // Only one item }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(requestTooFew))) .rejects.toThrow(); // Test maximum items const tooManyBarcodes = Array(11).fill('123456789'); const requestTooMany = { method: 'tools/call' as const, params: { name: 'compare_products', arguments: { barcodes: tooManyBarcodes, }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(requestTooMany))) .rejects.toThrow(); }); it('should validate focus enum values', async () => { const request = { method: 'tools/call' as const, params: { name: 'compare_products', arguments: { barcodes: [mockBarcodes.nutella, mockBarcodes.yogurt], focus: 'invalid_focus', }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); }); describe('CallTool - get_product_suggestions', () => { beforeEach(() => { const handlers = new ToolHandlers(mockClient); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'get_product_suggestions') { return await handlers.handleGetProductSuggestions(args); } throw new Error(`Unknown tool: ${name}`); }); }); it('should handle get_product_suggestions successfully', async () => { const expectedResponse = { content: [ { type: 'text' as const, text: 'Product suggestions in beverages:...', }, ], }; mockHandlers.handleGetProductSuggestions.mockResolvedValue(expectedResponse); const params = { category: 'beverages', dietary_preferences: ['vegan', 'organic'], max_results: 5, min_nutriscore: 'b', }; const request = { method: 'tools/call' as const, params: { name: 'get_product_suggestions', arguments: params, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(mockHandlers.handleGetProductSuggestions).toHaveBeenCalledWith(params); expect(response).toEqual(expectedResponse); }); it('should validate dietary_preferences enum values', async () => { const request = { method: 'tools/call' as const, params: { name: 'get_product_suggestions', arguments: { category: 'beverages', dietary_preferences: ['invalid_preference'], }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); it('should validate max_results limits', async () => { const requestTooLow = { method: 'tools/call' as const, params: { name: 'get_product_suggestions', arguments: { category: 'beverages', max_results: 0, }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(requestTooLow))) .rejects.toThrow(); const requestTooHigh = { method: 'tools/call' as const, params: { name: 'get_product_suggestions', arguments: { category: 'beverages', max_results: 51, }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(requestTooHigh))) .rejects.toThrow(); }); it('should validate min_nutriscore enum values', async () => { const request = { method: 'tools/call' as const, params: { name: 'get_product_suggestions', arguments: { category: 'beverages', min_nutriscore: 'f', // Invalid grade }, }, }; await expect(server.handleRequest(CallToolRequestSchema.parse(request))) .rejects.toThrow(); }); }); describe('Error Handling', () => { beforeEach(() => { const handlers = new ToolHandlers(mockClient); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_product': return await handlers.handleGetProduct(args.barcode); case 'search_products': return await handlers.handleSearchProducts(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text' as const, text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); }); it('should handle unknown tool names', async () => { const request = { method: 'tools/call' as const, params: { name: 'nonexistent_tool', arguments: {}, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(response.isError).toBe(true); expect(response.content[0].text).toContain('Unknown tool: nonexistent_tool'); }); it('should handle handler errors gracefully', async () => { mockHandlers.handleGetProduct.mockRejectedValue(new Error('Network timeout')); const request = { method: 'tools/call' as const, params: { name: 'get_product', arguments: { barcode: mockBarcodes.nutella, }, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(response.isError).toBe(true); expect(response.content[0].text).toContain('Error: Network timeout'); }); it('should handle non-Error exceptions', async () => { mockHandlers.handleSearchProducts.mockRejectedValue('String error'); const request = { method: 'tools/call' as const, params: { name: 'search_products', arguments: {}, }, }; const response = await server.handleRequest(CallToolRequestSchema.parse(request)); expect(response.isError).toBe(true); expect(response.content[0].text).toContain('Error: Unknown error occurred'); }); }); describe('Server Configuration', () => { it('should have correct server metadata', () => { expect(server).toBeDefined(); // Note: Server metadata is set in constructor, not easily accessible for testing // This test verifies the server can be constructed without errors }); it('should have tools capability enabled', () => { expect(server).toBeDefined(); // Capabilities are internal to the server implementation // This test verifies the server is configured with tools capability }); }); });

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