continentept-mcp 0.4.0 → 0.6.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.
@@ -205,14 +205,45 @@ 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
- currency: z.ZodLiteral<"EUR">;
209
- minorUnits: z.ZodNumber;
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
- unitPriceText: z.ZodNullable<z.ZodString>;
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>;
233
+ recommendedPrice: z.ZodNullable<z.ZodObject<{
234
+ amount: z.ZodObject<{
235
+ currency: z.ZodLiteral<"EUR">;
236
+ minorUnits: z.ZodNumber;
237
+ }, z.core.$strict>;
238
+ per: z.ZodEnum<{
239
+ item: "item";
240
+ kg: "kg";
241
+ l: "l";
242
+ piece: "piece";
243
+ }>;
244
+ }, z.core.$strict>>;
213
245
  minimumText: z.ZodNullable<z.ZodString>;
214
246
  favorite: z.ZodBoolean;
215
- similarity: z.ZodNumber;
216
247
  }, z.core.$strict>>;
217
248
  scanned: z.ZodNumber;
218
249
  returned: z.ZodNumber;
@@ -256,8 +287,16 @@ export declare const FavoritesData: z.ZodObject<{
256
287
  productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
257
288
  name: z.ZodString;
258
289
  price: z.ZodNullable<z.ZodObject<{
259
- currency: z.ZodLiteral<"EUR">;
260
- minorUnits: z.ZodNumber;
290
+ amount: z.ZodObject<{
291
+ currency: z.ZodLiteral<"EUR">;
292
+ minorUnits: z.ZodNumber;
293
+ }, z.core.$strict>;
294
+ per: z.ZodEnum<{
295
+ item: "item";
296
+ kg: "kg";
297
+ l: "l";
298
+ piece: "piece";
299
+ }>;
261
300
  }, z.core.$strict>>;
262
301
  url: z.ZodURL;
263
302
  }, z.core.$strict>>;
@@ -498,14 +537,45 @@ export declare const SearchOutput: z.ZodObject<{
498
537
  productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
499
538
  name: z.ZodString;
500
539
  price: z.ZodNullable<z.ZodObject<{
501
- currency: z.ZodLiteral<"EUR">;
502
- minorUnits: z.ZodNumber;
540
+ amount: z.ZodObject<{
541
+ currency: z.ZodLiteral<"EUR">;
542
+ minorUnits: z.ZodNumber;
543
+ }, z.core.$strict>;
544
+ per: z.ZodEnum<{
545
+ item: "item";
546
+ kg: "kg";
547
+ l: "l";
548
+ piece: "piece";
549
+ }>;
503
550
  }, z.core.$strict>>;
504
551
  url: z.ZodURL;
505
- unitPriceText: z.ZodNullable<z.ZodString>;
552
+ secondaryPrice: z.ZodNullable<z.ZodObject<{
553
+ amount: z.ZodObject<{
554
+ currency: z.ZodLiteral<"EUR">;
555
+ minorUnits: z.ZodNumber;
556
+ }, z.core.$strict>;
557
+ per: z.ZodEnum<{
558
+ item: "item";
559
+ kg: "kg";
560
+ l: "l";
561
+ piece: "piece";
562
+ }>;
563
+ }, z.core.$strict>>;
564
+ packageText: z.ZodNullable<z.ZodString>;
565
+ recommendedPrice: z.ZodNullable<z.ZodObject<{
566
+ amount: z.ZodObject<{
567
+ currency: z.ZodLiteral<"EUR">;
568
+ minorUnits: z.ZodNumber;
569
+ }, z.core.$strict>;
570
+ per: z.ZodEnum<{
571
+ item: "item";
572
+ kg: "kg";
573
+ l: "l";
574
+ piece: "piece";
575
+ }>;
576
+ }, z.core.$strict>>;
506
577
  minimumText: z.ZodNullable<z.ZodString>;
507
578
  favorite: z.ZodBoolean;
508
- similarity: z.ZodNumber;
509
579
  }, z.core.$strict>>;
510
580
  scanned: z.ZodNumber;
511
581
  returned: z.ZodNumber;
@@ -598,8 +668,16 @@ export declare const FavoritesOutput: z.ZodObject<{
598
668
  productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
599
669
  name: z.ZodString;
600
670
  price: z.ZodNullable<z.ZodObject<{
601
- currency: z.ZodLiteral<"EUR">;
602
- minorUnits: z.ZodNumber;
671
+ amount: z.ZodObject<{
672
+ currency: z.ZodLiteral<"EUR">;
673
+ minorUnits: z.ZodNumber;
674
+ }, z.core.$strict>;
675
+ per: z.ZodEnum<{
676
+ item: "item";
677
+ kg: "kg";
678
+ l: "l";
679
+ piece: "piece";
680
+ }>;
603
681
  }, z.core.$strict>>;
604
682
  url: z.ZodURL;
605
683
  }, z.core.$strict>>;
