Skip to main content
Glama
samwang0723

Restaurant Booking MCP Server

index.ts11.4 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; import { GoogleMapsService } from './services/googleMapsService.js'; import { RestaurantRecommendationService } from './services/restaurantRecommendationService.js'; import { RestaurantSearchParams } from './types/index.js'; // Load environment variables dotenv.config(); // Default coordinates for Taiwan const DEFAULT_LATITUDE = parseFloat( process.env.DEFAULT_LATITUDE || '24.1501164' ); const DEFAULT_LONGITUDE = parseFloat( process.env.DEFAULT_LONGITUDE || '120.6692299' ); const DEFAULT_SEARCH_RADIUS = parseInt( process.env.DEFAULT_SEARCH_RADIUS || '3000' ); // 3km in meters const PORT = parseInt(process.env.PORT || '3000'); class RestaurantBookingServer { private googleMapsService: GoogleMapsService; private recommendationService: RestaurantRecommendationService; constructor() { // Initialize services const apiKey = process.env.GOOGLE_MAPS_API_KEY; if (!apiKey) { throw new Error('GOOGLE_MAPS_API_KEY environment variable is required'); } this.googleMapsService = new GoogleMapsService(apiKey); this.recommendationService = new RestaurantRecommendationService(); } private createServer(): McpServer { const server = new McpServer({ name: 'restaurant-booking-server', version: '1.0.0', }); this.setupTools(server); return server; } private setupTools(server: McpServer) { // Search restaurants tool server.registerTool( 'search_restaurants', { title: 'Search for restaurants', description: 'Search for restaurants based on location, cuisine, keyword, mood, event, radius, price level, and locale', inputSchema: { latitude: z .number() .optional() .describe( `Latitude of the search location (default: ${DEFAULT_LATITUDE} - Taiwan)` ), longitude: z .number() .optional() .describe( `Longitude of the search location (default: ${DEFAULT_LONGITUDE} - Taiwan)` ), placeName: z .string() .optional() .describe( 'Place name to search near (e.g., "New York", "Tokyo", "London"). Alternative to providing latitude/longitude coordinates.' ), cuisineTypes: z .array(z.string()) .optional() .describe( 'Array of preferred cuisine types (e.g., ["Italian", "Japanese", "Mexican"])' ), keyword: z .string() .optional() .describe( 'Search for specific food types or dishes (e.g., "hotpot", "sushi", "pizza", "ramen", "dim sum", "barbecue")' ), mood: z .string() .describe( 'Desired mood/atmosphere (e.g., "romantic", "casual", "upscale", "fun", "quiet")' ), event: z .string() .describe( "Type of event or occasion (e.g., 'dating', 'gathering', 'business', 'casual', 'celebration')" ), radius: z .number() .optional() .describe( `Search radius in meters (default: ${DEFAULT_SEARCH_RADIUS} = ${DEFAULT_SEARCH_RADIUS / 1000}km)` ), priceLevel: z .number() .min(1) .max(4) .optional() .describe( 'Price level preference (1=inexpensive, 4=very expensive)' ), locale: z .string() .optional() .describe( 'Locale for search results and Google API responses (e.g., "en" for English, "zh-TW" for Traditional Chinese, "ja" for Japanese, "ko" for Korean, "th" for Thai). Affects restaurant names, reviews, and other text content.' ), strictCuisineFiltering: z .boolean() .optional() .describe( 'If true, only restaurants that match the specified cuisine types will be returned. If false (default), all restaurants will be returned but cuisine matches will be scored higher.' ), }, }, async args => { return await this.handleSearchRestaurants(args); } ); } private async handleSearchRestaurants(args: any) { const searchParams: RestaurantSearchParams = { // Only include location if placeName is not provided ...(args.placeName ? { placeName: args.placeName } : { location: { latitude: args.latitude || DEFAULT_LATITUDE, longitude: args.longitude || DEFAULT_LONGITUDE, }, }), cuisineTypes: args.cuisineTypes || [], keyword: args.keyword, mood: args.mood, event: args.event, radius: args.radius || DEFAULT_SEARCH_RADIUS, priceLevel: args.priceLevel, locale: args.locale || 'en', strictCuisineFiltering: args.strictCuisineFiltering || false, }; // Search for restaurants const restaurants = await this.googleMapsService.searchRestaurants(searchParams); if (restaurants.length === 0) { return { content: [ { type: 'text' as const, text: 'No restaurants found matching your criteria. Try expanding your search radius or adjusting your preferences.', }, ], }; } // Get AI recommendations const recommendations = await this.recommendationService.getRecommendations( restaurants, searchParams ); const result = { searchCriteria: searchParams, totalFound: restaurants.length, recommendations: recommendations.map(rec => ({ restaurant: { placeId: rec.restaurant.placeId, name: rec.restaurant.name, address: rec.restaurant.address, rating: rec.restaurant.rating, userRatingsTotal: rec.restaurant.userRatingsTotal, priceLevel: rec.restaurant.priceLevel, cuisineTypes: rec.restaurant.cuisineTypes, phoneNumber: rec.restaurant.phoneNumber, website: rec.restaurant.website, googleMapsUrl: rec.restaurant.googleMapsUrl, openingHours: rec.restaurant.openingHours, distance: rec.restaurant.distance, bookingInfo: rec.restaurant.bookingInfo, reservable: rec.restaurant.reservable, curbsidePickup: rec.restaurant.curbsidePickup, delivery: rec.restaurant.delivery, dineIn: rec.restaurant.dineIn, takeout: rec.restaurant.takeout, servesBreakfast: rec.restaurant.servesBreakfast, servesLunch: rec.restaurant.servesLunch, servesDinner: rec.restaurant.servesDinner, servesBrunch: rec.restaurant.servesBrunch, servesBeer: rec.restaurant.servesBeer, servesWine: rec.restaurant.servesWine, servesVegetarianFood: rec.restaurant.servesVegetarianFood, }, score: Math.round(rec.score * 10) / 10, reasoning: rec.reasoning, suitabilityForEvent: rec.suitabilityForEvent, moodMatch: rec.moodMatch, })), }; return { content: [ { type: 'text' as const, text: JSON.stringify(result, null, 2), }, ], }; } async run() { const app = express(); app.use(express.json()); // Map to store transports by session ID for stateful connections const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'restaurant-booking-mcp-server' }); }); // Handle POST requests for client-to-server communication app.post('/mcp', async (req, res) => { try { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; let transport: StreamableHTTPServerTransport; let server: McpServer; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: sessionId => { // Store the transport by session ID transports[sessionId] = transport; }, }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId]; } }; // Create new server instance server = this.createServer(); // Connect to the MCP server await server.connect(transport); } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // Handle the request await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Reusable handler for GET and DELETE requests const handleSessionRequest = async ( req: express.Request, res: express.Response ) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); return; } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; // Handle GET requests for server-to-client notifications via SSE app.get('/mcp', handleSessionRequest); // Handle DELETE requests for session termination app.delete('/mcp', handleSessionRequest); // Start the server app.listen(PORT, '0.0.0.0', () => { console.log( `Restaurant Booking MCP Server running on http://0.0.0.0:${PORT}` ); console.log(`Health check available at http://0.0.0.0:${PORT}/health`); console.log(`MCP endpoint available at http://0.0.0.0:${PORT}/mcp`); }); } } // Start the server const server = new RestaurantBookingServer(); server.run().catch(error => { console.error('Failed to start server:', error); process.exit(1); }); // Handle server shutdown process.on('SIGINT', async () => { console.log('Shutting down server...'); process.exit(0); });

Implementation Reference

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