Discover and book restaurants and other food establishments directly from your AI assistant.
- Reservation System Architecture - Complete guide to the reservation system including NIP-46 remote signing with Keycast, restaurant discovery, table management, and event schemas
- Nostr Event Schema - Documentation for profiles (kind:0), menus (kind:30405), and menu items (kind:30402)
The MCP server requires Nostr configuration for reservation protocol (NIP-RP) support:
-
Generate a Nostr key pair:
- Visit nostrkeygen.com to generate a new key pair
- Save both the public key (npub) and private key (nsec) securely
- Important: The private key (nsec) must be kept secret!
-
Configure environment variables:
Copy
.env.exampleto.envand update:# Your MCP server's Nostr private key (nsec format) MCP_SERVER_NSEC=nsec1... # Comma-separated list of Nostr relay URLs (optional) NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol # Timeout for reservation responses in milliseconds (optional) RESERVATION_TIMEOUT_MS=30000
-
Required variables:
MCP_SERVER_NSEC: Your Nostr private key in nsec format (starts with "nsec1")
-
Optional variables:
NOSTR_RELAYS: Comma-separated relay URLs (default:wss://relay.damus.io,wss://nos.lol)RESERVATION_TIMEOUT_MS: Timeout for waiting for responses (default: 30000ms, range: 1000-120000)
The configuration is validated on server startup. If required values are missing or invalid, you'll see a helpful error message explaining what needs to be fixed.
Find food establishments (restaurants, bakeries, cafes, etc.) by type, cuisine, dietary needs, or free-text search. All filters are combined with AND logic. Returns an array of JSON-LD formatted food establishment objects following the schema.org FoodEstablishment specification. The output may contain mixed types (Restaurant, Bakery, etc.).
Parameters:
foodEstablishmentType(optional): Filter by schema.org FoodEstablishment type- Valid values:
Bakery,BarOrPub,Brewery,CafeOrCoffeeShop,Distillery,FastFoodRestaurant,IceCreamShop,Restaurant,Winery - If not provided, returns all FoodEstablishment types
- Valid values:
cuisine(optional): Cuisine type (e.g., "Spanish", "Italian", "Mexican")- Searches
schema.org:servesCuisineproperty first, then falls back to description text matching
- Searches
query(optional): Free-text search for establishment name, location, or description- Matches establishment name, description text, or location data from
schema.org:PostalAddressproperty - Example: "Snoqualmie" to find establishments in that location
- Matches establishment name, description text, or location data from
dietary(optional): Dietary requirement (e.g., "vegan", "gluten free")- Matches against lowercase dietary keywords in food establishment profiles
- Keywords are normalized for flexible matching (handles "gluten free" vs "gluten-free")
Returns:
food_establishments: Array of JSON-LD formatted food establishment objects, each containing:@context(string): JSON-LD context ("https://schema.org")@type(string): Schema.org FoodEstablishment type (Bakery, BarOrPub, Brewery, CafeOrCoffeeShop, Distillery, FastFoodRestaurant, IceCreamShop, Restaurant, or Winery)name(string): Restaurant display namedescription(string): Full restaurant description@id(string): Food establishment identifier - use this asrestaurant_idforget_menu_itemsaddress(object, optional): PostalAddress with streetAddress, addressLocality, addressRegion, postalCode, addressCountrytelephone(string, optional): Phone numberemail(string, optional): Email inmailto:formatopeningHours(array, optional): Opening hours in format["Tu-Th 11:00-21:00", "Fr-Sa 11:00-00:00", "Su 11:00-21:00"]image(string, optional): Banner image URLservesCuisine(array, optional): Array of cuisine typesgeo(object, optional): GeoCoordinates with latitude and longitudeurl(string, optional): Website URLacceptsReservations(string, optional): "True", "False", or URLkeywords(string, optional): Comma-separated keywordshasMenu(array, optional): Array of menu objects, each containing:@type(string): "Menu"name(string): Menu namedescription(string, optional): Menu descriptionidentifier(string): Menu identifier - use this asmenu_identifierforget_menu_items
Example:
{"foodEstablishmentType": "Restaurant", "cuisine": "Spanish", "dietary": "vegan"}
{"foodEstablishmentType": "Bakery", "dietary": "gluten free"}
{"query": "Snoqualmie"}
{"cuisine": "Italian", "query": "pizza"}Behavior Notes:
- All filters use AND logic (must match all specified criteria)
- If
foodEstablishmentTypeparameter is provided, only establishments of that type are returned - If
foodEstablishmentTypeis not provided, all valid FoodEstablishment types are returned (mixed types in array) - Cuisine matching prioritizes schema.org tags for SEO compatibility
- Dietary tags are case-insensitive and handle variations
- Returns full description text (not truncated)
- Output format is JSON-LD (JSON for Linked Data) following schema.org FoodEstablishment specification
- All establishment objects include
@context,@id, and@typefor proper JSON-LD interpretation
Get all dishes from a specific food establishment menu. Returns a complete Menu object with menu items organized by sections.
Parameters:
restaurant_id(required): Food establishment identifier- MUST be the exact
@idvalue fromsearch_food_establishmentsresults
- MUST be the exact
menu_identifier(required): Menu identifier- MUST be the exact
identifiervalue from thehasMenuarray insearch_food_establishmentsresults
- MUST be the exact
Returns: A JSON-LD Menu object following schema.org Menu specification:
@context: "https://schema.org"@type: "Menu"name(string): Menu namedescription(string, optional): Menu descriptionidentifier(string): Menu identifierhasMenuSection(array): Array of MenuSection objects, each containing:@type: "MenuSection"name(string): Section name (e.g., "Appetizers", "Entrees", "Sides")description(string, optional): Section descriptionidentifier(string): Section identifierhasMenuItem(array): Array of MenuItem objects in this section, each containing:@context: "https://schema.org"@type: "MenuItem"name(string): name of the menu itemdescription(string): Description of the menu itemidentifier(string, optional): Menu item identifierimage(string, optional): Image URL for the menu itemsuitableForDiet(array, optional): Array of schema.org suitableForDiet values (e.g., "VeganDiet", "GlutenFreeDiet")offers(object, optional): Price information with@type: "Offer",price(number),priceCurrency(string)geo(object, optional): Geographic coordinates with@type: "GeoCoordinates",latitude, andlongitude
Example:
{
"restaurant_id": "nostr:npub1...",
"menu_identifier": "Dinner"
}Behavior Notes:
- Output format is JSON-LD (JSON for Linked Data) following schema.org Menu, MenuSection, and MenuItem specifications
- Menu items are grouped by sections (e.g., Entrees, Sides, Appetizers)
- All objects include
@contextand/or@typefor proper JSON-LD interpretation - Dietary tags are mapped to schema.org suitableForDiet values
- Unmapped dietary tags are appended to the description as text (e.g., "Nut free. Sulphites")
- If menu not found, returns an error with empty hasMenuSection array
- If food establishment not found, returns an error with empty hasMenuSection array
Find specific dishes across all food establishments by name, ingredient, or dietary preference. Returns a JSON-LD graph structure with food establishments grouped by their matching menu items.
Parameters:
dish_query(required): Dish name, ingredient, or dietary term to search for- Searches dish names and descriptions
- Auto-detects dietary terms: If query looks like a dietary term (vegan, vegetarian, gluten-free, etc.), it will also match dishes with matching dietary tags even if the word isn't in the dish name
- Example: "pizza" searches for pizza dishes, "vegan" finds all vegan dishes (by tag)
dietary(optional): Additional dietary filter- Combined with
dish_queryusing AND logic - If
dish_queryis already a dietary term, this adds an additional constraint
- Combined with
restaurant_id(optional): Filter results to a specific food establishment- Use the
@idfromsearch_food_establishmentsresults
- Use the
Returns: A JSON-LD graph structure following schema.org specifications:
@context: "https://schema.org"@graph: Array of FoodEstablishment objects, each containing:@type(string): Schema.org FoodEstablishment type (Restaurant, Bakery, etc.)name(string): Food establishment nameaddress(object, optional): PostalAddress with street, city, state, postal code@id(string): Food establishment identifier in bech32 format (nostr:npub1...)hasMenu(array): Array of Menu objects, each containing:@type(string): "Menu"name(string): Menu namedescription(string, optional): Menu descriptionidentifier(string): Menu identifierhasMenuItem(array): Array of MenuItem objects, each containing:@context: "https://schema.org"@type: "MenuItem"name(string): Name of the menu itemdescription(string): Description of the menu itemidentifier(string, optional): Menu item identifierimage(string, optional): Image URL for the menu itemsuitableForDiet(array, optional): Array of schema.org suitableForDiet values (e.g., "VeganDiet", "GlutenFreeDiet")offers(object, optional): Price information with@type: "Offer",price(number), andpriceCurrency(string)geo(object, optional): Geographic coordinates with@type: "GeoCoordinates",latitude, andlongitude
Example:
{"dish_query": "pizza"}
{"dish_query": "vegan"} // Auto-detects as dietary term
{"dish_query": "pizza", "dietary": "vegan"}
{"dish_query": "tomato", "restaurant_id": "nostr:npub1..."}Behavior Notes:
- Automatically detects common dietary terms: vegan, vegetarian, gluten-free, gluten free, dairy-free, dairy free, nut-free, nut free
- When a dietary term is detected, matches both text search AND dietary tags
- Products use uppercase dietary tags with underscores (e.g., "VEGAN", "GLUTEN_FREE") - normalized for matching
- Results are grouped by food establishment and menu - each establishment appears once with all matching menu items organized by menu
- Output format is JSON-LD (JSON for Linked Data) following schema.org specifications
- All objects include
@contextand@typefor proper JSON-LD interpretation - Dietary tags are mapped to schema.org suitableForDiet values
- Unmapped dietary tags are appended to the description as text (e.g., "Nut free. Sulphites")
Search for restaurant offers (promotions, happy hours, discounts, etc.) by type or restaurant. Returns a JSON-LD graph with FoodEstablishments and their active offers. Only active offers are returned.
Parameters:
offer_type(optional): Filter by offer type- Valid values:
coupon,discount,bogo,free-item,happy-hour - If not provided, returns all offer types
- Valid values:
restaurant_id(optional): Filter results to a specific food establishment- Use the
@idfromsearch_food_establishmentsresults
- Use the
offer_id(optional): Filter by specific offer identifier- Use the
identifiervalue from the offer object (d-tag)
- Use the
Returns: A JSON-LD graph structure following schema.org specifications:
@context: "https://schema.org"@graph: Array of FoodEstablishment objects, each containing:@type(string): Schema.org FoodEstablishment type (Restaurant, Bakery, etc.)name(string): Food establishment nameaddress(object, optional): PostalAddress with street, city, state, postal code@id(string): Food establishment identifier in bech32 format (nostr:npub1...)makesOffer(array): Array of Offer objects, each containing:@type: "Offer"identifier(string): Unique identifier for the offer (d-tag)description(string): Description of the offercategory(string): Category of the offer (coupon, discount, bogo, free-item, happy-hour)validFrom(string): Start date and time of the offer in ISO 8601 format with timezone offsetvalidThrough(string): End date and time of the offer in ISO 8601 format with timezone offset
Example:
{"offer_type": "happy-hour"}
{"offer_type": "discount", "restaurant_id": "nostr:npub1..."}
{"restaurant_id": "nostr:npub1..."}
{"offer_id": "MHWS1YD3"}
{} // Returns all active offers from all restaurantsBehavior Notes:
- Only returns offers with
status: "active"- restaurants can delete offers by publishing a new event withstatus: "inactive" - Offer times (
validFromandvalidThrough) are displayed in the restaurant's local timezone - Results are grouped by food establishment - each establishment appears once with all matching offers
- Output format is JSON-LD (JSON for Linked Data) following schema.org Offer specification
- All objects include
@contextand@typefor proper JSON-LD interpretation - Offers are stored as Nostr events (kind:31556) and leverage Nostr's replaceable event mechanism
The server supports both HTTP (for testing) and stdio (for Claude Desktop) transports.
-
Start the server in HTTP mode:
MCP_TRANSPORT=http npm run dev # or npm run dev -- --httpThe server will start on
http://localhost:3000(or the port specified inPORTenvironment variable). -
Test with MCP Inspector (in another terminal):
npm install -g @modelcontextprotocol/inspector mcp-inspector --transport http --server-url http://localhost:3000
The inspector will open in your browser. You can then:
- Click "List Tools" to see available tools
- Test each tool with sample parameters
- View structured responses
-
Build the server:
npm run build
-
Configure Claude Desktop - Add to
claude_desktop_config.json:{ "mcpServers": { "synvya": { "command": "node", "args": ["/absolute/path/to/mcp-server/dist/server.js"] } } } -
Restart Claude Desktop - The server will automatically start when Claude connects.
- Search food establishments:
{"foodEstablishmentType": "Restaurant", "cuisine": "Spanish", "dietary": "vegan"}or{"query": "Snoqualmie"}or{"foodEstablishmentType": "Bakery", "dietary": "gluten free"} - Get menu items:
{"restaurant_id": "nostr:npub1...", "menu_identifier": "Dinner"} - Search dishes:
{"dish_query": "pizza"}or{"dish_query": "vegan"}(auto-detects dietary term) - Search offers:
{"offer_type": "happy-hour"}or{"restaurant_id": "nostr:npub1..."}or{"offer_id": "MHWS1YD3"}or{}(all offers)
The MCP server can be deployed to Vercel as a serverless function, making it accessible to ChatGPT over HTTPS.
- A Vercel account (Hobby plan or higher)
- The Vercel CLI installed (optional, for local testing):
npm install -g vercel
-
Push your code to GitHub (if not already done):
git push origin main
-
Import project in Vercel:
- Go to vercel.com
- Click "Add New..." → "Project"
- Import your GitHub repository (
synvya/mcp-server) - Configure project settings:
- Framework Preset: "Other"
- Root Directory:
./ - Build Command:
npm run build(or leave default) - Output Directory: Leave empty
- Install Command:
npm install(or leave default)
-
Deploy:
- Click "Deploy"
- Wait for the deployment to complete
- Note your deployment URL (e.g.,
https://mcp.synvya.com)
-
Test the deployment:
npx @modelcontextprotocol/inspector@latest https://your-app.vercel.app/mcp
The MCP endpoint will be available at:
- Primary:
https://your-app.vercel.app/api/mcp - Alias:
https://your-app.vercel.app/mcp(via rewrite rule)
- Primary:
-
Enable Developer Mode in ChatGPT:
- Go to ChatGPT settings
- Enable "Developer Mode" or "Connectors"
-
Add MCP Server:
- In ChatGPT, go to the Connectors/Integrations section
- Add a new MCP server connector
- Enter your server URL:
https://your-app.vercel.app/mcp - Configure authentication if needed (currently not required)
-
Test the connection:
- ChatGPT should now be able to use your MCP tools
- Try asking: "Find me a vegan Spanish restaurant"
For CustomGPT or OpenAI Actions integration, use the auto-generated OpenAPI schema:
-
In CustomGPT/GPT Builder:
- Go to "Configure" → "Actions"
- Click "Import from URL"
- Enter:
https://mcp.synvya.com/api/schema-v1 - Click "Import"
-
Configure Authentication:
- Authentication: None (API is public)
- Privacy Policy: Optional (add your privacy policy URL if needed)
-
Test the Actions:
- Use the "Test" button in the Actions editor to verify each endpoint
- Try asking your GPT: "Find me a vegan Spanish restaurant in Snoqualmie"
Notes:
- The schema is auto-generated from the same Zod schemas used by the MCP interface
- Schema updates automatically when you redeploy to Vercel
- All endpoints support CORS for browser-based testing
Configure these in your Vercel project settings (Settings → Environment Variables):
DynamoDB Integration (optional - profiles, collections, and products can be loaded from live Nostr data):
USE_DYNAMODB- Enable DynamoDB integration (default:false)- Set to
trueto load profiles, collections, and products from DynamoDB instead of static files
- Set to
DYNAMODB_TABLE_NAME- DynamoDB table name (default:synvya-nostr-events)AWS_REGION- AWS region (default:us-east-1)AWS_ACCESS_KEY_ID- IAM user access key (required ifUSE_DYNAMODB=true)AWS_SECRET_ACCESS_KEY- IAM user secret key (required ifUSE_DYNAMODB=true)PROFILE_CACHE_TTL_SECONDS- Profile cache duration in seconds (default:300= 5 minutes)COLLECTION_CACHE_TTL_SECONDS- Collection cache duration in seconds (default:300= 5 minutes)PRODUCT_CACHE_TTL_SECONDS- Product cache duration in seconds (default:300= 5 minutes)OFFER_CACHE_TTL_SECONDS- Offer cache duration in seconds (default:300= 5 minutes)
Notes:
- When
USE_DYNAMODB=false(default), profiles, collections, and products are loaded fromdata/*.jsonfiles; offers are only loaded from DynamoDB - When
USE_DYNAMODB=true, profiles (kind:0), collections (kind:30405), products (kind:30402), and offers (kind:31556) are loaded from DynamoDB with automatic caching - If DynamoDB query fails, the system automatically falls back to static files
- Cache reduces DynamoDB costs and improves response times
- Data files not found: Ensure
data/directory is committed to git and not in.vercelignore - Build failures: Check that
npm run buildcompletes successfully locally - Cold starts: First request may be slower due to serverless cold starts
- CORS issues: The server includes CORS headers for browser-based testing
By default, the server reads from JSON files in the data/ directory:
profiles.json- Restaurant profiles (kind:0 events)collections.json- Menu collections (kind:30405 events)products.json- Individual dishes (kind:30402 events)- Note: Offers (kind:31556) are only loaded from DynamoDB, not from static files
When USE_DYNAMODB=true, the server loads profiles, collections, products, and offers from AWS DynamoDB:
- Table:
synvya-nostr-events(configurable) - Source: Live Nostr relay data (updated every 1 minute via Lambda)
- Event Kinds: Profiles (kind:0), Collections (kind:30405), Products (kind:30402), Offers (kind:31556)
- Caching: 5-minute in-memory cache per event type (configurable)
- Fallback: Automatically uses static files if DynamoDB fails (except for offers)
Benefits of DynamoDB:
- ✅ Real-time data updates from Nostr relays
- ✅ Automatic synchronization every 1 minute
- ✅ Scalable to thousands of restaurants and menu items
- ✅ No manual data file updates needed
Setup:
- ✅ Lambda infrastructure deployed (DynamoDB table, Lambda function, EventBridge schedule running every 1 minute)
- Configure Vercel environment variables (see above)
- Set
USE_DYNAMODB=truein production