@@ -677,6 +755,10 @@ export declare const CartOutput: z.ZodObject<{
677
755
  productId: z.core.$ZodBranded<z.ZodString, "ProductId", "out">;
678
756
  cartItemId: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "CartItemId", "out">>;
679
757
  name: z.ZodString;
758
+ kind: z.ZodEnum<{
759
+ bag: "bag";
760
+ product: "product";
761
+ }>;
680
762
  quantity: z.ZodDiscriminatedUnion<[z.ZodObject<{
681
763
  status: z.ZodLiteral<"known">;
682
764
  quantity: z.ZodObject<{
@@ -700,22 +782,49 @@ export declare const CartOutput: z.ZodObject<{
700
782
  unsupported_unit: "unsupported_unit";
701
783
  }>>;
702
784
  }, z.core.$strict>], "status">;
703
- price: z.ZodObject<{
704
- amount: z.ZodNullable<z.ZodObject<{
785
+ price: z.ZodNullable<z.ZodObject<{
786
+ amount: z.ZodObject<{
705
787
  currency: z.ZodLiteral<"EUR">;
706
788
  minorUnits: z.ZodNumber;
707
- }, z.core.$strict>>;
708
- basis: z.ZodEnum<{
709
- line: "line";
710
- unit: "unit";
711
- unknown: "unknown";
789
+ }, z.core.$strict>;
790
+ per: z.ZodEnum<{
791
+ item: "item";
792
+ kg: "kg";
793
+ l: "l";
794
+ piece: "piece";
712
795
  }>;
713
- }, z.core.$strict>;
714
- }, z.core.$strict>>;
715
- total: z.ZodNullable<z.ZodObject<{
716
- currency: z.ZodLiteral<"EUR">;
717
- minorUnits: z.ZodNumber;
796
+ }, z.core.$strict>>;
797
+ lineTotal: z.ZodNullable<z.ZodObject<{
798
+ currency: z.ZodLiteral<"EUR">;
799
+ minorUnits: z.ZodNumber;
800
+ }, z.core.$strict>>;
718
801
  }, z.core.$strict>>;
802
+ totals: z.ZodObject<{
803
+ products: z.ZodNullable<z.ZodObject<{
804
+ currency: z.ZodLiteral<"EUR">;
805
+ minorUnits: z.ZodNumber;
806
+ }, z.core.$strict>>;
807
+ bags: z.ZodNullable<z.ZodObject<{
808
+ currency: z.ZodLiteral<"EUR">;
809
+ minorUnits: z.ZodNumber;
810
+ }, z.core.$strict>>;
811
+ deposit: z.ZodNullable<z.ZodObject<{
812
+ currency: z.ZodLiteral<"EUR">;
813
+ minorUnits: z.ZodNumber;
814
+ }, z.core.$strict>>;
815
+ delivery: z.ZodNullable<z.ZodObject<{
816
+ currency: z.ZodLiteral<"EUR">;
817
+ minorUnits: z.ZodNumber;
818
+ }, z.core.$strict>>;
819
+ discounts: z.ZodNullable<z.ZodObject<{
820
+ currency: z.ZodLiteral<"EUR">;
821
+ minorUnits: z.ZodNumber;
822
+ }, z.core.$strict>>;
823
+ grandTotal: z.ZodNullable<z.ZodObject<{
824
+ currency: z.ZodLiteral<"EUR">;
825
+ minorUnits: z.ZodNumber;
826
+ }, z.core.$strict>>;
827
+ }, z.core.$strict>;
719
828
  }, z.core.$strict>;
720
829
  }, z.core.$strict>, z.ZodObject<{
721
830
  status: z.ZodLiteral<"error">;
@@ -1364,18 +1473,42 @@ export declare const ProductDetailsOutput: z.ZodObject<{
1364
1473
  productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
1365
1474
  name: z.ZodString;
1366
1475
  url: z.ZodURL;
1367
- price: z.ZodObject<{
1368
- amount: z.ZodNullable<z.ZodObject<{
1476
+ price: z.ZodNullable<z.ZodObject<{
1477
+ amount: z.ZodObject<{
1369
1478
  currency: z.ZodLiteral<"EUR">;
1370
1479
  minorUnits: z.ZodNumber;
1371
- }, z.core.$strict>>;
1372
- basis: z.ZodEnum<{
1373
- line: "line";
1374
- unit: "unit";
1375
- unknown: "unknown";
1480
+ }, z.core.$strict>;
1481
+ per: z.ZodEnum<{
1482
+ item: "item";
1483
+ kg: "kg";
1484
+ l: "l";
1485
+ piece: "piece";
1376
1486
  }>;
1377
- }, z.core.$strict>;
1378
- unitPriceText: z.ZodNullable<z.ZodString>;
1487
+ }, z.core.$strict>>;
1488
+ secondaryPrice: z.ZodNullable<z.ZodObject<{
1489
+ amount: z.ZodObject<{
1490
+ currency: z.ZodLiteral<"EUR">;
1491
+ minorUnits: z.ZodNumber;
1492
+ }, z.core.$strict>;
1493
+ per: z.ZodEnum<{
1494
+ item: "item";
1495
+ kg: "kg";
1496
+ l: "l";
1497
+ piece: "piece";
1498
+ }>;
1499
+ }, z.core.$strict>>;
1500
+ recommendedPrice: z.ZodNullable<z.ZodObject<{
1501
+ amount: z.ZodObject<{
1502
+ currency: z.ZodLiteral<"EUR">;
1503
+ minorUnits: z.ZodNumber;
1504
+ }, z.core.$strict>;
1505
+ per: z.ZodEnum<{
1506
+ item: "item";
1507
+ kg: "kg";
1508
+ l: "l";
1509
+ piece: "piece";
1510
+ }>;
1511
+ }, z.core.$strict>>;
1379
1512
  availability: z.ZodEnum<{
1380
1513
  available: "available";
1381
1514
  unavailable: "unavailable";
@@ -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(), similarity: z.number().min(0).max(1) })),
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']),
@@ -1,5 +1,5 @@
1
1
  import type { PromptHandler } from './server.js';
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).";
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. The amount to pay is `get_cart`'s `totals.grandTotal`, which includes the bags the website adds.\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- For planning a shopping list or basket, use the basket-planner prompt for the full workflow.";
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
@@ -9,7 +9,7 @@ Turn the user's shopping intent into a product list or a verified basket using t
9
9
  1. 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.
