continente-mcp 3.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.js ADDED
@@ -0,0 +1,613 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { chromium } from 'playwright';
6
+ import * as cheerio from 'cheerio';
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { normalizeCookies } from './utils.js';
9
+
10
+ const CONTINENTE_BASE = 'https://www.continente.pt';
11
+ const STATE_DIR = process.env.CONTINENTE_STATE_DIR || `${process.env.HOME}/.continente`;
12
+
13
+ function getPlatformUserAgent() {
14
+ if (process.platform === 'win32') {
15
+ return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
16
+ } else if (process.platform === 'linux') {
17
+ return 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
18
+ }
19
+ return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
20
+ }
21
+
22
+ // ─── Browser / Session ───────────────────────────────────────────────────────
23
+
24
+ let browser = null;
25
+ let context = null;
26
+ let page = null;
27
+
28
+ async function ensureBrowser() {
29
+ if (browser) return browser;
30
+
31
+ browser = await chromium.launch({ headless: true });
32
+ context = await browser.newContext({
33
+ userAgent: getPlatformUserAgent(),
34
+ locale: 'pt-PT'
35
+ });
36
+
37
+ // Load cookies from file
38
+ const cookieFile = `${STATE_DIR}/cookies.json`;
39
+ if (existsSync(cookieFile)) {
40
+ try {
41
+ const cookies = JSON.parse(readFileSync(cookieFile, 'utf8'));
42
+ const normalized = normalizeCookies(cookies);
43
+ await context.addCookies(normalized);
44
+ } catch (e) {
45
+ console.error('Failed to load cookies:', e.message);
46
+ }
47
+ }
48
+
49
+ page = await context.newPage();
50
+ return browser;
51
+ }
52
+
53
+ async function goto(url, wait = 'networkidle') {
54
+ await ensureBrowser();
55
+ await page.goto(url, { waitUntil: wait, timeout: 20000 });
56
+ await page.waitForTimeout(1500);
57
+ return page;
58
+ }
59
+
60
+ async function closeBrowser() {
61
+ if (page) { try { await page.close(); } catch(e) {} page = null; }
62
+ if (context) { try { await context.close(); } catch(e) {} context = null; }
63
+ if (browser) { try { await browser.close(); } catch(e) {} browser = null; }
64
+ }
65
+
66
+ // ─── Favorites / Preferences ───────────────────────────────────────────────────
67
+
68
+ async function fetchFavorites() {
69
+ await goto(`${CONTINENTE_BASE}/conta/lista-produtos/?list=favorites`);
70
+ const html = await page.content();
71
+ const $ = cheerio.load(html);
72
+ const products = [];
73
+
74
+ $('a[href*="/produto/"]').each((i, el) => {
75
+ const $el = $(el);
76
+ const text = $el.text().replace(/\s+/g, ' ').trim();
77
+ const href = $el.attr('href') || '';
78
+ if (text.length > 5 && href) {
79
+ const idMatch = href.match(/\/produto\/([^\/\?]+)/);
80
+ const productId = idMatch ? idMatch[1] : null;
81
+ const priceMatch = text.match(/(\d+[,.]\d+€)/);
82
+ const name = text.replace(/\d+[,.]\d+€/g, '').trim().substring(0, 120);
83
+ if (name && name.length > 3 && productId) {
84
+ products.push({ name, productId, price: priceMatch ? priceMatch[1] : null, url: href });
85
+ }
86
+ }
87
+ });
88
+
89
+ // Deduplicate
90
+ const seen = new Set();
91
+ return products.filter(p => {
92
+ if (seen.has(p.productId)) return false;
93
+ seen.add(p.productId);
94
+ return true;
95
+ });
96
+ }
97
+
98
+ // ─── Product Search ────────────────────────────────────────────────────────────
99
+
100
+ async function searchProducts(query, limit = 10) {
101
+ await goto(`${CONTINENTE_BASE}/pesquisa/?q=${encodeURIComponent(query)}`);
102
+ const html = await page.content();
103
+ return parseProducts(html, limit);
104
+ }
105
+
106
+ function parseProducts(html, limit = 30) {
107
+ const $ = cheerio.load(html);
108
+ const products = [];
109
+
110
+ // Prices live in the card container (.ct-inner-tile-wrap), not in the <a> tag itself
111
+ $('.ct-inner-tile-wrap').each((i, el) => {
112
+ if (products.length >= limit) return;
113
+ const $el = $(el);
114
+ const link = $el.find('a[href*="/produto/"]').first();
115
+ const href = link.attr('href') || '';
116
+ if (!href) return;
117
+
118
+ const idMatch = href.match(/\/produto\/([^\/\?]+?)(?:\.html)?(?:\?|$)/);
119
+ const productId = idMatch ? idMatch[1] : null;
120
+ if (!productId) return;
121
+
122
+ const cardText = $el.text().replace(/\s+/g, ' ').trim();
123
+ const priceMatch = cardText.match(/(\d+[,.]\d+)€/);
124
+ if (!priceMatch) return;
125
+
126
+ const price = parseFloat(priceMatch[1].replace(',', '.'));
127
+ const unitMatch = cardText.match(/(\d+[,.]\d+€)\/([a-zA-Z]+)/);
128
+
129
+ const nameLink = $el.find('a[href*="/produto/"]').filter((_, a) => $(a).text().trim().length > 0).first();
130
+ let name = nameLink.text().replace(/\s+/g, ' ').trim().substring(0, 100);
131
+ if (name.length < 3) return;
132
+
133
+ products.push({
134
+ name,
135
+ price,
136
+ currency: '€',
137
+ product_id: productId,
138
+ url: href.startsWith('http') ? href : CONTINENTE_BASE + href,
139
+ unit: unitMatch ? `${unitMatch[1]}/${unitMatch[2]}` : null
140
+ });
141
+ });
142
+
143
+ const seen = new Set();
144
+ return products.filter(p => {
145
+ if (seen.has(p.product_id)) return false;
146
+ seen.add(p.product_id);
147
+ return true;
148
+ });
149
+ }
150
+
151
+ // ─── Cart ─────────────────────────────────────────────────────────────────────
152
+
153
+ async function getCart() {
154
+ await goto(`${CONTINENTE_BASE}/checkout/carrinho/`);
155
+
156
+ if (page.url().includes('/login')) return { error: 'not_authenticated' };
157
+
158
+ return page.evaluate(() => {
159
+ const cartList = document.querySelector('.js-cart-product-list');
160
+ if (!cartList) return [];
161
+
162
+ const seen = new Set();
163
+ const items = [];
164
+
165
+ cartList.querySelectorAll('[data-pid][data-product-name]').forEach(el => {
166
+ const pid = el.dataset.pid;
167
+ const name = el.dataset.productName;
168
+ if (!name || seen.has(pid)) return;
169
+ seen.add(pid);
170
+
171
+ const priceEl = el.querySelector('.pwc-tile--price-primary');
172
+ const priceText = priceEl?.textContent?.trim().replace(/[^\d,]/g, '').replace(',', '.');
173
+ const price = priceText ? parseFloat(priceText) : null;
174
+
175
+ const qtyInput = el.querySelector('input.add-to-cart-quantity');
176
+ const qty = qtyInput ? parseInt(qtyInput.value) || 1 : 1;
177
+
178
+ items.push({ name, price, qty });
179
+ });
180
+
181
+ return items;
182
+ });
183
+ }
184
+
185
+ async function addToCart(productId, quantity = 1) {
186
+ const slug = productId.endsWith('.html') ? productId : `${productId}.html`;
187
+ await goto(`${CONTINENTE_BASE}/produto/${slug}`);
188
+
189
+ // Extract numeric PID and Cart-AddProduct URL from page
190
+ const result = await page.evaluate(async (qty) => {
191
+ const pidInput = document.querySelector('input[name="productID"]');
192
+ const urlInput = document.querySelector('input.add-to-cart-url');
193
+ const csrfInput = document.querySelector('input[name="csrf_token"]');
194
+
195
+ if (!pidInput || !urlInput) return { success: false, message: 'Could not find product form data' };
196
+
197
+ const pid = pidInput.value;
198
+ const cartUrl = urlInput.value;
199
+ const csrf = csrfInput ? csrfInput.value : '';
200
+
201
+ const body = new URLSearchParams({ pid, quantity: String(qty), options: '[]' });
202
+ if (csrf) body.append('csrf_token', csrf);
203
+
204
+ try {
205
+ const res = await fetch(cartUrl, {
206
+ method: 'POST',
207
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
208
+ body: body.toString(),
209
+ credentials: 'include',
210
+ signal: AbortSignal.timeout(10000)
211
+ });
212
+ const json = await res.json();
213
+ return { success: res.ok, status: res.status, cartCount: json?.quantityTotal, error: json?.message };
214
+ } catch (e) {
215
+ return { success: false, message: e.message };
216
+ }
217
+ }, quantity);
218
+
219
+ return result;
220
+ }
221
+
222
+ // ─── Order History ────────────────────────────────────────────────────────────
223
+
224
+ async function getOrderHistory(limit = 5) {
225
+ await goto(`${CONTINENTE_BASE}/conta/encomendas/`);
226
+ await page.waitForTimeout(3000);
227
+
228
+ if (page.url().includes('/login')) return { error: 'not_authenticated' };
229
+
230
+ const text = await page.evaluate(() => document.body.innerText);
231
+ const rawLines = text.split('\n').map(l => l.trim()).filter(Boolean);
232
+
233
+ const orders = [];
234
+ const orderNumRe = /^\d{9}_\d{3}$/;
235
+ const dateRe = /^\d{1,2} [A-Za-zÀ-ÿ]+ \d{2,4}/;
236
+
237
+ for (let i = 0; i < rawLines.length; i++) {
238
+ if (orderNumRe.test(rawLines[i])) {
239
+ const date = rawLines[i + 1] && dateRe.test(rawLines[i + 1]) ? rawLines[i + 1] : null;
240
+ orders.push({ orderNumber: rawLines[i], date });
241
+ }
242
+ }
243
+
244
+ // Collect order detail links
245
+ const orderLinks = await page.evaluate(() => {
246
+ const seen = new Set();
247
+ return Array.from(document.querySelectorAll('a[href*="detalhe-encomenda"]'))
248
+ .map(a => a.href)
249
+ .filter(h => { if (seen.has(h)) return false; seen.add(h); return true; });
250
+ });
251
+
252
+ // Fetch product lines for each order up to limit
253
+ const detailLinks = orderLinks.slice(0, limit);
254
+ for (let idx = 0; idx < detailLinks.length; idx++) {
255
+ try {
256
+ const products = await getOrderProducts(detailLinks[idx]);
257
+ if (orders[idx]) orders[idx].lines = products.map(p => `${p.qty}x ${p.name}`);
258
+ } catch (e) {
259
+ // skip failed pages
260
+ }
261
+ }
262
+
263
+ return orders;
264
+ }
265
+
266
+ async function getOrderProducts(orderDetailUrl) {
267
+ await page.goto(orderDetailUrl, { waitUntil: 'networkidle', timeout: 20000 });
268
+ await page.waitForTimeout(2000);
269
+
270
+ return page.evaluate(() => {
271
+ const products = [];
272
+ document.querySelectorAll('[class*="product-line"]').forEach(el => {
273
+ const qtyEl = el.querySelector('[class*="qty"], [class*="quantity"], [class*="amount"]');
274
+ if (!qtyEl) return; // skip category headers
275
+
276
+ const nameEl = el.querySelector('[class*="product-name"], [class*="name"], a[href*="produto"]');
277
+ const rawName = (nameEl || el).textContent.trim();
278
+ // Take only first line — rest is brand/subcopy
279
+ const name = rawName.split('\n')[0].trim();
280
+
281
+ const qtyMatch = qtyEl.textContent.trim().match(/^(\d+)/);
282
+ const qty = qtyMatch ? parseInt(qtyMatch[1], 10) : 1;
283
+
284
+ if (name && name.length > 3) products.push({ name, qty });
285
+ });
286
+ return products;
287
+ });
288
+ }
289
+
290
+ async function getMostBought() {
291
+ await goto(`${CONTINENTE_BASE}/conta/encomendas/`);
292
+ await page.waitForTimeout(3000);
293
+
294
+ if (page.url().includes('/login')) return { error: 'not_authenticated' };
295
+
296
+ // Collect all unique order detail links
297
+ const orderLinks = await page.evaluate(() => {
298
+ const seen = new Set();
299
+ return Array.from(document.querySelectorAll('a[href*="detalhe-encomenda"]'))
300
+ .map(a => a.href)
301
+ .filter(h => { if (seen.has(h)) return false; seen.add(h); return true; });
302
+ });
303
+
304
+ if (orderLinks.length === 0) return { error: 'no_orders' };
305
+
306
+ // Tally products across all orders
307
+ const tally = new Map(); // name -> { qty, orders }
308
+
309
+ for (const link of orderLinks) {
310
+ try {
311
+ const products = await getOrderProducts(link);
312
+ for (const { name, qty } of products) {
313
+ const existing = tally.get(name);
314
+ if (existing) {
315
+ existing.qty += qty;
316
+ existing.orders += 1;
317
+ } else {
318
+ tally.set(name, { qty, orders: 1 });
319
+ }
320
+ }
321
+ } catch (e) {
322
+ // skip failed order pages
323
+ }
324
+ }
325
+
326
+ return Array.from(tally.entries())
327
+ .map(([name, { qty, orders }]) => ({ name, qty, orders }))
328
+ .sort((a, b) => b.qty - a.qty);
329
+ }
330
+
331
+ // ─── Preferences ───────────────────────────────────────────────────────────────
332
+
333
+ async function getPreferences() {
334
+ const prefsFile = `${STATE_DIR}/preferences.json`;
335
+ if (!existsSync(prefsFile)) return null;
336
+ try {
337
+ return JSON.parse(readFileSync(prefsFile, 'utf8'));
338
+ } catch (e) {
339
+ return null;
340
+ }
341
+ }
342
+
343
+ async function savePreferences(prefs) {
344
+ try { mkdirSync(STATE_DIR, { recursive: true }); } catch (e) {}
345
+ const prefsFile = `${STATE_DIR}/preferences.json`;
346
+ writeFileSync(prefsFile, JSON.stringify(prefs, null, 2));
347
+ }
348
+
349
+ async function updatePreferencesFromFavorites() {
350
+ const favorites = await fetchFavorites();
351
+ const prefs = {
352
+ favorites,
353
+ lastUpdated: new Date().toISOString()
354
+ };
355
+ await savePreferences(prefs);
356
+ return prefs;
357
+ }
358
+
359
+ function rankByPreference(products, preferences) {
360
+ if (!preferences || !preferences.favorites) return products;
361
+
362
+ const favIds = new Set(preferences.favorites.map(f => f.productId));
363
+ const favNames = new Map(preferences.favorites.map(f => [f.name.toLowerCase(), f]));
364
+
365
+ return products.map(p => {
366
+ let score = 0;
367
+ if (favIds.has(p.product_id)) score += 100;
368
+ const nameLower = p.name.toLowerCase();
369
+ for (const [favName, fav] of favNames) {
370
+ if (nameLower.includes(favName) || favName.includes(nameLower)) score += 50;
371
+ }
372
+ return { ...p, score };
373
+ }).sort((a, b) => b.score - a.score);
374
+ }
375
+
376
+ // ─── MCP Server ───────────────────────────────────────────────────────────────
377
+
378
+ class ContinenteServer {
379
+ constructor() {
380
+ this.server = new Server(
381
+ { name: 'continente-mcp', version: '3.1.0' },
382
+ { capabilities: { tools: {} } }
383
+ );
384
+ this.setupTools();
385
+ }
386
+
387
+ setupTools() {
388
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
389
+ tools: [
390
+ {
391
+ name: 'search_products',
392
+ description: 'Search Continente products. Results ranked by your favorites if available.',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ query: { type: 'string', description: 'Search term (e.g., "leite", "pao")' },
397
+ limit: { type: 'number', description: 'Max results (default: 10)' }
398
+ },
399
+ required: ['query']
400
+ }
401
+ },
402
+ {
403
+ name: 'get_favorites',
404
+ description: 'Get your Continente favorites list (from your account).',
405
+ inputSchema: { type: 'object', properties: {} }
406
+ },
407
+ {
408
+ name: 'refresh_favorites',
409
+ description: 'Refresh your favorites from the website. Run this if your favorites have changed.',
410
+ inputSchema: { type: 'object', properties: {} }
411
+ },
412
+ {
413
+ name: 'get_cart',
414
+ description: 'View your current shopping cart.',
415
+ inputSchema: { type: 'object', properties: {} }
416
+ },
417
+ {
418
+ name: 'add_to_cart',
419
+ description: 'Add a product to cart by product ID.',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {
423
+ product_id: { type: 'string', description: 'Product ID from search results' },
424
+ quantity: { type: 'number', description: 'Quantity (default: 1)' }
425
+ },
426
+ required: ['product_id']
427
+ }
428
+ },
429
+ {
430
+ name: 'get_order_history',
431
+ description: 'Get your recent order history from your account.',
432
+ inputSchema: {
433
+ type: 'object',
434
+ properties: {
435
+ limit: { type: 'number', description: 'Number of orders (default: 5)' }
436
+ }
437
+ }
438
+ },
439
+ {
440
+ name: 'get_most_bought',
441
+ description: 'Get the products you buy most often, tallied across all orders. Scans every order page sequentially — slow on accounts with many orders.',
442
+ inputSchema: { type: 'object', properties: {} }
443
+ },
444
+ {
445
+ name: 'close_session',
446
+ description: 'Close the browser session. Call when done to free resources.',
447
+ inputSchema: { type: 'object', properties: {} }
448
+ }
449
+ ]
450
+ }));
451
+
452
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
453
+ const { name, arguments: args } = request.params;
454
+ try {
455
+ switch (name) {
456
+ case 'search_products':
457
+ return await this.handle_search(args.query, args.limit || 10);
458
+ case 'get_favorites':
459
+ return await this.handle_favorites();
460
+ case 'refresh_favorites':
461
+ return await this.handle_refresh_favorites();
462
+ case 'get_cart':
463
+ return await this.handle_get_cart();
464
+ case 'add_to_cart':
465
+ return await this.handle_add_to_cart(args.product_id, args.quantity || 1);
466
+ case 'get_order_history':
467
+ return await this.handle_order_history(args.limit || 5);
468
+ case 'get_most_bought':
469
+ return await this.handle_most_bought();
470
+ case 'close_session':
471
+ await closeBrowser();
472
+ return { content: [{ type: 'text', text: 'Session closed.' }] };
473
+ default:
474
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
475
+ }
476
+ } catch (error) {
477
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
478
+ }
479
+ });
480
+ }
481
+
482
+ formatProducts(products, preferences = null) {
483
+ return products.map((p, i) => {
484
+ const fav = preferences?.favorites?.find(f => f.productId === p.product_id);
485
+ const favBadge = fav ? ' ⭐ (favorite)' : '';
486
+ const unitStr = p.unit ? ` (${p.unit})` : '';
487
+ return `${i + 1}. ${p.name}${favBadge}\n 💰 ${p.price?.toFixed(2) || '?'}€${unitStr}\n 🆔 ${p.product_id}`;
488
+ }).join('\n\n');
489
+ }
490
+
491
+ async handle_search(query, limit) {
492
+ const prefs = await getPreferences();
493
+ const products = await searchProducts(query, limit * 2);
494
+ const ranked = rankByPreference(products, prefs);
495
+ const top = ranked.slice(0, limit);
496
+
497
+ const favCount = prefs?.favorites?.length || 0;
498
+ return {
499
+ content: [{
500
+ type: 'text',
501
+ text: `Found ${products.length} products for "${query}" (${favCount} favorites loaded)${top.some(p => p.score > 0) ? ' — ⭐ = in your favorites' : ''}:\n\n${this.formatProducts(top, prefs)}\n\nUse add_to_cart with the product_id.`
502
+ }]
503
+ };
504
+ }
505
+
506
+ async handle_favorites() {
507
+ const prefs = await getPreferences();
508
+ if (!prefs?.favorites?.length) {
509
+ return { content: [{ type: 'text', text: 'No favorites loaded. Run refresh_favorites first.' }] };
510
+ }
511
+
512
+ const list = prefs.favorites.map((f, i) =>
513
+ `${i + 1}. ${f.name}${f.price ? ` — ${f.price}` : ''}`
514
+ ).join('\n');
515
+
516
+ return {
517
+ content: [{
518
+ type: 'text',
519
+ text: `Your ${prefs.favorites.length} favorites:\n\n${list}`
520
+ }]
521
+ };
522
+ }
523
+
524
+ async handle_refresh_favorites() {
525
+ const prefs = await updatePreferencesFromFavorites();
526
+ return {
527
+ content: [{
528
+ type: 'text',
529
+ text: `✅ Refreshed! ${prefs.favorites.length} favorites saved.\n\nTop 5:\n${prefs.favorites.slice(0, 5).map((f, i) => `${i+1}. ${f.name}`).join('\n')}`
530
+ }]
531
+ };
532
+ }
533
+
534
+ async handle_get_cart() {
535
+ const items = await getCart();
536
+ if (items?.error === 'not_authenticated') {
537
+ return { content: [{ type: 'text', text: 'Not logged in — cookies may have expired. Re-run the cookie sync script.' }] };
538
+ }
539
+ if (!Array.isArray(items) || items.length === 0) {
540
+ return { content: [{ type: 'text', text: '🛒 Cart is empty.' }] };
541
+ }
542
+ const total = items.reduce((s, i) => s + (i.price || 0) * i.qty, 0);
543
+ const list = items.map((item, i) =>
544
+ `${i + 1}. ${item.name}\n Qtd: ${item.qty} × ${item.price?.toFixed(2) || '?'}€`
545
+ ).join('\n');
546
+ return {
547
+ content: [{
548
+ type: 'text',
549
+ text: `🛒 Cart (${items.length} items):\n\n${list}\n\n💶 Total: ${total.toFixed(2)}€\n\nGo to https://www.continente.pt/checkout/carrinho/ to checkout.`
550
+ }]
551
+ };
552
+ }
553
+
554
+ async handle_add_to_cart(productId, quantity) {
555
+ const result = await addToCart(productId, quantity);
556
+ if (result.success) {
557
+ return { content: [{ type: 'text', text: `✅ Added to cart! (${quantity}x)\n\nUse get_cart to review.` }] };
558
+ }
559
+ return { content: [{ type: 'text', text: `⚠️ ${result.message || 'Could not add to cart.'}` }], isError: true };
560
+ }
561
+
562
+ async handle_order_history(limit) {
563
+ const orders = await getOrderHistory(limit);
564
+ if (orders?.error === 'not_authenticated') {
565
+ return { content: [{ type: 'text', text: 'Not logged in — cookies may have expired. Re-sync your cookies.' }] };
566
+ }
567
+ if (!Array.isArray(orders) || orders.length === 0) {
568
+ return { content: [{ type: 'text', text: 'Could not load order history. Check https://www.continente.pt/conta/encomendas/' }] };
569
+ }
570
+ const list = orders.slice(0, limit).map((o, i) => {
571
+ if (o.raw) return `${i + 1}. ${o.raw}`;
572
+ const date = o.date ? `📅 ${o.date}` : '';
573
+ const total = o.total ? ` — ${o.total}` : '';
574
+ const lines = o.lines ? o.lines.join(' · ') : '';
575
+ return `${i + 1}. ${date}${total}\n ${lines}`;
576
+ }).join('\n\n');
577
+ return {
578
+ content: [{
579
+ type: 'text',
580
+ text: `Recent orders:\n\n${list}\n\nView full history at https://www.continente.pt/conta/encomendas/`
581
+ }]
582
+ };
583
+ }
584
+
585
+ async handle_most_bought() {
586
+ const result = await getMostBought();
587
+ if (result.error) {
588
+ return { content: [{ type: 'text', text: result.error === 'not_authenticated' ? 'Not logged in — cookies may have expired.' : `Error: ${result.error}` }] };
589
+ }
590
+ if (!Array.isArray(result) || result.length === 0) {
591
+ return { content: [{ type: 'text', text: 'Could not calculate most bought items from order history.' }] };
592
+ }
593
+ const top = result.slice(0, 25);
594
+ const list = top.map((item, i) =>
595
+ `${i + 1}. ${item.name} — ${item.qty} units across ${item.orders} order${item.orders > 1 ? 's' : ''}`
596
+ ).join('\n');
597
+ return {
598
+ content: [{
599
+ type: 'text',
600
+ text: `Most bought products (calculated from your order history):\n\n${list}`
601
+ }]
602
+ };
603
+ }
604
+
605
+ async start() {
606
+ const transport = new StdioServerTransport();
607
+ await this.server.connect(transport);
608
+ console.error('Continente MCP v3.1.0 started');
609
+ }
610
+ }
611
+
612
+ const server = new ContinenteServer();
613
+ server.start().catch(console.error);
package/src/utils.js ADDED
@@ -0,0 +1,15 @@
1
+ export function normalizeCookies(cookies) {
2
+ return cookies
3
+ .filter(c => c.name && c.name !== 'undefined')
4
+ .map(c => {
5
+ const n = {
6
+ name: c.name, value: c.value, domain: c.domain, path: c.path || '/',
7
+ httpOnly: Boolean(c.httpOnly), secure: Boolean(c.secure),
8
+ sameSite: (!c.sameSite || c.sameSite === 'unspecified') ? 'Lax'
9
+ : c.sameSite === 'no_restriction' ? 'None'
10
+ : c.sameSite.charAt(0).toUpperCase() + c.sameSite.slice(1).toLowerCase()
11
+ };
12
+ if (c.expires != null) n.expires = Number(c.expires);
13
+ return n;
14
+ });
15
+ }