@striderlabs/mcp-buffalowildwings 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,798 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { WING_FLAVORS, MENU_ITEMS, CATEGORIES } from "./menu.js";
5
+ import { searchLocations, getLocationById, SAMPLE_LOCATIONS } from "./locations.js";
6
+ import {
7
+ getSession,
8
+ updateSession,
9
+ resetSession,
10
+ CartItem,
11
+ } from "./session.js";
12
+
13
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
14
+
15
+ function ok(data: unknown) {
16
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
17
+ }
18
+
19
+ function err(message: string, details?: unknown) {
20
+ return {
21
+ content: [
22
+ {
23
+ type: "text" as const,
24
+ text: JSON.stringify({ error: message, ...(details ? { details } : {}) }, null, 2),
25
+ },
26
+ ],
27
+ isError: true,
28
+ };
29
+ }
30
+
31
+ function cartTotal(cart: CartItem[]): number {
32
+ return cart.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
33
+ }
34
+
35
+ function generateOrderId(): string {
36
+ return "BWW-" + Math.random().toString(36).substring(2, 10).toUpperCase();
37
+ }
38
+
39
+ // ─── Server Setup ─────────────────────────────────────────────────────────────
40
+
41
+ const server = new McpServer({
42
+ name: "buffalowildwings",
43
+ version: "0.1.0",
44
+ });
45
+
46
+ // ─── Tool: bww_find_locations ─────────────────────────────────────────────────
47
+
48
+ server.tool(
49
+ "bww_find_locations",
50
+ "Search for Buffalo Wild Wings locations near a city, address, or zip code. Returns a list of nearby restaurants with hours, features, and ordering options.",
51
+ {
52
+ query: z.string().describe("City name, state, zip code, or address to search near"),
53
+ show_all: z.boolean().optional().describe("If true, return all known locations. Default: false"),
54
+ },
55
+ async ({ query, show_all }) => {
56
+ try {
57
+ const results = show_all ? SAMPLE_LOCATIONS : searchLocations(query);
58
+ if (results.length === 0) {
59
+ return ok({
60
+ message: `No Buffalo Wild Wings locations found near "${query}". Try a major city name or zip code.`,
61
+ locations: [],
62
+ tip: "Use show_all: true to see all available locations.",
63
+ });
64
+ }
65
+ return ok({
66
+ count: results.length,
67
+ locations: results.map(loc => ({
68
+ id: loc.id,
69
+ name: loc.name,
70
+ address: `${loc.address}, ${loc.city}, ${loc.state} ${loc.zip}`,
71
+ phone: loc.phone,
72
+ features: loc.features,
73
+ supportsPickup: loc.supportsPickup,
74
+ supportsDelivery: loc.supportsDelivery,
75
+ supportsReservations: loc.supportsReservations,
76
+ waitTime: loc.waitTime,
77
+ deliveryRadius: loc.deliveryRadius ? `${loc.deliveryRadius} miles` : undefined,
78
+ todayHours: loc.hours.friday, // simplified — real impl would use current day
79
+ })),
80
+ tip: "Use bww_set_location to select a restaurant before ordering.",
81
+ });
82
+ } catch (e) {
83
+ return err("Failed to search locations", String(e));
84
+ }
85
+ }
86
+ );
87
+
88
+ // ─── Tool: bww_set_location ───────────────────────────────────────────────────
89
+
90
+ server.tool(
91
+ "bww_set_location",
92
+ "Set the active Buffalo Wild Wings location for ordering. Must be called before adding items to cart.",
93
+ {
94
+ location_id: z.string().describe("Location ID from bww_find_locations"),
95
+ order_type: z.enum(["pickup", "delivery"]).describe("How you want to receive your order"),
96
+ delivery_address: z.string().optional().describe("Street address for delivery orders"),
97
+ },
98
+ async ({ location_id, order_type, delivery_address }) => {
99
+ try {
100
+ const location = getLocationById(location_id);
101
+ if (!location) {
102
+ return err(`Location "${location_id}" not found. Use bww_find_locations to get valid IDs.`);
103
+ }
104
+ if (order_type === "delivery" && !location.supportsDelivery) {
105
+ return err(`${location.name} does not support delivery. Choose pickup or a different location.`);
106
+ }
107
+ if (order_type === "delivery" && !delivery_address) {
108
+ return err("delivery_address is required for delivery orders.");
109
+ }
110
+ if (order_type === "pickup" && !location.supportsPickup) {
111
+ return err(`${location.name} does not support pickup.`);
112
+ }
113
+
114
+ updateSession({
115
+ locationId: location_id,
116
+ locationName: location.name,
117
+ orderType: order_type,
118
+ deliveryAddress: delivery_address,
119
+ cart: [], // reset cart when changing location
120
+ });
121
+
122
+ return ok({
123
+ success: true,
124
+ location: {
125
+ id: location.id,
126
+ name: location.name,
127
+ address: `${location.address}, ${location.city}, ${location.state} ${location.zip}`,
128
+ phone: location.phone,
129
+ },
130
+ orderType: order_type,
131
+ deliveryAddress: delivery_address,
132
+ message: `Location set to ${location.name}. Cart cleared. Ready to order!`,
133
+ nextStep: "Use bww_view_menu to browse items, then bww_add_to_cart.",
134
+ });
135
+ } catch (e) {
136
+ return err("Failed to set location", String(e));
137
+ }
138
+ }
139
+ );
140
+
141
+ // ─── Tool: bww_view_menu ──────────────────────────────────────────────────────
142
+
143
+ server.tool(
144
+ "bww_view_menu",
145
+ "Browse the Buffalo Wild Wings menu. Can filter by category or search for specific items. Includes wing flavors and sauces.",
146
+ {
147
+ category: z.string().optional().describe(`Filter by category. Options: ${CATEGORIES.join(", ")}`),
148
+ search: z.string().optional().describe("Search menu items by name or description"),
149
+ show_flavors: z.boolean().optional().describe("If true, return detailed wing flavor/sauce list"),
150
+ heat_level: z.number().min(1).max(5).optional().describe("Filter wing flavors by heat level (1=mild, 5=blazin)"),
151
+ },
152
+ async ({ category, search, show_flavors, heat_level }) => {
153
+ try {
154
+ if (show_flavors) {
155
+ let flavors = WING_FLAVORS;
156
+ if (heat_level !== undefined) {
157
+ flavors = flavors.filter(f => f.heatLevel === heat_level);
158
+ }
159
+ return ok({
160
+ wingFlavors: flavors.map(f => ({
161
+ id: f.id,
162
+ name: f.name,
163
+ heatLevel: `${"🔥".repeat(f.heatLevel)} (${f.heatLevel}/5)`,
164
+ description: f.description,
165
+ ...(f.isNew ? { badge: "NEW" } : {}),
166
+ ...(f.isLimitedTime ? { badge: "LIMITED TIME" } : {}),
167
+ })),
168
+ count: flavors.length,
169
+ tip: "Use the flavor name when calling bww_add_to_cart.",
170
+ });
171
+ }
172
+
173
+ let items = MENU_ITEMS;
174
+ if (category) {
175
+ items = items.filter(i => i.category.toLowerCase() === category.toLowerCase());
176
+ }
177
+ if (search) {
178
+ const q = search.toLowerCase();
179
+ items = items.filter(
180
+ i => i.name.toLowerCase().includes(q) || i.description.toLowerCase().includes(q)
181
+ );
182
+ }
183
+
184
+ if (items.length === 0) {
185
+ return ok({
186
+ message: "No menu items found matching your criteria.",
187
+ availableCategories: CATEGORIES,
188
+ });
189
+ }
190
+
191
+ return ok({
192
+ count: items.length,
193
+ categories: CATEGORIES,
194
+ items: items.map(i => ({
195
+ id: i.id,
196
+ name: i.name,
197
+ category: i.category,
198
+ description: i.description,
199
+ price: `$${i.basePrice.toFixed(2)}`,
200
+ calories: i.calories,
201
+ ...(i.isPopular ? { popular: true } : {}),
202
+ ...(i.options ? { options: i.options } : {}),
203
+ })),
204
+ tip: "Use item ID and sauce name with bww_add_to_cart to add items.",
205
+ });
206
+ } catch (e) {
207
+ return err("Failed to load menu", String(e));
208
+ }
209
+ }
210
+ );
211
+
212
+ // ─── Tool: bww_add_to_cart ────────────────────────────────────────────────────
213
+
214
+ server.tool(
215
+ "bww_add_to_cart",
216
+ "Add a menu item to your Buffalo Wild Wings cart. Location must be set first with bww_set_location.",
217
+ {
218
+ item_id: z.string().describe("Menu item ID from bww_view_menu"),
219
+ quantity: z.number().min(1).max(50).describe("Number of items to add"),
220
+ sauce: z.string().optional().describe("Wing sauce or dry rub name (required for wing items)"),
221
+ notes: z.string().optional().describe("Special instructions for this item"),
222
+ },
223
+ async ({ item_id, quantity, sauce, notes }) => {
224
+ try {
225
+ const session = getSession();
226
+ if (!session.locationId) {
227
+ return err("No location selected. Use bww_set_location first.");
228
+ }
229
+
230
+ const menuItem = MENU_ITEMS.find(i => i.id === item_id);
231
+ if (!menuItem) {
232
+ return err(`Item "${item_id}" not found. Use bww_view_menu to see available items.`);
233
+ }
234
+
235
+ // Validate sauce for wing items
236
+ if (menuItem.category === "Wings" && !sauce) {
237
+ return err("Wing items require a sauce or dry rub. Use bww_view_menu with show_flavors: true to see options.");
238
+ }
239
+
240
+ if (sauce) {
241
+ const validFlavor = WING_FLAVORS.find(
242
+ f => f.name.toLowerCase() === sauce.toLowerCase() || f.id === sauce.toLowerCase()
243
+ );
244
+ if (!validFlavor) {
245
+ return err(`Sauce "${sauce}" not found. Use bww_view_menu with show_flavors: true to see available sauces.`);
246
+ }
247
+ }
248
+
249
+ const newItem: CartItem = {
250
+ itemId: item_id,
251
+ name: menuItem.name + (sauce ? ` (${sauce})` : ""),
252
+ quantity,
253
+ unitPrice: menuItem.basePrice,
254
+ sauce,
255
+ notes,
256
+ };
257
+
258
+ // Check if same item+sauce already in cart
259
+ const existing = session.cart.find(
260
+ ci => ci.itemId === item_id && ci.sauce === sauce
261
+ );
262
+ let updatedCart: CartItem[];
263
+ if (existing) {
264
+ updatedCart = session.cart.map(ci =>
265
+ ci.itemId === item_id && ci.sauce === sauce
266
+ ? { ...ci, quantity: ci.quantity + quantity }
267
+ : ci
268
+ );
269
+ } else {
270
+ updatedCart = [...session.cart, newItem];
271
+ }
272
+
273
+ updateSession({ cart: updatedCart });
274
+
275
+ return ok({
276
+ success: true,
277
+ added: {
278
+ name: newItem.name,
279
+ quantity,
280
+ unitPrice: `$${menuItem.basePrice.toFixed(2)}`,
281
+ subtotal: `$${(menuItem.basePrice * quantity).toFixed(2)}`,
282
+ },
283
+ cart: {
284
+ itemCount: updatedCart.reduce((s, i) => s + i.quantity, 0),
285
+ total: `$${cartTotal(updatedCart).toFixed(2)}`,
286
+ },
287
+ nextStep: "Use bww_view_cart to review, or bww_checkout to place your order.",
288
+ });
289
+ } catch (e) {
290
+ return err("Failed to add item to cart", String(e));
291
+ }
292
+ }
293
+ );
294
+
295
+ // ─── Tool: bww_view_cart ──────────────────────────────────────────────────────
296
+
297
+ server.tool(
298
+ "bww_view_cart",
299
+ "View the current Buffalo Wild Wings cart contents and order summary.",
300
+ {},
301
+ async () => {
302
+ try {
303
+ const session = getSession();
304
+ if (!session.locationId) {
305
+ return ok({
306
+ cart: [],
307
+ message: "No location selected. Use bww_set_location to begin.",
308
+ });
309
+ }
310
+ if (session.cart.length === 0) {
311
+ return ok({
312
+ location: session.locationName,
313
+ orderType: session.orderType,
314
+ cart: [],
315
+ message: "Your cart is empty. Use bww_add_to_cart to add items.",
316
+ });
317
+ }
318
+
319
+ const subtotal = cartTotal(session.cart);
320
+ const tax = subtotal * 0.0875;
321
+ const deliveryFee = session.orderType === "delivery" ? 3.99 : 0;
322
+ const total = subtotal + tax + deliveryFee;
323
+
324
+ return ok({
325
+ location: session.locationName,
326
+ orderType: session.orderType,
327
+ deliveryAddress: session.deliveryAddress,
328
+ items: session.cart.map(i => ({
329
+ name: i.name,
330
+ quantity: i.quantity,
331
+ unitPrice: `$${i.unitPrice.toFixed(2)}`,
332
+ subtotal: `$${(i.unitPrice * i.quantity).toFixed(2)}`,
333
+ ...(i.notes ? { notes: i.notes } : {}),
334
+ })),
335
+ summary: {
336
+ subtotal: `$${subtotal.toFixed(2)}`,
337
+ tax: `$${tax.toFixed(2)}`,
338
+ deliveryFee: session.orderType === "delivery" ? `$${deliveryFee.toFixed(2)}` : "$0.00",
339
+ estimatedTotal: `$${total.toFixed(2)}`,
340
+ },
341
+ nextSteps: "Use bww_checkout to place your order, or bww_remove_from_cart to modify.",
342
+ });
343
+ } catch (e) {
344
+ return err("Failed to load cart", String(e));
345
+ }
346
+ }
347
+ );
348
+
349
+ // ─── Tool: bww_remove_from_cart ───────────────────────────────────────────────
350
+
351
+ server.tool(
352
+ "bww_remove_from_cart",
353
+ "Remove an item from the cart or reduce its quantity.",
354
+ {
355
+ item_name: z.string().describe("Partial name of the item to remove (case-insensitive)"),
356
+ quantity: z.number().min(1).optional().describe("Quantity to remove. If omitted, removes the item entirely."),
357
+ },
358
+ async ({ item_name, quantity }) => {
359
+ try {
360
+ const session = getSession();
361
+ const q = item_name.toLowerCase();
362
+ const idx = session.cart.findIndex(i => i.name.toLowerCase().includes(q));
363
+
364
+ if (idx === -1) {
365
+ return err(`No cart item matching "${item_name}" found.`);
366
+ }
367
+
368
+ let updatedCart = [...session.cart];
369
+ const item = updatedCart[idx];
370
+
371
+ if (quantity && quantity < item.quantity) {
372
+ updatedCart[idx] = { ...item, quantity: item.quantity - quantity };
373
+ } else {
374
+ updatedCart.splice(idx, 1);
375
+ }
376
+
377
+ updateSession({ cart: updatedCart });
378
+
379
+ return ok({
380
+ success: true,
381
+ removed: item.name,
382
+ cart: {
383
+ itemCount: updatedCart.reduce((s, i) => s + i.quantity, 0),
384
+ total: `$${cartTotal(updatedCart).toFixed(2)}`,
385
+ },
386
+ });
387
+ } catch (e) {
388
+ return err("Failed to remove item", String(e));
389
+ }
390
+ }
391
+ );
392
+
393
+ // ─── Tool: bww_checkout ───────────────────────────────────────────────────────
394
+
395
+ server.tool(
396
+ "bww_checkout",
397
+ "Place your Buffalo Wild Wings order. Provide payment details and optionally apply Blazin Rewards. Returns order confirmation.",
398
+ {
399
+ name: z.string().describe("Full name for the order"),
400
+ phone: z.string().describe("Phone number for order notifications"),
401
+ email: z.string().optional().describe("Email for order confirmation"),
402
+ payment_method: z.enum(["credit_card", "apple_pay", "google_pay", "rewards_points"]).describe("Payment method"),
403
+ card_last4: z.string().length(4).optional().describe("Last 4 digits of credit card (for credit_card payment)"),
404
+ tip_percent: z.number().min(0).max(100).optional().describe("Tip percentage (0-100). Default: 18"),
405
+ apply_rewards: z.boolean().optional().describe("Apply available Blazin Rewards points to this order"),
406
+ pickup_time: z.string().optional().describe("Requested pickup time, e.g. 'ASAP' or '6:30 PM'"),
407
+ confirm: z.boolean().describe("Must be true to actually place the order. Set to false to preview only."),
408
+ },
409
+ async ({ name, phone, email, payment_method, card_last4, tip_percent, apply_rewards, pickup_time, confirm }) => {
410
+ try {
411
+ const session = getSession();
412
+
413
+ if (!session.locationId) {
414
+ return err("No location selected. Use bww_set_location first.");
415
+ }
416
+ if (session.cart.length === 0) {
417
+ return err("Cart is empty. Add items with bww_add_to_cart first.");
418
+ }
419
+ if (payment_method === "credit_card" && !card_last4) {
420
+ return err("card_last4 is required when payment_method is credit_card.");
421
+ }
422
+
423
+ const subtotal = cartTotal(session.cart);
424
+ const tax = subtotal * 0.0875;
425
+ const deliveryFee = session.orderType === "delivery" ? 3.99 : 0;
426
+ const tipAmount = subtotal * ((tip_percent ?? 18) / 100);
427
+
428
+ let rewardsDiscount = 0;
429
+ if (apply_rewards && session.rewardsPoints && session.rewardsPoints > 0) {
430
+ rewardsDiscount = Math.min(session.rewardsPoints * 0.01, subtotal * 0.2); // up to 20% off
431
+ }
432
+
433
+ const total = subtotal + tax + deliveryFee + tipAmount - rewardsDiscount;
434
+ const orderId = generateOrderId();
435
+ const estimatedTime =
436
+ session.orderType === "delivery" ? "35-50 minutes" : pickup_time || "20-25 minutes";
437
+
438
+ const orderPreview = {
439
+ orderId,
440
+ location: session.locationName,
441
+ orderType: session.orderType,
442
+ deliveryAddress: session.deliveryAddress,
443
+ name,
444
+ phone,
445
+ email,
446
+ items: session.cart.map(i => ({
447
+ name: i.name,
448
+ quantity: i.quantity,
449
+ price: `$${(i.unitPrice * i.quantity).toFixed(2)}`,
450
+ })),
451
+ pricing: {
452
+ subtotal: `$${subtotal.toFixed(2)}`,
453
+ tax: `$${tax.toFixed(2)}`,
454
+ tip: `$${tipAmount.toFixed(2)}`,
455
+ deliveryFee: `$${deliveryFee.toFixed(2)}`,
456
+ rewardsDiscount: rewardsDiscount > 0 ? `-$${rewardsDiscount.toFixed(2)}` : undefined,
457
+ total: `$${total.toFixed(2)}`,
458
+ },
459
+ payment: payment_method === "credit_card" ? `Card ending in ${card_last4}` : payment_method,
460
+ estimatedTime,
461
+ };
462
+
463
+ if (!confirm) {
464
+ return ok({
465
+ preview: true,
466
+ message: "Order preview (not placed). Set confirm: true to place the order.",
467
+ ...orderPreview,
468
+ });
469
+ }
470
+
471
+ // Simulate placing order
472
+ updateSession({
473
+ orderId,
474
+ orderStatus: "confirmed",
475
+ cart: [],
476
+ });
477
+
478
+ return ok({
479
+ success: true,
480
+ message: `Order placed! Your wings are on the way.`,
481
+ ...orderPreview,
482
+ status: "confirmed",
483
+ trackingTip: `Use bww_track_order with order_id: "${orderId}" to check your order status.`,
484
+ });
485
+ } catch (e) {
486
+ return err("Checkout failed", String(e));
487
+ }
488
+ }
489
+ );
490
+
491
+ // ─── Tool: bww_track_order ────────────────────────────────────────────────────
492
+
493
+ server.tool(
494
+ "bww_track_order",
495
+ "Track the status of a Buffalo Wild Wings order.",
496
+ {
497
+ order_id: z.string().optional().describe("Order ID from checkout confirmation. If omitted, uses most recent order."),
498
+ },
499
+ async ({ order_id }) => {
500
+ try {
501
+ const session = getSession();
502
+ const id = order_id || session.orderId;
503
+
504
+ if (!id) {
505
+ return err("No order ID provided and no recent order found in session.");
506
+ }
507
+
508
+ // Simulate order tracking states
509
+ const statuses = ["confirmed", "preparing", "ready_for_pickup", "out_for_delivery", "delivered"];
510
+ const simulatedStatus =
511
+ session.orderId === id ? session.orderStatus || "confirmed" : "confirmed";
512
+
513
+ const statusMessages: Record<string, string> = {
514
+ confirmed: "Order received and confirmed. Kitchen is warming up!",
515
+ preparing: "Your wings are being sauced and cooked fresh.",
516
+ ready_for_pickup: "Your order is ready! Head to the pickup counter.",
517
+ out_for_delivery: "Your order is on its way!",
518
+ delivered: "Order delivered. Enjoy your wings!",
519
+ };
520
+
521
+ return ok({
522
+ orderId: id,
523
+ status: simulatedStatus,
524
+ message: statusMessages[simulatedStatus] || "Order in progress.",
525
+ estimatedCompletion: "20-25 minutes from order time",
526
+ location: session.locationName,
527
+ note: "This is a simulated status. Live tracking requires BWW app integration.",
528
+ });
529
+ } catch (e) {
530
+ return err("Failed to track order", String(e));
531
+ }
532
+ }
533
+ );
534
+
535
+ // ─── Tool: bww_blazin_rewards ─────────────────────────────────────────────────
536
+
537
+ server.tool(
538
+ "bww_blazin_rewards",
539
+ "Manage your Blazin' Rewards account — check points, redeem rewards, view tier status, and link your account.",
540
+ {
541
+ action: z.enum(["status", "link", "check_points", "view_perks"]).describe(
542
+ "Action to perform: status=show account summary, link=connect rewards account, check_points=see point balance, view_perks=see available rewards"
543
+ ),
544
+ phone: z.string().optional().describe("Phone number to link/lookup rewards account"),
545
+ member_id: z.string().optional().describe("Blazin' Rewards member ID"),
546
+ },
547
+ async ({ action, phone, member_id }) => {
548
+ try {
549
+ const session = getSession();
550
+
551
+ if (action === "link") {
552
+ if (!phone && !member_id) {
553
+ return err("Provide phone or member_id to link your Blazin' Rewards account.");
554
+ }
555
+ // Simulate account lookup
556
+ const simulatedPoints = Math.floor(Math.random() * 5000) + 200;
557
+ const tier = simulatedPoints > 3000 ? "Blazin'" : simulatedPoints > 1000 ? "Medium" : "Mild";
558
+
559
+ updateSession({
560
+ rewardsPhone: phone,
561
+ rewardsMemberId: member_id || "BRW" + Date.now().toString().slice(-8),
562
+ rewardsPoints: simulatedPoints,
563
+ });
564
+
565
+ return ok({
566
+ success: true,
567
+ message: "Blazin' Rewards account linked!",
568
+ account: {
569
+ phone: phone || "on file",
570
+ memberId: session.rewardsMemberId,
571
+ points: simulatedPoints,
572
+ tier,
573
+ pointsValue: `$${(simulatedPoints * 0.01).toFixed(2)} in rewards`,
574
+ },
575
+ });
576
+ }
577
+
578
+ if (action === "status" || action === "check_points") {
579
+ if (!session.rewardsPhone && !session.rewardsMemberId) {
580
+ return ok({
581
+ linked: false,
582
+ message: "No Blazin' Rewards account linked. Use action: 'link' with your phone number.",
583
+ });
584
+ }
585
+ return ok({
586
+ linked: true,
587
+ points: session.rewardsPoints || 0,
588
+ pointsValue: `$${((session.rewardsPoints || 0) * 0.01).toFixed(2)}`,
589
+ tier: (session.rewardsPoints || 0) > 3000 ? "Blazin'" : (session.rewardsPoints || 0) > 1000 ? "Medium" : "Mild",
590
+ memberId: session.rewardsMemberId,
591
+ phone: session.rewardsPhone,
592
+ });
593
+ }
594
+
595
+ if (action === "view_perks") {
596
+ return ok({
597
+ perks: [
598
+ { name: "Free Wings Birthday Reward", description: "Free 6 wings on your birthday month", tier: "All Members" },
599
+ { name: "Points per Dollar", description: "Earn 10 pts per $1 spent", tier: "All Members" },
600
+ { name: "Bonus Points Days", description: "Double points on Tuesdays", tier: "All Members" },
601
+ { name: "Free Sauce Upgrade", description: "Try any new sauce free", tier: "Medium (1000+ pts)" },
602
+ { name: "Priority Seating", description: "Skip the wait on game days", tier: "Blazin' (3000+ pts)" },
603
+ { name: "Monthly Free Wings", description: "6 free traditional wings per month", tier: "Blazin' (3000+ pts)" },
604
+ { name: "Exclusive Sauce Access", description: "Early access to new limited-time sauces", tier: "Blazin' (3000+ pts)" },
605
+ ],
606
+ redeemTip: "Points are worth $0.01 each. Redeem at checkout with apply_rewards: true.",
607
+ });
608
+ }
609
+
610
+ return err("Unknown action.");
611
+ } catch (e) {
612
+ return err("Blazin' Rewards action failed", String(e));
613
+ }
614
+ }
615
+ );
616
+
617
+ // ─── Tool: bww_reserve_table ──────────────────────────────────────────────────
618
+
619
+ server.tool(
620
+ "bww_reserve_table",
621
+ "Reserve a table at a Buffalo Wild Wings location that supports reservations. Check bww_find_locations for supportsReservations.",
622
+ {
623
+ location_id: z.string().describe("Location ID from bww_find_locations"),
624
+ date: z.string().describe("Reservation date in YYYY-MM-DD format"),
625
+ time: z.string().describe("Reservation time, e.g. '7:00 PM'"),
626
+ party_size: z.number().min(1).max(50).describe("Number of guests"),
627
+ name: z.string().describe("Name for the reservation"),
628
+ phone: z.string().describe("Contact phone number"),
629
+ special_requests: z.string().optional().describe("Special requests, e.g. 'near a TV', 'birthday celebration'"),
630
+ },
631
+ async ({ location_id, date, time, party_size, name, phone, special_requests }) => {
632
+ try {
633
+ const location = getLocationById(location_id);
634
+ if (!location) {
635
+ return err(`Location "${location_id}" not found.`);
636
+ }
637
+ if (!location.supportsReservations) {
638
+ return ok({
639
+ success: false,
640
+ message: `${location.name} does not currently accept online reservations.`,
641
+ suggestion: `Call directly at ${location.phone} to check walk-in availability or make a reservation.`,
642
+ });
643
+ }
644
+
645
+ // Validate date
646
+ const reservationDate = new Date(date);
647
+ if (isNaN(reservationDate.getTime())) {
648
+ return err("Invalid date format. Use YYYY-MM-DD.");
649
+ }
650
+
651
+ const confirmationId = "RES-" + Math.random().toString(36).substring(2, 8).toUpperCase();
652
+
653
+ return ok({
654
+ success: true,
655
+ confirmationId,
656
+ reservation: {
657
+ location: location.name,
658
+ address: `${location.address}, ${location.city}, ${location.state} ${location.zip}`,
659
+ date,
660
+ time,
661
+ partySize: party_size,
662
+ name,
663
+ phone,
664
+ specialRequests: special_requests || "None",
665
+ },
666
+ message: `Table reserved for ${party_size} at ${location.name} on ${date} at ${time}.`,
667
+ reminder: "Please arrive 5-10 minutes early. Reservations held for 15 minutes.",
668
+ cancelPolicy: "Cancel at least 2 hours in advance by calling the restaurant.",
669
+ restaurantPhone: location.phone,
670
+ });
671
+ } catch (e) {
672
+ return err("Failed to make reservation", String(e));
673
+ }
674
+ }
675
+ );
676
+
677
+ // ─── Tool: bww_game_day_specials ──────────────────────────────────────────────
678
+
679
+ server.tool(
680
+ "bww_game_day_specials",
681
+ "View current game day specials, happy hour deals, wing Tuesday promotions, and limited-time offers at Buffalo Wild Wings.",
682
+ {
683
+ location_id: z.string().optional().describe("Location ID to check location-specific specials. Omit for general specials."),
684
+ day: z.string().optional().describe("Day of week to check specials for, e.g. 'tuesday', 'sunday'. Defaults to today."),
685
+ },
686
+ async ({ location_id, day }) => {
687
+ try {
688
+ const location = location_id ? getLocationById(location_id) : null;
689
+ const targetDay = (day || new Date().toLocaleDateString("en-US", { weekday: "long" })).toLowerCase();
690
+
691
+ const specials: Record<string, unknown[]> = {
692
+ monday: [
693
+ { name: "Monday Night Football Special", deal: "$1 off all draft beers during MNF", validDuring: "Game time" },
694
+ { name: "Boneless Monday", deal: "50¢ boneless wings all day", validDuring: "All day" },
695
+ ],
696
+ tuesday: [
697
+ { name: "Wing Tuesday (National Wing Day!)", deal: "Traditional wings at reduced price — biggest deal of the week", validDuring: "All day", popular: true },
698
+ { name: "Double Points Tuesday", deal: "Earn 2x Blazin' Rewards points on all orders", validDuring: "All day" },
699
+ ],
700
+ wednesday: [
701
+ { name: "Happy Hour", deal: "$3 domestic drafts, $2 off appetizers", validDuring: "3:00 PM - 7:00 PM" },
702
+ ],
703
+ thursday: [
704
+ { name: "Throwback Thursday", deal: "$10 for 10 traditional wings with any sauce", validDuring: "4:00 PM - close" },
705
+ { name: "Happy Hour", deal: "$3 domestic drafts, $2 off appetizers", validDuring: "3:00 PM - 7:00 PM" },
706
+ ],
707
+ friday: [
708
+ { name: "TGIF Happy Hour", deal: "$4 craft drafts, $5 select cocktails", validDuring: "3:00 PM - 7:00 PM" },
709
+ { name: "Game Night Combo", deal: "20 wings + pitcher of beer — $29.99", validDuring: "All evening" },
710
+ ],
711
+ saturday: [
712
+ { name: "College Game Day", deal: "College football specials — $1 off wings during games", validDuring: "During college games" },
713
+ { name: "Saturday Night Bucket Deal", deal: "50 wings for $44.99", validDuring: "5:00 PM - close", popular: true },
714
+ ],
715
+ sunday: [
716
+ { name: "NFL Sunday Package", deal: "$25 for 20 wings + 2 beers during NFL games", validDuring: "During NFL games", popular: true },
717
+ { name: "Sunday Funday", deal: "Bottomless tater tots with any wing order — $5 add-on", validDuring: "All day" },
718
+ { name: "Blazin' Challenge Sunday", deal: "Complete the Blazin' Challenge — free wings + T-shirt + Blazin' Rewards bonus", validDuring: "All day" },
719
+ ],
720
+ };
721
+
722
+ const todaySpecials = specials[targetDay] || [];
723
+ const limitedTimeOffers = [
724
+ {
725
+ name: "Carolina Reaper Challenge",
726
+ deal: "Eat 12 Blazin' Carolina Reaper wings in 6 minutes — win a limited-edition shirt",
727
+ validThrough: "Limited time",
728
+ sauce: "Blazin' Carolina Reaper",
729
+ },
730
+ {
731
+ name: "Smoky Cheddar BBQ Launch",
732
+ deal: "Try the new Smoky Cheddar BBQ sauce — free upgrade on any wing order this week",
733
+ validThrough: "This week only",
734
+ sauce: "Smoky Cheddar BBQ",
735
+ },
736
+ {
737
+ name: "March Madness Bundle",
738
+ deal: "50 traditional wings + 2 pitchers — $54.99 during tournament games",
739
+ validThrough: "March Madness season",
740
+ },
741
+ ];
742
+
743
+ return ok({
744
+ day: targetDay.charAt(0).toUpperCase() + targetDay.slice(1),
745
+ location: location ? `${location.name} — ${location.address}` : "All locations",
746
+ todaySpecials: todaySpecials.length > 0 ? todaySpecials : [{ message: "No specials today. Check back tomorrow!" }],
747
+ limitedTimeOffers,
748
+ alwaysAvailable: [
749
+ { name: "Happy Hour", deal: "$3 domestics, $4 crafts", validDuring: "Mon–Fri 3:00–7:00 PM" },
750
+ { name: "Blazin' Challenge", deal: "12 Blazin' wings in 6 min — beat the clock, win glory", validDuring: "Always" },
751
+ { name: "Rewards Double-Dip", deal: "Earn points + use a special simultaneously", validDuring: "Always with active rewards account" },
752
+ ],
753
+ tip: "Link your Blazin' Rewards account with bww_blazin_rewards to maximize deal value.",
754
+ });
755
+ } catch (e) {
756
+ return err("Failed to load specials", String(e));
757
+ }
758
+ }
759
+ );
760
+
761
+ // ─── Tool: bww_session ────────────────────────────────────────────────────────
762
+
763
+ server.tool(
764
+ "bww_session",
765
+ "View or reset the current session — active location, order type, cart, and linked rewards account.",
766
+ {
767
+ action: z.enum(["view", "reset"]).describe("view=show current session, reset=clear all session data"),
768
+ },
769
+ async ({ action }) => {
770
+ try {
771
+ if (action === "reset") {
772
+ resetSession();
773
+ return ok({ success: true, message: "Session cleared. Location, cart, and rewards data removed." });
774
+ }
775
+
776
+ const session = getSession();
777
+ return ok({
778
+ location: session.locationName || "Not set",
779
+ locationId: session.locationId || null,
780
+ orderType: session.orderType || "Not set",
781
+ deliveryAddress: session.deliveryAddress || null,
782
+ cartItems: session.cart.length,
783
+ cartTotal: `$${cartTotal(session.cart).toFixed(2)}`,
784
+ rewards: session.rewardsMemberId
785
+ ? { memberId: session.rewardsMemberId, points: session.rewardsPoints || 0 }
786
+ : "Not linked",
787
+ lastOrderId: session.orderId || null,
788
+ });
789
+ } catch (e) {
790
+ return err("Session action failed", String(e));
791
+ }
792
+ }
793
+ );
794
+
795
+ // ─── Start ────────────────────────────────────────────────────────────────────
796
+
797
+ const transport = new StdioServerTransport();
798
+ await server.connect(transport);