10
10
  2. 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.
11
11
  3. 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.
12
- 4. Show unresolved choices. Missing prices, availability, quantities or sale rules stay unknown. Money is EUR in integer \`minorUnits\` (129 means €1.29). Keep product prices, basket totals and estimated list costs distinct.
12
+ 4. Show unresolved choices. Missing prices, availability, quantities or sale rules stay unknown. Money is EUR in integer \`minorUnits\` (129 means €1.29). Keep product prices, basket totals and estimated list costs distinct. The amount to pay is \`get_cart\`'s \`totals.grandTotal\`, which includes the bags the website adds.
13
13
 
14
14
  Searches read a bounded number of result pages; respect \`completeness\` and \`failures\` and do not claim a search was exhaustive.
15
15
 
@@ -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
- - For planning a shopping list or basket, use the basket-planner prompt for the full workflow.`;
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',
@@ -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>;
@@ -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 NOT_LOGGED_IN = 'Sign-in required. Configure CONTINENTE_EMAIL and CONTINENTE_PASSWORD (see the configuration guide) or run `continentept-mcp login`.';
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,25 +21,25 @@ 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)`;
12
- const quoted = (price) => price.amount === null ? 'price unavailable'
13
- : `${euros(price.amount)}${price.basis === 'unit' ? ' each' : price.basis === 'line' ? ' line total' : ''}`;
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]}`;
14
26
  const numbered = (lines) => lines.map((line, index) => `${index + 1}. ${line}`).join('\n');
15
27
  function response(data, text, status = 'success') {
16
28
  return { structuredContent: { result: { status, data } }, content: [{ type: 'text', text }] };
17
29
  }
18
30
  function errorResult(error, mutation) {
19
31
  const unknown = mutation?.outcome === 'unknown';
20
- const publicError = { ...error, message: error.code === 'not_authenticated' && !unknown ? NOT_LOGGED_IN : error.message,
21
- ...(unknown ? { retryable: false } : {}) };
32
+ const publicError = { ...error, ...(unknown ? { retryable: false } : {}) };
22
33
  const note = unknown ? '\nThe cart may have changed. Call get_cart before making another change; do not repeat this request automatically.'
23
34
  : mutation ? '\nThe cart was not changed.' : '';
24
35
  return { isError: true, content: [{ type: 'text', text: `Error (${publicError.code}): ${publicError.message}${note}` }],
25
36
  structuredContent: { result: { status: 'error', error: publicError, ...(mutation ? { mutation } : {}) } } };
26
37
  }
