continentept-mcp 0.4.0 → 0.5.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/CHANGELOG.md +18 -1
- package/dist/clients/storefront.js +1 -1
- package/dist/domain/models.d.ts +65 -17
- package/dist/domain/models.js +10 -6
- package/dist/domain/money.d.ts +19 -0
- package/dist/domain/money.js +25 -0
- package/dist/mcp/contracts.d.ts +87 -21
- package/dist/mcp/contracts.js +1 -1
- package/dist/mcp/guide.d.ts +1 -1
- package/dist/mcp/guide.js +2 -2
- package/dist/mcp/shopping.d.ts +4 -0
- package/dist/mcp/shopping.js +43 -22
- package/dist/parsers/catalog.d.ts +13 -5
- package/dist/parsers/catalog.js +48 -45
- package/dist/parsers/product.js +7 -5
- package/dist/services/catalog.d.ts +10 -4
- package/dist/services/catalog.js +3 -2
- package/docs/tools.md +23 -9
- package/docs/troubleshooting.md +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Searching for produce, meat and other products priced per kilogram failed with "Invalid money amount". Prices now record what they are for: per item, per piece, per kg or per litre.
|
|
14
|
+
- One unreadable search result no longer fails the whole search; the other results are returned as a partial result, and the unreadable one is described.
|
|
15
|
+
- Search results keep the website's relevance order. Products whose names merely resembled a favorite no longer jump ahead of better matches; exact favorites still come first.
|
|
16
|
+
- Favorites priced per kilogram are read correctly, and an unrecognised favorite price is reported as unknown instead of failing the refresh.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Breaking:** catalogue prices (`price` in search, favorites and `get_product`) are now `{ amount, per }`. `unitPriceText` is replaced by `secondaryPrice`, and the search field `similarity` is removed. Saved favorites are fetched again once after updating.
|
|
21
|
+
- Search results include `packageText`, the pack size as shown on the website.
|
|
22
|
+
- Sign-in errors say whether no credentials are configured or Continente did not accept them, with the next step for each case. The instructions present the `basket-planner` prompt as optional, for clients that support prompts.
|
|
23
|
+
- Browser engine updated to Playwright 1.64 (Chrome 156). After updating, the new Chromium downloads automatically on first use (about 150 MB, once).
|
|
24
|
+
|
|
9
25
|
## [0.4.0] - 2026-10-15
|
|
10
26
|
|
|
11
27
|
### Added
|
|
@@ -55,7 +71,8 @@ All notable changes to this project are documented here. The format follows
|
|
|
55
71
|
- Automatic sign-in from environment variables or a private credentials file, and a `login` command.
|
|
56
72
|
- The `basket-planner` skill for planning a shopping list and updating the basket.
|
|
57
73
|
|
|
58
|
-
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.
|
|
74
|
+
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.5.0...HEAD
|
|
75
|
+
[0.5.0]: https://github.com/andredelgado/continente-mcp/compare/v0.4.0...v0.5.0
|
|
59
76
|
[0.4.0]: https://github.com/andredelgado/continente-mcp/compare/v0.3.1...v0.4.0
|
|
60
77
|
[0.3.1]: https://github.com/andredelgado/continente-mcp/compare/v0.3.0...v0.3.1
|
|
61
78
|
[0.3.0]: https://github.com/andredelgado/continente-mcp/compare/v0.2.0...v0.3.0
|
|
@@ -93,7 +93,7 @@ export class CatalogClient {
|
|
|
93
93
|
break;
|
|
94
94
|
}
|
|
95
95
|
const previousCount = ids.size;
|
|
96
|
-
parsed.value.forEach(product => ids.add(product.productId));
|
|
96
|
+
parsed.value.products.forEach(product => ids.add(product.productId));
|
|
97
97
|
pages.push(html);
|
|
98
98
|
reportProgress(pages.length, 5, 'Reading catalogue pages');
|
|
99
99
|
const continuation = parseSearchPagination(html);
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -4,8 +4,16 @@ export declare const FavoriteProductSchema: z.ZodObject<{
|
|
|
4
4
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
5
5
|
name: z.ZodString;
|
|
6
6
|
price: z.ZodNullable<z.ZodObject<{
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
amount: z.ZodObject<{
|
|
8
|
+
currency: z.ZodLiteral<"EUR">;
|
|
9
|
+
minorUnits: z.ZodNumber;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
per: z.ZodEnum<{
|
|
12
|
+
item: "item";
|
|
13
|
+
kg: "kg";
|
|
14
|
+
l: "l";
|
|
15
|
+
piece: "piece";
|
|
16
|
+
}>;
|
|
9
17
|
}, z.core.$strict>>;
|
|
10
18
|
url: z.ZodURL;
|
|
11
19
|
}, z.core.$strict>;
|
|
@@ -15,11 +23,31 @@ export declare const ProductSchema: z.ZodObject<{
|
|
|
15
23
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
16
24
|
name: z.ZodString;
|
|
17
25
|
price: z.ZodNullable<z.ZodObject<{
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
amount: z.ZodObject<{
|
|
27
|
+
currency: z.ZodLiteral<"EUR">;
|
|
28
|
+
minorUnits: z.ZodNumber;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
per: z.ZodEnum<{
|
|
31
|
+
item: "item";
|
|
32
|
+
kg: "kg";
|
|
33
|
+
l: "l";
|
|
34
|
+
piece: "piece";
|
|
35
|
+
}>;
|
|
20
36
|
}, z.core.$strict>>;
|
|
21
37
|
url: z.ZodURL;
|
|
22
|
-
|
|
38
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
39
|
+
amount: z.ZodObject<{
|
|
40
|
+
currency: z.ZodLiteral<"EUR">;
|
|
41
|
+
minorUnits: z.ZodNumber;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
per: z.ZodEnum<{
|
|
44
|
+
item: "item";
|
|
45
|
+
kg: "kg";
|
|
46
|
+
l: "l";
|
|
47
|
+
piece: "piece";
|
|
48
|
+
}>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
50
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
23
51
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
24
52
|
}, z.core.$strict>;
|
|
25
53
|
export type Product = z.infer<typeof ProductSchema>;
|
|
@@ -227,7 +255,7 @@ export declare const PurchaseFrequencySchema: z.ZodObject<{
|
|
|
227
255
|
}, z.core.$strict>;
|
|
228
256
|
export type PurchaseFrequency = z.infer<typeof PurchaseFrequencySchema>;
|
|
229
257
|
export declare const FavoritesCacheSchema: z.ZodObject<{
|
|
230
|
-
schemaVersion: z.ZodLiteral<
|
|
258
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
231
259
|
accountScope: z.ZodString;
|
|
232
260
|
fetchedAt: z.ZodISODateTime;
|
|
233
261
|
favorites: z.ZodArray<z.ZodObject<{
|
|
@@ -235,8 +263,16 @@ export declare const FavoritesCacheSchema: z.ZodObject<{
|
|
|
235
263
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
236
264
|
name: z.ZodString;
|
|
237
265
|
price: z.ZodNullable<z.ZodObject<{
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
amount: z.ZodObject<{
|
|
267
|
+
currency: z.ZodLiteral<"EUR">;
|
|
268
|
+
minorUnits: z.ZodNumber;
|
|
269
|
+
}, z.core.$strict>;
|
|
270
|
+
per: z.ZodEnum<{
|
|
271
|
+
item: "item";
|
|
272
|
+
kg: "kg";
|
|
273
|
+
l: "l";
|
|
274
|
+
piece: "piece";
|
|
275
|
+
}>;
|
|
240
276
|
}, z.core.$strict>>;
|
|
241
277
|
url: z.ZodURL;
|
|
242
278
|
}, z.core.$strict>>;
|
|
@@ -248,18 +284,30 @@ export declare const ProductDetailsSchema: z.ZodObject<{
|
|
|
248
284
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
249
285
|
name: z.ZodString;
|
|
250
286
|
url: z.ZodURL;
|
|
251
|
-
price: z.ZodObject<{
|
|
252
|
-
amount: z.
|
|
287
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
288
|
+
amount: z.ZodObject<{
|
|
253
289
|
currency: z.ZodLiteral<"EUR">;
|
|
254
290
|
minorUnits: z.ZodNumber;
|
|
255
|
-
}, z.core.$strict
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
}, z.core.$strict>;
|
|
292
|
+
per: z.ZodEnum<{
|
|
293
|
+
item: "item";
|
|
294
|
+
kg: "kg";
|
|
295
|
+
l: "l";
|
|
296
|
+
piece: "piece";
|
|
260
297
|
}>;
|
|
261
|
-
}, z.core.$strict
|
|
262
|
-
|
|
298
|
+
}, z.core.$strict>>;
|
|
299
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
300
|
+
amount: z.ZodObject<{
|
|
301
|
+
currency: z.ZodLiteral<"EUR">;
|
|
302
|
+
minorUnits: z.ZodNumber;
|
|
303
|
+
}, z.core.$strict>;
|
|
304
|
+
per: z.ZodEnum<{
|
|
305
|
+
item: "item";
|
|
306
|
+
kg: "kg";
|
|
307
|
+
l: "l";
|
|
308
|
+
piece: "piece";
|
|
309
|
+
}>;
|
|
310
|
+
}, z.core.$strict>>;
|
|
263
311
|
availability: z.ZodEnum<{
|
|
264
312
|
available: "available";
|
|
265
313
|
unavailable: "unavailable";
|
package/dist/domain/models.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ToolErrorSchema } from './errors.js';
|
|
3
3
|
import { CartItemIdSchema, OrderIdSchema, OrderNumberSchema, ProductIdSchema, ProductReferenceSchema } from './identity.js';
|
|
4
|
-
import { MoneySchema, QuotedMoneySchema } from './money.js';
|
|
4
|
+
import { MoneySchema, PriceTagSchema, QuotedMoneySchema } from './money.js';
|
|
5
5
|
import { ObservedQuantitySchema, QuantitySchema, ProductSaleRulesSchema } from './quantity.js';
|
|
6
6
|
const productDetails = {
|
|
7
7
|
name: z.string().trim().min(1).max(250),
|
|
8
|
-
price
|
|
8
|
+
/** The main price as displayed; null when absent or unrecognised. */
|
|
9
|
+
price: PriceTagSchema.nullable(),
|
|
9
10
|
url: z.url(),
|
|
10
11
|
};
|
|
11
12
|
export const FavoriteProductSchema = ProductReferenceSchema.safeExtend(productDetails);
|
|
12
13
|
export const ProductSchema = FavoriteProductSchema.safeExtend({
|
|
13
|
-
|
|
14
|
+
/** The comparison price shown next to the main one, such as per kg or per piece. */
|
|
15
|
+
secondaryPrice: PriceTagSchema.nullable(),
|
|
16
|
+
/** Pack size as displayed, such as `940 gr`, `2 x 52 gr` or `1,2 kg (aprox.)`. */
|
|
17
|
+
packageText: z.string().nullable(),
|
|
14
18
|
minimumText: z.string().nullable(),
|
|
15
19
|
});
|
|
16
20
|
export const CartItemSchema = z.strictObject({
|
|
@@ -51,15 +55,15 @@ export const PurchaseFrequencySchema = z.strictObject({
|
|
|
51
55
|
}).refine(frequency => (frequency.identityBasis === 'product') === (frequency.productId !== null), { message: 'Identity basis must agree with the available product ID' })
|
|
52
56
|
.refine(frequency => new Set(frequency.quantities.map(quantity => quantity.unit)).size === frequency.quantities.length, { message: 'Purchase totals must contain each unit only once' });
|
|
53
57
|
export const FavoritesCacheSchema = z.strictObject({
|
|
54
|
-
schemaVersion: z.literal(
|
|
58
|
+
schemaVersion: z.literal(2),
|
|
55
59
|
accountScope: z.string().trim().min(1).max(128),
|
|
56
60
|
fetchedAt: z.iso.datetime({ offset: true }),
|
|
57
61
|
favorites: z.array(FavoriteProductSchema),
|
|
58
62
|
completeness: z.literal('complete').optional(),
|
|
59
63
|
});
|
|
60
64
|
export const ProductDetailsSchema = ProductReferenceSchema.safeExtend({
|
|
61
|
-
name: productDetails.name, url: productDetails.url, price:
|
|
62
|
-
|
|
65
|
+
name: productDetails.name, url: productDetails.url, price: PriceTagSchema.nullable(),
|
|
66
|
+
secondaryPrice: PriceTagSchema.nullable(), availability: z.enum(['available', 'unavailable', 'unknown']),
|
|
63
67
|
saleRules: z.discriminatedUnion('status', [
|
|
64
68
|
z.strictObject({ status: z.literal('available'), rules: ProductSaleRulesSchema }),
|
|
65
69
|
z.strictObject({ status: z.literal('unavailable'), error: ToolErrorSchema }),
|
package/dist/domain/money.d.ts
CHANGED
|
@@ -21,3 +21,22 @@ export declare const QuotedMoneySchema: z.ZodObject<{
|
|
|
21
21
|
export type QuotedMoney = z.infer<typeof QuotedMoneySchema>;
|
|
22
22
|
/** Convert decimal text through integer arithmetic; never round an unrecognized precision. */
|
|
23
23
|
export declare function parseMoneyAmount(input: unknown): Result<Money | null, ToolError>;
|
|
24
|
+
/**
|
|
25
|
+
* What a displayed price is for. The storefront shows packaged goods per `item` (one pack or
|
|
26
|
+
* article), produce and meat per `kg`, drinks per `l`, and an approximate price per `piece`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PriceTagSchema: z.ZodObject<{
|
|
29
|
+
amount: z.ZodObject<{
|
|
30
|
+
currency: z.ZodLiteral<"EUR">;
|
|
31
|
+
minorUnits: z.ZodNumber;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
per: z.ZodEnum<{
|
|
34
|
+
item: "item";
|
|
35
|
+
kg: "kg";
|
|
36
|
+
l: "l";
|
|
37
|
+
piece: "piece";
|
|
38
|
+
}>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
export type PriceTag = z.infer<typeof PriceTagSchema>;
|
|
41
|
+
/** Parse labels such as `1,29€`, `1,19€/kg` or `0,24€/un`. An empty label is an unknown price. */
|
|
42
|
+
export declare function parsePriceTag(input: unknown): Result<PriceTag | null, ToolError>;
|
package/dist/domain/money.js
CHANGED
|
@@ -28,3 +28,28 @@ export function parseMoneyAmount(input) {
|
|
|
28
28
|
}
|
|
29
29
|
return { ok: true, value: { currency: 'EUR', minorUnits: Number(minorUnits) } };
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What a displayed price is for. The storefront shows packaged goods per `item` (one pack or
|
|
33
|
+
* article), produce and meat per `kg`, drinks per `l`, and an approximate price per `piece`.
|
|
34
|
+
*/
|
|
35
|
+
export const PriceTagSchema = z.strictObject({
|
|
36
|
+
amount: MoneySchema,
|
|
37
|
+
per: z.enum(['item', 'piece', 'kg', 'l']),
|
|
38
|
+
});
|
|
39
|
+
const PRICE_UNITS = { kg: 'kg', l: 'l', lt: 'l', un: 'piece' };
|
|
40
|
+
/** Parse labels such as `1,29€`, `1,19€/kg` or `0,24€/un`. An empty label is an unknown price. */
|
|
41
|
+
export function parsePriceTag(input) {
|
|
42
|
+
if (input === null || input === undefined)
|
|
43
|
+
return { ok: true, value: null };
|
|
44
|
+
if (typeof input !== 'string')
|
|
45
|
+
return { ok: false, error: invalidData('price') };
|
|
46
|
+
const text = input.replace(/\s+/g, '');
|
|
47
|
+
if (!text)
|
|
48
|
+
return { ok: true, value: null };
|
|
49
|
+
const match = /^(\d+(?:[.,]\d{1,2})?)€(?:\/([a-z]+))?$/i.exec(text);
|
|
50
|
+
const per = match ? PRICE_UNITS[(match[2] ?? '').toLowerCase()] ?? (match[2] === undefined ? 'item' : undefined) : undefined;
|
|
51
|
+
const amount = match && per ? parseMoneyAmount(match[1]) : null;
|
|
52
|
+
if (!amount?.ok || !amount.value || !per)
|
|
53
|
+
return { ok: false, error: { ...invalidData('price'), message: 'Unrecognised price format' } };
|
|
54
|
+
return { ok: true, value: { amount: amount.value, per } };
|
|
55
|
+
}
|
package/dist/mcp/contracts.d.ts
CHANGED
|
@@ -205,14 +205,33 @@ export declare const SearchData: z.ZodObject<{
|
|
|
205
205
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
206
206
|
name: z.ZodString;
|
|
207
207
|
price: z.ZodNullable<z.ZodObject<{
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
amount: z.ZodObject<{
|
|
209
|
+
currency: z.ZodLiteral<"EUR">;
|
|
210
|
+
minorUnits: z.ZodNumber;
|
|
211
|
+
}, z.core.$strict>;
|
|
212
|
+
per: z.ZodEnum<{
|
|
213
|
+
item: "item";
|
|
214
|
+
kg: "kg";
|
|
215
|
+
l: "l";
|
|
216
|
+
piece: "piece";
|
|
217
|
+
}>;
|
|
210
218
|
}, z.core.$strict>>;
|
|
211
219
|
url: z.ZodURL;
|
|
212
|
-
|
|
220
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
221
|
+
amount: z.ZodObject<{
|
|
222
|
+
currency: z.ZodLiteral<"EUR">;
|
|
223
|
+
minorUnits: z.ZodNumber;
|
|
224
|
+
}, z.core.$strict>;
|
|
225
|
+
per: z.ZodEnum<{
|
|
226
|
+
item: "item";
|
|
227
|
+
kg: "kg";
|
|
228
|
+
l: "l";
|
|
229
|
+
piece: "piece";
|
|
230
|
+
}>;
|
|
231
|
+
}, z.core.$strict>>;
|
|
232
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
213
233
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
214
234
|
favorite: z.ZodBoolean;
|
|
215
|
-
similarity: z.ZodNumber;
|
|
216
235
|
}, z.core.$strict>>;
|
|
217
236
|
scanned: z.ZodNumber;
|
|
218
237
|
returned: z.ZodNumber;
|
|
@@ -256,8 +275,16 @@ export declare const FavoritesData: z.ZodObject<{
|
|
|
256
275
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
257
276
|
name: z.ZodString;
|
|
258
277
|
price: z.ZodNullable<z.ZodObject<{
|
|
259
|
-
|
|
260
|
-
|
|
278
|
+
amount: z.ZodObject<{
|
|
279
|
+
currency: z.ZodLiteral<"EUR">;
|
|
280
|
+
minorUnits: z.ZodNumber;
|
|
281
|
+
}, z.core.$strict>;
|
|
282
|
+
per: z.ZodEnum<{
|
|
283
|
+
item: "item";
|
|
284
|
+
kg: "kg";
|
|
285
|
+
l: "l";
|
|
286
|
+
piece: "piece";
|
|
287
|
+
}>;
|
|
261
288
|
}, z.core.$strict>>;
|
|
262
289
|
url: z.ZodURL;
|
|
263
290
|
}, z.core.$strict>>;
|
|
@@ -498,14 +525,33 @@ export declare const SearchOutput: z.ZodObject<{
|
|
|
498
525
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
499
526
|
name: z.ZodString;
|
|
500
527
|
price: z.ZodNullable<z.ZodObject<{
|
|
501
|
-
|
|
502
|
-
|
|
528
|
+
amount: z.ZodObject<{
|
|
529
|
+
currency: z.ZodLiteral<"EUR">;
|
|
530
|
+
minorUnits: z.ZodNumber;
|
|
531
|
+
}, z.core.$strict>;
|
|
532
|
+
per: z.ZodEnum<{
|
|
533
|
+
item: "item";
|
|
534
|
+
kg: "kg";
|
|
535
|
+
l: "l";
|
|
536
|
+
piece: "piece";
|
|
537
|
+
}>;
|
|
503
538
|
}, z.core.$strict>>;
|
|
504
539
|
url: z.ZodURL;
|
|
505
|
-
|
|
540
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
541
|
+
amount: z.ZodObject<{
|
|
542
|
+
currency: z.ZodLiteral<"EUR">;
|
|
543
|
+
minorUnits: z.ZodNumber;
|
|
544
|
+
}, z.core.$strict>;
|
|
545
|
+
per: z.ZodEnum<{
|
|
546
|
+
item: "item";
|
|
547
|
+
kg: "kg";
|
|
548
|
+
l: "l";
|
|
549
|
+
piece: "piece";
|
|
550
|
+
}>;
|
|
551
|
+
}, z.core.$strict>>;
|
|
552
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
506
553
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
507
554
|
favorite: z.ZodBoolean;
|
|
508
|
-
similarity: z.ZodNumber;
|
|
509
555
|
}, z.core.$strict>>;
|
|
510
556
|
scanned: z.ZodNumber;
|
|
511
557
|
returned: z.ZodNumber;
|
|
@@ -598,8 +644,16 @@ export declare const FavoritesOutput: z.ZodObject<{
|
|
|
598
644
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
599
645
|
name: z.ZodString;
|
|
600
646
|
price: z.ZodNullable<z.ZodObject<{
|
|
601
|
-
|
|
602
|
-
|
|
647
|
+
amount: z.ZodObject<{
|
|
648
|
+
currency: z.ZodLiteral<"EUR">;
|
|
649
|
+
minorUnits: z.ZodNumber;
|
|
650
|
+
}, z.core.$strict>;
|
|
651
|
+
per: z.ZodEnum<{
|
|
652
|
+
item: "item";
|
|
653
|
+
kg: "kg";
|
|
654
|
+
l: "l";
|
|
655
|
+
piece: "piece";
|
|
656
|
+
}>;
|
|
603
657
|
}, z.core.$strict>>;
|
|
604
658
|
url: z.ZodURL;
|
|
605
659
|
}, z.core.$strict>>;
|
|
@@ -1364,18 +1418,30 @@ export declare const ProductDetailsOutput: z.ZodObject<{
|
|
|
1364
1418
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
1365
1419
|
name: z.ZodString;
|
|
1366
1420
|
url: z.ZodURL;
|
|
1367
|
-
price: z.ZodObject<{
|
|
1368
|
-
amount: z.
|
|
1421
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
1422
|
+
amount: z.ZodObject<{
|
|
1369
1423
|
currency: z.ZodLiteral<"EUR">;
|
|
1370
1424
|
minorUnits: z.ZodNumber;
|
|
1371
|
-
}, z.core.$strict
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1425
|
+
}, z.core.$strict>;
|
|
1426
|
+
per: z.ZodEnum<{
|
|
1427
|
+
item: "item";
|
|
1428
|
+
kg: "kg";
|
|
1429
|
+
l: "l";
|
|
1430
|
+
piece: "piece";
|
|
1376
1431
|
}>;
|
|
1377
|
-
}, z.core.$strict
|
|
1378
|
-
|
|
1432
|
+
}, z.core.$strict>>;
|
|
1433
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
1434
|
+
amount: z.ZodObject<{
|
|
1435
|
+
currency: z.ZodLiteral<"EUR">;
|
|
1436
|
+
minorUnits: z.ZodNumber;
|
|
1437
|
+
}, z.core.$strict>;
|
|
1438
|
+
per: z.ZodEnum<{
|
|
1439
|
+
item: "item";
|
|
1440
|
+
kg: "kg";
|
|
1441
|
+
l: "l";
|
|
1442
|
+
piece: "piece";
|
|
1443
|
+
}>;
|
|
1444
|
+
}, z.core.$strict>>;
|
|
1379
1445
|
availability: z.ZodEnum<{
|
|
1380
1446
|
available: "available";
|
|
1381
1447
|
unavailable: "unavailable";
|
package/dist/mcp/contracts.js
CHANGED
|
@@ -42,7 +42,7 @@ export function outputSchema(data, extra = {}) {
|
|
|
42
42
|
]), ...extra });
|
|
43
43
|
}
|
|
44
44
|
const integer = z.number().int().nonnegative();
|
|
45
|
-
export const SearchData = z.strictObject({ products: z.array(ProductSchema.safeExtend({ favorite: z.boolean()
|
|
45
|
+
export const SearchData = z.strictObject({ products: z.array(ProductSchema.safeExtend({ favorite: z.boolean() })),
|
|
46
46
|
scanned: integer, returned: integer, favoritesLoaded: integer, completeness: z.enum(['complete', 'partial', 'unknown']),
|
|
47
47
|
failures: z.array(ToolErrorSchema), nextCursor: z.string().nullable() });
|
|
48
48
|
export const FavoritesData = z.strictObject({ favorites: z.array(FavoriteProductSchema), fetchedAt: z.iso.datetime({ offset: true }), completeness: z.enum(['complete', 'unknown']),
|
package/dist/mcp/guide.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PromptHandler } from './server.js';
|
|
2
2
|
export declare const BASKET_PLANNER_GUIDE = "# Basket planner\n\nTurn the user's shopping intent into a product list or a verified basket using the **continentept-mcp** tools. A request for a plan authorizes research only; a request to add, remove or set items authorizes those basket changes. Keep the user's existing choices and constraints. Ask only about choices that change the result, such as an unspecified substitute or whether a quantity means \"more\" or \"in total\".\n\n## Choose products and quantities\n\n1. Use `search_products` with `query` (and `limit` when useful). Treat results as candidates: match size, brand, dietary constraints and intended use before choosing. Keep `productId` as a string, including leading zeros.\n2. Use `get_product` when price, availability or sale rules matter. Quantities are in the returned sale unit. Respect minimum, maximum and step; never assume a weighted product is sold by the kilogram and never silently round a requested amount.\n3. Before changing the basket, read `get_cart` for current quantities and line identities. If several lines share a product ID, use the intended line's `cartItemId`, or ask the user which line they mean.\n4. Show unresolved choices. Missing prices, availability, quantities or sale rules stay unknown. Money is EUR in integer `minorUnits` (129 means \u20AC1.29). Keep product prices, basket totals and estimated list costs distinct.\n\nSearches read a bounded number of result pages; respect `completeness` and `failures` and do not claim a search was exhaustive.\n\n## Change the basket\n\n- `set_cart_item_quantity({ productId, quantity, cartItemId? })` makes the basket hold exactly `quantity`, adding the product if needed. Zero removes it. Prefer this whenever the user states a desired total; repeating it is safe.\n- `add_cart_item({ productId, quantity, cartItemId? })` adds `quantity` on top of what is already there (default one sale unit). Use it only for \"add more\".\n- `remove_cart_item({ productId, cartItemId? })` removes a line. An already absent item is confirmed without a write.\n\nIf the basket holds two units, \"add two more\" is `add_cart_item` with 2, while \"I need two\" is `set_cart_item_quantity` with 2 (which changes nothing). Process a list one item at a time and read each result before continuing. There is no batch, checkout or payment tool; checkout happens on the website.\n\n## Read outcomes before reporting success\n\nRead `structuredContent.result` rather than inferring success from prose.\n\n- `status: success`, `empty` or `partial` carries `data`. Report partial-result warnings and unavailable data.\n- A confirmed change has `data.outcome: \"confirmed\"` and `confirmedQuantity`, the quantity now in the basket. Report that observed quantity.\n- `status: error` carries `error`. For basket changes also read `mutation`: `not_applied` means nothing was submitted or the change was rejected; fix the reported cause before trying again.\n- `mutation.outcome: \"unknown\"` means the change may have happened. Stop dependent basket changes, call `get_cart`, and compare with the state before the change. Never repeat the original request just because the response was lost. If the basket state cannot settle the intent, explain the uncertainty and ask the user.\n\nA timeout, cancellation or lost connection during a change is not evidence that the basket stayed the same. Apply the same reconciliation when no structured result arrives.\n\n## Use account context when it helps\n\nFor \"my usual items\", `list_favorites` and `list_frequently_bought_products` can inform a proposed list. Past purchases are evidence, not permission to buy them again.\n\n`list_favorites` returns a local snapshot with `source` and `freshness`; pass `refresh: true` when the user wants the current website list or the snapshot is stale. An empty list is a valid result, and a failed refresh does not mean favorites disappeared.\n\n`list_orders` supports `limit`, `includeLines`, `detailLimit` and `cursor` (the returned `nextCursor`). Use the internal `orderId` with `get_order`, not the displayed order number. `list_frequently_bought_products` has separate `orderLimit` and `limit`; its scan counts and failures show how representative the ranking is. Never add quantities expressed in different units.\n\n`get_session_status` reports local state only; it does not verify that the login still works. Do not include credentials or account details in summaries.\n\nFinish with the chosen or changed items, confirmed quantities and units, and any unresolved choices or failures. Keep planned items separate from changes the server confirmed.\n\nFor exact schemas and limits, see the [tool reference](https://github.com/andredelgado/continente-mcp/blob/main/docs/tools.md).";
|
|
3
3
|
/** Sent to every client on connect; the essential rules for working with the cart safely. */
|
|
4
|
-
export declare const SERVER_INSTRUCTIONS = "Continente MCP shops at Continente.pt for one account. Checkout and payment always happen on the website.\n\n- Quantities are in the product's sale unit. Call get_product before choosing a quantity for weighted or bulk products, and respect its minimum, maximum and step.\n- set_cart_item_quantity sets an exact total (adding the product if needed; 0 removes it). add_cart_item adds on top of what is already there. When the user says how many they need, use set_cart_item_quantity.\n- Change the cart one item at a time and read structuredContent.result before reporting success. confirmedQuantity is what the cart now holds.\n- If a cart change returns mutation.outcome \"unknown\", the cart may have changed. Call get_cart and decide from its contents. Never repeat the change automatically.\n- Money is EUR in integer minorUnits (129 means \u20AC1.29). Missing prices, quantities or completeness stay unknown; do not guess them.\n-
|
|
4
|
+
export declare const SERVER_INSTRUCTIONS = "Continente MCP shops at Continente.pt for one account. Checkout and payment always happen on the website.\n\n- Quantities are in the product's sale unit. Call get_product before choosing a quantity for weighted or bulk products, and respect its minimum, maximum and step.\n- set_cart_item_quantity sets an exact total (adding the product if needed; 0 removes it). add_cart_item adds on top of what is already there. When the user says how many they need, use set_cart_item_quantity.\n- Change the cart one item at a time and read structuredContent.result before reporting success. confirmedQuantity is what the cart now holds.\n- If a cart change returns mutation.outcome \"unknown\", the cart may have changed. Call get_cart and decide from its contents. Never repeat the change automatically.\n- Money is EUR in integer minorUnits (129 means \u20AC1.29). Each price says what it is for: per item, per piece, per kg or per l. A price per kg or per l is not the price of one pack; check packageText and secondaryPrice before comparing. Missing prices, quantities or completeness stay unknown; do not guess them.\n- If your client supports MCP prompts, the basket-planner prompt has the full workflow for planning a shopping list or basket.";
|
|
5
5
|
export declare const basketPlannerPrompt: PromptHandler;
|
package/dist/mcp/guide.js
CHANGED
|
@@ -52,8 +52,8 @@ export const SERVER_INSTRUCTIONS = `Continente MCP shops at Continente.pt for on
|
|
|
52
52
|
- set_cart_item_quantity sets an exact total (adding the product if needed; 0 removes it). add_cart_item adds on top of what is already there. When the user says how many they need, use set_cart_item_quantity.
|
|
53
53
|
- Change the cart one item at a time and read structuredContent.result before reporting success. confirmedQuantity is what the cart now holds.
|
|
54
54
|
- If a cart change returns mutation.outcome "unknown", the cart may have changed. Call get_cart and decide from its contents. Never repeat the change automatically.
|
|
55
|
-
- Money is EUR in integer minorUnits (129 means €1.29). Missing prices, quantities or completeness stay unknown; do not guess them.
|
|
56
|
-
-
|
|
55
|
+
- Money is EUR in integer minorUnits (129 means €1.29). Each price says what it is for: per item, per piece, per kg or per l. A price per kg or per l is not the price of one pack; check packageText and secondaryPrice before comparing. Missing prices, quantities or completeness stay unknown; do not guess them.
|
|
56
|
+
- If your client supports MCP prompts, the basket-planner prompt has the full workflow for planning a shopping list or basket.`;
|
|
57
57
|
export const basketPlannerPrompt = {
|
|
58
58
|
name: 'basket-planner',
|
|
59
59
|
title: 'Plan a Continente basket',
|
package/dist/mcp/shopping.d.ts
CHANGED
|
@@ -13,11 +13,15 @@ export interface ShoppingServices {
|
|
|
13
13
|
history: PurchaseHistoryService;
|
|
14
14
|
session: SessionService;
|
|
15
15
|
}
|
|
16
|
+
/** What to do about a sign-in failure, depending on whether credentials are configured at all. */
|
|
17
|
+
export declare function signInHelp(credentialsConfigured: boolean | null): string;
|
|
16
18
|
export declare function formatProductSearchResults(products: SearchResult['page']['items']): string;
|
|
17
19
|
export declare class ShoppingHandlers {
|
|
18
20
|
private readonly services;
|
|
19
21
|
private readonly update;
|
|
20
22
|
constructor(services: ShoppingServices, update: UpdateStatus);
|
|
23
|
+
/** An error result; sign-in failures before any cart write get guidance for fixing them. */
|
|
24
|
+
private failed;
|
|
21
25
|
private recover;
|
|
22
26
|
searchProducts(query: string, limit: number): Promise<CallToolResult>;
|
|
23
27
|
getProduct(reference: string): Promise<CallToolResult>;
|
package/dist/mcp/shopping.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { failure } from '../domain/index.js';
|
|
2
2
|
import * as contracts from './contracts.js';
|
|
3
3
|
import { withAuthenticationRecovery } from '../services/recovery.js';
|
|
4
|
-
const
|
|
4
|
+
const CONFIGURATION_GUIDE = 'https://github.com/andredelgado/continente-mcp/blob/main/docs/configuration.md';
|
|
5
|
+
const PUBLIC_TOOLS = 'Searching and product details work without signing in.';
|
|
6
|
+
/** What to do about a sign-in failure, depending on whether credentials are configured at all. */
|
|
7
|
+
export function signInHelp(credentialsConfigured) {
|
|
8
|
+
if (credentialsConfigured === false) {
|
|
9
|
+
return 'Sign-in required: no Continente email and password are configured. In the Claude Desktop extension, enter them in the '
|
|
10
|
+
+ `extension's settings; in other setups, set CONTINENTE_EMAIL and CONTINENTE_PASSWORD (see ${CONFIGURATION_GUIDE}). ${PUBLIC_TOOLS}`;
|
|
11
|
+
}
|
|
12
|
+
const cause = credentialsConfigured ? 'Continente did not accept the configured email and password, or asked for extra verification'
|
|
13
|
+
: 'Continente did not accept the sign-in, or no credentials are configured';
|
|
14
|
+
return `Sign-in failed: ${cause}. Check the credentials, or run \`continentept-mcp login --show-browser\` in a terminal `
|
|
15
|
+
+ `to sign in and complete any verification (see ${CONFIGURATION_GUIDE}). ${PUBLIC_TOOLS}`;
|
|
16
|
+
}
|
|
5
17
|
const CHECKOUT_URL = 'https://www.continente.pt/checkout/carrinho/';
|
|
6
18
|
// Text content mirrors the structured result for clients that only show text to the model,
|
|
7
19
|
// so it always includes the identifiers needed for follow-up calls.
|
|
@@ -9,6 +21,8 @@ const euros = (money) => money === null ? 'price unavailable' : `€${(money.min
|
|
|
9
21
|
const amount = (quantity) => `${Number(quantity.value.toPrecision(12))} ${quantity.unit}`;
|
|
10
22
|
const observed = (quantity) => quantity.status === 'known' ? amount(quantity.quantity)
|
|
11
23
|
: quantity.value === null ? 'quantity unknown' : `${quantity.value} (unit unknown)`;
|
|
24
|
+
const PER = { item: '', piece: ' per piece', kg: '/kg', l: '/l' };
|
|
25
|
+
const priced = (price) => price === null ? 'price unknown' : `${euros(price.amount)}${PER[price.per]}`;
|
|
12
26
|
const quoted = (price) => price.amount === null ? 'price unavailable'
|
|
13
27
|
: `${euros(price.amount)}${price.basis === 'unit' ? ' each' : price.basis === 'line' ? ' line total' : ''}`;
|
|
14
28
|
const numbered = (lines) => lines.map((line, index) => `${index + 1}. ${line}`).join('\n');
|
|
@@ -17,17 +31,16 @@ function response(data, text, status = 'success') {
|
|
|
17
31
|
}
|
|
18
32
|
function errorResult(error, mutation) {
|
|
19
33
|
const unknown = mutation?.outcome === 'unknown';
|
|
20
|
-
const publicError = { ...error,
|
|
21
|
-
...(unknown ? { retryable: false } : {}) };
|
|
34
|
+
const publicError = { ...error, ...(unknown ? { retryable: false } : {}) };
|
|
22
35
|
const note = unknown ? '\nThe cart may have changed. Call get_cart before making another change; do not repeat this request automatically.'
|
|
23
36
|
: mutation ? '\nThe cart was not changed.' : '';
|
|
24
37
|
return { isError: true, content: [{ type: 'text', text: `Error (${publicError.code}): ${publicError.message}${note}` }],
|
|
25
38
|
structuredContent: { result: { status: 'error', error: publicError, ...(mutation ? { mutation } : {}) } } };
|
|
26
39
|
}
|
|
27
40
|
export function formatProductSearchResults(products) {
|
|
28
|
-
return numbered(products.map(product => `${product.name}${product.favorite ? ' ⭐ favorite' : ''} — ${
|
|
29
|
-
`${product.
|
|
30
|
-
|
|
41
|
+
return numbered(products.map(product => `${product.name}${product.favorite ? ' ⭐ favorite' : ''} — ${priced(product.price)}` +
|
|
42
|
+
`${product.secondaryPrice ? ` (${priced(product.secondaryPrice)})` : ''}${product.packageText ? `, pack ${product.packageText}` : ''}` +
|
|
43
|
+
`${product.minimumText ? `, minimum ${product.minimumText}` : ''}\n productId: ${product.productId}`));
|
|
31
44
|
}
|
|
32
45
|
function formatSaleRules(rules) {
|
|
33
46
|
const bounds = [rules.minimum && `minimum ${amount(rules.minimum)}`, rules.maximum && `maximum ${amount(rules.maximum)}`,
|
|
@@ -36,7 +49,7 @@ function formatSaleRules(rules) {
|
|
|
36
49
|
}
|
|
37
50
|
function formatProduct(product) {
|
|
38
51
|
return [`${product.name}`, `productId: ${product.productId}`,
|
|
39
|
-
`Price: ${
|
|
52
|
+
`Price: ${priced(product.price)}${product.secondaryPrice ? ` (${priced(product.secondaryPrice)})` : ''}`,
|
|
40
53
|
`Availability: ${product.availability}`,
|
|
41
54
|
product.saleRules.status === 'available' ? `Sale rules: ${formatSaleRules(product.saleRules.rules)}`
|
|
42
55
|
: `Sale rules unavailable: ${product.saleRules.error.message}`, `URL: ${product.url}`].join('\n');
|
|
@@ -74,43 +87,51 @@ export class ShoppingHandlers {
|
|
|
74
87
|
this.services = services;
|
|
75
88
|
this.update = update;
|
|
76
89
|
}
|
|
90
|
+
/** An error result; sign-in failures before any cart write get guidance for fixing them. */
|
|
91
|
+
async failed(error, mutation) {
|
|
92
|
+
if (error.code !== 'not_authenticated' || mutation?.outcome === 'unknown')
|
|
93
|
+
return errorResult(error, mutation);
|
|
94
|
+
const status = await this.services.session.getStatus?.().catch(() => undefined);
|
|
95
|
+
const configured = status ? status.credentials.hasEmail === true && status.credentials.hasPassword === true : null;
|
|
96
|
+
return errorResult({ ...error, message: signInHelp(status?.credentials.hasEmail === null ? null : configured) }, mutation);
|
|
97
|
+
}
|
|
77
98
|
recover(operation) {
|
|
78
99
|
return withAuthenticationRecovery(operation, () => this.services.session.refreshAuthentication());
|
|
79
100
|
}
|
|
80
101
|
async searchProducts(query, limit) {
|
|
81
102
|
const result = await this.services.catalog.searchProducts(query, limit);
|
|
82
103
|
if (!result.ok)
|
|
83
|
-
return
|
|
104
|
+
return this.failed(result.error);
|
|
84
105
|
const { page, scanned, favorites } = result.value;
|
|
85
106
|
const partial = page.completeness === 'partial';
|
|
86
107
|
const text = page.items.length
|
|
87
108
|
? `${page.items.length} of ${scanned} scanned products for "${query}":\n\n${formatProductSearchResults(page.items)}`
|
|
88
109
|
: `No products found for "${query}".`;
|
|
89
110
|
return response({ products: page.items, scanned, returned: page.items.length, favoritesLoaded: favorites.length, completeness: page.completeness,
|
|
90
|
-
failures: page.failures, nextCursor: page.nextCursor }, text + (partial ?
|
|
111
|
+
failures: page.failures, nextCursor: page.nextCursor }, text + (partial ? `\n\nPartial results: ${page.failures.length} problem(s) reading results; see failures.` : ''), partial ? 'partial' : page.items.length ? 'success' : 'empty');
|
|
91
112
|
}
|
|
92
113
|
async getProduct(reference) {
|
|
93
114
|
const result = await this.services.catalog.getProduct(reference);
|
|
94
115
|
if (!result.ok)
|
|
95
|
-
return
|
|
116
|
+
return this.failed(result.error);
|
|
96
117
|
const product = result.value;
|
|
97
|
-
const partial = product.saleRules.status === 'unavailable' || product.price
|
|
118
|
+
const partial = product.saleRules.status === 'unavailable' || product.price === null || product.availability === 'unknown';
|
|
98
119
|
return response(product, formatProduct(product), partial ? 'partial' : 'success');
|
|
99
120
|
}
|
|
100
121
|
async listFavorites(refresh) {
|
|
101
122
|
const result = await this.recover(() => refresh ? this.services.favorites.refreshFavorites() : this.services.favorites.listFavorites());
|
|
102
123
|
if (!result.ok)
|
|
103
|
-
return
|
|
124
|
+
return this.failed(result.error);
|
|
104
125
|
const { favorites, source, freshness } = result.value;
|
|
105
126
|
const header = `${favorites.length} favorites (${source === 'website' ? 'fetched from the website' : `cached ${result.value.fetchedAt}`}):`;
|
|
106
|
-
const list = numbered(favorites.map(item => `${item.name} — ${
|
|
127
|
+
const list = numbered(favorites.map(item => `${item.name} — ${priced(item.price)}\n productId: ${item.productId}`));
|
|
107
128
|
return response(favoritesData(result.value), (favorites.length ? `${header}\n\n${list}` : 'No favorites saved.') +
|
|
108
129
|
(freshness === 'stale' ? '\n\nThis snapshot is more than 24 hours old. Call list_favorites with refresh: true to update it.' : ''), favorites.length ? 'success' : 'empty');
|
|
109
130
|
}
|
|
110
131
|
async getCart() {
|
|
111
132
|
const result = await this.recover(() => this.services.cart.getCart());
|
|
112
133
|
if (!result.ok)
|
|
113
|
-
return
|
|
134
|
+
return this.failed(result.error);
|
|
114
135
|
const cart = result.value;
|
|
115
136
|
if (!cart.items.length)
|
|
116
137
|
return response(cart, 'The cart is empty.', 'empty');
|
|
@@ -119,13 +140,13 @@ export class ShoppingHandlers {
|
|
|
119
140
|
return response(cart, `Cart (${cart.items.length} lines):\n\n${list}\n\nTotal: ${cart.total === null ? 'unavailable' : euros(cart.total)}` +
|
|
120
141
|
`\nCheckout happens on the website: ${CHECKOUT_URL}`);
|
|
121
142
|
}
|
|
122
|
-
mutation(result, operation) {
|
|
143
|
+
async mutation(result, operation) {
|
|
123
144
|
if (!result.ok)
|
|
124
|
-
return
|
|
145
|
+
return this.failed(result.error, { outcome: 'not_applied' });
|
|
125
146
|
const value = result.value;
|
|
126
147
|
if (value.outcome === 'confirmed')
|
|
127
148
|
return response(value, mutationText(value, operation));
|
|
128
|
-
return
|
|
149
|
+
return this.failed(value.error, value.outcome === 'unknown' ? { outcome: 'unknown', recovery: value.recovery } : { outcome: 'not_applied' });
|
|
129
150
|
}
|
|
130
151
|
async addCartItem(reference, quantity, cartItemId) {
|
|
131
152
|
return this.mutation(await this.recover(() => this.services.cart.addCartItem(reference, quantity, cartItemId)), 'add');
|
|
@@ -139,7 +160,7 @@ export class ShoppingHandlers {
|
|
|
139
160
|
async listOrders(limit, options) {
|
|
140
161
|
const result = await this.recover(() => this.services.orders.listOrders(limit, options));
|
|
141
162
|
if (!result.ok)
|
|
142
|
-
return
|
|
163
|
+
return this.failed(result.error);
|
|
143
164
|
const page = result.value;
|
|
144
165
|
const partial = page.completeness === 'partial';
|
|
145
166
|
const list = numbered(page.items.map(order => `${orderHeading(order)}\n${formatOrderLines(order)}`));
|
|
@@ -151,7 +172,7 @@ export class ShoppingHandlers {
|
|
|
151
172
|
async getOrder(orderId) {
|
|
152
173
|
const result = await this.recover(() => this.services.orders.getOrder(orderId));
|
|
153
174
|
if (!result.ok)
|
|
154
|
-
return
|
|
175
|
+
return this.failed(result.error);
|
|
155
176
|
const order = result.value;
|
|
156
177
|
const lines = order.lines.status === 'available' ? order.lines.items : [];
|
|
157
178
|
const unknownQuantityLines = lines.filter(line => line.quantity.status === 'unknown').length;
|
|
@@ -161,7 +182,7 @@ export class ShoppingHandlers {
|
|
|
161
182
|
async listFrequentlyBoughtProducts(orderLimit, limit) {
|
|
162
183
|
const result = await this.recover(() => this.services.history.listFrequentlyBoughtProducts(orderLimit));
|
|
163
184
|
if (!result.ok)
|
|
164
|
-
return
|
|
185
|
+
return this.failed(result.error);
|
|
165
186
|
const scan = result.value;
|
|
166
187
|
if (!scan.succeeded && (scan.failed || scan.unscanned)) {
|
|
167
188
|
const unavailable = errorResult(failure('order_details_unavailable', 'No order details could be read. See scan.failures.').error);
|
|
@@ -198,8 +219,8 @@ export function registerShoppingTools(services, update = NO_UPDATE_INFO) {
|
|
|
198
219
|
execute: raw => services.session.run(() => execute(raw)) };
|
|
199
220
|
}
|
|
200
221
|
return [
|
|
201
|
-
tool('search_products', 'Search products', 'Search the Continente.pt catalogue. No login needed. Results
|
|
202
|
-
'Reads at most five result pages, so completeness may be "unknown" for broad queries.', contracts.SearchInput, contracts.SearchOutput, READ, args => handlers.searchProducts(args.query, args.limit)),
|
|
222
|
+
tool('search_products', 'Search products', 'Search the Continente.pt catalogue. No login needed. Results keep the website\'s relevance order, with products from your saved favorites first, and show ' +
|
|
223
|
+
'each price with what it is for (per item, piece, kg or litre), the pack size and productId. Reads at most five result pages, so completeness may be "unknown" for broad queries.', contracts.SearchInput, contracts.SearchOutput, READ, args => handlers.searchProducts(args.query, args.limit)),
|
|
203
224
|
tool('get_product', 'Get product', 'Get one product by productId: price, availability and sale rules (sale unit, minimum, maximum and step). ' +
|
|
204
225
|
'Check this before choosing a quantity for weighted or bulk products. No login needed.', contracts.GetProductInput, contracts.ProductDetailsOutput, READ, args => handlers.getProduct(args.productId)),
|
|
205
226
|
tool('list_favorites', 'List favorites', 'List the products saved as favorites on the account. Uses a local snapshot that is refreshed automatically when missing; ' +
|
|
@@ -13,7 +13,13 @@ export declare function productReferenceFromUrl(href: string): Result<{
|
|
|
13
13
|
code: string;
|
|
14
14
|
}[] | undefined;
|
|
15
15
|
}>;
|
|
16
|
-
export
|
|
16
|
+
export interface SearchPage {
|
|
17
|
+
products: Product[];
|
|
18
|
+
/** One entry per result that was skipped or kept with missing data. */
|
|
19
|
+
problems: ToolError[];
|
|
20
|
+
}
|
|
21
|
+
/** Parse one results page. A bad card is reported in `problems` instead of failing the page. */
|
|
22
|
+
export declare function parseProductSearchPage(html: string): Result<SearchPage, ToolError>;
|
|
17
23
|
export declare function parseFavoritesPageData(html: string): Result<{
|
|
18
24
|
items: FavoriteProduct[];
|
|
19
25
|
expectedIds: string[];
|
|
@@ -23,12 +29,14 @@ export declare function parseFavoritesContinuation(html: string): Result<string
|
|
|
23
29
|
export declare function parseFavoritesPages(pages: string[]): Result<FavoriteProduct[], ToolError>;
|
|
24
30
|
export declare function uniqueProducts<T extends FavoriteProduct>(products: T[]): T[];
|
|
25
31
|
export interface RankedProduct extends Product {
|
|
26
|
-
/**
|
|
32
|
+
/** This exact product is in the saved favorites. */
|
|
27
33
|
favorite: boolean;
|
|
28
|
-
/** Highest word overlap (0–1) between this name and a favorite's name. */
|
|
29
|
-
similarity: number;
|
|
30
34
|
}
|
|
31
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The website orders results by relevance to the query; keep that order and only move exact
|
|
37
|
+
* favorites to the front. Name similarity to favorites is not used: shared words such as a
|
|
38
|
+
* brand name would pull unrelated products above real matches.
|
|
39
|
+
*/
|
|
32
40
|
export declare function rankProductsByFavorites(products: Product[], favorites: FavoriteProduct[]): RankedProduct[];
|
|
33
41
|
export declare function parseSearchPagination(html: string): Result<{
|
|
34
42
|
url: string;
|
package/dist/parsers/catalog.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { load } from 'cheerio';
|
|
2
|
-
import { FavoriteProductSchema, ProductSchema,
|
|
2
|
+
import { FavoriteProductSchema, ProductSchema, parsePriceTag, parseProductReference, parseValue } from '../domain/index.js';
|
|
3
3
|
import { failure, success } from '../domain/index.js';
|
|
4
4
|
export const STOREFRONT_ORIGIN = 'https://www.continente.pt';
|
|
5
5
|
export function storefrontUrl(value) {
|
|
@@ -19,6 +19,7 @@ export function productReferenceFromUrl(href) {
|
|
|
19
19
|
// Slugs can contain encoded punctuation that tool inputs do not accept; fall back to the numeric ID.
|
|
20
20
|
return parsed.ok ? parsed : parseProductReference(slug?.match(/-(\d+)(?:-master)?$/)?.[1]);
|
|
21
21
|
}
|
|
22
|
+
const PRICE_IN_LABEL = /\d+[,.]\d+\s*€(?:\s*\/\s*[a-z]+)?/gi;
|
|
22
23
|
const SEARCH_EMPTY = '.search-noresults-wrapper, .search-no-results, [data-search-empty]';
|
|
23
24
|
const PRODUCT_LINK = 'a[href*="/produto/"]';
|
|
24
25
|
/** Text after "=" in labels such as "Quant. Mínima = 600 gr (3 un)". */
|
|
@@ -26,35 +27,52 @@ function minimumLabel(label) {
|
|
|
26
27
|
const [caption, ...rest] = label.split('=');
|
|
27
28
|
return /m[ií]nim/i.test(caption ?? '') && rest.length ? text(rest.join('=')) || null : null;
|
|
28
29
|
}
|
|
30
|
+
/** Pack size from labels such as "emb. 940 gr" or "emb. 2 x 52 gr". */
|
|
31
|
+
function packageLabel(label) {
|
|
32
|
+
return text(/^emb\.\s*(.+)$/i.exec(text(label))?.[1] ?? '') || null;
|
|
33
|
+
}
|
|
34
|
+
/** An unrecognised price is reported, not fatal: the product stays usable with an unknown price. */
|
|
35
|
+
function priceOrNull(label) {
|
|
36
|
+
const parsed = parsePriceTag(label);
|
|
37
|
+
return parsed.ok ? { price: parsed.value, recognised: true } : { price: null, recognised: false };
|
|
38
|
+
}
|
|
39
|
+
/** Parse one results page. A bad card is reported in `problems` instead of failing the page. */
|
|
29
40
|
export function parseProductSearchPage(html) {
|
|
30
41
|
const $ = load(html);
|
|
31
42
|
if ($(SEARCH_EMPTY).length)
|
|
32
|
-
return success([]);
|
|
43
|
+
return success({ products: [], problems: [] });
|
|
33
44
|
const tiles = $($('.search-results-grid').length ? '.search-results-grid .ct-inner-tile-wrap' : '.ct-inner-tile-wrap').toArray();
|
|
34
45
|
if (!tiles.length)
|
|
35
46
|
return failure('invalid_data', 'The search results page was not recognised');
|
|
36
47
|
const products = [];
|
|
37
|
-
|
|
48
|
+
const problems = [];
|
|
49
|
+
const problem = (message) => problems.push(failure('invalid_data', message).error);
|
|
50
|
+
tiles.forEach((tile, index) => {
|
|
38
51
|
const card = $(tile);
|
|
52
|
+
const position = `Search result ${index + 1}`;
|
|
39
53
|
const links = card.find(PRODUCT_LINK).toArray().map(link => ({ href: $(link).attr('href'), label: text($(link).text()) }));
|
|
40
54
|
const href = links[0]?.href;
|
|
41
55
|
const name = links.map(link => link.label).find(Boolean);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const reference = href ? productReferenceFromUrl(href) : null;
|
|
57
|
+
if (!href || !name || !reference?.ok) {
|
|
58
|
+
problem(`${position} was not recognised and was skipped.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const main = priceOrNull(text(card.find('.pwc-tile--price-primary').first().text()));
|
|
62
|
+
const secondary = priceOrNull(text(card.find('.pwc-tile--price-secondary').first().text()));
|
|
63
|
+
if (!main.recognised)
|
|
64
|
+
problem(`${position} (product ${reference.value.productId}) has a price in an unrecognised format; its price is unknown.`);
|
|
65
|
+
const quantity = card.find('.pwc-tile--quantity').first().text();
|
|
66
|
+
const product = parseValue(ProductSchema, { ...reference.value, name, url: storefrontUrl(href), price: main.price,
|
|
67
|
+
secondaryPrice: secondary.price, packageText: packageLabel(quantity), minimumText: minimumLabel(quantity) }, 'product');
|
|
68
|
+
if (product.ok)
|
|
69
|
+
products.push(product.value);
|
|
70
|
+
else
|
|
71
|
+
problem(`${position} (product ${reference.value.productId}) was not recognised and was skipped.`);
|
|
72
|
+
});
|
|
73
|
+
if (!products.length && problems.length)
|
|
74
|
+
return failure('invalid_data', 'No search result on the page could be recognised');
|
|
75
|
+
return success({ products: uniqueProducts(products), problems });
|
|
58
76
|
}
|
|
59
77
|
export function parseFavoritesPageData(html) {
|
|
60
78
|
const $ = load(html);
|
|
@@ -82,23 +100,19 @@ export function parseFavoritesPageData(html) {
|
|
|
82
100
|
const anchor = card.find('a[href*="/produto/"]').filter((_, element) => text($(element).text()).length > 0).first();
|
|
83
101
|
const label = text(anchor.text());
|
|
84
102
|
const href = anchor.attr('href') ?? '';
|
|
85
|
-
const name = label.replace(
|
|
103
|
+
const name = label.replace(PRICE_IN_LABEL, '').trim();
|
|
86
104
|
if (!name) {
|
|
87
105
|
error = failure('invalid_data', 'A favorite product was not recognised').error;
|
|
88
106
|
return;
|
|
89
107
|
}
|
|
90
108
|
const ref = productReferenceFromUrl(href);
|
|
91
|
-
const rawPrice = text(card.find('.pwc-tile--price-primary').first().text()) || label.match(
|
|
92
|
-
const price = parseMoneyAmount(rawPrice);
|
|
109
|
+
const rawPrice = text(card.find('.pwc-tile--price-primary').first().text()) || label.match(PRICE_IN_LABEL)?.[0] || '';
|
|
93
110
|
if (!ref.ok) {
|
|
94
111
|
error = ref.error;
|
|
95
112
|
return;
|
|
96
113
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const result = parseValue(FavoriteProductSchema, { ...ref.value, name, price: price.value, url: storefrontUrl(href) }, 'favorite');
|
|
114
|
+
// Favorites must be complete, but an unrecognised price only makes that price unknown.
|
|
115
|
+
const result = parseValue(FavoriteProductSchema, { ...ref.value, name, price: priceOrNull(rawPrice).price, url: storefrontUrl(href) }, 'favorite');
|
|
102
116
|
if (result.ok)
|
|
103
117
|
products.push(result.value);
|
|
104
118
|
else
|
|
@@ -151,26 +165,15 @@ export function uniqueProducts(products) {
|
|
|
151
165
|
return products.filter(product => { if (seen.has(product.productId))
|
|
152
166
|
return false; seen.add(product.productId); return true; });
|
|
153
167
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (b.has(word))
|
|
160
|
-
shared++;
|
|
161
|
-
const union = a.size + b.size - shared;
|
|
162
|
-
return union ? shared / union : 0;
|
|
163
|
-
}
|
|
164
|
-
/** Favorites first, then names most similar to a favorite; otherwise keep the site's order. */
|
|
168
|
+
/**
|
|
169
|
+
* The website orders results by relevance to the query; keep that order and only move exact
|
|
170
|
+
* favorites to the front. Name similarity to favorites is not used: shared words such as a
|
|
171
|
+
* brand name would pull unrelated products above real matches.
|
|
172
|
+
*/
|
|
165
173
|
export function rankProductsByFavorites(products, favorites) {
|
|
166
174
|
const favoriteIds = new Set(favorites.map(product => product.productId));
|
|
167
|
-
const
|
|
168
|
-
return
|
|
169
|
-
const name = words(product.name);
|
|
170
|
-
const similarity = Math.round(Math.max(0, ...favoriteNames.map(favorite => overlap(name, favorite))) * 100) / 100;
|
|
171
|
-
return { product: { ...product, favorite: favoriteIds.has(product.productId), similarity }, position };
|
|
172
|
-
}).sort((a, b) => Number(b.product.favorite) - Number(a.product.favorite) || b.product.similarity - a.product.similarity || a.position - b.position)
|
|
173
|
-
.map(entry => entry.product);
|
|
175
|
+
const ranked = products.map(product => ({ ...product, favorite: favoriteIds.has(product.productId) }));
|
|
176
|
+
return [...ranked.filter(product => product.favorite), ...ranked.filter(product => !product.favorite)];
|
|
174
177
|
}
|
|
175
178
|
export function parseSearchPagination(html) {
|
|
176
179
|
const actions = load(html)('[data-url*="Search-UpdateGrid"]');
|
package/dist/parsers/product.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { load } from 'cheerio';
|
|
2
|
-
import { ProductDetailsSchema,
|
|
2
|
+
import { ProductDetailsSchema, parsePriceTag, parseProductReference, parseValue } from '../domain/index.js';
|
|
3
3
|
import { success, failure } from '../domain/index.js';
|
|
4
4
|
import { parseSaleRules } from './cart.js';
|
|
5
5
|
import { productReferenceFromUrl, storefrontUrl } from './catalog.js';
|
|
@@ -30,12 +30,14 @@ export function parseProductDetails(html, sourceUrl, expectedId) {
|
|
|
30
30
|
});
|
|
31
31
|
const offer = products.length === 1 ? record(products[0].offers) : null;
|
|
32
32
|
const availability = typeof offer?.availability === 'string' ? offer.availability.replace(/^http:\/\//, 'https://') : '';
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const priceTag = (selector) => {
|
|
34
|
+
const parsed = parsePriceTag($(`.ct-pdp--prices ${selector}`).first().text());
|
|
35
|
+
return parsed.ok ? parsed.value : null;
|
|
36
|
+
};
|
|
35
37
|
const product = {
|
|
36
38
|
...reference.value, name, url,
|
|
37
|
-
price:
|
|
38
|
-
|
|
39
|
+
price: priceTag('.pwc-tile--price-primary'),
|
|
40
|
+
secondaryPrice: priceTag('.pwc-tile--price-secondary'),
|
|
39
41
|
availability: availability === 'https://schema.org/InStock' ? 'available' : ['https://schema.org/OutOfStock', 'https://schema.org/Discontinued'].includes(availability) ? 'unavailable' : 'unknown',
|
|
40
42
|
saleRules: rules.ok ? { status: 'available', rules: rules.value.rules } : { status: 'unavailable', error: rules.error },
|
|
41
43
|
};
|
|
@@ -40,10 +40,16 @@ export declare class CatalogService {
|
|
|
40
40
|
amount: {
|
|
41
41
|
currency: "EUR";
|
|
42
42
|
minorUnits: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
|
|
43
|
+
};
|
|
44
|
+
per: "item" | "kg" | "l" | "piece";
|
|
45
|
+
} | null;
|
|
46
|
+
secondaryPrice: {
|
|
47
|
+
amount: {
|
|
48
|
+
currency: "EUR";
|
|
49
|
+
minorUnits: number;
|
|
50
|
+
};
|
|
51
|
+
per: "item" | "kg" | "l" | "piece";
|
|
52
|
+
} | null;
|
|
47
53
|
availability: "available" | "unavailable" | "unknown";
|
|
48
54
|
saleRules: {
|
|
49
55
|
status: "available";
|
package/dist/services/catalog.js
CHANGED
|
@@ -68,7 +68,7 @@ export class FavoritesService {
|
|
|
68
68
|
return products;
|
|
69
69
|
if (scope !== this.scope())
|
|
70
70
|
return failure('cache_unavailable', 'Favorites account/session changed during refresh');
|
|
71
|
-
const cache = { schemaVersion:
|
|
71
|
+
const cache = { schemaVersion: 2, accountScope: scope,
|
|
72
72
|
fetchedAt: this.clock.now().toISOString(), favorites: products.value, completeness: 'complete' };
|
|
73
73
|
const saved = await requestIO(() => this.store.save(cache));
|
|
74
74
|
return saved.ok ? success(this.snapshot(cache, 'website')) : saved;
|
|
@@ -106,7 +106,8 @@ export class CatalogService {
|
|
|
106
106
|
failures.push(parsed.error);
|
|
107
107
|
break;
|
|
108
108
|
}
|
|
109
|
-
products.push(...parsed.value);
|
|
109
|
+
products.push(...parsed.value.products);
|
|
110
|
+
failures.push(...parsed.value.problems);
|
|
110
111
|
}
|
|
111
112
|
const unique = uniqueProducts(products);
|
|
112
113
|
const favorites = cache?.favorites ?? [];
|
package/docs/tools.md
CHANGED
|
@@ -50,9 +50,15 @@ calls, for clients that do not pass structured content to the model.
|
|
|
50
50
|
|
|
51
51
|
### Values
|
|
52
52
|
|
|
53
|
-
- **Money** is `{ "currency": "EUR", "minorUnits": 129 }` (€1.29).
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- **Money** is `{ "currency": "EUR", "minorUnits": 129 }` (€1.29).
|
|
54
|
+
- **Catalogue prices** (search, favorites, `get_product`) are `{ amount, per }`, saying what the
|
|
55
|
+
price is for: `item` (one pack or article), `piece` (one unit, often approximate for
|
|
56
|
+
produce), `kg` or `l`. Produce and meat show their main `price` per kg, with an approximate
|
|
57
|
+
per-piece `secondaryPrice`; packaged goods show the pack price with a per-kg or per-litre
|
|
58
|
+
`secondaryPrice`. A price per kg or per litre is not the price of one pack. An unknown or
|
|
59
|
+
unrecognised price is `null`.
|
|
60
|
+
- **Cart prices** are `{ amount, basis }`, where `basis` is `unit`, `line` (a line total) or
|
|
61
|
+
`unknown`.
|
|
56
62
|
- **Quantities** are `{ value, unit }` with unit `un`, `kg`, `g`, `l` or `ml`. Quantities read
|
|
57
63
|
from the website are either `{ status: "known", quantity }` or
|
|
58
64
|
`{ status: "unknown", value, rawText, reason }`; an unknown unit is never guessed.
|
|
@@ -64,17 +70,25 @@ calls, for clients that do not pass structured content to the model.
|
|
|
64
70
|
|
|
65
71
|
### `search_products`
|
|
66
72
|
|
|
67
|
-
Searches the public catalogue.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
Searches the public catalogue. Results keep the website's order, which reflects relevance to
|
|
74
|
+
the query, except that products in your saved favorites move to the front (`favorite: true`).
|
|
75
|
+
Search uses the local favorites snapshot and never signs in to refresh it.
|
|
76
|
+
|
|
77
|
+
Each product has `productId`, `productSlug`, `name`, `url`, `price`, `secondaryPrice`,
|
|
78
|
+
`packageText` (the pack size as shown, such as `940 gr`, `2 x 52 gr` or
|
|
79
|
+
`400 gr (peso escorrido 260 gr)`), `minimumText` (a minimum purchase such as `600 gr (3 un)`)
|
|
80
|
+
and `favorite`.
|
|
81
|
+
|
|
82
|
+
A result the server cannot read does not fail the search. It is skipped, or kept with an
|
|
83
|
+
unknown price when only the price is unreadable, and described in `failures`; the result is
|
|
84
|
+
then `partial`.
|
|
71
85
|
|
|
72
86
|
`data`: `products`, `scanned`, `returned`, `favoritesLoaded`, `completeness`, `failures`,
|
|
73
87
|
`nextCursor`.
|
|
74
88
|
|
|
75
89
|
### `get_product`
|
|
76
90
|
|
|
77
|
-
Returns `productId`, `productSlug`, `name`, `url`, `price`, `
|
|
91
|
+
Returns `productId`, `productSlug`, `name`, `url`, `price`, `secondaryPrice`, `availability`
|
|
78
92
|
(`available`, `unavailable` or `unknown`) and `saleRules`:
|
|
79
93
|
|
|
80
94
|
```json
|
|
@@ -189,7 +203,7 @@ credentials, cookies or file paths.
|
|
|
189
203
|
| `quantity_below_minimum` | The resulting quantity is below the product minimum. |
|
|
190
204
|
| `sale_rules_unknown` | The product's sale rules could not be interpreted safely. |
|
|
191
205
|
| `not_found` | The product, cart line or order does not exist. |
|
|
192
|
-
| `not_authenticated` | Sign-in is required and automatic sign-in failed or is not configured. |
|
|
206
|
+
| `not_authenticated` | Sign-in is required and automatic sign-in failed or is not configured. The message says which, and what to do. |
|
|
193
207
|
| `upstream_unavailable` | The website could not be reached or returned an error. |
|
|
194
208
|
| `invalid_data` | The website returned something the server does not recognise. |
|
|
195
209
|
| `order_details_unavailable` | Order details could not be read. |
|
package/docs/troubleshooting.md
CHANGED
|
@@ -7,7 +7,7 @@ without contacting the website.
|
|
|
7
7
|
| --- | --- |
|
|
8
8
|
| `browser_unavailable`: Chromium is being downloaded | The first use downloads Chromium (about 150 MB). Wait a minute and try again. |
|
|
9
9
|
| `browser_unavailable`: Chromium could not be downloaded | Check the internet connection, then run `npx -y continentept-mcp install-browser` to see the download output. On Linux, add `--with-deps` to install system libraries. |
|
|
10
|
-
| `not_authenticated` |
|
|
10
|
+
| `not_authenticated` | The message says whether no credentials are configured or Continente did not accept them. Check them in the extension's settings or your configuration, or run `continentept-mcp login --show-browser` to sign in and complete any extra verification. See [configuration](configuration.md). |
|
|
11
11
|
| Sign-in keeps failing | Run `continentept-mcp login --show-browser` to watch the browser and complete any extra verification the website asks for. |
|
|
12
12
|
| Status shows `authentication: unknown` | Nothing has been observed since the server started or since the saved session changed. Any account tool will establish it. |
|
|
13
13
|
| Favorites are stale or missing from search ranking | Call `list_favorites` with `refresh: true`, then search again. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continentept-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.andredelgado/continente-mcp",
|
|
5
5
|
"description": "MCP server for Continente.pt: search products, check sale rules, read favorites and orders, and make verified cart changes.",
|
|
6
6
|
"keywords": [
|
|
@@ -69,12 +69,15 @@
|
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
71
71
|
"cheerio": "1.2.0",
|
|
72
|
-
"playwright": "1.
|
|
72
|
+
"playwright": "1.64.0",
|
|
73
73
|
"zod": "4.6.7"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@anthropic-ai/mcpb": "2.1.2",
|
|
77
77
|
"@types/node": "^22.18.0",
|
|
78
78
|
"typescript": "^7.0.2"
|
|
79
|
+
},
|
|
80
|
+
"overrides": {
|
|
81
|
+
"tmp": "^0.2.7"
|
|
79
82
|
}
|
|
80
83
|
}
|