continentept-mcp 0.6.1 → 0.7.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 +32 -1
- package/README.md +4 -2
- package/dist/clients/storefront.d.ts +12 -3
- package/dist/clients/storefront.js +52 -12
- package/dist/domain/models.d.ts +599 -2
- package/dist/domain/models.js +102 -4
- package/dist/domain/money.d.ts +20 -0
- package/dist/domain/money.js +12 -0
- package/dist/mcp/contracts.d.ts +1704 -324
- package/dist/mcp/contracts.js +53 -4
- package/dist/mcp/guide.d.ts +2 -2
- package/dist/mcp/guide.js +5 -2
- package/dist/mcp/shopping.d.ts +16 -3
- package/dist/mcp/shopping.js +124 -21
- package/dist/parsers/cart.d.ts +12 -1
- package/dist/parsers/cart.js +40 -8
- package/dist/parsers/catalog.d.ts +13 -3
- package/dist/parsers/catalog.js +43 -15
- package/dist/parsers/label.d.ts +6 -0
- package/dist/parsers/label.js +79 -0
- package/dist/parsers/package.d.ts +7 -0
- package/dist/parsers/package.js +91 -0
- package/dist/parsers/product.js +16 -4
- package/dist/ports.d.ts +14 -2
- package/dist/runtime.js +1 -1
- package/dist/services/cart.js +17 -2
- package/dist/services/catalog.d.ts +75 -5
- package/dist/services/catalog.js +141 -13
- package/dist/services/preview.d.ts +19 -0
- package/dist/services/preview.js +91 -0
- package/docs/tools.md +157 -16
- package/package.json +1 -1
- package/skills/basket-planner/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-10-16
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `get_product` reported some products as available that could not be bought: the page's structured data said in stock while its add button was disabled. Availability now follows the page's own add button, with the structured data as a fallback.
|
|
14
|
+
- A product page the website redirects to its home page was reported as "no longer sold". It may only be hidden from this session: the same product can open signed in and redirect signed out, or not be sold in your area. The `not_found` message now says so.
|
|
15
|
+
|
|
16
|
+
- Some products sold by the piece but priced per kg, such as onions, were shown in the cart with the wrong number of pieces: 4 onions read as 1, so setting them to 4 always ended with an unknown outcome. For these products the cart's JSON does not count pieces; the quantity is now read from the kilograms on the cart page, and stays unknown when that is not a whole number of pieces.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Breaking:** `search_products` keeps the website's relevance order. Saved favorites are still marked `favorite: true`, but only move to the top with the new `favoritesFirst: true`, and then only within the returned products.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `search_products` filters by `category`, `excludeCategory` and `brand`, and sorts by `unit_price`. Results have `brand` and `category` (the website's category path), filtered searches read further pages to fill `limit`, and `unknownToFilter` counts products left out only because the results page gave no category or brand for them.
|
|
25
|
+
- `search_products` and `get_product` return `drainedPrice`, the price per kg of drained weight, for packs whose label states a drained weight.
|
|
26
|
+
- `get_product_labels`: ingredients, allergens and nutrition for up to 20 products, as the website's copy of each label declares them. It returns the ingredients verbatim, the words they print in capitals, the allergen and "may contain traces" statements, and nutrients per 100 g or ml. Nothing is inferred.
|
|
27
|
+
- Cart and `preview_basket` lines have `storefrontQuantity` for products sold by the piece but priced by weight: the kilograms in the cart, which are also the estimated weight. `get_cart` text shows it, as in "4 un (about 1.16 kg)".
|
|
28
|
+
- `search_products`, `get_product` and `preview_basket` return `catalogView`: whether the result is the signed-in or the signed-out catalogue, which can differ in availability and in which products open.
|
|
29
|
+
- Search results have `availability`, from the results page: `available` or `unavailable` when its "Indisponível" badge and add button agree, `unknown` otherwise. Unavailable results are marked in the text.
|
|
30
|
+
- `preview_basket`: for a list of products and desired totals, what the cart holds now, what `set_cart_item_quantity` would do, what would be refused and why, availability, pack details and the estimated cost. It changes nothing.
|
|
31
|
+
- `search_products` accepts `cursor`: pass `nextCursor` back with the same query to continue right after the last product returned. Following cursors never skips or repeats a result. Before, products read beyond `limit` were dropped and `nextCursor` could not be used.
|
|
32
|
+
- `search_products`, `get_product` and `get_cart` return `observedAt`, when the website was read.
|
|
33
|
+
- Cart lines and totals have `estimated`: products sold by the piece but priced by weight are charged by the weight picked, so their line total, and then the cart totals, are estimates. `get_cart` text says "about" for them.
|
|
34
|
+
|
|
35
|
+
- `search_products` and `get_product` return `package`, the pack label read into numbers: `count`, `each`, `net` (including any free extra), `drained` and `approximate`. For example, `6 x 1 lt` is 6 items of 1 l, 6 l in total. A label with an unknown part gives `null`.
|
|
36
|
+
- `get_product` returns `packageText` and `minimumText`, like `search_products`, and shows the pack size in its text.
|
|
37
|
+
- A `not_authenticated` error before any cart write carries `signIn`: whether credentials are configured, what to do next, the settings and command involved, and which tools work without signing in.
|
|
38
|
+
|
|
9
39
|
## [0.6.1] - 2026-10-15
|
|
10
40
|
|
|
11
41
|
### Fixed
|
|
@@ -97,7 +127,8 @@ All notable changes to this project are documented here. The format follows
|
|
|
97
127
|
- Automatic sign-in from environment variables or a private credentials file, and a `login` command.
|
|
98
128
|
- The `basket-planner` skill for planning a shopping list and updating the basket.
|
|
99
129
|
|
|
100
|
-
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.
|
|
130
|
+
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.7.0...HEAD
|
|
131
|
+
[0.7.0]: https://github.com/andredelgado/continente-mcp/compare/v0.6.1...v0.7.0
|
|
101
132
|
[0.6.1]: https://github.com/andredelgado/continente-mcp/compare/v0.6.0...v0.6.1
|
|
102
133
|
[0.6.0]: https://github.com/andredelgado/continente-mcp/compare/v0.5.0...v0.6.0
|
|
103
134
|
[0.5.0]: https://github.com/andredelgado/continente-mcp/compare/v0.4.0...v0.5.0
|
package/README.md
CHANGED
|
@@ -116,10 +116,12 @@ Continente cart"* or *"What do I usually buy? Build me a list for this week."*
|
|
|
116
116
|
|
|
117
117
|
| Tool | What it does |
|
|
118
118
|
| --- | --- |
|
|
119
|
-
| `search_products` | Search the catalogue
|
|
120
|
-
| `get_product` | Price, availability and sale rules for one product |
|
|
119
|
+
| `search_products` | Search the catalogue in relevance order, with favorites marked and a cursor for more |
|
|
120
|
+
| `get_product` | Price, pack size, availability and sale rules for one product |
|
|
121
|
+
| `get_product_labels` | Ingredients, allergens and nutrition as the label declares them |
|
|
121
122
|
| `list_favorites` | Your saved favorites (`refresh: true` fetches the latest) |
|
|
122
123
|
| `get_cart` | Cart lines, quantities, bags and totals |
|
|
124
|
+
| `preview_basket` | What a list of changes would do and cost, without changing the cart |
|
|
123
125
|
| `add_cart_item` | Add an amount on top of what is in the cart |
|
|
124
126
|
| `set_cart_item_quantity` | Make the cart hold exactly a quantity; zero removes |
|
|
125
127
|
| `remove_cart_item` | Remove a line from the cart |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CatalogGateway, FavoritesGateway, CartGateway, OrdersGateway, PreparedCartMutation, RetrievedPages } from '../ports.js';
|
|
2
|
-
import type { Result, ToolError } from '../domain/index.js';
|
|
1
|
+
import type { CatalogGateway, FavoritesGateway, CartGateway, OrdersGateway, PreparedCartMutation, RetrievedPages, SearchResume } from '../ports.js';
|
|
2
|
+
import type { Product, Result, ToolError } from '../domain/index.js';
|
|
3
3
|
import type { BrowserLease, StorefrontSession } from '../session/browser.js';
|
|
4
4
|
export declare const CART_SUMMARY_URL = "https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Cart-MiniCartShow";
|
|
5
5
|
export declare class StorefrontClient {
|
|
@@ -26,8 +26,17 @@ export declare class CatalogClient implements CatalogGateway {
|
|
|
26
26
|
html: string;
|
|
27
27
|
url: string;
|
|
28
28
|
}, ToolError>>;
|
|
29
|
-
|
|
29
|
+
fetchProductLabel(url: string): Promise<Result<string, ToolError>>;
|
|
30
|
+
/**
|
|
31
|
+
* Read result pages until `limit` products are collected, at most five pages. `resume` continues
|
|
32
|
+
* at a position of the full list: from the results grid when its URL is known, otherwise from
|
|
33
|
+
* the first page, whose earlier results the caller drops.
|
|
34
|
+
*/
|
|
35
|
+
searchProducts(query: string, limit: number, resume?: SearchResume, accept?: (product: Product) => boolean): Promise<Result<RetrievedPages, ToolError>>;
|
|
30
36
|
}
|
|
37
|
+
/** The results grid URL without its position, so a search can continue anywhere in the list. */
|
|
38
|
+
export declare function searchGridBase(url: string): string;
|
|
39
|
+
export declare function searchGridUrl(base: string, start: number): string;
|
|
31
40
|
export declare class FavoritesClient implements FavoritesGateway {
|
|
32
41
|
private readonly site;
|
|
33
42
|
constructor(site: StorefrontClient);
|
|
@@ -77,33 +77,57 @@ export class CatalogClient {
|
|
|
77
77
|
this.site = site;
|
|
78
78
|
}
|
|
79
79
|
async fetchProductDetails(reference) {
|
|
80
|
-
// The website redirects a product it
|
|
81
|
-
|
|
80
|
+
// The website redirects a product page it does not show to this session to its home page. It may be
|
|
81
|
+
// sold elsewhere: the same product can open signed in and redirect signed out.
|
|
82
|
+
const page = await this.site.readPage(productPath(reference), 'h1.product-name, .ct-pdp--prices', url => url.pathname.startsWith('/produto/') ? null : failure('not_found', 'The website does not show this product to this session: it may not be sold in your area, or no longer be sold').error);
|
|
82
83
|
if (!page.ok)
|
|
83
84
|
return page;
|
|
84
85
|
return page.value.ready ? success({ html: page.value.html, url: page.value.url }) : failure('invalid_data', 'Product detail content is unavailable');
|
|
85
86
|
}
|
|
86
|
-
async
|
|
87
|
-
const
|
|
87
|
+
async fetchProductLabel(url) {
|
|
88
|
+
const href = storefrontUrl(url);
|
|
89
|
+
if (!href || !new URL(href).pathname.endsWith('/Product-ProductNutritionalInfoTab'))
|
|
90
|
+
return failure('invalid_input', 'Invalid product label URL');
|
|
91
|
+
return readContinuation(this.site, href);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read result pages until `limit` products are collected, at most five pages. `resume` continues
|
|
95
|
+
* at a position of the full list: from the results grid when its URL is known, otherwise from
|
|
96
|
+
* the first page, whose earlier results the caller drops.
|
|
97
|
+
*/
|
|
98
|
+
async searchProducts(query, limit, resume, accept = () => true) {
|
|
99
|
+
const from = resume?.start ?? 0;
|
|
100
|
+
const resumeUrl = resume?.grid ? searchGridUrl(resume.grid, from) : null;
|
|
101
|
+
const first = resumeUrl ? await readContinuation(this.site, resumeUrl)
|
|
102
|
+
: await this.site.html(`/pesquisa/?q=${encodeURIComponent(query)}`, '.ct-inner-tile-wrap, .search-noresults-wrapper, .search-no-results, [data-search-empty]');
|
|
88
103
|
if (!first.ok)
|
|
89
104
|
return first;
|
|
90
105
|
const pages = [];
|
|
106
|
+
const starts = [];
|
|
91
107
|
const failures = [];
|
|
92
108
|
const ids = new Set();
|
|
109
|
+
const seen = new Set();
|
|
93
110
|
const visited = new Set();
|
|
94
111
|
let html = first.value;
|
|
112
|
+
let start = resumeUrl ? from : 0;
|
|
113
|
+
let grid = resume?.grid ?? null;
|
|
95
114
|
let nextCursor = null;
|
|
96
115
|
for (let index = 0; index < 5; index++) {
|
|
97
|
-
const parsed = parseProductSearchPage(html);
|
|
116
|
+
const parsed = parseProductSearchPage(html, start);
|
|
98
117
|
if (!parsed.ok) {
|
|
99
118
|
if (!pages.length)
|
|
100
119
|
return parsed;
|
|
101
120
|
failures.push(parsed.error);
|
|
102
121
|
break;
|
|
103
122
|
}
|
|
104
|
-
const previousCount =
|
|
105
|
-
parsed.value.products.forEach(product =>
|
|
123
|
+
const previousCount = seen.size;
|
|
124
|
+
parsed.value.products.forEach((product, position) => {
|
|
125
|
+
seen.add(product.productId);
|
|
126
|
+
if ((parsed.value.positions[position] ?? 0) >= from && accept(product))
|
|
127
|
+
ids.add(product.productId);
|
|
128
|
+
});
|
|
106
129
|
pages.push(html);
|
|
130
|
+
starts.push(start);
|
|
107
131
|
reportProgress(pages.length, 5, 'Reading catalogue pages');
|
|
108
132
|
const continuation = parseSearchPagination(html);
|
|
109
133
|
if (!continuation.ok) {
|
|
@@ -112,25 +136,41 @@ export class CatalogClient {
|
|
|
112
136
|
break;
|
|
113
137
|
}
|
|
114
138
|
nextCursor = continuation.value?.url ?? null;
|
|
115
|
-
if (
|
|
139
|
+
if (continuation.value)
|
|
140
|
+
grid ??= searchGridBase(continuation.value.url);
|
|
141
|
+
if (!continuation.value || ids.size >= limit)
|
|
116
142
|
break;
|
|
117
|
-
if (visited.has(
|
|
143
|
+
if (visited.has(continuation.value.url) || (index > 0 && seen.size === previousCount)) {
|
|
118
144
|
failures.push(failure('invalid_data', 'Search pagination did not advance').error);
|
|
119
145
|
break;
|
|
120
146
|
}
|
|
121
147
|
if (index === 4)
|
|
122
148
|
break;
|
|
123
|
-
visited.add(
|
|
124
|
-
const response = await readContinuation(this.site,
|
|
149
|
+
visited.add(continuation.value.url);
|
|
150
|
+
const response = await readContinuation(this.site, continuation.value.url);
|
|
125
151
|
if (!response.ok) {
|
|
126
152
|
failures.push(response.error);
|
|
127
153
|
break;
|
|
128
154
|
}
|
|
129
155
|
html = response.value;
|
|
156
|
+
start = continuation.value.start;
|
|
130
157
|
}
|
|
131
|
-
return success({ pages, failures, nextCursor, completeness: failures.length ? 'partial' : nextCursor ? 'unknown' : 'complete' });
|
|
158
|
+
return success({ pages, starts, grid, failures, nextCursor, completeness: failures.length ? 'partial' : nextCursor ? 'unknown' : 'complete' });
|
|
132
159
|
}
|
|
133
160
|
}
|
|
161
|
+
/** The results grid URL without its position, so a search can continue anywhere in the list. */
|
|
162
|
+
export function searchGridBase(url) {
|
|
163
|
+
const base = new URL(url);
|
|
164
|
+
base.searchParams.delete('start');
|
|
165
|
+
base.searchParams.delete('sz');
|
|
166
|
+
return base.href;
|
|
167
|
+
}
|
|
168
|
+
export function searchGridUrl(base, start) {
|
|
169
|
+
const url = new URL(base);
|
|
170
|
+
url.searchParams.set('start', String(start));
|
|
171
|
+
url.searchParams.set('sz', '35');
|
|
172
|
+
return url.href;
|
|
173
|
+
}
|
|
134
174
|
async function readContinuation(site, url) {
|
|
135
175
|
try {
|
|
136
176
|
const lease = await site.session.ensureReady();
|