An MCP server for restaurant discovery and reservations using Cloudflare Workers and Yelp API, designed to work with Claude and other AI assistants.
Before you begin, ensure you have:
- Yelp API Key (from Yelp Fusion)
- Yelp Client ID (from Yelp Fusion)
Navigate to the project directory:
cd use-cases/restaurant-reservationnpm install
cp .dev.vars.example .dev.varsGet your Yelp API Key:
- Visit Yelp Fusion
- Create a free account and app, you get 30days free trial
- Copy your API key and Client ID to
.dev.vars
Add your Yelp API key and client ID to the .dev.vars file:
YELP_API_KEY=your_actual_yelp_api_key
YELP_CLIENT_ID=your_actual_yelp_client_id
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerAllTools } from './tools';
export class MyMCP extends McpAgent<Env, Record<string, never>> {
server = new McpServer({
name: "Restaurant Reservation MCP",
version: "1.0.0",
});
async init() {
// Register all tools using the tools directory
registerAllTools(this.server);
}
}
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
if (url.pathname === '/sse' || url.pathname === '/sse/message') {
return MyMCP.serveSSE('/sse').fetch(request, env, ctx);
}
if (url.pathname === '/mcp') {
return MyMCP.serve('/mcp').fetch(request, env, ctx);
}
return new Response('Not found', { status: 404 });
},
};mkdir src/toolsCreate src/tools/index.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerRestaurantTools } from './restaurant';
import { registerReservationTools } from './reservation';
/**
* Registers all restaurant reservation MCP tools with the server
*/
export function registerAllTools(server: McpServer) {
registerRestaurantTools(server);
registerReservationTools(server);
}Create src/tools/restaurant.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { RestaurantService } from '../services/restaurant-service';
import { env } from 'cloudflare:workers';
export function registerRestaurantTools(server: McpServer) {
const apiKey = env.YELP_API_KEY;
if (!apiKey) {
throw new Error('YELP_API_KEY environment variable is required');
}
const restaurantService = new RestaurantService(apiKey);
// Tool to search restaurants
server.tool(
'search_restaurants',
'Search for restaurants with optional filters for location, cuisine, price level, and rating',
{
location: z.string().optional().describe('Filter by location (e.g., "Downtown", "Midtown")'),
cuisine: z.string().optional().describe('Filter by cuisine type (e.g., "Italian", "Japanese", "French")'),
priceLevel: z.number().min(1).max(4).optional().describe('Maximum price level (1-4, where 1 is cheapest)'),
minRating: z.number().min(1).max(5).optional().describe('Minimum rating (1-5 stars)')
},
async ({ location, cuisine, priceLevel, minRating }) => {
try {
const filters = { location, cuisine, priceLevel, minRating };
const restaurants = await restaurantService.searchRestaurants(filters);
if (restaurants.length === 0) {
return {
content: [{
type: "text",
text: "π No restaurants found matching your criteria. Try adjusting your filters."
}]
};
}
const formattedResults = restaurants.map(r => restaurantService.formatRestaurant(r)).join("\n");
return {
content: [{
type: "text",
text: `Found ${restaurants.length} restaurants:\n\n${formattedResults}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `β Error searching restaurants: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
);
// Tool to get detailed restaurant information
server.tool(
'get_restaurant_details',
'Get detailed information about a specific restaurant including contact info and description',
{
restaurantId: z.string().describe('The unique ID of the restaurant')
},
async ({ restaurantId }) => {
try {
const restaurant = await restaurantService.getRestaurantById(restaurantId);
if (!restaurant) {
return {
content: [{
type: "text",
text: "β Restaurant not found. Please check the restaurant ID."
}]
};
}
return {
content: [{
type: "text",
text: restaurantService.formatRestaurantDetails(restaurant)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `β Error getting restaurant details: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
);
}Create src/tools/reservation.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { ReservationService } from '../services/reservation-service';
import { RestaurantService } from '../services/restaurant-service';
import { env } from 'cloudflare:workers';
/**
* Registers reservation management tools with the MCP server
*/
export function registerReservationTools(server: McpServer) {
const apiKey = env.YELP_API_KEY;
if (!apiKey) {
throw new Error('YELP_API_KEY environment variable is required');
}
const reservationService = ReservationService.getInstance();
const restaurantService = new RestaurantService(apiKey);
// Tool to check restaurant availability
server.tool(
'check_availability',
'Check if a restaurant has availability for a specific date, time, and party size',
{
restaurantId: z.string().describe('The unique ID of the restaurant'),
date: z.string().describe('Reservation date (e.g., "2024-08-15")'),
time: z.string().describe('Preferred time (e.g., "7:00 PM")'),
partySize: z.number().min(1).max(20).describe('Number of people in the party')
},
async ({ restaurantId, date, time, partySize }) => {
try {
const restaurant = await restaurantService.getRestaurantById(restaurantId);
if (!restaurant) {
return {
content: [{
type: "text",
text: "β Restaurant not found. Please check the restaurant ID."
}]
};
}
const availability = await reservationService.checkAvailability({
restaurantId,
date,
time,
partySize
});
const statusIcon = availability.isAvailable ? "β
" : "β";
const message = `${statusIcon} **${restaurant.name}** ${availability.message}\n\n` +
`π
Date: ${date}\n` +
`π Time: ${time}\n` +
`π₯ Party Size: ${partySize}\n\n` +
`Other available times: ${availability.alternativeTimes.join(", ")}`;
return {
content: [{
type: "text",
text: message
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `β Error checking availability: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
);
// Tool to make a reservation
server.tool(
'make_reservation',
'Make a restaurant reservation with customer details and special requests',
{
restaurantId: z.string().describe('The unique ID of the restaurant'),
date: z.string().describe('Reservation date (e.g., "2024-08-15")'),
time: z.string().describe('Reservation time (e.g., "7:00 PM")'),
partySize: z.number().min(1).max(20).describe('Number of people in the party'),
customerName: z.string().describe('Customer full name'),
customerEmail: z.string().describe('Customer email address'),
customerPhone: z.string().describe('Customer phone number'),
specialRequests: z.string().optional().describe('Any special requests or dietary restrictions')
},
async ({ restaurantId, date, time, partySize, customerName, customerEmail, customerPhone, specialRequests }) => {
try {
const restaurant = await restaurantService.getRestaurantById(restaurantId);
if (!restaurant) {
return {
content: [{
type: "text",
text: "β Restaurant not found. Please check the restaurant ID."
}]
};
}
const reservation = await reservationService.makeReservation({
restaurantId,
date,
time,
partySize,
customerName,
customerEmail,
customerPhone,
specialRequests
});
// Update the reservation with restaurant name
reservation.restaurantName = restaurant.name;
return {
content: [{
type: "text",
text: reservationService.formatReservationConfirmation(reservation, restaurant.phone)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `β Error making reservation: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
);
// Tool to view customer reservations
server.tool(
'view_reservations',
'View all reservations for a customer by their email address',
{
customerEmail: z.string().describe('Customer email address to look up reservations')
},
async ({ customerEmail }) => {
try {
const reservations = await reservationService.getReservationsByEmail(customerEmail);
if (reservations.length === 0) {
return {
content: [{
type: "text",
text: `π No reservations found for ${customerEmail}`
}]
};
}
// For each reservation, fetch the restaurant details to ensure we have the name
for (const reservation of reservations) {
if (!reservation.restaurantName) {
const restaurant = await restaurantService.getRestaurantById(reservation.restaurantId);
if (restaurant) {
reservation.restaurantName = restaurant.name;
} else {
reservation.restaurantName = "Unknown Restaurant";
}
}
}
const formattedReservations = reservations.map(r =>
reservationService.formatReservation(r)
).join("\n\n");
return {
content: [{
type: "text",
text: `π **Your Reservations:**\n\n${formattedReservations}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `β Error retrieving reservations: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
);
// Tool to cancel a reservation
server.tool(
'cancel_reservation',
'Cancel an existing reservation using the reservation ID and customer email',
{
reservationId: z.string().describe('The reservation confirmation ID'),
customerEmail: z.string().describe('Customer email address for verification')
},
async ({ reservationId, customerEmail }) => {
try {
// First, get the reservation to check if it exists
const existingReservation = await reservationService.getReservationByIdAndEmail(reservationId, customerEmail);
if (!existingReservation) {
return {
content: [{
type: "text",
text: "β Reservation not found or email doesn't match. Please check your reservation ID and email address."
}]
};
}
// If the restaurant name is missing, try to get it
if (!existingReservation.restaurantName) {
const restaurant = await restaurantService.getRestaurantById(existingReservation.restaurantId);
if (restaurant) {
existingReservation.restaurantName = restaurant.name;
} else {
existingReservation.restaurantName = "Unknown Restaurant";
}
}
// Now cancel the reservation
const cancelledReservation = await reservationService.cancelReservation(reservationId, customerEmail);
if (!cancelledReservation) {
return {
content: [{
type: "text",
text: "β Error cancelling reservation. Please try again."
}]
};
}
return {
content: [{
type: "text",
text: reservationService.formatCancellationConfirmation(cancelledReservation)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `β Error cancelling reservation: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
);
}Create src/types/index.ts:
// Restaurant data types
export interface Restaurant {
id: string;
name: string;
cuisine: string;
location: string;
rating: number;
priceLevel: number;
phone?: string;
website?: string;
imageUrl?: string;
description?: string;
}
export interface Reservation {
id: string;
restaurantId: string;
restaurantName: string;
date: string;
time: string;
partySize: number;
customerName: string;
customerEmail: string;
customerPhone: string;
status: "confirmed" | "pending" | "cancelled";
specialRequests?: string;
}
// Search and filter types
export interface RestaurantSearchFilters {
location?: string;
cuisine?: string;
priceLevel?: number;
minRating?: number;
}
export interface AvailabilityRequest {
restaurantId: string;
date: string;
time: string;
partySize: number;
}
export interface ReservationRequest {
restaurantId: string;
date: string;
time: string;
partySize: number;
customerName: string;
customerEmail: string;
customerPhone: string;
specialRequests?: string;
}
// Yelp API response types
export interface YelpBusiness {
id: string;
alias: string;
name: string;
image_url: string;
is_closed: boolean;
url: string;
review_count: number;
categories: Array<{
alias: string;
title: string;
}>;
rating: number;
coordinates: {
latitude: number;
longitude: number;
};
transactions: string[];
price?: string;
location: {
address1: string;
address2?: string;
address3?: string;
city: string;
zip_code: string;
country: string;
state: string;
display_address: string[];
};
phone: string;
display_phone: string;
distance?: number;
}
export interface YelpSearchResponse {
businesses: YelpBusiness[];
total: number;
region: {
center: {
longitude: number;
latitude: number;
};
};
}
export interface YelpBusinessDetails extends YelpBusiness {
hours?: Array<{
open: Array<{
is_overnight: boolean;
start: string;
end: string;
day: number;
}>;
hours_type: string;
is_open_now: boolean;
}>;
photos: string[];
}Create src/services/restaurant-service.ts:
import type {
Restaurant,
RestaurantSearchFilters,
YelpBusiness,
YelpSearchResponse,
YelpBusinessDetails,
} from '../types';
/**
* Service for restaurant discovery and management using Yelp Fusion API
*/
export class RestaurantService {
private apiKey: string;
private baseUrl = 'https://api.yelp.com/v3';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
/**
* Make authenticated request to Yelp API
*/
private async makeYelpRequest(endpoint: string): Promise<any> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(
`Yelp API error: ${response.status} ${response.statusText}`
);
}
return response.json();
}
/**
* Convert Yelp business to our Restaurant interface
*/
private convertYelpToRestaurant(yelpBusiness: YelpBusiness): Restaurant {
// Extract primary cuisine from categories
const primaryCuisine =
yelpBusiness.categories.length > 0
? yelpBusiness.categories[0].title
: 'Restaurant';
// Convert Yelp price ($, $$, $$$, $$$$) to numeric level (1-4)
const priceLevel = yelpBusiness.price ? yelpBusiness.price.length : 2;
// Create location string from address
const location = yelpBusiness.location.display_address.join(', ');
return {
id: yelpBusiness.id,
name: yelpBusiness.name,
cuisine: primaryCuisine,
location: location,
rating: yelpBusiness.rating,
priceLevel: priceLevel,
phone: yelpBusiness.display_phone,
website: yelpBusiness.url,
imageUrl: yelpBusiness.image_url,
description: `${primaryCuisine} restaurant with ${yelpBusiness.review_count} reviews`,
};
}
/**
* Search restaurants with optional filters using Yelp API
*/
async searchRestaurants(
filters: RestaurantSearchFilters
): Promise<Restaurant[]> {
try {
// Build Yelp API search parameters
const params = new URLSearchParams();
// Default search parameters
params.append('categories', 'restaurants');
params.append('limit', '20');
params.append('sort_by', 'best_match');
// Location is required for Yelp API
const location = filters.location || 'San Francisco, CA';
params.append('location', location);
// Add cuisine filter if specified
if (filters.cuisine) {
// Map common cuisine types to Yelp categories
const cuisineMap: { [key: string]: string } = {
italian: 'italian',
japanese: 'japanese',
french: 'french',
indian: 'indpak',
chinese: 'chinese',
mexican: 'mexican',
american: 'newamerican',
thai: 'thai',
mediterranean: 'mediterranean',
};
const yelpCategory =
cuisineMap[filters.cuisine.toLowerCase()] ||
filters.cuisine.toLowerCase();
params.set('categories', yelpCategory);
}
// Add price filter if specified
if (filters.priceLevel) {
// Convert our 1-4 scale to Yelp's 1-4 scale
const priceFilter = Array.from(
{ length: filters.priceLevel },
(_, i) => i + 1
).join(',');
params.append('price', priceFilter);
}
const response: YelpSearchResponse = await this.makeYelpRequest(
`/businesses/search?${params.toString()}`
);
// Convert Yelp businesses to our Restaurant format
let restaurants = response.businesses.map((business) =>
this.convertYelpToRestaurant(business)
);
// Apply minimum rating filter (Yelp API doesn't support this directly)
if (filters.minRating) {
restaurants = restaurants.filter((r) => r.rating >= filters.minRating!);
}
return restaurants;
} catch (error) {
console.error('Error searching restaurants:', error);
throw new Error(
`Failed to search restaurants: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}
/**
* Get restaurant by ID using Yelp API
*/
async getRestaurantById(restaurantId: string): Promise<Restaurant | null> {
try {
const response: YelpBusinessDetails = await this.makeYelpRequest(
`/businesses/${restaurantId}`
);
return this.convertYelpToRestaurant(response);
} catch (error) {
console.error('Error getting restaurant by ID:', error);
return null;
}
}
/**
* Get restaurants by location (default search)
*/
async getAllRestaurants(
location: string = 'San Francisco, CA'
): Promise<Restaurant[]> {
return this.searchRestaurants({ location });
}
/**
* Format restaurant for display
*/
formatRestaurant(restaurant: Restaurant): string {
return (
`π½οΈ ${restaurant.name}\n\n` +
`Id: ${restaurant.id}\n` +
`Cuisine: ${restaurant.cuisine}\n` +
`Location: ${restaurant.location}\n` +
`Rating: ${restaurant.rating}/5 β\n` +
`Price: $${'$'.repeat(restaurant.priceLevel)}\n` +
`${restaurant.description || ''}`
);
}
/**
* Format restaurant details for detailed view
*/
formatRestaurantDetails(restaurant: Restaurant): string {
return (
`π½οΈ ${restaurant.name}\n\n` +
`π΄ Cuisine: ${restaurant.cuisine}\n` +
`π Location: ${restaurant.location}\n` +
`β Rating: ${restaurant.rating}/5\n` +
`π° Price Level: ${'$'.repeat(restaurant.priceLevel)}\n` +
`π Phone: ${restaurant.phone || 'Not available'}\n` +
`π Website: ${restaurant.website || 'Not available'}\n\n` +
`π Description: ${restaurant.description || 'No description available'}`
);
}
}Create src/services/reservation-service.ts:
import type { Reservation, AvailabilityRequest, ReservationRequest } from '../types';
/**
* Service for managing restaurant reservations
* In production, this would integrate with restaurant booking systems
*/
export class ReservationService {
private static instance: ReservationService;
private mockReservations: Reservation[] = [];
/**
* Private constructor to prevent direct instantiation
*/
private constructor() {}
/**
* Get the singleton instance of ReservationService
*/
public static getInstance(): ReservationService {
if (!ReservationService.instance) {
ReservationService.instance = new ReservationService();
}
return ReservationService.instance;
}
/**
* Check availability for a restaurant at specific date/time
*/
async checkAvailability(request: AvailabilityRequest): Promise<{
isAvailable: boolean;
alternativeTimes: string[];
message: string;
}> {
// Mock availability logic - 70% chance of availability
const isAvailable = Math.random() > 0.3;
const alternativeTimes = ["6:00 PM", "6:30 PM", "7:00 PM", "7:30 PM", "8:00 PM"];
if (isAvailable) {
return {
isAvailable: true,
alternativeTimes,
message: `β
Available at ${request.time} for ${request.partySize} people`
};
} else {
return {
isAvailable: false,
alternativeTimes,
message: `β Not available at ${request.time}. Alternative times available: ${alternativeTimes.join(", ")}`
};
}
}
/**
* Make a new reservation
*/
async makeReservation(request: ReservationRequest): Promise<Reservation> {
const reservationId = `res_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const reservation: Reservation = {
id: reservationId,
restaurantId: request.restaurantId,
restaurantName: "", // Will be filled by the tool handler
date: request.date,
time: request.time,
partySize: request.partySize,
customerName: request.customerName,
customerEmail: request.customerEmail,
customerPhone: request.customerPhone,
status: "confirmed",
specialRequests: request.specialRequests,
};
this.mockReservations.push(reservation);
return reservation;
}
/**
* Get reservations by customer email
*/
async getReservationsByEmail(customerEmail: string): Promise<Reservation[]> {
return this.mockReservations.filter(r =>
r.customerEmail.toLowerCase() === customerEmail.toLowerCase()
);
}
/**
* Get reservation by ID and email (for security)
*/
async getReservationByIdAndEmail(reservationId: string, customerEmail: string): Promise<Reservation | null> {
return this.mockReservations.find(r =>
r.id === reservationId &&
r.customerEmail.toLowerCase() === customerEmail.toLowerCase()
) || null;
}
/**
* Cancel a reservation
*/
async cancelReservation(reservationId: string, customerEmail: string): Promise<Reservation | null> {
const reservationIndex = this.mockReservations.findIndex(r =>
r.id === reservationId &&
r.customerEmail.toLowerCase() === customerEmail.toLowerCase()
);
if (reservationIndex === -1) {
return null;
}
const reservation = this.mockReservations[reservationIndex];
reservation.status = "cancelled";
return reservation;
}
/**
* Format reservation for display
*/
formatReservation(reservation: Reservation): string {
return `π½οΈ **${reservation.restaurantName}**\n` +
` π ID: ${reservation.id}\n` +
` π
Date: ${reservation.date}\n` +
` π Time: ${reservation.time}\n` +
` π₯ Party Size: ${reservation.partySize}\n` +
` β
Status: ${reservation.status}\n` +
` ${reservation.specialRequests ? `π Special Requests: ${reservation.specialRequests}\n` : ""}`;
}
/**
* Format reservation confirmation
*/
formatReservationConfirmation(reservation: Reservation, restaurantPhone?: string): string {
return `π **Reservation Confirmed!**\n\n` +
`π **IMPORTANT - YOUR RESERVATION ID: ${reservation.id}**\n\n` +
`π½οΈ Restaurant: ${reservation.restaurantName}\n` +
`π
Date: ${reservation.date}\n` +
`π Time: ${reservation.time}\n` +
`π₯ Party Size: ${reservation.partySize}\n` +
`π€ Name: ${reservation.customerName}\n` +
`π§ Email: ${reservation.customerEmail}\n` +
`π Phone: ${reservation.customerPhone}\n` +
`${reservation.specialRequests ? `π Special Requests: ${reservation.specialRequests}\n` : ""}` +
`\nβ
Status: ${reservation.status}\n\n` +
`Please arrive 15 minutes early. ${restaurantPhone ? `Call ${restaurantPhone} if you need to make changes.` : ""}\n\n` +
`To view or cancel your reservation, use your reservation ID and email address.`;
}
/**
* Format cancellation confirmation
*/
formatCancellationConfirmation(reservation: Reservation): string {
return `β
**Reservation Cancelled**\n\n` +
`π Confirmation #: ${reservation.id}\n` +
`π½οΈ Restaurant: ${reservation.restaurantName}\n` +
`π
Date: ${reservation.date}\n` +
`π Time: ${reservation.time}\n\n` +
`Your reservation has been successfully cancelled.`;
}
}# Deploy the worker
npm run deploy
# Your MCP server will be deployed to: `https://restaurant-reservation-mcp.<your-account>.workers.dev`
# Set environment variables
wrangler secret bulk .dev.vars- Go to https://playground.ai.cloudflare.com/
- Enter your deployed URL:
https://restaurant-reservation-mcp.<your-account>.workers.dev/sse - Start using the restaurant tools!
- Open Claude Desktop settings
- Go to Settings > Developer > Edit Config
- Add this configuration:
{
"mcpServers": {
"restaurant-reservation": {
"command": "npx",
"args": [
"mcp-remote",
"https://restaurant-reservation-mcp.<your-account>.workers.dev/sse"
]
}
}
}- Restart Claude Desktop
Your MCP server provides these tools to the AI assistants:
search_restaurants: Find restaurants by location, cuisine, price, ratingget_restaurant_details: Get detailed info about a specific restaurantcheck_availability: Check if a restaurant has availabilitymake_reservation: Book a restaurant reservationcancel_reservation: Cancel an existing reservation
Try these commands with Claude:
- "Find Italian restaurants in downtown with at least 4 stars"
- "Check availability at restaurant Michiu Amsterdam for 4 people on December 25th at 7 PM"
- "Make a reservation at restaurant Michiu Amsterdam for John Doe on Friday at 8 PM for 2 people"
- "Cancel reservation for restaurant Michiu Amsterdam for John Doe on Friday at 8 PM"
-
"YELP_API_KEY environment variable is required"
- Ensure you've set the API key:
wrangler secret put YELP_API_KEY
- Ensure you've set the API key:
-
"Restaurant not found"
- Use restaurant IDs from search results
-
"Reservation not found or email doesn't match"
- Make sure you're using the exact reservation ID provided when booking
- Use the same email address used during reservation creation
- Note that reservations are stored in memory and will be lost if the server restarts
-
Can't view or cancel reservations
- The ReservationService uses a singleton pattern to persist reservations in memory
- If you're testing locally and restarting the server often, reservations will be lost
- In production, you would use a database to store reservations permanently
-
Claude Desktop not connecting
- Verify your URL includes
/sseendpoint
- Verify your URL includes
-
Build errors
- Run
npm run type-checkto verify TypeScript - Ensure all dependencies are installed:
npm install
- Run
Congratulations! You've built a complete restaurant reservation MCP server.