Skip to main content
Glama
samwang0723

Restaurant Booking MCP Server

restaurantRecommendationService.ts11 kB
import { Restaurant, RestaurantSearchParams, RestaurantRecommendation, } from '../types/index.js'; export class RestaurantRecommendationService { /** * Analyze and score restaurants based on search criteria * Optimized with parallel processing */ async getRecommendations( restaurants: Restaurant[], params: RestaurantSearchParams ): Promise<RestaurantRecommendation[]> { // Apply strict cuisine filtering if requested let filteredRestaurants = restaurants; if (params.strictCuisineFiltering && params.cuisineTypes.length > 0) { filteredRestaurants = restaurants.filter(restaurant => this.calculateCuisineMatch(restaurant, params.cuisineTypes) > 0 ); // If no restaurants match the strict criteria, fall back to all restaurants // with a warning that no exact matches were found if (filteredRestaurants.length === 0) { console.warn('No restaurants found matching strict cuisine criteria, falling back to all results'); filteredRestaurants = restaurants; } } // Process all restaurants in parallel for better performance const recommendationPromises = filteredRestaurants.map(async restaurant => { const [score, suitabilityForEvent, moodMatch] = await Promise.all([ // These can run in parallel since they're independent calculations Promise.resolve(this.calculateRestaurantScore(restaurant, params)), Promise.resolve( this.calculateEventSuitability(restaurant, params.event) ), Promise.resolve(this.calculateMoodMatch(restaurant, params.mood)), ]); const reasoning = this.generateReasoning( restaurant, params, score, suitabilityForEvent, moodMatch ); return { restaurant, score, reasoning, suitabilityForEvent, moodMatch, }; }); const recommendations = await Promise.all(recommendationPromises); // Sort by score (highest first) and return top 3 return recommendations.sort((a, b) => b.score - a.score).slice(0, 3); } /** * Calculate overall score for a restaurant based on multiple factors */ private calculateRestaurantScore( restaurant: Restaurant, params: RestaurantSearchParams ): number { let score = 0; let factors = 0; // Rating factor (40% weight) if (restaurant.rating > 0) { score += (restaurant.rating / 5) * 40; factors++; } // Review count factor (20% weight) - more reviews = more reliable if (restaurant.userRatingsTotal > 0) { const reviewScore = Math.min(restaurant.userRatingsTotal / 100, 1) * 20; score += reviewScore; factors++; } // Cuisine match factor (20% weight) const cuisineMatch = this.calculateCuisineMatch( restaurant, params.cuisineTypes ); score += cuisineMatch * 20; factors++; // Event suitability factor (10% weight) const eventSuitability = this.calculateEventSuitability( restaurant, params.event ); score += (eventSuitability / 10) * 10; factors++; // Mood match factor (10% weight) const moodMatch = this.calculateMoodMatch(restaurant, params.mood); score += (moodMatch / 10) * 10; factors++; return factors > 0 ? score : 0; } /** * Calculate how well restaurant cuisine matches search criteria */ private calculateCuisineMatch( restaurant: Restaurant, searchCuisines: string[] ): number { if (searchCuisines.length === 0) return 1; // No specific cuisine preference const restaurantCuisines = restaurant.cuisineTypes.map(c => c.toLowerCase() ); const searchCuisinesLower = searchCuisines.map(c => c.toLowerCase()); let matches = 0; for (const searchCuisine of searchCuisinesLower) { for (const restaurantCuisine of restaurantCuisines) { if ( restaurantCuisine.includes(searchCuisine) || searchCuisine.includes(restaurantCuisine) ) { matches++; break; } } } return matches / searchCuisines.length; } /** * Calculate suitability for specific events (1-10 scale) */ private calculateEventSuitability( restaurant: Restaurant, event: string ): number { const eventFactors = { dating: { preferredPriceLevel: [2, 3, 4], // Mid to high-end preferredCuisines: [ 'italian', 'french', 'japanese', 'mediterranean', 'fine dining', ], avoidCuisines: ['fast food', 'buffet'], minRating: 4.0, atmosphereKeywords: ['romantic', 'intimate', 'cozy', 'elegant'], }, gathering: { preferredPriceLevel: [1, 2, 3], // Budget to mid-range preferredCuisines: [ 'american', 'italian', 'chinese', 'mexican', 'pizza', ], avoidCuisines: ['fine dining'], minRating: 3.5, atmosphereKeywords: ['family-friendly', 'spacious', 'casual', 'kids'], }, business: { preferredPriceLevel: [2, 3, 4], // Mid to high-end preferredCuisines: ['american', 'italian', 'steakhouse', 'seafood'], avoidCuisines: ['fast food', 'buffet'], minRating: 4.0, atmosphereKeywords: ['quiet', 'professional', 'upscale', 'private'], }, casual: { preferredPriceLevel: [1, 2], // Budget to mid-range preferredCuisines: ['american', 'pizza', 'cafe', 'mexican', 'asian'], avoidCuisines: [], minRating: 3.0, atmosphereKeywords: ['casual', 'relaxed', 'friendly'], }, celebration: { preferredPriceLevel: [3, 4], // High-end preferredCuisines: [ 'fine dining', 'steakhouse', 'seafood', 'french', 'italian', ], avoidCuisines: ['fast food', 'cafe'], minRating: 4.2, atmosphereKeywords: ['upscale', 'elegant', 'special', 'celebration'], }, }; const factors = eventFactors[event as keyof typeof eventFactors]; if (!factors) return 5; // Default score let score = 5; // Base score // Price level suitability if ( restaurant.priceLevel && factors.preferredPriceLevel.includes(restaurant.priceLevel) ) { score += 2; } // Cuisine suitability const restaurantCuisines = restaurant.cuisineTypes.map(c => c.toLowerCase() ); const hasPreferredCuisine = factors.preferredCuisines.some(cuisine => restaurantCuisines.some(rc => rc.includes(cuisine)) ); const hasAvoidedCuisine = factors.avoidCuisines.some(cuisine => restaurantCuisines.some(rc => rc.includes(cuisine)) ); if (hasPreferredCuisine) score += 2; if (hasAvoidedCuisine) score -= 3; // Rating suitability if (restaurant.rating >= factors.minRating) { score += 1; } else { score -= 2; } return Math.max(1, Math.min(10, score)); } /** * Calculate mood match (1-10 scale) */ private calculateMoodMatch(restaurant: Restaurant, mood: string): number { const moodKeywords = { romantic: ['intimate', 'cozy', 'candlelit', 'wine', 'date', 'romantic'], casual: ['casual', 'relaxed', 'friendly', 'laid-back', 'comfortable'], upscale: ['upscale', 'elegant', 'sophisticated', 'fine', 'luxury'], fun: ['lively', 'energetic', 'vibrant', 'entertainment', 'music'], quiet: ['quiet', 'peaceful', 'serene', 'calm', 'tranquil'], adventurous: ['unique', 'exotic', 'fusion', 'creative', 'innovative'], traditional: [ 'traditional', 'authentic', 'classic', 'heritage', 'original', ], }; const keywords = moodKeywords[mood.toLowerCase() as keyof typeof moodKeywords] || []; if (keywords.length === 0) return 5; // Default score let score = 5; // Base score let matches = 0; // Check restaurant name, cuisine types, and reviews for mood keywords const searchText = [ restaurant.name, ...restaurant.cuisineTypes, ...(restaurant.reviews?.map(r => r.text) || []), ] .join(' ') .toLowerCase(); for (const keyword of keywords) { if (searchText.includes(keyword)) { matches++; } } // Adjust score based on matches if (matches > 0) { score += Math.min(matches * 1.5, 4); // Cap at +4 } // Consider price level for certain moods if (restaurant.priceLevel) { if (mood.toLowerCase() === 'upscale' && restaurant.priceLevel >= 3) { score += 1; } else if ( mood.toLowerCase() === 'casual' && restaurant.priceLevel <= 2 ) { score += 1; } } return Math.max(1, Math.min(10, score)); } /** * Generate human-readable reasoning for the recommendation */ private generateReasoning( restaurant: Restaurant, params: RestaurantSearchParams, score: number, eventSuitability: number, moodMatch: number ): string { const reasons: string[] = []; // Rating and reviews if (restaurant.rating >= 4.5) { reasons.push( `Excellent rating of ${restaurant.rating}/5 with ${restaurant.userRatingsTotal} reviews` ); } else if (restaurant.rating >= 4.0) { reasons.push( `High rating of ${restaurant.rating}/5 with ${restaurant.userRatingsTotal} reviews` ); } else if (restaurant.rating >= 3.5) { reasons.push(`Good rating of ${restaurant.rating}/5`); } // Cuisine match if (params.cuisineTypes.length > 0) { const matchingCuisines = restaurant.cuisineTypes.filter(rc => params.cuisineTypes.some( sc => rc.toLowerCase().includes(sc.toLowerCase()) || sc.toLowerCase().includes(rc.toLowerCase()) ) ); if (matchingCuisines.length > 0) { reasons.push( `Serves ${matchingCuisines.join(', ')} cuisine as requested` ); } } // Event suitability if (eventSuitability >= 8) { reasons.push(`Perfect for ${params.event}`); } else if (eventSuitability >= 6) { reasons.push(`Well-suited for ${params.event}`); } // Mood match if (moodMatch >= 8) { reasons.push(`Excellent match for ${params.mood} mood`); } else if (moodMatch >= 6) { reasons.push(`Good fit for ${params.mood} atmosphere`); } // Price level if (restaurant.priceLevel) { const priceLabels = [ '', 'Budget-friendly', 'Moderately priced', 'Upscale', 'High-end', ]; reasons.push(priceLabels[restaurant.priceLevel]); } // Opening hours if (restaurant.openingHours?.openNow) { reasons.push('Currently open'); } return reasons.length > 0 ? reasons.join('. ') + '.' : 'Recommended based on location and general criteria.'; } }

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/samwang0723/mcp-booking'

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