UNPKG

@pizzamcp/server

Version:
515 lines (514 loc) 20.2 kB
#!/usr/bin/env node /** * MCPizza - Simple MCP Server for Domino's Pizza Ordering */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // Simple in-memory order state let currentOrder = { store: null, items: [], customer: null }; class MCPizzaServer { server; constructor() { this.server = new Server({ name: 'MCPizza', version: '1.3.2', }, { capabilities: { tools: {}, }, }); this.setupHandlers(); } setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'find_dominos_store', description: 'Find the nearest Domino\'s store by address or zip code', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Full address or zip code to search near' } }, required: ['address'] } }, { name: 'search_menu', description: 'Search for menu items by name or description', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search term (e.g., "pepperoni pizza", "wings")' } }, required: ['query'] } }, { name: 'add_to_order', description: 'Add items to pizza order', inputSchema: { type: 'object', properties: { item_code: { type: 'string', description: 'Product code from menu search' }, quantity: { type: 'integer', description: 'Number of items to add', default: 1 } }, required: ['item_code'] } }, { name: 'view_order', description: 'View current order contents and total', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'set_customer_info', description: 'Set customer information for delivery', inputSchema: { type: 'object', properties: { firstName: { type: 'string', description: 'First name' }, lastName: { type: 'string', description: 'Last name' }, email: { type: 'string', description: 'Email address' }, phone: { type: 'string', description: 'Phone number' }, address: { type: 'object', properties: { street: { type: 'string', description: 'Street address' }, city: { type: 'string', description: 'City' }, state: { type: 'string', description: 'State' }, zip: { type: 'string', description: 'ZIP code' } }, required: ['street', 'city', 'state', 'zip'] } }, required: ['firstName', 'lastName', 'email', 'phone', 'address'] } }, { name: 'place_order', description: 'Place the actual pizza order for delivery or pickup', inputSchema: { type: 'object', properties: { orderType: { type: 'string', enum: ['Delivery', 'Carryout'], description: 'Order type' }, paymentType: { type: 'string', enum: ['Cash', 'CreditCard'], description: 'Payment method' }, creditCard: { type: 'object', properties: { number: { type: 'string', description: 'Card number' }, expiration: { type: 'string', description: 'MM/YY format' }, securityCode: { type: 'string', description: 'CVV' }, postalCode: { type: 'string', description: 'Billing ZIP' } } } }, required: ['orderType', 'paymentType'] } } ] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (!args) { throw new Error('No arguments provided'); } switch (name) { case 'find_dominos_store': return await this.findStore(args.address); case 'search_menu': return await this.searchMenu(args.query); case 'add_to_order': return await this.addToOrder(args.item_code, args.quantity || 1); case 'view_order': return await this.viewOrder(); case 'set_customer_info': return await this.setCustomerInfo(args); case 'place_order': return await this.placeOrder(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}` } ] }; } }); } async findStore(address) { // Use a real store locator API call try { const response = await fetch('https://order.dominos.com/power/store-locator', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ s: address, type: 'Delivery' }) }); if (!response.ok) { throw new Error('Store locator service unavailable'); } const data = await response.json(); if (data.Stores && data.Stores.length > 0) { const store = data.Stores[0]; currentOrder.store = store; return { content: [ { type: 'text', text: `Found store: ${store.StoreName}\nAddress: ${store.AddressDescription}\nPhone: ${store.Phone}\nStore ID: ${store.StoreID}` } ] }; } else { throw new Error('No stores found near that address'); } } catch (error) { return { content: [ { type: 'text', text: `Store search failed: ${error.message}. Try a different address or zip code.` } ] }; } } async searchMenu(query) { if (!currentOrder.store) { return { content: [ { type: 'text', text: 'Please find a store first using find_dominos_store' } ] }; } try { const response = await fetch(`https://order.dominos.com/power/store/${currentOrder.store.StoreID}/menu`, { method: 'GET', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { throw new Error('Menu service unavailable'); } const menu = await response.json(); const results = []; // Search through menu categories for (const [categoryKey, category] of Object.entries(menu.Categories || {})) { if (typeof category === 'object' && category !== null && 'Products' in category) { const products = category.Products; for (const [productCode, product] of Object.entries(products || {})) { if (typeof product === 'object' && product !== null) { const name = product.Name || ''; const description = product.Description || ''; if (name.toLowerCase().includes(query.toLowerCase()) || description.toLowerCase().includes(query.toLowerCase())) { results.push({ code: productCode, name, description, category: categoryKey }); } } } } } if (results.length === 0) { return { content: [ { type: 'text', text: `No menu items found for "${query}". Try searching for "pizza", "wings", or "pasta".` } ] }; } return { content: [ { type: 'text', text: `Found ${results.length} items:\n\n${results.map(item => `• ${item.name} (${item.code})\n ${item.description}`).join('\n\n')}` } ] }; } catch (error) { return { content: [ { type: 'text', text: `Menu search failed: ${error.message}` } ] }; } } async addToOrder(itemCode, quantity) { if (!currentOrder.store) { return { content: [ { type: 'text', text: 'Please find a store first using find_dominos_store' } ] }; } currentOrder.items.push({ code: itemCode, quantity: quantity, addedAt: new Date().toISOString() }); return { content: [ { type: 'text', text: `Added ${quantity}x ${itemCode} to your order` } ] }; } async viewOrder() { if (currentOrder.items.length === 0) { return { content: [ { type: 'text', text: 'Your order is empty. Use search_menu to find items, then add_to_order to add them.' } ] }; } const itemList = currentOrder.items.map(item => `• ${item.quantity}x ${item.code}`).join('\n'); return { content: [ { type: 'text', text: `Your current order:\n${itemList}\n\nStore: ${currentOrder.store?.StoreName || 'None selected'}\nTotal items: ${currentOrder.items.length}` } ] }; } async setCustomerInfo(info) { currentOrder.customer = { firstName: info.firstName, lastName: info.lastName, email: info.email, phone: info.phone, address: info.address }; return { content: [ { type: 'text', text: `Customer info set for ${info.firstName} ${info.lastName}` } ] }; } async placeOrder(orderInfo) { if (!currentOrder.store) { return { content: [ { type: 'text', text: 'Please find a store first using find_dominos_store' } ] }; } if (!currentOrder.customer) { return { content: [ { type: 'text', text: 'Please set customer info first using set_customer_info' } ] }; } if (currentOrder.items.length === 0) { return { content: [ { type: 'text', text: 'Your order is empty. Add items first using add_to_order' } ] }; } try { // Build the order payload for Domino's API const orderPayload = { Order: { Address: currentOrder.customer.address, Coupons: [], CustomerID: "", Email: currentOrder.customer.email, Extension: "", FirstName: currentOrder.customer.firstName, LastName: currentOrder.customer.lastName, LanguageCode: "en", OrderChannel: "OLO", OrderMethod: "Web", OrderTaker: null, Payments: [], Phone: currentOrder.customer.phone, PhonePrefix: "", Products: currentOrder.items.map(item => ({ Code: item.code, Qty: item.quantity, ID: 1, isNew: true, Options: {} })), ServiceMethod: orderInfo.orderType, SourceOrganizationURI: "order.dominos.com", StoreID: currentOrder.store.StoreID, Tags: {}, Version: "1.0", NoCombine: true, Partners: {}, OrderInfos: [], HotspotsLite: false, OrderSourceCode: "CustomerWebsite", IP: "" } }; // Add payment info if (orderInfo.paymentType === 'CreditCard' && orderInfo.creditCard) { orderPayload.Order.Payments = [{ Type: "CreditCard", Amount: 0, // Will be calculated by Domino's Number: orderInfo.creditCard.number, CardType: "VISA", // Should detect this Expiration: orderInfo.creditCard.expiration, SecurityCode: orderInfo.creditCard.securityCode, PostalCode: orderInfo.creditCard.postalCode }]; } else if (orderInfo.paymentType === 'Cash') { orderPayload.Order.Payments = [{ Type: "Cash", Amount: 0 }]; } // Price the order first const priceResponse = await fetch(`https://order.dominos.com/power/price-order`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(orderPayload) }); if (!priceResponse.ok) { throw new Error('Failed to price order'); } const pricedOrder = await priceResponse.json(); if (pricedOrder.Status !== 0) { throw new Error(`Order pricing failed: ${JSON.stringify(pricedOrder.StatusItems)}`); } // Place the actual order const placeResponse = await fetch(`https://order.dominos.com/power/place-order`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(pricedOrder.Order) }); if (!placeResponse.ok) { throw new Error('Failed to place order'); } const result = await placeResponse.json(); if (result.Status === 0) { // Order success - clear the current order currentOrder.items = []; return { content: [ { type: 'text', text: `🍕 ORDER PLACED SUCCESSFULLY!\n\nOrder ID: ${result.OrderID}\nEstimated Wait: ${result.EstimatedWaitMinutes} minutes\nTotal: $${result.Order?.Amounts?.Customer || 'Unknown'}\n\nYour pizza is being prepared!` } ] }; } else { return { content: [ { type: 'text', text: `Order failed: ${JSON.stringify(result.StatusItems)}` } ] }; } } catch (error) { return { content: [ { type: 'text', text: `Failed to place order: ${error.message}` } ] }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('MCPizza MCP Server running...'); } } const server = new MCPizzaServer(); server.run().catch(console.error);