good-eggs-mcp-server 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.
@@ -0,0 +1,620 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { createRegisterTools } from './tools.js';
3
+ const BASE_URL = 'https://www.goodeggs.com';
4
+ /**
5
+ * Good Eggs client implementation using Playwright
6
+ */
7
+ export class GoodEggsClient {
8
+ browser = null;
9
+ context = null;
10
+ page = null;
11
+ config;
12
+ isInitialized = false;
13
+ constructor(config) {
14
+ this.config = config;
15
+ }
16
+ async ensureBrowser() {
17
+ if (!this.page) {
18
+ throw new Error('Browser not initialized. Call initialize() first.');
19
+ }
20
+ return this.page;
21
+ }
22
+ async initialize() {
23
+ if (this.isInitialized) {
24
+ return;
25
+ }
26
+ // Use playwright-extra with stealth plugin for better bot detection avoidance
27
+ const { chromium } = await import('playwright-extra');
28
+ const StealthPlugin = (await import('puppeteer-extra-plugin-stealth')).default;
29
+ chromium.use(StealthPlugin());
30
+ this.browser = await chromium.launch({
31
+ headless: this.config.headless,
32
+ args: [
33
+ '--disable-blink-features=AutomationControlled',
34
+ '--disable-dev-shm-usage',
35
+ '--no-sandbox',
36
+ ],
37
+ });
38
+ this.context = await this.browser.newContext({
39
+ viewport: { width: 1920, height: 1080 },
40
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
41
+ });
42
+ this.page = await this.context.newPage();
43
+ // Navigate to login page
44
+ await this.page.goto(`${BASE_URL}/signin`, { waitUntil: 'networkidle' });
45
+ // Fill in login credentials
46
+ await this.page.fill('input[name="email"], input[type="email"]', this.config.username);
47
+ await this.page.fill('input[name="password"], input[type="password"]', this.config.password);
48
+ // Click sign in button
49
+ await this.page.click('button:has-text("Sign In")');
50
+ // Wait for navigation to complete (should redirect to home or previous page)
51
+ await this.page.waitForURL((url) => !url.pathname.includes('/signin'), {
52
+ timeout: this.config.timeout,
53
+ });
54
+ // Verify login was successful by checking if we're no longer on signin page
55
+ const currentUrl = this.page.url();
56
+ if (currentUrl.includes('/signin')) {
57
+ throw new Error('Login failed - still on signin page. Check your credentials.');
58
+ }
59
+ this.isInitialized = true;
60
+ }
61
+ async searchGroceries(query) {
62
+ const page = await this.ensureBrowser();
63
+ // Navigate to search page
64
+ await page.goto(`${BASE_URL}/search?q=${encodeURIComponent(query)}`, {
65
+ waitUntil: 'networkidle',
66
+ });
67
+ // Wait for products to load
68
+ await page.waitForTimeout(1000);
69
+ // Extract product information from the page
70
+ const items = await page.evaluate(() => {
71
+ const products = [];
72
+ // Find all product cards/links by looking for product name elements
73
+ const productElements = document.querySelectorAll('a[href*="/product/"], a[href*="goodeggs"]');
74
+ const seen = new Set();
75
+ productElements.forEach((el) => {
76
+ const link = el;
77
+ const href = link.href;
78
+ // Skip if not a product link or already seen
79
+ if (!href || seen.has(href) || href.includes('/search') || href.includes('/signin')) {
80
+ return;
81
+ }
82
+ // Try to find product info within or near this element
83
+ const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
84
+ const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]') ||
85
+ link.querySelector('h2, h3');
86
+ const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
87
+ const priceEl = container.querySelector('[class*="price"]');
88
+ const discountEl = container.querySelector('[class*="off"], [class*="discount"]');
89
+ const imgEl = container.querySelector('img');
90
+ const name = nameEl?.textContent?.trim();
91
+ if (!name || name.length < 3)
92
+ return;
93
+ seen.add(href);
94
+ products.push({
95
+ url: href,
96
+ name: name,
97
+ brand: brandEl?.textContent?.trim() || '',
98
+ price: priceEl?.textContent?.trim() || '',
99
+ discount: discountEl?.textContent?.trim() || undefined,
100
+ imageUrl: imgEl?.src || undefined,
101
+ });
102
+ });
103
+ return products;
104
+ });
105
+ return items;
106
+ }
107
+ async getFavorites() {
108
+ const page = await this.ensureBrowser();
109
+ // Navigate to favorites page
110
+ await page.goto(`${BASE_URL}/favorites`, { waitUntil: 'networkidle' });
111
+ // Wait for page to load
112
+ await page.waitForTimeout(1000);
113
+ // Check if we're redirected to signin
114
+ if (page.url().includes('/signin')) {
115
+ throw new Error('Not logged in. Cannot access favorites.');
116
+ }
117
+ // Extract favorite items
118
+ const items = await page.evaluate(() => {
119
+ const products = [];
120
+ const seen = new Set();
121
+ const productElements = document.querySelectorAll('a[href*="/product/"], a[href*="goodeggs"]');
122
+ productElements.forEach((el) => {
123
+ const link = el;
124
+ const href = link.href;
125
+ if (!href || seen.has(href) || href.includes('/favorites') || href.includes('/signin')) {
126
+ return;
127
+ }
128
+ const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
129
+ const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
130
+ const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
131
+ const priceEl = container.querySelector('[class*="price"]');
132
+ const imgEl = container.querySelector('img');
133
+ const name = nameEl?.textContent?.trim();
134
+ if (!name || name.length < 3)
135
+ return;
136
+ seen.add(href);
137
+ products.push({
138
+ url: href,
139
+ name: name,
140
+ brand: brandEl?.textContent?.trim() || '',
141
+ price: priceEl?.textContent?.trim() || '',
142
+ imageUrl: imgEl?.src || undefined,
143
+ });
144
+ });
145
+ return products;
146
+ });
147
+ return items;
148
+ }
149
+ async getGroceryDetails(groceryUrl) {
150
+ const page = await this.ensureBrowser();
151
+ // Check if we're already on the product page
152
+ const currentUrl = page.url();
153
+ if (!currentUrl.includes(groceryUrl) && !groceryUrl.includes(currentUrl)) {
154
+ // Navigate to the product page
155
+ const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
156
+ await page.goto(fullUrl, { waitUntil: 'networkidle' });
157
+ }
158
+ // Wait for page content to load
159
+ await page.waitForTimeout(1000);
160
+ // Extract product details
161
+ const details = await page.evaluate((url) => {
162
+ // Find the main product info
163
+ const nameEl = document.querySelector('h1, [class*="product-name"], [class*="title"]');
164
+ const brandEl = document.querySelector('[class*="brand"], [class*="producer"]');
165
+ const priceEl = document.querySelector('[class*="sale-price"], [class*="current-price"]');
166
+ const originalPriceEl = document.querySelector('[class*="original-price"], [class*="regular-price"], s');
167
+ const discountEl = document.querySelector('[class*="off"], [class*="discount"]');
168
+ const descriptionEl = document.querySelector('[class*="description"], [class*="details"] p, .product-description');
169
+ const productDetailsEl = document.querySelector('[class*="product-details"]');
170
+ const imgEl = document.querySelector('[class*="product"] img, main img');
171
+ // Get availability dates
172
+ const availabilityEls = document.querySelectorAll('[class*="availability"] span, [class*="delivery"] span');
173
+ const availability = [];
174
+ availabilityEls.forEach((el) => {
175
+ const text = el.textContent?.trim();
176
+ if (text)
177
+ availability.push(text);
178
+ });
179
+ return {
180
+ url: url,
181
+ name: nameEl?.textContent?.trim() || 'Unknown',
182
+ brand: brandEl?.textContent?.trim() || '',
183
+ price: priceEl?.textContent?.trim() || '',
184
+ originalPrice: originalPriceEl?.textContent?.trim() || undefined,
185
+ discount: discountEl?.textContent?.trim() || undefined,
186
+ description: descriptionEl?.textContent?.trim() || undefined,
187
+ productDetails: productDetailsEl?.textContent?.trim() || undefined,
188
+ availability: availability.length > 0 ? availability : undefined,
189
+ imageUrl: imgEl?.src || undefined,
190
+ };
191
+ }, groceryUrl);
192
+ return details;
193
+ }
194
+ async addToCart(groceryUrl, quantity) {
195
+ const page = await this.ensureBrowser();
196
+ // Check if we're already on the product page
197
+ const currentUrl = page.url();
198
+ const normalizedGroceryUrl = groceryUrl.replace(BASE_URL, '');
199
+ const normalizedCurrentUrl = currentUrl.replace(BASE_URL, '');
200
+ if (!normalizedCurrentUrl.includes(normalizedGroceryUrl.split('/').pop() || '')) {
201
+ // Navigate to the product page
202
+ const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
203
+ await page.goto(fullUrl, { waitUntil: 'networkidle' });
204
+ }
205
+ // Get the product name for the result
206
+ const itemName = await page.evaluate(() => {
207
+ const nameEl = document.querySelector('h1, [class*="product-name"], [class*="title"]');
208
+ return nameEl?.textContent?.trim() || 'Unknown item';
209
+ });
210
+ // Set quantity if different from 1
211
+ let quantitySet = false;
212
+ if (quantity > 1) {
213
+ // Try to find and use quantity selector
214
+ const quantitySelector = await page.$('select[class*="quantity"], [class*="quantity"] select');
215
+ if (quantitySelector) {
216
+ await quantitySelector.selectOption(String(quantity));
217
+ quantitySet = true;
218
+ }
219
+ else {
220
+ // Try clicking + button multiple times
221
+ const plusButton = await page.$('button[aria-label*="increase"], button:has-text("+")');
222
+ if (plusButton) {
223
+ for (let i = 1; i < quantity; i++) {
224
+ await plusButton.click();
225
+ await page.waitForTimeout(200);
226
+ }
227
+ quantitySet = true;
228
+ }
229
+ }
230
+ if (!quantitySet) {
231
+ return {
232
+ success: false,
233
+ message: `Could not set quantity to ${quantity} - quantity controls not found. Item may only support single-item adds.`,
234
+ itemName,
235
+ quantity: 1,
236
+ };
237
+ }
238
+ }
239
+ // Click the add to cart/basket button
240
+ const addButton = await page.$('button:has-text("ADD TO BASKET"), button:has-text("Add to Cart"), button:has-text("Add to Basket")');
241
+ if (!addButton) {
242
+ return {
243
+ success: false,
244
+ message: 'Could not find add to cart button',
245
+ itemName,
246
+ quantity,
247
+ };
248
+ }
249
+ await addButton.click();
250
+ // Wait for cart update
251
+ await page.waitForTimeout(1000);
252
+ return {
253
+ success: true,
254
+ message: `Successfully added ${quantity} x ${itemName} to cart`,
255
+ itemName,
256
+ quantity,
257
+ };
258
+ }
259
+ async searchFreebieGroceries() {
260
+ const page = await this.ensureBrowser();
261
+ const allFreeItems = [];
262
+ const seen = new Set();
263
+ // Helper function to extract free items ($0.00) from current page
264
+ const extractFreeItems = async () => {
265
+ return await page.evaluate(() => {
266
+ const products = [];
267
+ const seenUrls = new Set();
268
+ // Find all product links
269
+ const productElements = document.querySelectorAll('a[href*="/product/"], a[href*="goodeggs"]');
270
+ productElements.forEach((el) => {
271
+ const link = el;
272
+ const href = link.href;
273
+ if (!href ||
274
+ seenUrls.has(href) ||
275
+ href.includes('/fresh-picks') ||
276
+ href.includes('/signin')) {
277
+ return;
278
+ }
279
+ const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
280
+ // Look for price element and check if it's $0.00
281
+ const priceEl = container.querySelector('[class*="price"]');
282
+ const priceText = priceEl?.textContent?.trim() || '';
283
+ // Only include items that are truly free ($0.00 or $0)
284
+ // Must use exact matching to avoid false positives like $10.00, $20.50
285
+ const isFree = /^\$0(\.00)?$/.test(priceText) || priceText === '$0.00' || priceText === '$0';
286
+ if (!isFree) {
287
+ return;
288
+ }
289
+ const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
290
+ const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
291
+ const imgEl = container.querySelector('img');
292
+ const name = nameEl?.textContent?.trim();
293
+ if (!name || name.length < 3)
294
+ return;
295
+ seenUrls.add(href);
296
+ products.push({
297
+ url: href,
298
+ name: name,
299
+ brand: brandEl?.textContent?.trim() || '',
300
+ price: priceText,
301
+ discount: 'FREE',
302
+ imageUrl: imgEl?.src || undefined,
303
+ });
304
+ });
305
+ return products;
306
+ });
307
+ };
308
+ // Check homepage for free items
309
+ await page.goto(BASE_URL, { waitUntil: 'networkidle' });
310
+ await page.waitForTimeout(1000);
311
+ const homePageItems = await extractFreeItems();
312
+ for (const item of homePageItems) {
313
+ if (!seen.has(item.url)) {
314
+ seen.add(item.url);
315
+ allFreeItems.push(item);
316
+ }
317
+ }
318
+ // Check /fresh-picks page for free items
319
+ await page.goto(`${BASE_URL}/fresh-picks`, { waitUntil: 'networkidle' });
320
+ await page.waitForTimeout(1000);
321
+ const freshPicksItems = await extractFreeItems();
322
+ for (const item of freshPicksItems) {
323
+ if (!seen.has(item.url)) {
324
+ seen.add(item.url);
325
+ allFreeItems.push(item);
326
+ }
327
+ }
328
+ return allFreeItems;
329
+ }
330
+ async getPastOrderDates() {
331
+ const page = await this.ensureBrowser();
332
+ // Navigate to reorder/past orders page
333
+ await page.goto(`${BASE_URL}/reorder`, { waitUntil: 'networkidle' });
334
+ // Wait for page to load
335
+ await page.waitForTimeout(1000);
336
+ // Check if we're redirected to signin
337
+ if (page.url().includes('/signin')) {
338
+ throw new Error('Not logged in. Cannot access past orders.');
339
+ }
340
+ // Extract past order information
341
+ const orders = await page.evaluate(() => {
342
+ const orderList = [];
343
+ // Look for order date elements
344
+ const orderElements = document.querySelectorAll('[class*="order"], [class*="history"] > div, [class*="past-order"]');
345
+ orderElements.forEach((el) => {
346
+ const dateEl = el.querySelector('[class*="date"], time');
347
+ const totalEl = el.querySelector('[class*="total"], [class*="price"]');
348
+ const countEl = el.querySelector('[class*="count"], [class*="items"]');
349
+ const dateText = dateEl?.textContent?.trim();
350
+ if (!dateText)
351
+ return;
352
+ orderList.push({
353
+ date: dateText,
354
+ total: totalEl?.textContent?.trim() || undefined,
355
+ itemCount: countEl?.textContent ? parseInt(countEl.textContent) : undefined,
356
+ });
357
+ });
358
+ return orderList;
359
+ });
360
+ return orders;
361
+ }
362
+ async getPastOrderGroceries(orderDate) {
363
+ const page = await this.ensureBrowser();
364
+ // First, go to reorder page
365
+ if (!page.url().includes('/reorder')) {
366
+ await page.goto(`${BASE_URL}/reorder`, { waitUntil: 'networkidle' });
367
+ await page.waitForTimeout(1000);
368
+ }
369
+ // Check if we're redirected to signin
370
+ if (page.url().includes('/signin')) {
371
+ throw new Error('Not logged in. Cannot access past orders.');
372
+ }
373
+ // Try to click on the specific order date
374
+ const orderLink = await page.$(`text=${orderDate}`);
375
+ if (orderLink) {
376
+ await orderLink.click();
377
+ await page.waitForTimeout(1000);
378
+ }
379
+ // Extract items from the order
380
+ const items = await page.evaluate(() => {
381
+ const products = [];
382
+ const seen = new Set();
383
+ const productElements = document.querySelectorAll('a[href*="/product/"], a[href*="goodeggs"]');
384
+ productElements.forEach((el) => {
385
+ const link = el;
386
+ const href = link.href;
387
+ if (!href || seen.has(href) || href.includes('/reorder') || href.includes('/signin')) {
388
+ return;
389
+ }
390
+ const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
391
+ const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
392
+ const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
393
+ const priceEl = container.querySelector('[class*="price"]');
394
+ const imgEl = container.querySelector('img');
395
+ const name = nameEl?.textContent?.trim();
396
+ if (!name || name.length < 3)
397
+ return;
398
+ seen.add(href);
399
+ products.push({
400
+ url: href,
401
+ name: name,
402
+ brand: brandEl?.textContent?.trim() || '',
403
+ price: priceEl?.textContent?.trim() || '',
404
+ imageUrl: imgEl?.src || undefined,
405
+ });
406
+ });
407
+ return products;
408
+ });
409
+ return items;
410
+ }
411
+ async addFavorite(groceryUrl) {
412
+ const page = await this.ensureBrowser();
413
+ // Navigate to the product page if not already there
414
+ const currentUrl = page.url();
415
+ const normalizedGroceryUrl = groceryUrl.replace(BASE_URL, '');
416
+ const normalizedCurrentUrl = currentUrl.replace(BASE_URL, '');
417
+ if (!normalizedCurrentUrl.includes(normalizedGroceryUrl.split('/').pop() || '')) {
418
+ const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
419
+ await page.goto(fullUrl, { waitUntil: 'networkidle' });
420
+ }
421
+ // Get the product name for the result
422
+ const itemName = await page.evaluate(() => {
423
+ const nameEl = document.querySelector('h1, [class*="product-name"], [class*="title"]');
424
+ return nameEl?.textContent?.trim() || 'Unknown item';
425
+ });
426
+ // Look for the favorite/heart button
427
+ const favoriteButton = await page.$('button[aria-label*="favorite"], button[aria-label*="heart"], button:has([class*="heart"]), [class*="favorite"] button, button[class*="favorite"]');
428
+ if (!favoriteButton) {
429
+ return {
430
+ success: false,
431
+ message: 'Could not find favorite button',
432
+ itemName,
433
+ };
434
+ }
435
+ // Check if already favorited (button might have "filled" or "active" state)
436
+ const isAlreadyFavorited = await page.evaluate((btn) => {
437
+ const classList = btn.className || '';
438
+ const ariaPressed = btn.getAttribute('aria-pressed');
439
+ return (classList.includes('active') ||
440
+ classList.includes('filled') ||
441
+ classList.includes('favorited') ||
442
+ ariaPressed === 'true');
443
+ }, favoriteButton);
444
+ if (isAlreadyFavorited) {
445
+ return {
446
+ success: true,
447
+ message: `${itemName} is already in favorites`,
448
+ itemName,
449
+ };
450
+ }
451
+ await favoriteButton.click();
452
+ await page.waitForTimeout(500);
453
+ return {
454
+ success: true,
455
+ message: `Successfully added ${itemName} to favorites`,
456
+ itemName,
457
+ };
458
+ }
459
+ async removeFavorite(groceryUrl) {
460
+ const page = await this.ensureBrowser();
461
+ // Navigate to the product page if not already there
462
+ const currentUrl = page.url();
463
+ const normalizedGroceryUrl = groceryUrl.replace(BASE_URL, '');
464
+ const normalizedCurrentUrl = currentUrl.replace(BASE_URL, '');
465
+ if (!normalizedCurrentUrl.includes(normalizedGroceryUrl.split('/').pop() || '')) {
466
+ const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
467
+ await page.goto(fullUrl, { waitUntil: 'networkidle' });
468
+ }
469
+ // Get the product name for the result
470
+ const itemName = await page.evaluate(() => {
471
+ const nameEl = document.querySelector('h1, [class*="product-name"], [class*="title"]');
472
+ return nameEl?.textContent?.trim() || 'Unknown item';
473
+ });
474
+ // Look for the favorite/heart button
475
+ const favoriteButton = await page.$('button[aria-label*="favorite"], button[aria-label*="heart"], button:has([class*="heart"]), [class*="favorite"] button, button[class*="favorite"]');
476
+ if (!favoriteButton) {
477
+ return {
478
+ success: false,
479
+ message: 'Could not find favorite button',
480
+ itemName,
481
+ };
482
+ }
483
+ // Check if already favorited (button might have "filled" or "active" state)
484
+ const isFavorited = await page.evaluate((btn) => {
485
+ const classList = btn.className || '';
486
+ const ariaPressed = btn.getAttribute('aria-pressed');
487
+ return (classList.includes('active') ||
488
+ classList.includes('filled') ||
489
+ classList.includes('favorited') ||
490
+ ariaPressed === 'true');
491
+ }, favoriteButton);
492
+ if (!isFavorited) {
493
+ return {
494
+ success: true,
495
+ message: `${itemName} is not in favorites`,
496
+ itemName,
497
+ };
498
+ }
499
+ await favoriteButton.click();
500
+ await page.waitForTimeout(500);
501
+ return {
502
+ success: true,
503
+ message: `Successfully removed ${itemName} from favorites`,
504
+ itemName,
505
+ };
506
+ }
507
+ async removeFromCart(groceryUrl) {
508
+ const page = await this.ensureBrowser();
509
+ // Navigate to cart page
510
+ await page.goto(`${BASE_URL}/basket`, { waitUntil: 'networkidle' });
511
+ await page.waitForTimeout(1000);
512
+ // Try to find the item in the cart by its URL or name
513
+ // First, let's get the product name from the URL if possible
514
+ const productSlug = groceryUrl.split('/').pop() || '';
515
+ // Validate that we have a valid product slug to search for
516
+ if (!productSlug || productSlug.length < 3) {
517
+ return {
518
+ success: false,
519
+ message: 'Invalid grocery URL - could not extract product identifier',
520
+ itemName: groceryUrl,
521
+ };
522
+ }
523
+ // Look for the item in the cart
524
+ const cartItems = await page.$$('[class*="cart-item"], [class*="basket-item"], [class*="line-item"]');
525
+ let itemFound = false;
526
+ let itemName = 'Unknown item';
527
+ for (const cartItem of cartItems) {
528
+ // Check if this cart item contains a link to our product
529
+ const itemLink = await cartItem.$(`a[href*="${productSlug}"]`);
530
+ if (itemLink) {
531
+ itemFound = true;
532
+ // Get the item name
533
+ const nameEl = await cartItem.$('[class*="name"], [class*="title"], h3, h4');
534
+ if (nameEl) {
535
+ itemName = (await nameEl.textContent()) || 'Unknown item';
536
+ }
537
+ // Find and click the remove button
538
+ const removeButton = await cartItem.$('button[aria-label*="remove"], button:has-text("Remove"), button:has-text("×"), [class*="remove"] button');
539
+ if (removeButton) {
540
+ await removeButton.click();
541
+ await page.waitForTimeout(500);
542
+ return {
543
+ success: true,
544
+ message: `Successfully removed ${itemName.trim()} from cart`,
545
+ itemName: itemName.trim(),
546
+ };
547
+ }
548
+ }
549
+ }
550
+ if (!itemFound) {
551
+ return {
552
+ success: false,
553
+ message: 'Item not found in cart',
554
+ itemName: productSlug,
555
+ };
556
+ }
557
+ return {
558
+ success: false,
559
+ message: 'Could not find remove button for item in cart',
560
+ itemName,
561
+ };
562
+ }
563
+ async getCurrentUrl() {
564
+ const page = await this.ensureBrowser();
565
+ return page.url();
566
+ }
567
+ async close() {
568
+ if (this.browser) {
569
+ await this.browser.close();
570
+ this.browser = null;
571
+ this.context = null;
572
+ this.page = null;
573
+ this.isInitialized = false;
574
+ }
575
+ }
576
+ getConfig() {
577
+ return this.config;
578
+ }
579
+ }
580
+ export function createMCPServer() {
581
+ const server = new Server({
582
+ name: 'good-eggs-mcp-server',
583
+ version: '0.1.0',
584
+ }, {
585
+ capabilities: {
586
+ tools: {},
587
+ },
588
+ });
589
+ // Track active client for cleanup
590
+ let activeClient = null;
591
+ const registerHandlers = async (server, clientFactory) => {
592
+ // Use provided factory or create default client
593
+ const factory = clientFactory ||
594
+ (() => {
595
+ const username = process.env.GOOD_EGGS_USERNAME;
596
+ const password = process.env.GOOD_EGGS_PASSWORD;
597
+ const headless = process.env.HEADLESS !== 'false';
598
+ const timeout = parseInt(process.env.TIMEOUT || '30000', 10);
599
+ if (!username || !password) {
600
+ throw new Error('GOOD_EGGS_USERNAME and GOOD_EGGS_PASSWORD environment variables must be configured');
601
+ }
602
+ activeClient = new GoodEggsClient({
603
+ username,
604
+ password,
605
+ headless,
606
+ timeout,
607
+ });
608
+ return activeClient;
609
+ });
610
+ const registerTools = createRegisterTools(factory);
611
+ registerTools(server);
612
+ };
613
+ const cleanup = async () => {
614
+ if (activeClient) {
615
+ await activeClient.close();
616
+ activeClient = null;
617
+ }
618
+ };
619
+ return { server, registerHandlers, cleanup };
620
+ }
@@ -0,0 +1,4 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ClientFactory } from './server.js';
3
+ export declare function createRegisterTools(clientFactory: ClientFactory): (server: Server) => void;
4
+ //# sourceMappingURL=tools.d.ts.map