Skip to main content
Glama
paulabaal12

Kitchen MCP Server

by paulabaal12
mcp-server.js16.2 kB
const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } = require('@modelcontextprotocol/sdk/types.js'); const fs = require('fs'); const path = require('path'); const { levenshtein, getUtensilsForRecipe } = require('./utils'); const dataDir = path.join(__dirname, 'data'); const ingredientesPath = path.join(dataDir, 'ingredientes_unificados.json'); const recetasPath = path.join(dataDir, 'recetas_unificadas.json'); const substitutionsPath = path.join(dataDir, 'ingredient_substitutions.json'); const allDietsPath = path.join(dataDir, 'All_Diets.json'); let allDiets = []; let foods = []; let recetas = []; let substitutions = {}; // Cargar los datos JSON function loadFoods() { if (fs.existsSync(allDietsPath)) { try { allDiets = JSON.parse(fs.readFileSync(allDietsPath, 'utf-8')); } catch (e) { console.error('Error loading All_Diets.json:', e); } } if (fs.existsSync(ingredientesPath)) { try { foods = JSON.parse(fs.readFileSync(ingredientesPath, 'utf-8')); } catch (e) { console.error('Error loading ingredientes_unificados.json:', e); } } if (fs.existsSync(recetasPath)) { try { recetas = JSON.parse(fs.readFileSync(recetasPath, 'utf-8')); } catch (e) { console.error('Error loading recetas_unificadas.json:', e); } } if (fs.existsSync(substitutionsPath)) { try { substitutions = JSON.parse(fs.readFileSync(substitutionsPath, 'utf-8')); } catch (e) { console.error('Error loading ingredient_substitutions.json:', e); } } } // Crear servidor MCP const server = new Server( { name: 'kitchen-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Definir herramientas MCP server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'recommend_by_mood_and_season', description: 'Recommends foods or recipes based on mood and optionally season (e.g., happy + summer).', inputSchema: { type: 'object', properties: { mood: { type: 'string', description: 'Main mood (happy, excited, tender, scared, angry, sad)' }, season: { type: 'string', description: 'Season (spring, summer, autumn, winter) (optional)' }, type: { type: 'string', description: 'Type: "food" or "recipe" (optional, default: recipe)' } }, required: ['mood'] } }, { name: 'suggest_utensils_for_recipe', description: 'Suggests necessary kitchen utensils for a given recipe (by name).', inputSchema: { type: 'object', properties: { recipe_name: { type: 'string', description: 'Name of the recipe or dish', }, }, required: ['recipe_name'], }, }, { name: 'get_foods', description: 'Get all available foods', inputSchema: { type: 'object', properties: {}, }, }, { name: 'suggest_recipe_by_diet', description: 'Suggests recipes by diet type (e.g., vegan, keto, Mediterranean, paleo, DASH).', inputSchema: { type: 'object', properties: { diet: { type: 'string', description: 'Diet type: vegan, keto, Mediterranean, paleo, DASH', }, maxCalories: { type: 'number', description: 'Maximum calories (optional)', }, }, required: ['diet'], }, }, { name: 'suggest_ingredient_substitution', description: 'Suggests substitutes for a given ingredient (e.g., orange juice).', inputSchema: { type: 'object', properties: { ingredient: { type: 'string', description: 'Name of the ingredient to substitute', }, }, required: ['ingredient'], }, }, { name: 'get_food_by_name', description: 'Find a specific food by name', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the food to search for', }, }, required: ['name'], }, }, { name: 'search_foods', description: 'Search foods by nutritional criteria', inputSchema: { type: 'object', properties: { minProtein: { type: 'number', description: 'Minimum protein in grams', }, maxFat: { type: 'number', description: 'Maximum fat in grams', }, maxCalories: { type: 'number', description: 'Maximum calories', }, }, }, }, { name: 'get_ingredients', description: 'Get list of available ingredients', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_recipe_suggestions', description: 'Get recipe suggestions based on nutritional content', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_recipes', description: 'Get all available recipes', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_recipes_by_ingredients', description: 'Find recipes by specific ingredients', inputSchema: { type: 'object', properties: { ingredients: { type: 'array', items: { type: 'string', }, description: 'List of ingredients to search for', }, }, required: ['ingredients'], }, }, ], }; }); // Implementar manejadores de herramientas server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Permitir ambos formatos: directo y tools/call let toolName, toolArgs; if (request.params && request.params.name && request.params.arguments) { // Formato genérico: { method: "tools/call", params: { name, arguments } } toolName = request.params.name; toolArgs = request.params.arguments; } else { // Formato directo: { method: "tool_name", params: { ... } } toolName = request.method; toolArgs = request.params; } try { switch (toolName) { case 'recommend_by_mood_and_season': { const { mood, season, type } = toolArgs; if (!mood) { throw new McpError(ErrorCode.InvalidParams, 'Parameter "mood" is required'); } // Elegir fuente: recetas o foods let items = []; if (type === 'food') { items = foods; } else { items = recetas.length > 0 ? recetas : foods; } const { getFoodOrRecipeByMoodAndSeason } = require('./utils'); const result = getFoodOrRecipeByMoodAndSeason(items, mood, season); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'suggest_utensils_for_recipe': { if (!toolArgs.recipe_name || typeof toolArgs.recipe_name !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Parámetro "recipe_name" requerido (string)'); } const utensils = getUtensilsForRecipe(toolArgs.recipe_name); return { content: [ { type: 'text', text: JSON.stringify({ recipe: args.recipe_name, utensils }, null, 2), }, ], }; } case 'suggest_recipe_by_diet': { // diet: vegan, keto, mediterranean, paleo, dash if (!toolArgs.diet || typeof toolArgs.diet !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Parámetro "diet" requerido (string)'); } const dietMap = { vegan: 'vegan', keto: 'keto', mediterranean: 'mediterranean', paleo: 'paleo', dash: 'dash', }; // Permitir variantes en minúsculas y acentos const inputDiet = toolArgs.diet.trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); let mappedDiet = null; for (const key in dietMap) { if (inputDiet.includes(key)) { mappedDiet = dietMap[key]; break; } } if (!mappedDiet) { return { content: [{ type: 'text', text: 'No se reconoce el tipo de dieta solicitado.' }], }; } let filtered = allDiets.filter(r => r.Diet_type && r.Diet_type.toLowerCase() === mappedDiet); if (toolArgs.maxCalories) { filtered = filtered.filter(r => parseFloat(r['Carbs(g)'] || 0) + parseFloat(r['Fat(g)'] || 0) + parseFloat(r['Protein(g)'] || 0) <= toolArgs.maxCalories); } // Si no hay resultados, devolver mensaje if (!filtered.length) { return { content: [{ type: 'text', text: 'No se encontraron recetas para esa dieta.' }], }; } // Devolver hasta 5 recetas const result = filtered.slice(0, 5); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'get_foods': return { content: [ { type: 'text', text: JSON.stringify(foods, null, 2), }, ], }; case 'get_food_by_name': const food = foods.find(f => f.food && f.food.toLowerCase() === toolArgs.name.toLowerCase() ) || null; return { content: [ { type: 'text', text: JSON.stringify(food, null, 2), }, ], }; case 'search_foods': const filtered = foods.filter(f => { let ok = true; if (args.minProtein) ok = ok && parseFloat(f.protein_g) >= parseFloat(args.minProtein); if (args.maxFat) ok = ok && parseFloat(f.fat) <= parseFloat(args.maxFat); if (args.maxCalories) ok = ok && parseFloat(f.energy_kcal) <= parseFloat(args.maxCalories); return ok; }); return { content: [ { type: 'text', text: JSON.stringify(filtered, null, 2), }, ], }; case 'get_ingredients': const ingredients = Array.from(new Set(foods.map(f => f.food))).filter(Boolean); return { content: [ { type: 'text', text: JSON.stringify(ingredients, null, 2), }, ], }; case 'get_recipe_suggestions': const source = recetas.length > 0 ? recetas : foods; const suggestions = source .filter(r => r.protein_g && r.fat) .sort((a, b) => parseFloat(b.protein_g) - parseFloat(a.protein_g) || parseFloat(a.fat) - parseFloat(b.fat)) .slice(0, 5); return { content: [ { type: 'text', text: JSON.stringify(suggestions, null, 2), }, ], }; case 'get_recipes': return { content: [ { type: 'text', text: JSON.stringify(recetas, null, 2), }, ], }; case 'get_recipes_by_ingredients': if (!toolArgs.ingredients || !Array.isArray(toolArgs.ingredients)) { throw new McpError(ErrorCode.InvalidParams, 'Parámetro "ingredients" requerido (array de strings)'); } const searchIngredients = toolArgs.ingredients.map(i => i.trim().toLowerCase().normalize('NFD').replace(/[^\w\s]/gi, '') ); let recetasCoincidentes = recetas .map(r => { let recetaIngs = Array.isArray(r.ingredients) ? r.ingredients : typeof r.ingredients === 'string' ? r.ingredients.split(',') : []; recetaIngs = recetaIngs.map(i => i.trim().toLowerCase().normalize('NFD').replace(/[^\w\s]/gi, '') ); const coincidencias = searchIngredients.filter(ing => { return recetaIngs.some(ring => { if (ring.includes(ing)) return true; return levenshtein(ring, ing) <= 3; }); }); return { receta: r, coincidencias: coincidencias.length }; }) .filter(x => x.coincidencias > 0); recetasCoincidentes.sort((a, b) => b.coincidencias - a.coincidencias); const result = recetasCoincidentes.slice(0, 5).map(x => x.receta); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; case 'suggest_ingredient_substitution': { // Buscar sustitutos para el ingrediente dado if (!toolArgs.ingredient || typeof toolArgs.ingredient !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Parámetro "ingredient" requerido (string)'); } // Normalizar el nombre para buscar coincidencias cercanas const input = toolArgs.ingredient.trim().toLowerCase(); let foundKey = null; // 1. Coincidencia exacta for (const key of Object.keys(substitutions)) { if (key.toLowerCase() === input) { foundKey = key; break; } } // 2. Coincidencia parcial (input incluido en la clave o viceversa) if (!foundKey) { for (const key of Object.keys(substitutions)) { const keyNorm = key.toLowerCase(); if (keyNorm.includes(input) || input.includes(keyNorm)) { foundKey = key; break; } } } // 3. Coincidencia por similitud (levenshtein) if (!foundKey) { let minDist = 4; for (const key of Object.keys(substitutions)) { const dist = levenshtein(key.toLowerCase(), input); if (dist < minDist) { minDist = dist; foundKey = key; } } } if (foundKey) { return { content: [ { type: 'text', text: JSON.stringify({ ingredient: foundKey, substitutions: substitutions[foundKey], }, null, 2), }, ], }; } else { return { content: [ { type: 'text', text: 'No se encontraron sustitutos para el ingrediente solicitado.' }, ], }; } } default: throw new McpError(ErrorCode.MethodNotFound, `Herramienta desconocida: ${toolName}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Error ejecutando ${name}: ${error.message}`); } }); // Inicializar y ejecutar servidor async function runServer() { loadFoods(); const transport = new StdioServerTransport(); await server.connect(transport); console.error('Kitchen MCP Server ejecutándose...'); } runServer().catch(console.error);

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/paulabaal12/kitchen-mcp'

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