27
38
  export function formatProductSearchResults(products) {
28
- return numbered(products.map(product => `${product.name}${product.favorite ? ' ⭐ favorite' : ''} — ${euros(product.price)}` +
29
- `${product.unitPriceText ? ` (${product.unitPriceText})` : ''}${product.minimumText ? `, minimum ${product.minimumText}` : ''}` +
30
- `\n productId: ${product.productId}`));
39
+ return numbered(products.map(product => `${product.name}${product.favorite ? ' ⭐ favorite' : ''} — ${priced(product.price)}` +
40
+ `${product.secondaryPrice ? ` (${priced(product.secondaryPrice)})` : ''}${product.recommendedPrice ? `, PVPR ${priced(product.recommendedPrice)}` : ''}` +
41
+ `${product.packageText ? `, pack ${product.packageText}` : ''}` +
42
+ `${product.minimumText ? `, minimum ${product.minimumText}` : ''}\n productId: ${product.productId}`));
31
43
  }
32
44
  function formatSaleRules(rules) {
33
45
  const bounds = [rules.minimum && `minimum ${amount(rules.minimum)}`, rules.maximum && `maximum ${amount(rules.maximum)}`,
@@ -36,7 +48,8 @@ function formatSaleRules(rules) {
36
48
  }
37
49
  function formatProduct(product) {
38
50
  return [`${product.name}`, `productId: ${product.productId}`,
39
- `Price: ${quoted(product.price)}${product.unitPriceText ? ` (${product.unitPriceText})` : ''}`,
51
+ `Price: ${priced(product.price)}${product.secondaryPrice ? ` (${priced(product.secondaryPrice)})` : ''}` +
52
+ `${product.recommendedPrice ? `, PVPR ${priced(product.recommendedPrice)}` : ''}`,
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,58 +87,72 @@ 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 errorResult(result.error);
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 ? '\n\nPartial results: some result pages could not be read.' : ''), partial ? 'partial' : page.items.length ? 'success' : 'empty');
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 errorResult(result.error);
116
+ return this.failed(result.error);
96
117
  const product = result.value;
97
- const partial = product.saleRules.status === 'unavailable' || product.price.amount === null || product.availability === 'unknown';
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 errorResult(result.error);
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} — ${euros(item.price)}\n productId: ${item.productId}`));
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 errorResult(result.error);
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');
117
- const list = numbered(cart.items.map(item => `${item.name} — ${observed(item.quantity)} × ${quoted(item.price)}` +
138
+ const list = numbered(cart.items.map(item => `${item.name}${item.kind === 'bag' ? ' (bag)' : ''} — ${observed(item.quantity)} × ${priced(item.price)}` +
139
+ `${item.lineTotal ? `, line ${euros(item.lineTotal)}` : ''}` +
118
140
  `\n productId: ${item.productId}${item.cartItemId ? `, cartItemId: ${item.cartItemId}` : ''}`));
119
- return response(cart, `Cart (${cart.items.length} lines):\n\n${list}\n\nTotal: ${cart.total === null ? 'unavailable' : euros(cart.total)}` +
120
- `\nCheckout happens on the website: ${CHECKOUT_URL}`);
141
+ const { totals } = cart;
142
+ const optional = [['Deposit', totals.deposit], ['Delivery', totals.delivery], ['Discounts', totals.discounts]]
143
+ .filter(([, value]) => value !== null && value.minorUnits > 0).map(([label, value]) => `${label}: ${euros(value)}`);
144
+ const summary = [`Products: ${totals.products === null ? 'unavailable' : euros(totals.products)}`,
145
+ ...(totals.bags !== null && totals.bags.minorUnits > 0 ? [`Bags: ${euros(totals.bags)}`] : []), ...optional,
146
+ `Total to pay: ${totals.grandTotal === null ? 'unavailable' : euros(totals.grandTotal)}`].join('\n');
147
+ return response(cart, `Cart (${cart.items.length} lines):\n\n${list}\n\n${summary}\nCheckout happens on the website: ${CHECKOUT_URL}`);
121
148
  }
122
- mutation(result, operation) {
149
+ async mutation(result, operation) {
123
150
  if (!result.ok)
124
- return errorResult(result.error, { outcome: 'not_applied' });
151
+ return this.failed(result.error, { outcome: 'not_applied' });
125
152
  const value = result.value;
126
153
  if (value.outcome === 'confirmed')
127
154
  return response(value, mutationText(value, operation));
128
- return errorResult(value.error, value.outcome === 'unknown' ? { outcome: 'unknown', recovery: value.recovery } : { outcome: 'not_applied' });
155
+ return this.failed(value.error, value.outcome === 'unknown' ? { outcome: 'unknown', recovery: value.recovery } : { outcome: 'not_applied' });
129
156
  }
130
157
  async addCartItem(reference, quantity, cartItemId) {
131
158
  return this.mutation(await this.recover(() => this.services.cart.addCartItem(reference, quantity, cartItemId)), 'add');
@@ -139,7 +166,7 @@ export class ShoppingHandlers {
139
166
  async listOrders(limit, options) {
140
167
  const result = await this.recover(() => this.services.orders.listOrders(limit, options));
141
168
  if (!result.ok)
142
- return errorResult(result.error);
169
+ return this.failed(result.error);
143
170
  const page = result.value;
144
171
  const partial = page.completeness === 'partial';
145
172
  const list = numbered(page.items.map(order => `${orderHeading(order)}\n${formatOrderLines(order)}`));
@@ -151,7 +178,7 @@ export class ShoppingHandlers {
151
178
  async getOrder(orderId) {
152
179
  const result = await this.recover(() => this.services.orders.getOrder(orderId));
153
180
  if (!result.ok)
154
- return errorResult(result.error);
181
+ return this.failed(result.error);
155
182
  const order = result.value;
156
183
  const lines = order.lines.status === 'available' ? order.lines.items : [];
157
184
  const unknownQuantityLines = lines.filter(line => line.quantity.status === 'unknown').length;
@@ -161,7 +188,7 @@ export class ShoppingHandlers {
161
188
  async listFrequentlyBoughtProducts(orderLimit, limit) {
162
189
  const result = await this.recover(() => this.services.history.listFrequentlyBoughtProducts(orderLimit));
163
190
  if (!result.ok)
164
- return errorResult(result.error);
191
+ return this.failed(result.error);
165
192
  const scan = result.value;
166
193
  if (!scan.succeeded && (scan.failed || scan.unscanned)) {
167
194
  const unavailable = errorResult(failure('order_details_unavailable', 'No order details could be read. See scan.failures.').error);
@@ -198,13 +225,15 @@ export function registerShoppingTools(services, update = NO_UPDATE_INFO) {
198
225
  execute: raw => services.session.run(() => execute(raw)) };
199
226
  }
200
227
  return [
201
- tool('search_products', 'Search products', 'Search the Continente.pt catalogue. No login needed. Results show price, unit price and productId; products in your saved favorites rank first. ' +
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)),
228
+ 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 ' +
229
+ '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
230
  tool('get_product', 'Get product', 'Get one product by productId: price, availability and sale rules (sale unit, minimum, maximum and step). ' +
204
231
  '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
232
  tool('list_favorites', 'List favorites', 'List the products saved as favorites on the account. Uses a local snapshot that is refreshed automatically when missing; ' +
206
233
  'snapshots older than 24 hours are marked stale. Pass refresh: true to fetch the current list from the website.', contracts.FavoritesInput, contracts.FavoritesOutput, READ, args => handlers.listFavorites(args.refresh)),
207
- tool('get_cart', 'Get cart', 'Read the shopping cart: each line with productId, cartItemId, quantity and price, plus the total. ' +
234
+ tool('get_cart', 'Get cart', 'Read the shopping cart: each line with productId, cartItemId, kind (product or bag), quantity, unit price (per item or per kg) ' +
235
+ 'and lineTotal, plus the totals ' +
236
+ '(products, bags, deposit, delivery, discounts and grandTotal, the amount to pay). ' +
208
237
  'Use it to check current quantities before changing them and to verify any uncertain change.', contracts.EmptyInput, contracts.CartOutput, READ, () => handlers.getCart()),
209
238
  tool('add_cart_item', 'Add to cart', 'Add an amount of a product to the cart, on top of any quantity already there. quantity is in the product sale unit (default 1). ' +
210
239
  `Use set_cart_item_quantity instead when the user wants a specific total. ${UNCERTAIN_WRITE}`, contracts.AddCartItemInput, contracts.MutationOutput, ADD, args => handlers.addCartItem(args.productId, args.quantity, args.cartItemId), true),