good-eggs-mcp-server 0.1.0 → 0.1.2
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/build/index.js +18 -1
- package/package.json +1 -1
- package/shared/server.d.ts +6 -0
- package/shared/server.js +186 -57
- package/shared/tools.d.ts +6 -2
- package/shared/tools.js +8 -1
package/build/index.js
CHANGED
|
@@ -70,7 +70,7 @@ async function main() {
|
|
|
70
70
|
// Step 1: Validate environment variables
|
|
71
71
|
validateEnvironment();
|
|
72
72
|
// Step 2: Create server using factory
|
|
73
|
-
const { server, registerHandlers, cleanup } = createMCPServer();
|
|
73
|
+
const { server, registerHandlers, cleanup, startBackgroundLogin } = createMCPServer();
|
|
74
74
|
// Step 3: Register all handlers (tools)
|
|
75
75
|
await registerHandlers(server);
|
|
76
76
|
// Step 4: Set up graceful shutdown
|
|
@@ -85,6 +85,23 @@ async function main() {
|
|
|
85
85
|
const transport = new StdioServerTransport();
|
|
86
86
|
await server.connect(transport);
|
|
87
87
|
logServerStart('Good Eggs');
|
|
88
|
+
// Step 6: Start background login process
|
|
89
|
+
// This kicks off Playwright and performs login without blocking the stdio connection.
|
|
90
|
+
// If login fails, the server will close with an error.
|
|
91
|
+
logWarning('login', 'Starting background login to Good Eggs...');
|
|
92
|
+
startBackgroundLogin((error) => {
|
|
93
|
+
// Login failed - log error and exit
|
|
94
|
+
logError('login', `Background login failed: ${error.message}`);
|
|
95
|
+
logError('login', 'Server shutting down due to authentication failure.');
|
|
96
|
+
// Clean up and exit with error
|
|
97
|
+
cleanup()
|
|
98
|
+
.catch((cleanupError) => {
|
|
99
|
+
logError('cleanup', `Error during cleanup: ${cleanupError}`);
|
|
100
|
+
})
|
|
101
|
+
.finally(() => {
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
88
105
|
}
|
|
89
106
|
// Run the server
|
|
90
107
|
main().catch((error) => {
|
package/package.json
CHANGED
package/shared/server.d.ts
CHANGED
|
@@ -90,6 +90,11 @@ export declare class GoodEggsClient implements IGoodEggsClient {
|
|
|
90
90
|
getConfig(): GoodEggsConfig;
|
|
91
91
|
}
|
|
92
92
|
export type ClientFactory = () => IGoodEggsClient;
|
|
93
|
+
/**
|
|
94
|
+
* Callback invoked when background login fails
|
|
95
|
+
* @param error The error that caused login to fail
|
|
96
|
+
*/
|
|
97
|
+
export type LoginFailedCallback = (error: Error) => void;
|
|
93
98
|
export declare function createMCPServer(): {
|
|
94
99
|
server: Server<{
|
|
95
100
|
method: string;
|
|
@@ -116,5 +121,6 @@ export declare function createMCPServer(): {
|
|
|
116
121
|
}>;
|
|
117
122
|
registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
|
|
118
123
|
cleanup: () => Promise<void>;
|
|
124
|
+
startBackgroundLogin: (onFailed?: LoginFailedCallback) => void;
|
|
119
125
|
};
|
|
120
126
|
//# sourceMappingURL=server.d.ts.map
|
package/shared/server.js
CHANGED
|
@@ -41,7 +41,9 @@ export class GoodEggsClient {
|
|
|
41
41
|
});
|
|
42
42
|
this.page = await this.context.newPage();
|
|
43
43
|
// Navigate to login page
|
|
44
|
-
|
|
44
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections that prevent networkidle
|
|
45
|
+
await this.page.goto(`${BASE_URL}/signin`, { waitUntil: 'domcontentloaded' });
|
|
46
|
+
await this.page.waitForTimeout(2000);
|
|
45
47
|
// Fill in login credentials
|
|
46
48
|
await this.page.fill('input[name="email"], input[type="email"]', this.config.username);
|
|
47
49
|
await this.page.fill('input[name="password"], input[type="password"]', this.config.password);
|
|
@@ -61,22 +63,40 @@ export class GoodEggsClient {
|
|
|
61
63
|
async searchGroceries(query) {
|
|
62
64
|
const page = await this.ensureBrowser();
|
|
63
65
|
// Navigate to search page
|
|
66
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
64
67
|
await page.goto(`${BASE_URL}/search?q=${encodeURIComponent(query)}`, {
|
|
65
|
-
waitUntil: '
|
|
68
|
+
waitUntil: 'domcontentloaded',
|
|
66
69
|
});
|
|
67
|
-
// Wait for
|
|
68
|
-
await page.waitForTimeout(
|
|
70
|
+
// Wait for React to render the product list
|
|
71
|
+
await page.waitForTimeout(3000);
|
|
69
72
|
// Extract product information from the page
|
|
70
73
|
const items = await page.evaluate(() => {
|
|
71
74
|
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
75
|
const seen = new Set();
|
|
76
|
+
// Good Eggs uses 'js-product-link' class for product links
|
|
77
|
+
// URL format: /producer-slug/product-slug/product-id (e.g., /cloversfbay/organic-whole-milk/53fe295358ed090200000f2d)
|
|
78
|
+
const productElements = document.querySelectorAll('a.js-product-link');
|
|
75
79
|
productElements.forEach((el) => {
|
|
76
80
|
const link = el;
|
|
77
81
|
const href = link.href;
|
|
78
|
-
// Skip if not a product
|
|
79
|
-
if (!href || seen.has(href)
|
|
82
|
+
// Skip if already seen or not a valid product URL
|
|
83
|
+
if (!href || seen.has(href)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Validate URL structure: should have at least 3 path segments after domain
|
|
87
|
+
const urlPath = new URL(href).pathname;
|
|
88
|
+
const segments = urlPath.split('/').filter((s) => s.length > 0);
|
|
89
|
+
if (segments.length < 3) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Skip navigation pages
|
|
93
|
+
if (href.includes('/search') ||
|
|
94
|
+
href.includes('/signin') ||
|
|
95
|
+
href.includes('/home') ||
|
|
96
|
+
href.includes('/basket') ||
|
|
97
|
+
href.includes('/account') ||
|
|
98
|
+
href.includes('/favorites') ||
|
|
99
|
+
href.includes('/reorder')) {
|
|
80
100
|
return;
|
|
81
101
|
}
|
|
82
102
|
// Try to find product info within or near this element
|
|
@@ -87,7 +107,11 @@ export class GoodEggsClient {
|
|
|
87
107
|
const priceEl = container.querySelector('[class*="price"]');
|
|
88
108
|
const discountEl = container.querySelector('[class*="off"], [class*="discount"]');
|
|
89
109
|
const imgEl = container.querySelector('img');
|
|
90
|
-
|
|
110
|
+
// Get name from element or from the link text itself
|
|
111
|
+
let name = nameEl?.textContent?.trim();
|
|
112
|
+
if (!name || name.length < 3) {
|
|
113
|
+
name = link.textContent?.trim();
|
|
114
|
+
}
|
|
91
115
|
if (!name || name.length < 3)
|
|
92
116
|
return;
|
|
93
117
|
seen.add(href);
|
|
@@ -107,9 +131,10 @@ export class GoodEggsClient {
|
|
|
107
131
|
async getFavorites() {
|
|
108
132
|
const page = await this.ensureBrowser();
|
|
109
133
|
// Navigate to favorites page
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
135
|
+
await page.goto(`${BASE_URL}/favorites`, { waitUntil: 'domcontentloaded' });
|
|
136
|
+
// Wait for React to render
|
|
137
|
+
await page.waitForTimeout(3000);
|
|
113
138
|
// Check if we're redirected to signin
|
|
114
139
|
if (page.url().includes('/signin')) {
|
|
115
140
|
throw new Error('Not logged in. Cannot access favorites.');
|
|
@@ -118,19 +143,29 @@ export class GoodEggsClient {
|
|
|
118
143
|
const items = await page.evaluate(() => {
|
|
119
144
|
const products = [];
|
|
120
145
|
const seen = new Set();
|
|
121
|
-
|
|
146
|
+
// Good Eggs uses 'js-product-link' class for product links
|
|
147
|
+
const productElements = document.querySelectorAll('a.js-product-link');
|
|
122
148
|
productElements.forEach((el) => {
|
|
123
149
|
const link = el;
|
|
124
150
|
const href = link.href;
|
|
125
151
|
if (!href || seen.has(href) || href.includes('/favorites') || href.includes('/signin')) {
|
|
126
152
|
return;
|
|
127
153
|
}
|
|
154
|
+
// Validate URL structure
|
|
155
|
+
const urlPath = new URL(href).pathname;
|
|
156
|
+
const segments = urlPath.split('/').filter((s) => s.length > 0);
|
|
157
|
+
if (segments.length < 3) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
128
160
|
const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
|
|
129
161
|
const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
|
|
130
162
|
const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
|
|
131
163
|
const priceEl = container.querySelector('[class*="price"]');
|
|
132
164
|
const imgEl = container.querySelector('img');
|
|
133
|
-
|
|
165
|
+
let name = nameEl?.textContent?.trim();
|
|
166
|
+
if (!name || name.length < 3) {
|
|
167
|
+
name = link.textContent?.trim();
|
|
168
|
+
}
|
|
134
169
|
if (!name || name.length < 3)
|
|
135
170
|
return;
|
|
136
171
|
seen.add(href);
|
|
@@ -152,11 +187,12 @@ export class GoodEggsClient {
|
|
|
152
187
|
const currentUrl = page.url();
|
|
153
188
|
if (!currentUrl.includes(groceryUrl) && !groceryUrl.includes(currentUrl)) {
|
|
154
189
|
// Navigate to the product page
|
|
190
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
155
191
|
const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
|
|
156
|
-
await page.goto(fullUrl, { waitUntil: '
|
|
192
|
+
await page.goto(fullUrl, { waitUntil: 'domcontentloaded' });
|
|
157
193
|
}
|
|
158
|
-
// Wait for
|
|
159
|
-
await page.waitForTimeout(
|
|
194
|
+
// Wait for React to render
|
|
195
|
+
await page.waitForTimeout(3000);
|
|
160
196
|
// Extract product details
|
|
161
197
|
const details = await page.evaluate((url) => {
|
|
162
198
|
// Find the main product info
|
|
@@ -199,8 +235,10 @@ export class GoodEggsClient {
|
|
|
199
235
|
const normalizedCurrentUrl = currentUrl.replace(BASE_URL, '');
|
|
200
236
|
if (!normalizedCurrentUrl.includes(normalizedGroceryUrl.split('/').pop() || '')) {
|
|
201
237
|
// Navigate to the product page
|
|
238
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
202
239
|
const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
|
|
203
|
-
await page.goto(fullUrl, { waitUntil: '
|
|
240
|
+
await page.goto(fullUrl, { waitUntil: 'domcontentloaded' });
|
|
241
|
+
await page.waitForTimeout(3000);
|
|
204
242
|
}
|
|
205
243
|
// Get the product name for the result
|
|
206
244
|
const itemName = await page.evaluate(() => {
|
|
@@ -265,8 +303,8 @@ export class GoodEggsClient {
|
|
|
265
303
|
return await page.evaluate(() => {
|
|
266
304
|
const products = [];
|
|
267
305
|
const seenUrls = new Set();
|
|
268
|
-
//
|
|
269
|
-
const productElements = document.querySelectorAll('a
|
|
306
|
+
// Good Eggs uses 'js-product-link' class for product links
|
|
307
|
+
const productElements = document.querySelectorAll('a.js-product-link');
|
|
270
308
|
productElements.forEach((el) => {
|
|
271
309
|
const link = el;
|
|
272
310
|
const href = link.href;
|
|
@@ -276,6 +314,12 @@ export class GoodEggsClient {
|
|
|
276
314
|
href.includes('/signin')) {
|
|
277
315
|
return;
|
|
278
316
|
}
|
|
317
|
+
// Validate URL structure
|
|
318
|
+
const urlPath = new URL(href).pathname;
|
|
319
|
+
const segments = urlPath.split('/').filter((s) => s.length > 0);
|
|
320
|
+
if (segments.length < 3) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
279
323
|
const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
|
|
280
324
|
// Look for price element and check if it's $0.00
|
|
281
325
|
const priceEl = container.querySelector('[class*="price"]');
|
|
@@ -289,7 +333,10 @@ export class GoodEggsClient {
|
|
|
289
333
|
const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
|
|
290
334
|
const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
|
|
291
335
|
const imgEl = container.querySelector('img');
|
|
292
|
-
|
|
336
|
+
let name = nameEl?.textContent?.trim();
|
|
337
|
+
if (!name || name.length < 3) {
|
|
338
|
+
name = link.textContent?.trim();
|
|
339
|
+
}
|
|
293
340
|
if (!name || name.length < 3)
|
|
294
341
|
return;
|
|
295
342
|
seenUrls.add(href);
|
|
@@ -306,8 +353,9 @@ export class GoodEggsClient {
|
|
|
306
353
|
});
|
|
307
354
|
};
|
|
308
355
|
// Check homepage for free items
|
|
309
|
-
|
|
310
|
-
await page.
|
|
356
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
357
|
+
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
|
|
358
|
+
await page.waitForTimeout(3000);
|
|
311
359
|
const homePageItems = await extractFreeItems();
|
|
312
360
|
for (const item of homePageItems) {
|
|
313
361
|
if (!seen.has(item.url)) {
|
|
@@ -316,8 +364,8 @@ export class GoodEggsClient {
|
|
|
316
364
|
}
|
|
317
365
|
}
|
|
318
366
|
// Check /fresh-picks page for free items
|
|
319
|
-
await page.goto(`${BASE_URL}/fresh-picks`, { waitUntil: '
|
|
320
|
-
await page.waitForTimeout(
|
|
367
|
+
await page.goto(`${BASE_URL}/fresh-picks`, { waitUntil: 'domcontentloaded' });
|
|
368
|
+
await page.waitForTimeout(3000);
|
|
321
369
|
const freshPicksItems = await extractFreeItems();
|
|
322
370
|
for (const item of freshPicksItems) {
|
|
323
371
|
if (!seen.has(item.url)) {
|
|
@@ -330,9 +378,10 @@ export class GoodEggsClient {
|
|
|
330
378
|
async getPastOrderDates() {
|
|
331
379
|
const page = await this.ensureBrowser();
|
|
332
380
|
// Navigate to reorder/past orders page
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
381
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
382
|
+
await page.goto(`${BASE_URL}/reorder`, { waitUntil: 'domcontentloaded' });
|
|
383
|
+
// Wait for React to render
|
|
384
|
+
await page.waitForTimeout(3000);
|
|
336
385
|
// Check if we're redirected to signin
|
|
337
386
|
if (page.url().includes('/signin')) {
|
|
338
387
|
throw new Error('Not logged in. Cannot access past orders.');
|
|
@@ -363,8 +412,9 @@ export class GoodEggsClient {
|
|
|
363
412
|
const page = await this.ensureBrowser();
|
|
364
413
|
// First, go to reorder page
|
|
365
414
|
if (!page.url().includes('/reorder')) {
|
|
366
|
-
|
|
367
|
-
await page.
|
|
415
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
416
|
+
await page.goto(`${BASE_URL}/reorder`, { waitUntil: 'domcontentloaded' });
|
|
417
|
+
await page.waitForTimeout(3000);
|
|
368
418
|
}
|
|
369
419
|
// Check if we're redirected to signin
|
|
370
420
|
if (page.url().includes('/signin')) {
|
|
@@ -374,25 +424,35 @@ export class GoodEggsClient {
|
|
|
374
424
|
const orderLink = await page.$(`text=${orderDate}`);
|
|
375
425
|
if (orderLink) {
|
|
376
426
|
await orderLink.click();
|
|
377
|
-
await page.waitForTimeout(
|
|
427
|
+
await page.waitForTimeout(2000);
|
|
378
428
|
}
|
|
379
429
|
// Extract items from the order
|
|
380
430
|
const items = await page.evaluate(() => {
|
|
381
431
|
const products = [];
|
|
382
432
|
const seen = new Set();
|
|
383
|
-
|
|
433
|
+
// Good Eggs uses 'js-product-link' class for product links
|
|
434
|
+
const productElements = document.querySelectorAll('a.js-product-link');
|
|
384
435
|
productElements.forEach((el) => {
|
|
385
436
|
const link = el;
|
|
386
437
|
const href = link.href;
|
|
387
438
|
if (!href || seen.has(href) || href.includes('/reorder') || href.includes('/signin')) {
|
|
388
439
|
return;
|
|
389
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
|
+
}
|
|
390
447
|
const container = link.closest('div[class*="product"], article, [class*="card"]') || link;
|
|
391
448
|
const nameEl = container.querySelector('h2, h3, [class*="title"], [class*="name"]');
|
|
392
449
|
const brandEl = container.querySelector('[class*="brand"], [class*="producer"]');
|
|
393
450
|
const priceEl = container.querySelector('[class*="price"]');
|
|
394
451
|
const imgEl = container.querySelector('img');
|
|
395
|
-
|
|
452
|
+
let name = nameEl?.textContent?.trim();
|
|
453
|
+
if (!name || name.length < 3) {
|
|
454
|
+
name = link.textContent?.trim();
|
|
455
|
+
}
|
|
396
456
|
if (!name || name.length < 3)
|
|
397
457
|
return;
|
|
398
458
|
seen.add(href);
|
|
@@ -415,8 +475,10 @@ export class GoodEggsClient {
|
|
|
415
475
|
const normalizedGroceryUrl = groceryUrl.replace(BASE_URL, '');
|
|
416
476
|
const normalizedCurrentUrl = currentUrl.replace(BASE_URL, '');
|
|
417
477
|
if (!normalizedCurrentUrl.includes(normalizedGroceryUrl.split('/').pop() || '')) {
|
|
478
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
418
479
|
const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
|
|
419
|
-
await page.goto(fullUrl, { waitUntil: '
|
|
480
|
+
await page.goto(fullUrl, { waitUntil: 'domcontentloaded' });
|
|
481
|
+
await page.waitForTimeout(3000);
|
|
420
482
|
}
|
|
421
483
|
// Get the product name for the result
|
|
422
484
|
const itemName = await page.evaluate(() => {
|
|
@@ -463,8 +525,10 @@ export class GoodEggsClient {
|
|
|
463
525
|
const normalizedGroceryUrl = groceryUrl.replace(BASE_URL, '');
|
|
464
526
|
const normalizedCurrentUrl = currentUrl.replace(BASE_URL, '');
|
|
465
527
|
if (!normalizedCurrentUrl.includes(normalizedGroceryUrl.split('/').pop() || '')) {
|
|
528
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
466
529
|
const fullUrl = groceryUrl.startsWith('http') ? groceryUrl : `${BASE_URL}${groceryUrl}`;
|
|
467
|
-
await page.goto(fullUrl, { waitUntil: '
|
|
530
|
+
await page.goto(fullUrl, { waitUntil: 'domcontentloaded' });
|
|
531
|
+
await page.waitForTimeout(3000);
|
|
468
532
|
}
|
|
469
533
|
// Get the product name for the result
|
|
470
534
|
const itemName = await page.evaluate(() => {
|
|
@@ -507,8 +571,9 @@ export class GoodEggsClient {
|
|
|
507
571
|
async removeFromCart(groceryUrl) {
|
|
508
572
|
const page = await this.ensureBrowser();
|
|
509
573
|
// Navigate to cart page
|
|
510
|
-
|
|
511
|
-
await page.
|
|
574
|
+
// Use domcontentloaded instead of networkidle - Good Eggs has persistent connections
|
|
575
|
+
await page.goto(`${BASE_URL}/basket`, { waitUntil: 'domcontentloaded' });
|
|
576
|
+
await page.waitForTimeout(3000);
|
|
512
577
|
// Try to find the item in the cart by its URL or name
|
|
513
578
|
// First, let's get the product name from the URL if possible
|
|
514
579
|
const productSlug = groceryUrl.split('/').pop() || '';
|
|
@@ -588,26 +653,86 @@ export function createMCPServer() {
|
|
|
588
653
|
});
|
|
589
654
|
// Track active client for cleanup
|
|
590
655
|
let activeClient = null;
|
|
656
|
+
// Track background login state
|
|
657
|
+
let loginPromise = null;
|
|
658
|
+
let loginFailed = false;
|
|
659
|
+
let loginError = null;
|
|
660
|
+
let onLoginFailed = null;
|
|
661
|
+
/**
|
|
662
|
+
* Create the client instance (but don't initialize/login yet)
|
|
663
|
+
*/
|
|
664
|
+
const createClient = () => {
|
|
665
|
+
const username = process.env.GOOD_EGGS_USERNAME;
|
|
666
|
+
const password = process.env.GOOD_EGGS_PASSWORD;
|
|
667
|
+
const headless = process.env.HEADLESS !== 'false';
|
|
668
|
+
const timeout = parseInt(process.env.TIMEOUT || '30000', 10);
|
|
669
|
+
if (!username || !password) {
|
|
670
|
+
throw new Error('GOOD_EGGS_USERNAME and GOOD_EGGS_PASSWORD environment variables must be configured');
|
|
671
|
+
}
|
|
672
|
+
activeClient = new GoodEggsClient({
|
|
673
|
+
username,
|
|
674
|
+
password,
|
|
675
|
+
headless,
|
|
676
|
+
timeout,
|
|
677
|
+
});
|
|
678
|
+
return activeClient;
|
|
679
|
+
};
|
|
680
|
+
/**
|
|
681
|
+
* Start background login process
|
|
682
|
+
* This should be called after the server is connected to start authentication
|
|
683
|
+
* without blocking the stdio connection.
|
|
684
|
+
*
|
|
685
|
+
* @param onFailed Callback invoked if login fails - use this to close the server
|
|
686
|
+
*/
|
|
687
|
+
const startBackgroundLogin = (onFailed) => {
|
|
688
|
+
if (loginPromise) {
|
|
689
|
+
// Already started
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
onLoginFailed = onFailed || null;
|
|
693
|
+
// Create client if not already created
|
|
694
|
+
if (!activeClient) {
|
|
695
|
+
createClient();
|
|
696
|
+
}
|
|
697
|
+
// Start login in background
|
|
698
|
+
loginPromise = activeClient.initialize().catch((error) => {
|
|
699
|
+
loginFailed = true;
|
|
700
|
+
loginError = error instanceof Error ? error : new Error(String(error));
|
|
701
|
+
// Invoke callback to notify about login failure
|
|
702
|
+
if (onLoginFailed) {
|
|
703
|
+
onLoginFailed(loginError);
|
|
704
|
+
}
|
|
705
|
+
// Re-throw to make the promise rejected
|
|
706
|
+
throw loginError;
|
|
707
|
+
});
|
|
708
|
+
};
|
|
709
|
+
/**
|
|
710
|
+
* Get a client that is ready to use (login completed)
|
|
711
|
+
* If background login was started, this waits for it to complete.
|
|
712
|
+
* If not started, this will initialize synchronously (blocking).
|
|
713
|
+
*/
|
|
714
|
+
const getReadyClient = async () => {
|
|
715
|
+
// If login already failed, throw immediately
|
|
716
|
+
if (loginFailed && loginError) {
|
|
717
|
+
throw new Error(`Login failed: ${loginError.message}`);
|
|
718
|
+
}
|
|
719
|
+
// If background login is in progress, wait for it
|
|
720
|
+
if (loginPromise) {
|
|
721
|
+
await loginPromise;
|
|
722
|
+
return activeClient;
|
|
723
|
+
}
|
|
724
|
+
// No background login started - create and initialize client now (legacy behavior)
|
|
725
|
+
if (!activeClient) {
|
|
726
|
+
createClient();
|
|
727
|
+
}
|
|
728
|
+
await activeClient.initialize();
|
|
729
|
+
return activeClient;
|
|
730
|
+
};
|
|
591
731
|
const registerHandlers = async (server, clientFactory) => {
|
|
592
|
-
// Use provided factory or create
|
|
593
|
-
const factory = clientFactory ||
|
|
594
|
-
|
|
595
|
-
|
|
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);
|
|
732
|
+
// Use provided factory or create our managed client getter
|
|
733
|
+
const factory = clientFactory || (() => activeClient || createClient());
|
|
734
|
+
// Create tools with a special async getter that waits for background login
|
|
735
|
+
const registerTools = createRegisterTools(factory, getReadyClient);
|
|
611
736
|
registerTools(server);
|
|
612
737
|
};
|
|
613
738
|
const cleanup = async () => {
|
|
@@ -615,6 +740,10 @@ export function createMCPServer() {
|
|
|
615
740
|
await activeClient.close();
|
|
616
741
|
activeClient = null;
|
|
617
742
|
}
|
|
743
|
+
// Reset login state
|
|
744
|
+
loginPromise = null;
|
|
745
|
+
loginFailed = false;
|
|
746
|
+
loginError = null;
|
|
618
747
|
};
|
|
619
|
-
return { server, registerHandlers, cleanup };
|
|
748
|
+
return { server, registerHandlers, cleanup, startBackgroundLogin };
|
|
620
749
|
}
|
package/shared/tools.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import { ClientFactory } from './server.js';
|
|
3
|
-
|
|
2
|
+
import { ClientFactory, IGoodEggsClient } from './server.js';
|
|
3
|
+
/**
|
|
4
|
+
* Async getter that returns a ready-to-use client (login completed)
|
|
5
|
+
*/
|
|
6
|
+
export type GetReadyClientFn = () => Promise<IGoodEggsClient>;
|
|
7
|
+
export declare function createRegisterTools(clientFactory: ClientFactory, getReadyClient?: GetReadyClientFn): (server: Server) => void;
|
|
4
8
|
//# sourceMappingURL=tools.d.ts.map
|
package/shared/tools.js
CHANGED
|
@@ -86,11 +86,18 @@ Provide the Good Eggs URL of the item to remove from your cart.
|
|
|
86
86
|
Navigates to the cart and removes the specified item.
|
|
87
87
|
|
|
88
88
|
Returns confirmation of the removal or an error if the item is not in the cart.`;
|
|
89
|
-
export function createRegisterTools(clientFactory) {
|
|
89
|
+
export function createRegisterTools(clientFactory, getReadyClient) {
|
|
90
90
|
// Create a single client instance that persists across calls
|
|
91
91
|
let client = null;
|
|
92
92
|
let isInitialized = false;
|
|
93
|
+
// If getReadyClient is provided (background login mode), use it
|
|
94
|
+
// Otherwise fall back to lazy initialization (legacy behavior)
|
|
93
95
|
const getClient = async () => {
|
|
96
|
+
if (getReadyClient) {
|
|
97
|
+
// Use the provided getter that handles background login
|
|
98
|
+
return getReadyClient();
|
|
99
|
+
}
|
|
100
|
+
// Legacy behavior: lazy initialization
|
|
94
101
|
if (!client) {
|
|
95
102
|
client = clientFactory();
|
|
96
103
|
}
|