good-eggs-mcp-server 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "good-eggs-mcp-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "MCP server for Good Eggs grocery shopping with Playwright automation",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
package/shared/server.js CHANGED
@@ -389,19 +389,32 @@ export class GoodEggsClient {
389
389
  // Extract past order information
390
390
  const orders = await page.evaluate(() => {
391
391
  const orderList = [];
392
- // Look for order date elements
393
- const orderElements = document.querySelectorAll('[class*="order"], [class*="history"] > div, [class*="past-order"]');
394
- orderElements.forEach((el) => {
395
- const dateEl = el.querySelector('[class*="date"], time');
396
- const totalEl = el.querySelector('[class*="total"], [class*="price"]');
397
- const countEl = el.querySelector('[class*="count"], [class*="items"]');
392
+ // Good Eggs reorder page uses 'reorder-page__grid__header' class for order date headers
393
+ // Each header contains a <p> element with text like "Delivered Saturday, January 10th"
394
+ const headerElements = document.querySelectorAll('.reorder-page__grid__header');
395
+ headerElements.forEach((header) => {
396
+ const dateEl = header.querySelector('p');
398
397
  const dateText = dateEl?.textContent?.trim();
399
398
  if (!dateText)
400
399
  return;
400
+ // Skip non-order headers like "Based on your shopping" recommendations section
401
+ if (!dateText.includes('Delivered'))
402
+ return;
403
+ // Extract just the date part (e.g., "Saturday, January 10th" from "Delivered Saturday, January 10th")
404
+ const dateMatch = dateText.match(/Delivered\s+(.+)/);
405
+ const cleanDate = dateMatch ? dateMatch[1] : dateText;
406
+ // Count the products in this order section
407
+ // The products follow the header in the DOM as sibling elements with js-product-link
408
+ let itemCount = 0;
409
+ let sibling = header.nextElementSibling;
410
+ while (sibling && !sibling.classList.contains('reorder-page__grid__header')) {
411
+ const productLinks = sibling.querySelectorAll('a.js-product-link');
412
+ itemCount += productLinks.length;
413
+ sibling = sibling.nextElementSibling;
414
+ }
401
415
  orderList.push({
402
- date: dateText,
403
- total: totalEl?.textContent?.trim() || undefined,
404
- itemCount: countEl?.textContent ? parseInt(countEl.textContent) : undefined,
416
+ date: cleanDate,
417
+ itemCount: itemCount > 0 ? itemCount : undefined,
405
418
  });
406
419
  });
407
420
  return orderList;
@@ -410,59 +423,113 @@ export class GoodEggsClient {
410
423
  }
411
424
  async getPastOrderGroceries(orderDate) {
412
425
  const page = await this.ensureBrowser();
413
- // First, go to reorder page
414
- if (!page.url().includes('/reorder')) {
415
- // Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
416
- await page.goto(`${BASE_URL}/reorder`, { waitUntil: 'domcontentloaded' });
426
+ // First, go to account orders page to find the order ID
427
+ // Check if we're on the orders list page (not a specific order details page)
428
+ const currentUrl = page.url();
429
+ const isOnOrdersList = currentUrl.includes('/account/orders') && !/\/account\/orders\/[^/]+/.test(currentUrl);
430
+ if (!isOnOrdersList) {
431
+ await page.goto(`${BASE_URL}/account/orders`, { waitUntil: 'domcontentloaded' });
417
432
  await page.waitForTimeout(3000);
418
433
  }
419
434
  // Check if we're redirected to signin
420
435
  if (page.url().includes('/signin')) {
421
436
  throw new Error('Not logged in. Cannot access past orders.');
422
437
  }
423
- // Try to click on the specific order date
424
- const orderLink = await page.$(`text=${orderDate}`);
425
- if (orderLink) {
426
- await orderLink.click();
438
+ // Find the order that matches the date and get its URL
439
+ const orderUrl = await page.evaluate((targetDate) => {
440
+ // Try to find by looking at the page structure for order links
441
+ const orderLinks = Array.from(document.querySelectorAll('a[href*="/account/orders/"]'));
442
+ for (const link of orderLinks) {
443
+ const linkEl = link;
444
+ // Get the parent container to check the date
445
+ const container = linkEl.closest('[class*="card"], [class*="row"], div') || linkEl;
446
+ const text = container.textContent || '';
447
+ // Check if this order matches our target date
448
+ // Dates can be like "Saturday 1/10", "January 10th", etc.
449
+ if (text.includes(targetDate)) {
450
+ return linkEl.href;
451
+ }
452
+ // Try more flexible date matching
453
+ // Extract date components from targetDate and compare
454
+ const datePatterns = [
455
+ /(\d{1,2})\/(\d{1,2})/, // 1/10
456
+ /([A-Za-z]+)\s+(\d{1,2})/, // January 10
457
+ /(\d{1,2})(?:st|nd|rd|th)/, // 10th
458
+ ];
459
+ for (const pattern of datePatterns) {
460
+ const targetMatch = targetDate.match(pattern);
461
+ const textMatch = text.match(pattern);
462
+ if (targetMatch && textMatch && targetMatch[0] === textMatch[0]) {
463
+ return linkEl.href;
464
+ }
465
+ }
466
+ }
467
+ return null;
468
+ }, orderDate);
469
+ if (!orderUrl) {
470
+ // Fallback: try clicking on "Order Details" for a matching date
471
+ const clickedUrl = await page.evaluate((targetDate) => {
472
+ const cards = Array.from(document.querySelectorAll('[class*="single-order"], [class*="order-summary"], div'));
473
+ for (const card of cards) {
474
+ const text = card.textContent || '';
475
+ if (text.includes(targetDate)) {
476
+ // This card contains the target date, look for Order Details link
477
+ const detailsLink = card.querySelector('a[href*="/account/orders/"]');
478
+ if (detailsLink) {
479
+ return detailsLink.href;
480
+ }
481
+ }
482
+ }
483
+ return null;
484
+ }, orderDate);
485
+ if (!clickedUrl) {
486
+ return [];
487
+ }
488
+ await page.goto(clickedUrl, { waitUntil: 'domcontentloaded' });
489
+ await page.waitForTimeout(2000);
490
+ }
491
+ else {
492
+ await page.goto(orderUrl, { waitUntil: 'domcontentloaded' });
427
493
  await page.waitForTimeout(2000);
428
494
  }
429
- // Extract items from the order
495
+ // Now we're on the order details page - extract items with quantity ordered
430
496
  const items = await page.evaluate(() => {
431
497
  const products = [];
432
- const seen = new Set();
433
- // Good Eggs uses 'js-product-link' class for product links
434
- const productElements = document.querySelectorAll('a.js-product-link');
435
- productElements.forEach((el) => {
436
- const link = el;
437
- const href = link.href;
438
- if (!href || seen.has(href) || href.includes('/reorder') || href.includes('/signin')) {
439
- return;
440
- }
441
- // Validate URL structure
442
- const urlPath = new URL(href).pathname;
443
- const segments = urlPath.split('/').filter((s) => s.length > 0);
444
- if (segments.length < 3) {
445
- return;
446
- }
447
- const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
448
- const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
449
- const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
450
- const priceEl = container.querySelector('[class*="price"]');
451
- const imgEl = container.querySelector('img');
452
- let name = nameEl?.textContent?.trim();
453
- if (!name || name.length < 3) {
454
- name = link.textContent?.trim();
498
+ // Find all line items on the order details page
499
+ const lineItems = document.querySelectorAll('.single-order-page__line-item');
500
+ lineItems.forEach((item) => {
501
+ // Extract quantity ordered (the number on the left)
502
+ const quantityOrderedEl = item.querySelector('.single-order-page__line-item-quantity-value');
503
+ const quantityOrdered = parseInt(quantityOrderedEl?.textContent?.trim() || '1', 10) || 1;
504
+ // Extract product name and URL
505
+ const nameLink = item.querySelector('.single-order-page__line-item-details-name a');
506
+ const name = nameLink?.textContent?.trim() || '';
507
+ const url = nameLink?.href || '';
508
+ // Extract brand
509
+ const brandEl = item.querySelector('.single-order-page__line-item-details-vendor-name a');
510
+ const brand = brandEl?.textContent?.trim() || '';
511
+ // Extract unit (e.g., "1 bunch", "1 lb")
512
+ const unitEl = item.querySelector('.single-order-page__line-item-details-unit-quantity');
513
+ const unit = unitEl?.textContent?.trim() || '';
514
+ // Extract price
515
+ const priceEl = item.querySelector('.summary-item__price');
516
+ const price = priceEl?.textContent?.trim() || '';
517
+ // Extract image URL
518
+ const imageDiv = item.querySelector('.single-order-page__line-item-image-image');
519
+ const bgImage = imageDiv?.style?.backgroundImage || '';
520
+ const imageMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
521
+ const imageUrl = imageMatch ? imageMatch[1] : undefined;
522
+ if (name && url) {
523
+ products.push({
524
+ url,
525
+ name,
526
+ brand,
527
+ price,
528
+ quantity: unit, // unit of sale (e.g., "1 bunch")
529
+ quantityOrdered, // number ordered (e.g., 2)
530
+ imageUrl,
531
+ });
455
532
  }
456
- if (!name || name.length < 3)
457
- return;
458
- seen.add(href);
459
- products.push({
460
- url: href,
461
- name: name,
462
- brand: brandEl?.textContent?.trim() || '',
463
- price: priceEl?.textContent?.trim() || '',
464
- imageUrl: imgEl?.src || undefined,
465
- });
466
533
  });
467
534
  return products;
468
535
  });
package/shared/tools.js CHANGED
@@ -64,6 +64,8 @@ Provide the past_order_date (from get_list_of_past_order_dates) to see what was
64
64
  Returns a list of items from that order including:
65
65
  - Product URL
66
66
  - Product name and brand
67
+ - Quantity ordered (e.g., 2 if you ordered 2 of something)
68
+ - Unit of sale (e.g., "1 bunch", "1 lb", "15 oz")
67
69
  - Price at time of order
68
70
 
69
71
  Useful for reordering frequently purchased items.
@@ -441,7 +443,7 @@ export function createRegisterTools(clientFactory, getReadyClient) {
441
443
  };
442
444
  }
443
445
  const formattedResults = results
444
- .map((item, i) => `${i + 1}. **${item.name}**\n Brand: ${item.brand || 'N/A'}\n Price: ${item.price || 'N/A'}\n URL: ${item.url}`)
446
+ .map((item, i) => `${i + 1}. **${item.name}**\n Brand: ${item.brand || 'N/A'}\n Quantity Ordered: ${item.quantityOrdered || 1}\n Unit: ${item.quantity || 'N/A'}\n Price: ${item.price || 'N/A'}\n URL: ${item.url}`)
445
447
  .join('\n\n');
446
448
  return {
447
449
  content: [
package/shared/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface GroceryItem {
8
8
  discount?: string;
9
9
  unit?: string;
10
10
  imageUrl?: string;
11
+ quantity?: string;
12
+ quantityOrdered?: number;
11
13
  }
12
14
  export interface GroceryDetails {
13
15
  url: string;