continentept-mcp 0.0.0-stage → 0.2.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/.env.example +14 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +156 -2
- package/dist/application.d.ts +12 -0
- package/dist/application.js +5 -0
- package/dist/auto-login.d.ts +3 -0
- package/dist/auto-login.js +10 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +101 -0
- package/dist/clients/authentication.d.ts +26 -0
- package/dist/clients/authentication.js +92 -0
- package/dist/clients/policy.d.ts +26 -0
- package/dist/clients/policy.js +37 -0
- package/dist/clients/storefront.d.ts +40 -0
- package/dist/clients/storefront.js +319 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +74 -0
- package/dist/domain/errors.d.ts +46 -0
- package/dist/domain/errors.js +17 -0
- package/dist/domain/identity.d.ts +22 -0
- package/dist/domain/identity.js +40 -0
- package/dist/domain/index.d.ts +9 -0
- package/dist/domain/index.js +9 -0
- package/dist/domain/models.d.ts +345 -0
- package/dist/domain/models.js +67 -0
- package/dist/domain/money.d.ts +23 -0
- package/dist/domain/money.js +30 -0
- package/dist/domain/outcomes.d.ts +147 -0
- package/dist/domain/outcomes.js +38 -0
- package/dist/domain/quantity.d.ts +94 -0
- package/dist/domain/quantity.js +41 -0
- package/dist/domain/result.d.ts +15 -0
- package/dist/domain/result.js +7 -0
- package/dist/domain/session.d.ts +40 -0
- package/dist/domain/session.js +11 -0
- package/dist/domain/validation.d.ts +6 -0
- package/dist/domain/validation.js +28 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/main-module.d.ts +2 -0
- package/dist/main-module.js +7 -0
- package/dist/mcp/contracts.d.ts +1474 -0
- package/dist/mcp/contracts.js +65 -0
- package/dist/mcp/guide.d.ts +5 -0
- package/dist/mcp/guide.js +67 -0
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.js +114 -0
- package/dist/mcp/shopping.d.ts +33 -0
- package/dist/mcp/shopping.js +220 -0
- package/dist/parsers/cart.d.ts +48 -0
- package/dist/parsers/cart.js +229 -0
- package/dist/parsers/catalog.d.ts +37 -0
- package/dist/parsers/catalog.js +191 -0
- package/dist/parsers/orders.d.ts +19 -0
- package/dist/parsers/orders.js +158 -0
- package/dist/parsers/product.d.ts +3 -0
- package/dist/parsers/product.js +44 -0
- package/dist/parsers/records.d.ts +3 -0
- package/dist/parsers/records.js +12 -0
- package/dist/ports.d.ts +45 -0
- package/dist/ports.js +1 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.js +40 -0
- package/dist/services/cart.d.ts +19 -0
- package/dist/services/cart.js +193 -0
- package/dist/services/catalog.d.ts +89 -0
- package/dist/services/catalog.js +118 -0
- package/dist/services/orders.d.ts +42 -0
- package/dist/services/orders.js +173 -0
- package/dist/services/recovery.d.ts +7 -0
- package/dist/services/recovery.js +17 -0
- package/dist/session/browser.d.ts +60 -0
- package/dist/session/browser.js +177 -0
- package/dist/session/queue.d.ts +5 -0
- package/dist/session/queue.js +35 -0
- package/dist/session/request.d.ts +37 -0
- package/dist/session/request.js +82 -0
- package/dist/stores/files.d.ts +59 -0
- package/dist/stores/files.js +106 -0
- package/docs/configuration.md +113 -0
- package/docs/tools.md +223 -0
- package/docs/troubleshooting.md +25 -0
- package/package.json +75 -4
- package/skills/basket-planner/SKILL.md +50 -0
package/.env.example
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# continentept-mcp settings.
|
|
2
|
+
# Save as ~/.continentept-mcp/credentials.env, or anywhere you point CONTINENTE_CREDENTIALS_FILE at.
|
|
3
|
+
# Keep it private (chmod 600). Values set in the MCP client environment take precedence.
|
|
4
|
+
|
|
5
|
+
# Account credentials for automatic sign-in. Not needed for search or product details.
|
|
6
|
+
CONTINENTE_EMAIL=you@example.com
|
|
7
|
+
CONTINENTE_PASSWORD=replace-with-your-password
|
|
8
|
+
|
|
9
|
+
# Close the browser after this many idle seconds; 0 keeps it open. Default: 300
|
|
10
|
+
# CONTINENTE_BROWSER_IDLE_SECONDS=300
|
|
11
|
+
|
|
12
|
+
# These two only work as environment variables in the MCP client configuration:
|
|
13
|
+
# CONTINENTE_CREDENTIALS_FILE=/private/path/credentials.env
|
|
14
|
+
# CONTINENTE_DATA_DIR=/private/path/continentept-mcp
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Server instructions: assistants receive the cart rules (exact totals, one change at a time, check the cart after an uncertain change) as soon as they connect.
|
|
14
|
+
- A `basket-planner` prompt with the full shopping workflow, with an optional request to plan for. No skill install is needed.
|
|
15
|
+
- Documentation for supplying credentials from 1Password with `op run`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The `basket-planner` skill links to the online tool reference, so it also works when uploaded on its own.
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-10-15
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Catalogue search and product details with exact prices, availability and sale rules.
|
|
26
|
+
- Cart tools that confirm every change by reading the cart back and report uncertain changes instead of retrying them.
|
|
27
|
+
- Favorites, order history and a ranking of frequently bought products.
|
|
28
|
+
- Automatic sign-in from environment variables or a private credentials file, and a `login` command.
|
|
29
|
+
- The `basket-planner` skill for planning a shopping list and updating the basket.
|
|
30
|
+
|
|
31
|
+
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.2.0...HEAD
|
|
32
|
+
[0.2.0]: https://github.com/andredelgado/continente-mcp/compare/v0.1.0...v0.2.0
|
|
33
|
+
[0.1.0]: https://github.com/andredelgado/continente-mcp/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 André Delgado
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Continente MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/andredelgado/continente-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/continentept-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
An [MCP](https://modelcontextprotocol.io) server that lets AI assistants shop at
|
|
8
|
+
[Continente.pt](https://www.continente.pt): search products, check prices and sale rules,
|
|
9
|
+
read your favorites and past orders, and fill your cart. Checkout and payment always stay
|
|
10
|
+
on the website.
|
|
11
|
+
|
|
12
|
+
> **Unofficial.** This project is not affiliated with, endorsed by or supported by
|
|
13
|
+
> Continente or MC Sonae. It automates the public website with a headless browser, may break
|
|
14
|
+
> when the site changes, and should be used in line with the website's terms. You are
|
|
15
|
+
> responsible for anything placed in your cart.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Catalogue search and product details**, with exact prices, unit prices, availability and
|
|
20
|
+
sale rules for weighted and bulk products.
|
|
21
|
+
- **Safe cart changes.** Every change is confirmed by reading the cart back. If a change
|
|
22
|
+
cannot be confirmed, the result says so and tells the assistant to check the cart instead
|
|
23
|
+
of retrying.
|
|
24
|
+
- **Your account**: favorites, order history and a ranking of the products you buy most.
|
|
25
|
+
- **Runs locally** over stdio. Credentials and cookies stay on your machine.
|
|
26
|
+
- **Built-in guidance.** The server tells the assistant the cart rules as soon as it
|
|
27
|
+
connects, and offers a `basket-planner` prompt with the full shopping workflow. Nothing
|
|
28
|
+
extra to install; the same guide also ships as a [skill](skills/basket-planner/SKILL.md).
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Node.js 22 or newer
|
|
33
|
+
- A Continente.pt account for account features (search and product details work without one)
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
**1. Install the browser** the server uses to talk to the website (one-time download):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx -y continentept-mcp install-browser
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
On Linux, append `--with-deps` to also install the required system libraries.
|
|
44
|
+
|
|
45
|
+
**2. Save your credentials** in `~/.continentept-mcp/credentials.env` (see
|
|
46
|
+
[configuration](docs/configuration.md) for other options):
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
mkdir -p ~/.continentept-mcp && chmod 700 ~/.continentept-mcp
|
|
50
|
+
printf 'CONTINENTE_EMAIL=you@example.com\nCONTINENTE_PASSWORD=your-password\n' > ~/.continentept-mcp/credentials.env
|
|
51
|
+
chmod 600 ~/.continentept-mcp/credentials.env
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Using 1Password? Skip the file and pass secret references through `op run` instead; see
|
|
55
|
+
[Using 1Password](docs/configuration.md#using-1password).
|
|
56
|
+
|
|
57
|
+
Optionally check that login works:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npx -y continentept-mcp login
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**3. Add the server to your MCP client.**
|
|
64
|
+
|
|
65
|
+
Claude Code:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
claude mcp add continente -- npx -y continentept-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Claude Desktop, Cursor and other clients (`claude_desktop_config.json`, `mcp.json`, ...):
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"continente": {
|
|
77
|
+
"command": "npx",
|
|
78
|
+
"args": ["-y", "continentept-mcp"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To keep the credentials file elsewhere, add
|
|
85
|
+
`"env": { "CONTINENTE_CREDENTIALS_FILE": "/absolute/path/to/credentials.env" }`.
|
|
86
|
+
|
|
87
|
+
Then ask your assistant something like *"Add semi-skimmed milk and a dozen eggs to my
|
|
88
|
+
Continente cart"* or *"What do I usually buy? Build me a list for this week."*
|
|
89
|
+
|
|
90
|
+
## Tools
|
|
91
|
+
|
|
92
|
+
| Tool | What it does |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `search_products` | Search the catalogue; favorites rank first |
|
|
95
|
+
| `get_product` | Price, availability and sale rules for one product |
|
|
96
|
+
| `list_favorites` | Your saved favorites (`refresh: true` fetches the latest) |
|
|
97
|
+
| `get_cart` | Cart lines, quantities and total |
|
|
98
|
+
| `add_cart_item` | Add an amount on top of what is in the cart |
|
|
99
|
+
| `set_cart_item_quantity` | Make the cart hold exactly a quantity; zero removes |
|
|
100
|
+
| `remove_cart_item` | Remove a line from the cart |
|
|
101
|
+
| `list_orders` | Recent orders with their product lines |
|
|
102
|
+
| `get_order` | One order's product lines |
|
|
103
|
+
| `list_frequently_bought_products` | Products ranked by how often you order them |
|
|
104
|
+
| `get_session_status` | Local login, cookie and cache state (no network) |
|
|
105
|
+
|
|
106
|
+
See the [tool reference](docs/tools.md) for inputs, outputs, error codes and limits.
|
|
107
|
+
|
|
108
|
+
### About uncertain cart changes
|
|
109
|
+
|
|
110
|
+
A cart change can fail after the request has reached the website, for example on a timeout.
|
|
111
|
+
In that case the result has `mutation.outcome: "unknown"` and asks the client to call
|
|
112
|
+
`get_cart` before doing anything else. The server never retries such a change on its own,
|
|
113
|
+
and clients should not either: repeating an `add_cart_item` could add the item twice.
|
|
114
|
+
|
|
115
|
+
### Guidance for the assistant
|
|
116
|
+
|
|
117
|
+
When a client connects, the server sends short instructions with these cart rules, so the
|
|
118
|
+
assistant follows them without any setup. For planning a shopping list, pick the
|
|
119
|
+
**basket-planner** prompt from your client's prompt or attachment menu, optionally with a
|
|
120
|
+
request such as "weekly basket for two".
|
|
121
|
+
|
|
122
|
+
The same guide is available as a skill in
|
|
123
|
+
[`skills/basket-planner`](skills/basket-planner/SKILL.md). Clients that support skills, such
|
|
124
|
+
as Claude Desktop (Settings → Capabilities → Skills) or Claude Code (`~/.claude/skills/`),
|
|
125
|
+
then apply it automatically whenever you ask about groceries.
|
|
126
|
+
|
|
127
|
+
## Command line
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
continentept-mcp Start the MCP server on stdio
|
|
131
|
+
continentept-mcp login Sign in and save the session (--show-browser to watch)
|
|
132
|
+
continentept-mcp install-browser Download Chromium (extra arguments go to Playwright)
|
|
133
|
+
continentept-mcp --version
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Limitations
|
|
137
|
+
|
|
138
|
+
- One account per server process.
|
|
139
|
+
- Searches read at most five result pages and order listings at most ten pages, so very
|
|
140
|
+
broad queries or long histories are reported as incomplete rather than read in full.
|
|
141
|
+
- Login handles the standard email and password flow. If the website asks for extra
|
|
142
|
+
verification, run `continentept-mcp login --show-browser` and complete it in the browser
|
|
143
|
+
window.
|
|
144
|
+
- There are no checkout, payment, delivery-slot or favorites-editing tools.
|
|
145
|
+
|
|
146
|
+
## Documentation
|
|
147
|
+
|
|
148
|
+
- [Tool reference](docs/tools.md)
|
|
149
|
+
- [Configuration](docs/configuration.md)
|
|
150
|
+
- [Troubleshooting](docs/troubleshooting.md)
|
|
151
|
+
- [Contributing](CONTRIBUTING.md)
|
|
152
|
+
- [Security policy](SECURITY.md)
|
|
153
|
+
- [Changelog](CHANGELOG.md)
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ContinenteMcpServer } from './mcp/server.js';
|
|
2
|
+
import type { PromptHandler, ToolHandler } from './mcp/server.js';
|
|
3
|
+
export interface ApplicationOptions {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly tools?: readonly ToolHandler[];
|
|
6
|
+
readonly prompts?: readonly PromptHandler[];
|
|
7
|
+
readonly instructions?: string;
|
|
8
|
+
readonly closeResources?: () => Promise<void>;
|
|
9
|
+
readonly requestTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Create an MCP server from tool handlers. Importing the library performs no configuration or I/O. */
|
|
12
|
+
export declare function createApplication(options: ApplicationOptions): ContinenteMcpServer;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ContinenteMcpServer } from './mcp/server.js';
|
|
2
|
+
/** Create an MCP server from tool handlers. Importing the library performs no configuration or I/O. */
|
|
3
|
+
export function createApplication(options) {
|
|
4
|
+
return new ContinenteMcpServer({ ...options, tools: options.tools ?? [] });
|
|
5
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadSettings } from './config.js';
|
|
2
|
+
import { FileCookieStore } from './stores/files.js';
|
|
3
|
+
import { AuthenticationClient } from './clients/authentication.js';
|
|
4
|
+
import { launchBrowser } from './runtime.js';
|
|
5
|
+
/** Sign in with the configured credentials and save the session for the server to reuse. */
|
|
6
|
+
export async function runAutoLogin(options = {}) {
|
|
7
|
+
const settings = loadSettings();
|
|
8
|
+
const logger = (message) => console.error(message);
|
|
9
|
+
return new AuthenticationClient(launchBrowser, new FileCookieStore(settings.cookieFile), settings, logger, options).loginWithCredentials();
|
|
10
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { ContinenteMcpServer } from './mcp/server.js';
|
|
3
|
+
export declare function startStdioServer(): Promise<ContinenteMcpServer>;
|
|
4
|
+
/** The SDK stdio transport does not treat input EOF as closure, so the CLI owns that lifecycle. */
|
|
5
|
+
export declare function connectStdioApplication(application: ContinenteMcpServer): Promise<void>;
|
|
6
|
+
export declare function main(args: string[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { createShoppingApplication } from './runtime.js';
|
|
8
|
+
import { runAutoLogin } from './auto-login.js';
|
|
9
|
+
import { isMainModule } from './main-module.js';
|
|
10
|
+
const USAGE = `Usage: continentept-mcp [command]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
(none) Start the MCP server on stdio
|
|
14
|
+
login Sign in with CONTINENTE_EMAIL and CONTINENTE_PASSWORD and save the session
|
|
15
|
+
(--show-browser displays the browser window)
|
|
16
|
+
install-browser Download the Chromium build used by the server (extra arguments go to Playwright)
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
-h, --help Show this help
|
|
20
|
+
-v, --version Show the version
|
|
21
|
+
`;
|
|
22
|
+
function packageVersion() {
|
|
23
|
+
const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
24
|
+
if (typeof metadata !== 'object' || metadata === null || !('version' in metadata) ||
|
|
25
|
+
typeof metadata.version !== 'string')
|
|
26
|
+
throw new Error('Missing package version');
|
|
27
|
+
return metadata.version;
|
|
28
|
+
}
|
|
29
|
+
export async function startStdioServer() {
|
|
30
|
+
const version = packageVersion();
|
|
31
|
+
const application = createShoppingApplication({ version });
|
|
32
|
+
await connectStdioApplication(application);
|
|
33
|
+
console.error(`continentept-mcp ${version} ready on stdio`);
|
|
34
|
+
return application;
|
|
35
|
+
}
|
|
36
|
+
/** The SDK stdio transport does not treat input EOF as closure, so the CLI owns that lifecycle. */
|
|
37
|
+
export async function connectStdioApplication(application) {
|
|
38
|
+
const detach = () => {
|
|
39
|
+
process.off('SIGINT', shutdown);
|
|
40
|
+
process.off('SIGTERM', shutdown);
|
|
41
|
+
process.stdin.off('end', shutdown);
|
|
42
|
+
process.stdin.off('close', shutdown);
|
|
43
|
+
};
|
|
44
|
+
const shutdown = () => {
|
|
45
|
+
detach();
|
|
46
|
+
void application.close().catch(() => { console.error('Could not close Continente MCP resources.'); process.exitCode = 1; });
|
|
47
|
+
};
|
|
48
|
+
process.once('SIGINT', shutdown);
|
|
49
|
+
process.once('SIGTERM', shutdown);
|
|
50
|
+
process.stdin.once('end', shutdown);
|
|
51
|
+
process.stdin.once('close', shutdown);
|
|
52
|
+
try {
|
|
53
|
+
await application.connect(new StdioServerTransport());
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
detach();
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function installBrowser(extra) {
|
|
61
|
+
const playwright = dirname(createRequire(import.meta.url).resolve('playwright/package.json'));
|
|
62
|
+
const result = spawnSync(process.execPath, [join(playwright, 'cli.js'), 'install', 'chromium', ...extra], { stdio: 'inherit' });
|
|
63
|
+
return result.status ?? 1;
|
|
64
|
+
}
|
|
65
|
+
export async function main(args) {
|
|
66
|
+
const [command, ...rest] = args;
|
|
67
|
+
switch (command) {
|
|
68
|
+
case undefined:
|
|
69
|
+
case 'serve':
|
|
70
|
+
await startStdioServer();
|
|
71
|
+
return 0;
|
|
72
|
+
case 'login':
|
|
73
|
+
try {
|
|
74
|
+
return (await runAutoLogin({ showBrowser: rest.includes('--show-browser') })) ? 0 : 1;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`Login failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
case 'install-browser':
|
|
81
|
+
return installBrowser(rest);
|
|
82
|
+
case '-v':
|
|
83
|
+
case '--version':
|
|
84
|
+
console.log(packageVersion());
|
|
85
|
+
return 0;
|
|
86
|
+
case '-h':
|
|
87
|
+
case '--help':
|
|
88
|
+
console.log(USAGE);
|
|
89
|
+
return 0;
|
|
90
|
+
default:
|
|
91
|
+
console.error(`Unknown command: ${command}\n\n${USAGE}`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (isMainModule(import.meta.url)) {
|
|
96
|
+
main(process.argv.slice(2)).then(code => { if (code)
|
|
97
|
+
process.exitCode = code; }, (error) => {
|
|
98
|
+
console.error(error instanceof Error ? error.message : 'Could not start Continente MCP');
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BrowserContext, Frame, Page } from 'playwright';
|
|
2
|
+
import type { Logger } from '../ports.js';
|
|
3
|
+
import type { CookieStore } from '../stores/files.js';
|
|
4
|
+
import type { BrowserLauncher } from '../session/browser.js';
|
|
5
|
+
export interface Credentials {
|
|
6
|
+
readonly email: string;
|
|
7
|
+
readonly password: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginOptions {
|
|
10
|
+
readonly showBrowser?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Signs in through the website's login form and saves the resulting session cookies. */
|
|
13
|
+
export declare class AuthenticationClient {
|
|
14
|
+
private readonly launch;
|
|
15
|
+
private readonly cookies;
|
|
16
|
+
private readonly credentials;
|
|
17
|
+
private readonly logger;
|
|
18
|
+
private readonly options;
|
|
19
|
+
constructor(launch: BrowserLauncher, cookies: CookieStore, credentials: Credentials, logger: Logger, options?: LoginOptions);
|
|
20
|
+
/** Ask the storefront whether this browser context belongs to a signed-in customer. */
|
|
21
|
+
isSignedIn(context: BrowserContext): Promise<boolean>;
|
|
22
|
+
/** The login form is served in a frame from the login origin; wait for it to load. */
|
|
23
|
+
loginFrame(page: Page): Promise<Frame>;
|
|
24
|
+
private submit;
|
|
25
|
+
loginWithCredentials(): Promise<boolean>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { SessionCookiesSchema } from '../stores/files.js';
|
|
2
|
+
import { browserUserAgent } from '../session/browser.js';
|
|
3
|
+
import { field } from '../parsers/records.js';
|
|
4
|
+
import { STOREFRONT_ORIGIN } from '../parsers/catalog.js';
|
|
5
|
+
import { isLoginUrl, responseStatus } from './policy.js';
|
|
6
|
+
import { CART_SUMMARY_URL } from './storefront.js';
|
|
7
|
+
import { requestIO, acquireResource, checkRequest } from '../session/request.js';
|
|
8
|
+
const LOGIN_ORIGIN = 'https://login.continente.pt';
|
|
9
|
+
const EMAIL_INPUT = '#userNameCC';
|
|
10
|
+
const PASSWORD_INPUT = '#password_input';
|
|
11
|
+
const CONSENT_BUTTON = /^permitir todos$/i;
|
|
12
|
+
/** Signs in through the website's login form and saves the resulting session cookies. */
|
|
13
|
+
export class AuthenticationClient {
|
|
14
|
+
launch;
|
|
15
|
+
cookies;
|
|
16
|
+
credentials;
|
|
17
|
+
logger;
|
|
18
|
+
options;
|
|
19
|
+
constructor(launch, cookies, credentials, logger, options = {}) {
|
|
20
|
+
this.launch = launch;
|
|
21
|
+
this.cookies = cookies;
|
|
22
|
+
this.credentials = credentials;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
/** Ask the storefront whether this browser context belongs to a signed-in customer. */
|
|
27
|
+
async isSignedIn(context) {
|
|
28
|
+
const response = await context.request.get(CART_SUMMARY_URL, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, timeout: 10000 });
|
|
29
|
+
if (!responseStatus(response, response.url?.() ?? CART_SUMMARY_URL).ok)
|
|
30
|
+
return false;
|
|
31
|
+
const body = await response.json().catch(() => null);
|
|
32
|
+
return field(body, 'resources', 'customerAuthenticated') === true;
|
|
33
|
+
}
|
|
34
|
+
/** The login form is served in a frame from the login origin; wait for it to load. */
|
|
35
|
+
async loginFrame(page) {
|
|
36
|
+
const fromLoginOrigin = (frame) => {
|
|
37
|
+
try {
|
|
38
|
+
return new URL(frame.url()).origin === LOGIN_ORIGIN;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return page.frames().find(fromLoginOrigin) ?? page.waitForEvent('framenavigated', { predicate: fromLoginOrigin, timeout: 15000 });
|
|
45
|
+
}
|
|
46
|
+
async submit(frame, input, value) {
|
|
47
|
+
checkRequest();
|
|
48
|
+
const field = frame.locator(input);
|
|
49
|
+
await field.fill(value, { timeout: 15000 });
|
|
50
|
+
const button = frame.locator('button[type="submit"]:visible').first();
|
|
51
|
+
if (await button.count())
|
|
52
|
+
await button.click();
|
|
53
|
+
else
|
|
54
|
+
await field.press('Enter');
|
|
55
|
+
}
|
|
56
|
+
async loginWithCredentials() {
|
|
57
|
+
const { email, password } = this.credentials;
|
|
58
|
+
if (!email || !password) {
|
|
59
|
+
this.logger('Login skipped: CONTINENTE_EMAIL and CONTINENTE_PASSWORD are required.');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const browser = await acquireResource(() => this.launch({ headless: !this.options.showBrowser }), browser => browser.close());
|
|
63
|
+
let closing;
|
|
64
|
+
const close = () => closing ??= browser.close();
|
|
65
|
+
try {
|
|
66
|
+
return await requestIO(async () => {
|
|
67
|
+
const context = await acquireResource(() => browser.newContext({ userAgent: browserUserAgent(browser.version()), locale: 'pt-PT' }), context => context.close());
|
|
68
|
+
const page = await acquireResource(() => context.newPage(), page => page.close());
|
|
69
|
+
this.logger('Signing in.');
|
|
70
|
+
await page.goto(`${STOREFRONT_ORIGIN}/login/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
71
|
+
await page.getByRole('button', { name: CONSENT_BUTTON }).click({ timeout: 5000 }).catch(() => { });
|
|
72
|
+
const frame = await this.loginFrame(page);
|
|
73
|
+
await this.submit(frame, EMAIL_INPUT, email);
|
|
74
|
+
const leftLogin = page.waitForURL(url => url.origin === STOREFRONT_ORIGIN && !isLoginUrl(url.href), { timeout: 20000 }).catch(() => { });
|
|
75
|
+
await this.submit(frame, PASSWORD_INPUT, password);
|
|
76
|
+
await leftLogin;
|
|
77
|
+
checkRequest();
|
|
78
|
+
if (!(await this.isSignedIn(context)))
|
|
79
|
+
throw new Error('The website did not accept the login');
|
|
80
|
+
const cookies = SessionCookiesSchema.safeParse(await context.cookies());
|
|
81
|
+
if (!cookies.success)
|
|
82
|
+
throw new Error('The website returned an unexpected session');
|
|
83
|
+
await this.cookies.save(cookies.data);
|
|
84
|
+
this.logger('Signed in; session saved.');
|
|
85
|
+
return true;
|
|
86
|
+
}, close);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function isLoginUrl(value: string): boolean;
|
|
2
|
+
export interface HttpResponse {
|
|
3
|
+
ok(): boolean;
|
|
4
|
+
status(): number;
|
|
5
|
+
url?(): string;
|
|
6
|
+
headers?(): Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/** Only known authentication evidence triggers recovery; arbitrary upstream failures do not. */
|
|
9
|
+
export declare function responseStatus(response: HttpResponse | null, finalUrl: string): import("../domain/result.js").Result<undefined, {
|
|
10
|
+
code: "cache_unavailable" | "cancelled" | "cart_change_rejected" | "cart_change_unconfirmed" | "deadline_exceeded" | "invalid_data" | "invalid_input" | "not_authenticated" | "not_found" | "order_details_unavailable" | "quantity_below_minimum" | "queue_full" | "sale_rules_unknown" | "server_closed" | "upstream_unavailable";
|
|
11
|
+
message: string;
|
|
12
|
+
retryable: boolean;
|
|
13
|
+
issues?: {
|
|
14
|
+
path: string;
|
|
15
|
+
code: string;
|
|
16
|
+
}[] | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function authenticatedPayload(input: unknown): import("../domain/result.js").Result<unknown, {
|
|
19
|
+
code: "cache_unavailable" | "cancelled" | "cart_change_rejected" | "cart_change_unconfirmed" | "deadline_exceeded" | "invalid_data" | "invalid_input" | "not_authenticated" | "not_found" | "order_details_unavailable" | "quantity_below_minimum" | "queue_full" | "sale_rules_unknown" | "server_closed" | "upstream_unavailable";
|
|
20
|
+
message: string;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
issues?: {
|
|
23
|
+
path: string;
|
|
24
|
+
code: string;
|
|
25
|
+
}[] | undefined;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { STOREFRONT_ORIGIN, storefrontUrl } from '../parsers/catalog.js';
|
|
2
|
+
import { field } from '../parsers/records.js';
|
|
3
|
+
import { failure, success } from '../domain/index.js';
|
|
4
|
+
export function isLoginUrl(value) {
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
return url.protocol === 'https:' && (url.origin === 'https://login.continente.pt' ||
|
|
8
|
+
(url.origin === STOREFRONT_ORIGIN && /^\/login(?:\/|$)/i.test(url.pathname)));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Only known authentication evidence triggers recovery; arbitrary upstream failures do not. */
|
|
15
|
+
export function responseStatus(response, finalUrl) {
|
|
16
|
+
const status = response?.status();
|
|
17
|
+
const location = response?.headers?.().location;
|
|
18
|
+
let redirect = '';
|
|
19
|
+
try {
|
|
20
|
+
if (location)
|
|
21
|
+
redirect = new URL(location, STOREFRONT_ORIGIN).href;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return failure('invalid_data', 'Invalid response redirect');
|
|
25
|
+
}
|
|
26
|
+
if (status === 401 || status === 403 || isLoginUrl(finalUrl) || (status && status >= 300 && status < 400 && isLoginUrl(redirect))) {
|
|
27
|
+
return failure('not_authenticated');
|
|
28
|
+
}
|
|
29
|
+
if (!storefrontUrl(finalUrl))
|
|
30
|
+
return failure('invalid_data', 'Unexpected response origin');
|
|
31
|
+
if (status === 404)
|
|
32
|
+
return failure('not_found');
|
|
33
|
+
return response?.ok() ? success(undefined) : failure('upstream_unavailable', `The website returned HTTP ${status ?? 'with no status'}`);
|
|
34
|
+
}
|
|
35
|
+
export function authenticatedPayload(input) {
|
|
36
|
+
return field(input, 'resources', 'customerAuthenticated') === false ? failure('not_authenticated') : success(input);
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CatalogGateway, FavoritesGateway, CartGateway, OrdersGateway, PreparedCartMutation, RetrievedPages } from '../ports.js';
|
|
2
|
+
import type { Result, ToolError } from '../domain/index.js';
|
|
3
|
+
import type { BrowserLease, StorefrontSession } from '../session/browser.js';
|
|
4
|
+
export declare const CART_SUMMARY_URL = "https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Cart-MiniCartShow";
|
|
5
|
+
export declare class StorefrontClient {
|
|
6
|
+
readonly session: StorefrontSession;
|
|
7
|
+
constructor(session: StorefrontSession);
|
|
8
|
+
io<T>(lease: BrowserLease, operation: () => Promise<T>): Promise<T>;
|
|
9
|
+
navigateToStorefrontPage(path: string): Promise<Result<BrowserLease, ToolError>>;
|
|
10
|
+
html(path: string, marker?: string): Promise<Result<string, ToolError>>;
|
|
11
|
+
}
|
|
12
|
+
export declare class CatalogClient implements CatalogGateway {
|
|
13
|
+
private readonly site;
|
|
14
|
+
constructor(site: StorefrontClient);
|
|
15
|
+
fetchProductDetails(reference: string): Promise<Result<{
|
|
16
|
+
html: string;
|
|
17
|
+
url: string;
|
|
18
|
+
}, ToolError>>;
|
|
19
|
+
searchProducts(query: string, limit: number): Promise<Result<RetrievedPages, ToolError>>;
|
|
20
|
+
}
|
|
21
|
+
export declare class FavoritesClient implements FavoritesGateway {
|
|
22
|
+
private readonly site;
|
|
23
|
+
constructor(site: StorefrontClient);
|
|
24
|
+
fetchFavorites(): Promise<Result<RetrievedPages, ToolError>>;
|
|
25
|
+
}
|
|
26
|
+
export declare class CartClient implements CartGateway {
|
|
27
|
+
private readonly site;
|
|
28
|
+
constructor(site: StorefrontClient);
|
|
29
|
+
fetchCart(): Promise<Result<unknown, ToolError>>;
|
|
30
|
+
fetchProductForm(reference: string): Promise<Result<unknown, ToolError>>;
|
|
31
|
+
fetchCartControls(): Promise<Result<string, ToolError>>;
|
|
32
|
+
submitCartMutation(action: string, parameters: URLSearchParams, method: 'GET' | 'POST'): Promise<Result<unknown, ToolError>>;
|
|
33
|
+
prepareCartMutation(action: string, parameters: URLSearchParams, method: 'GET' | 'POST'): Promise<Result<PreparedCartMutation, ToolError>>;
|
|
34
|
+
}
|
|
35
|
+
export declare class OrdersClient implements OrdersGateway {
|
|
36
|
+
private readonly site;
|
|
37
|
+
constructor(site: StorefrontClient);
|
|
38
|
+
fetchOrderHistory(cursor?: string): Promise<Result<string, ToolError>>;
|
|
39
|
+
fetchOrderLines(url: string): Promise<Result<string, ToolError>>;
|
|
40
|
+
}
|