continente-mcp 3.1.0 → 4.0.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 +19 -11
- package/README.md +53 -276
- package/continente-auto-login.js +232 -0
- package/package.json +6 -8
- package/skills/groceries/SKILL.md +3 -0
- package/src/auth-session.js +37 -0
- package/src/cart-utils.js +76 -0
- package/src/index.js +291 -88
- package/check-change.js +0 -157
- package/continente-backup.js +0 -166
- package/continente-cookie-reader.py +0 -164
- package/continente-keepalive.py +0 -135
- package/src/auth.js +0 -180
- package/src/changedetector.js +0 -158
package/.env.example
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# continente-mcp environment variables
|
|
2
|
-
# Copy this to
|
|
2
|
+
# Copy this to a private env file, for example:
|
|
3
|
+
# ~/.continente/credentials.env
|
|
4
|
+
#
|
|
5
|
+
# Keep the real file outside your repo, chmod 600, and never commit it.
|
|
6
|
+
|
|
7
|
+
# Required for automatic login.
|
|
8
|
+
# CONTINENTE_EMAIL=you@example.com
|
|
9
|
+
# CONTINENTE_PASSWORD=your-password
|
|
10
|
+
|
|
11
|
+
# Optional: point the tools at a different private env file.
|
|
12
|
+
# CONTINENTE_ENV_PATH=/absolute/path/to/credentials.env
|
|
13
|
+
|
|
14
|
+
# Set to false only for local debugging, so you can watch the login browser.
|
|
15
|
+
# CONTINENTE_LOGIN_HEADLESS=true
|
|
3
16
|
|
|
4
17
|
# Path where cookies are stored (default: ~/.continente/cookies.json)
|
|
5
|
-
#
|
|
18
|
+
# Cookies are an internal session cache; you usually do not need to edit this.
|
|
19
|
+
# CONTINENTE_COOKIES_PATH=/absolute/path/to/cookies.json
|
|
6
20
|
|
|
7
21
|
# If you run the MCP server on a second machine and sync cookies via a shared
|
|
8
22
|
# folder (e.g. Obsidian vault, Syncthing, Dropbox), set this to the shared path
|
|
9
|
-
#
|
|
10
|
-
# both the default path and this one whenever it runs.
|
|
23
|
+
# for automatic login to also write cookies there.
|
|
11
24
|
# CONTINENTE_VAULT_COOKIE_PATH=/path/to/shared/folder/cookies.json
|
|
12
25
|
|
|
13
|
-
# Override the state directory (preferences
|
|
14
|
-
# CONTINENTE_STATE_DIR
|
|
15
|
-
|
|
16
|
-
# Webhook URL to notify when the website structure changes (check-change.js).
|
|
17
|
-
# Supports anything that accepts a POST with { "text": "..." } — Slack, Discord,
|
|
18
|
-
# n8n, Make, etc. If not set, changes are logged to stdout only.
|
|
19
|
-
# CONTINENTE_ALERT_WEBHOOK=https://hooks.slack.com/services/...
|
|
26
|
+
# Override the state directory (preferences and default cookies)
|
|
27
|
+
# CONTINENTE_STATE_DIR=/absolute/path/to/state
|
package/README.md
CHANGED
|
@@ -1,325 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Continente MCP
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Lets your AI assistant search products, manage your shopping cart, and browse order history using your real Continente account.
|
|
6
|
-
|
|
7
|
-
> **⚠️ Stability note:** This server works by automating a real browser session against Continente's website using CSS selectors. If Continente updates their site structure, some tools may break. See [Detecting changes](#detecting-changes).
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## What it can do
|
|
12
|
-
|
|
13
|
-
| Tool | Description |
|
|
14
|
-
|------|-------------|
|
|
15
|
-
| `search_products` | Search the catalogue — results ranked by your favourites |
|
|
16
|
-
| `get_favorites` | List your saved favourite products |
|
|
17
|
-
| `refresh_favorites` | Re-sync favourites from the website |
|
|
18
|
-
| `get_cart` | View your current basket |
|
|
19
|
-
| `add_to_cart` | Add a product by ID |
|
|
20
|
-
| `get_order_history` | Recent orders with product lines |
|
|
21
|
-
| `get_most_bought` | Products you order most often (scans all orders — slow) |
|
|
22
|
-
| `close_session` | Close the browser and free memory |
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Requirements
|
|
27
|
-
|
|
28
|
-
- [Node.js](https://nodejs.org/) 18 or later
|
|
29
|
-
- [Python](https://www.python.org/) 3.10+ (for the cookie export script)
|
|
30
|
-
- A Continente.pt account, logged in on one of the [supported browsers](#supported-browsers)
|
|
31
|
-
|
|
32
|
-
---
|
|
3
|
+
A local MCP server for Continente.pt: search products, rank favourites, manage your basket, and read order history. Checkout and payment stay on Continente.pt.
|
|
33
4
|
|
|
34
5
|
## Setup
|
|
35
6
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
The setup requires the repository — the Python cookie scripts live here and are needed before the server can authenticate.
|
|
7
|
+
Requires **Node.js 20.18.1 or later** and a Continente account for favourites, basket and order history. Public product search works without login.
|
|
39
8
|
|
|
40
9
|
```bash
|
|
41
10
|
git clone https://github.com/davidmarqu3s/continente-mcp
|
|
42
11
|
cd continente-mcp
|
|
43
12
|
npm install
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Install the Chromium browser that Playwright uses internally:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
13
|
npm run setup
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
> **Linux note:** `--with-deps` installs OS-level dependencies via apt. On macOS and Windows this flag is a no-op — `npx playwright install chromium` is sufficient.
|
|
54
|
-
|
|
55
|
-
Once set up, you can run the server either via the local clone or via `npx continente-mcp` — both work in your MCP client config (see [step 3](#3-configure-your-mcp-client)).
|
|
56
|
-
|
|
57
|
-
### 2. Export your cookies
|
|
58
|
-
|
|
59
|
-
The server authenticates using cookies from your existing browser session — no password handling, no OAuth flow. You need to be **logged into Continente.pt** in your browser first.
|
|
60
|
-
|
|
61
|
-
Install the cookie reader dependencies:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
python3 -m pip install browser-cookie3 requests # macOS / Linux
|
|
65
|
-
python -m pip install browser-cookie3 requests # Windows
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Export your cookies:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
python3 continente-cookie-reader.py # macOS / Linux
|
|
72
|
-
python continente-cookie-reader.py # Windows
|
|
73
|
-
# Auto-detecting browser with Continente cookies...
|
|
74
|
-
# Found 20 cookies in chrome.
|
|
75
|
-
# Saved 20 cookies → ~/.continente/cookies.json (browser: chrome)
|
|
76
|
-
# On Windows: %USERPROFILE%\.continente\cookies.json
|
|
14
|
+
mkdir -p ~/.continente
|
|
15
|
+
cp .env.example ~/.continente/credentials.env
|
|
16
|
+
chmod 600 ~/.continente/credentials.env
|
|
77
17
|
```
|
|
78
18
|
|
|
79
|
-
|
|
19
|
+
Edit the private credentials file and set `CONTINENTE_EMAIL` and `CONTINENTE_PASSWORD`, then verify login:
|
|
80
20
|
|
|
81
21
|
```bash
|
|
82
|
-
|
|
83
|
-
python3 continente-cookie-reader.py --list-browsers # see all supported browsers
|
|
22
|
+
node continente-auto-login.js
|
|
84
23
|
```
|
|
85
24
|
|
|
86
|
-
|
|
25
|
+
Login uses Playwright Chromium and saves a private session cache in `~/.continente/cookies.json`. Protected tool calls automatically log in again when the session expires. Python and scheduled keepalive jobs are not required to use the MCP server.
|
|
87
26
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
This server works with any [MCP-compatible client](https://modelcontextprotocol.io/clients). The config format is the same across most of them — add an entry under `mcpServers` pointing to the server command.
|
|
91
|
-
|
|
92
|
-
**Via npx (no local install needed):**
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"mcpServers": {
|
|
97
|
-
"continente": {
|
|
98
|
-
"command": "npx",
|
|
99
|
-
"args": ["continente-mcp"]
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**Via local clone:**
|
|
27
|
+
Add the server to your MCP client using its local-server configuration. Clients using JSON `mcpServers` configuration can use:
|
|
106
28
|
|
|
107
29
|
```json
|
|
108
30
|
{
|
|
109
31
|
"mcpServers": {
|
|
110
32
|
"continente": {
|
|
111
33
|
"command": "node",
|
|
112
|
-
"args": ["/path/to/continente-mcp/src/index.js"]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Where to put this config:
|
|
119
|
-
|
|
120
|
-
| Client | Config file |
|
|
121
|
-
|--------|-------------|
|
|
122
|
-
| Claude Desktop (Mac) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
123
|
-
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
124
|
-
| Cursor (Mac/Linux) | `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` globally |
|
|
125
|
-
| Cursor (Windows) | `.cursor\mcp.json` in your project, or `%USERPROFILE%\.cursor\mcp.json` globally |
|
|
126
|
-
| Windsurf (Mac/Linux) | `~/.codeium/windsurf/mcp_config.json` |
|
|
127
|
-
| Windsurf (Windows) | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` |
|
|
128
|
-
| Other clients | See your client's MCP documentation |
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## Keeping the session alive
|
|
133
|
-
|
|
134
|
-
Continente sessions expire after a period of inactivity. Run the keepalive script on a schedule to prevent this:
|
|
135
|
-
|
|
136
|
-
**macOS (launchd), every 20 minutes:**
|
|
137
|
-
|
|
138
|
-
Create `~/Library/LaunchAgents/com.continente.keepalive.plist`:
|
|
139
|
-
|
|
140
|
-
```xml
|
|
141
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
142
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
143
|
-
<plist version="1.0">
|
|
144
|
-
<dict>
|
|
145
|
-
<key>Label</key>
|
|
146
|
-
<string>com.continente.keepalive</string>
|
|
147
|
-
<key>ProgramArguments</key>
|
|
148
|
-
<array>
|
|
149
|
-
<string>/usr/bin/python3</string>
|
|
150
|
-
<string>/path/to/continente-keepalive.py</string>
|
|
151
|
-
</array>
|
|
152
|
-
<key>StartInterval</key>
|
|
153
|
-
<integer>1200</integer>
|
|
154
|
-
<key>StandardOutPath</key>
|
|
155
|
-
<string>/tmp/continente-keepalive.log</string>
|
|
156
|
-
<key>StandardErrorPath</key>
|
|
157
|
-
<string>/tmp/continente-keepalive.log</string>
|
|
158
|
-
</dict>
|
|
159
|
-
</plist>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
launchctl load ~/Library/LaunchAgents/com.continente.keepalive.plist
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
**Linux / WSL (cron):**
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
crontab -e
|
|
170
|
-
# Add:
|
|
171
|
-
*/20 * * * * python3 /path/to/continente-keepalive.py >> ~/.continente/keepalive.log 2>&1
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
**Windows (Task Scheduler):**
|
|
175
|
-
|
|
176
|
-
```powershell
|
|
177
|
-
schtasks /create /tn "ContinenteKeepalive" /tr "python C:\path\to\continente-keepalive.py" /sc minute /mo 20 /f
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Or open Task Scheduler and create a task that runs `python C:\path\to\continente-keepalive.py` every 20 minutes.
|
|
181
|
-
|
|
182
|
-
If the session expires, the keepalive script will automatically re-read cookies from your browser.
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## Security
|
|
187
|
-
|
|
188
|
-
- **Cookies give full account access** — they can view your address, manage your cart, and see your order history. Treat them like a password.
|
|
189
|
-
- **macOS / Linux:** Cookies are stored at `~/.continente/cookies.json` with owner-only read permissions (`chmod 600`).
|
|
190
|
-
- **Windows:** Cookies are stored at `%USERPROFILE%\.continente\cookies.json`. To restrict access: `icacls "%USERPROFILE%\.continente\cookies.json" /inheritance:r /grant:r "%USERNAME%:R"`
|
|
191
|
-
- This path is excluded from git via `.gitignore`. Never commit it.
|
|
192
|
-
- Cookies expire roughly every 30 days. Re-run `continente-cookie-reader.py` to refresh them.
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Detecting changes
|
|
197
|
-
|
|
198
|
-
Run `check-change.js` on a schedule to get notified if Continente updates their site structure in a way that might break scraping:
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
# First run saves a structural fingerprint
|
|
202
|
-
node check-change.js
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**Linux / WSL (cron):**
|
|
206
|
-
```bash
|
|
207
|
-
# Daily at 9am
|
|
208
|
-
0 9 * * * node /path/to/check-change.js >> ~/.continente/change-detector.log 2>&1
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
**macOS (launchd):** Add a plist similar to the keepalive one above, with `StartInterval` set to `86400`.
|
|
212
|
-
|
|
213
|
-
**Windows (Task Scheduler):**
|
|
214
|
-
```powershell
|
|
215
|
-
schtasks /create /tn "ContinenteChangeDetector" /tr "node C:\path\to\check-change.js" /sc daily /st 09:00 /f
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
To receive a webhook notification (Slack, Discord, n8n, etc.) on change, set `CONTINENTE_ALERT_WEBHOOK` in your environment or MCP client config:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
# macOS / Linux
|
|
222
|
-
export CONTINENTE_ALERT_WEBHOOK=https://hooks.slack.com/services/...
|
|
223
|
-
|
|
224
|
-
# Windows cmd
|
|
225
|
-
set CONTINENTE_ALERT_WEBHOOK=https://hooks.slack.com/services/...
|
|
226
|
-
|
|
227
|
-
# Windows PowerShell
|
|
228
|
-
$env:CONTINENTE_ALERT_WEBHOOK = "https://hooks.slack.com/services/..."
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Or add it to your MCP client config under `env` (platform-neutral):
|
|
232
|
-
|
|
233
|
-
```json
|
|
234
|
-
{
|
|
235
|
-
"mcpServers": {
|
|
236
|
-
"continente": {
|
|
237
|
-
"command": "npx",
|
|
238
|
-
"args": ["continente-mcp"],
|
|
239
|
-
"env": {
|
|
240
|
-
"CONTINENTE_ALERT_WEBHOOK": "https://hooks.slack.com/services/..."
|
|
241
|
-
}
|
|
34
|
+
"args": ["/absolute/path/to/continente-mcp/src/index.js"]
|
|
242
35
|
}
|
|
243
36
|
}
|
|
244
37
|
}
|
|
245
38
|
```
|
|
246
39
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
## Utilities
|
|
250
|
-
|
|
251
|
-
**Back up order history** (incremental — only fetches new orders):
|
|
252
|
-
|
|
253
|
-
```bash
|
|
254
|
-
node continente-backup.js
|
|
255
|
-
# → ~/.continente/orders-backup.json
|
|
256
|
-
# → %USERPROFILE%\.continente\orders-backup.json (Windows)
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
## Supported browsers
|
|
262
|
-
|
|
263
|
-
`continente-cookie-reader.py` can read cookies from:
|
|
264
|
-
|
|
265
|
-
| Browser | macOS | Windows | Linux |
|
|
266
|
-
|---------|-------|---------|-------|
|
|
267
|
-
| Chrome | ✓ | ✓ | ✓ |
|
|
268
|
-
| Edge | ✓ | ✓ | ✓ |
|
|
269
|
-
| Brave | ✓ | ✓ | ✓ |
|
|
270
|
-
| Firefox | ✓ | ✓ | ✓ |
|
|
271
|
-
| Arc | ✓ | — | — |
|
|
272
|
-
| Chromium | ✓ | ✓ | ✓ |
|
|
273
|
-
| Vivaldi | ✓ | ✓ | ✓ |
|
|
274
|
-
| Opera | ✓ | ✓ | ✓ |
|
|
275
|
-
| Safari | ✓ | — | — |
|
|
40
|
+
Restart the client after configuration or server changes. The published package can also run with `npx continente-mcp`; the local clone configuration above runs the code you just installed and tested. This is a local stdio server, not a hosted MCP endpoint.
|
|
276
41
|
|
|
277
|
-
|
|
42
|
+
For Windows, use your user profile directory instead of `~` and restrict the credentials/state directory through Windows filesystem permissions. On Linux, `npm run setup` may need administrator permission to install Chromium system dependencies; if already installed, `npx playwright install chromium` installs only the browser.
|
|
278
43
|
|
|
279
|
-
|
|
44
|
+
## Tools
|
|
280
45
|
|
|
281
|
-
|
|
46
|
+
| Tool | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `search_products` | Search the catalogue, with saved favourites ranked first |
|
|
49
|
+
| `get_favorites` | Read cached favourites |
|
|
50
|
+
| `refresh_favorites` | Refresh favourites from your account |
|
|
51
|
+
| `get_cart` | Read the account basket |
|
|
52
|
+
| `add_to_cart` | Add a product by ID |
|
|
53
|
+
| `update_cart_item` | Set a basket quantity; zero removes the item |
|
|
54
|
+
| `get_order_history` | Read recent orders |
|
|
55
|
+
| `get_most_bought` | Aggregate products from order history |
|
|
56
|
+
| `close_session` | Close the browser session |
|
|
282
57
|
|
|
283
|
-
|
|
58
|
+
Run `refresh_favorites` once to personalise searches. Read the basket before changing it and verify quantities afterwards. An unrecognised basket page is an error, never proof that the basket is empty. Order aggregation can be slow because it visits order detail pages.
|
|
284
59
|
|
|
285
|
-
|
|
286
|
-
|----------|---------|-------------|
|
|
287
|
-
| `CONTINENTE_COOKIES_PATH` | `~/.continente/cookies.json` | Where cookies are read/written |
|
|
288
|
-
| `CONTINENTE_STATE_DIR` | `~/.continente` | Directory for preferences, fingerprint, backups |
|
|
289
|
-
| `CONTINENTE_ALERT_WEBHOOK` | — | Webhook URL for change detection alerts |
|
|
290
|
-
| `CONTINENTE_VAULT_COOKIE_PATH` | — | Optional secondary path to also write cookies to (for multi-machine sync) |
|
|
60
|
+
The bundled [groceries skill](skills/groceries/SKILL.md) guides agents through matching names to favourites and previous purchases. Add that skill directory through your client's supported skill installation mechanism.
|
|
291
61
|
|
|
292
|
-
|
|
62
|
+
## Configuration and security
|
|
293
63
|
|
|
294
|
-
|
|
64
|
+
Keep real credentials and cookie values out of the repository, client configuration, logs and screenshots. Both credentials and session cookies grant access to private account data. On macOS/Linux, use owner-only permissions (`chmod 600`) for secret files.
|
|
295
65
|
|
|
296
|
-
|
|
66
|
+
See [`.env.example`](.env.example). Process environment settings override the private credentials file.
|
|
297
67
|
|
|
298
|
-
|
|
68
|
+
| Variable | Purpose |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `CONTINENTE_ENV_PATH` | Private credentials file; defaults to `~/.continente/credentials.env` |
|
|
71
|
+
| `CONTINENTE_EMAIL`, `CONTINENTE_PASSWORD` | Account credentials for automatic login |
|
|
72
|
+
| `CONTINENTE_STATE_DIR` | Local state directory; defaults to `~/.continente` |
|
|
73
|
+
| `CONTINENTE_COOKIES_PATH` | Custom cookie cache path |
|
|
74
|
+
| `CONTINENTE_LOGIN_HEADLESS` | Set `false` to inspect login in a visible browser |
|
|
75
|
+
| `CONTINENTE_VAULT_COOKIE_PATH` | Optional secondary cookie file for private multi-machine sync |
|
|
299
76
|
|
|
300
|
-
|
|
77
|
+
Prefer absolute paths for custom settings.
|
|
301
78
|
|
|
302
|
-
|
|
79
|
+
## Troubleshooting and development
|
|
303
80
|
|
|
304
|
-
|
|
81
|
+
- **Login required:** check the private credentials file and run `node continente-auto-login.js`.
|
|
82
|
+
- **Browser missing:** run `npm run setup`.
|
|
83
|
+
- **No favourites:** run `refresh_favorites`; catalogue search still works without them.
|
|
84
|
+
- **Unexpected cart or product page:** report the tool and error, excluding account details and secrets.
|
|
305
85
|
|
|
306
86
|
```bash
|
|
307
|
-
|
|
308
|
-
|
|
87
|
+
npm test
|
|
88
|
+
npm audit --omit=dev
|
|
89
|
+
npm pack --dry-run
|
|
309
90
|
```
|
|
310
91
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
```
|
|
314
|
-
/groceries queijo flamengo, leite mimosa, ovos
|
|
315
|
-
```
|
|
92
|
+
Unit tests do not establish that Continente's live website still matches every selector. Verify the affected tool against the live site after parser changes; do not place orders during testing.
|
|
316
93
|
|
|
317
|
-
|
|
94
|
+
Personal backup, keepalive, browser-cookie export and monitoring scripts are maintained separately in the private operations repository. They are not part of the npm package or the normal login workflow. Existing operators must migrate scheduled script paths before switching to this checkout.
|
|
318
95
|
|
|
319
|
-
|
|
96
|
+
## License
|
|
320
97
|
|
|
321
|
-
|
|
98
|
+
[MIT](LICENSE)
|
|
322
99
|
|
|
323
|
-
##
|
|
100
|
+
## Releases
|
|
324
101
|
|
|
325
|
-
[
|
|
102
|
+
See the [changelog](CHANGELOG.md) and [release instructions](docs/releases.md). Version tags run the platform checks and publish matching npm/GitHub releases. Release candidates use npm's `next` channel; `latest` stays on the last stable release.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { chromium } from 'playwright';
|
|
7
|
+
|
|
8
|
+
const CONTINENTE_LOGIN_URL = 'https://www.continente.pt/login/';
|
|
9
|
+
const CONTINENTE_CHECK_URL = 'https://www.continente.pt/conta/encomendas/';
|
|
10
|
+
const DEFAULT_ENV = process.env;
|
|
11
|
+
|
|
12
|
+
function log(message) {
|
|
13
|
+
console.error(`[continente-auto-login] ${message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeCookies(path, cookies) {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
writeFileSync(path, JSON.stringify(cookies, null, 2));
|
|
19
|
+
chmodSync(path, 0o600);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unquoteEnvValue(value) {
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
if (
|
|
25
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
26
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
27
|
+
) {
|
|
28
|
+
return trimmed.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
return trimmed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseEnvFile(contents) {
|
|
34
|
+
const parsed = {};
|
|
35
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
36
|
+
let raw = line.trim();
|
|
37
|
+
if (!raw || raw.startsWith('#')) continue;
|
|
38
|
+
if (raw.startsWith('export ')) raw = raw.slice('export '.length).trim();
|
|
39
|
+
const index = raw.indexOf('=');
|
|
40
|
+
if (index === -1) continue;
|
|
41
|
+
const key = raw.slice(0, index).trim();
|
|
42
|
+
const value = raw.slice(index + 1);
|
|
43
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(key)) {
|
|
44
|
+
parsed[key] = unquoteEnvValue(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function credentialEnvPath(env = DEFAULT_ENV) {
|
|
51
|
+
const home = env.HOME || env.USERPROFILE || (env === DEFAULT_ENV ? homedir() : null);
|
|
52
|
+
const stateDir = env.CONTINENTE_STATE_DIR || (home ? join(home, '.continente') : null);
|
|
53
|
+
return env.CONTINENTE_ENV_PATH || (stateDir ? join(stateDir, 'credentials.env') : null);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function loadCredentialEnv(env = DEFAULT_ENV) {
|
|
57
|
+
const file = credentialEnvPath(env);
|
|
58
|
+
if (!file || !existsSync(file)) {
|
|
59
|
+
return { ...env };
|
|
60
|
+
}
|
|
61
|
+
return { ...parseEnvFile(readFileSync(file, 'utf8')), ...env, CONTINENTE_ENV_PATH: file };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveStatePaths(env = DEFAULT_ENV) {
|
|
65
|
+
const settings = loadCredentialEnv(env);
|
|
66
|
+
const stateDir = settings.CONTINENTE_STATE_DIR || join(settings.HOME || settings.USERPROFILE || homedir(), '.continente');
|
|
67
|
+
return { stateDir, cookieFile: settings.CONTINENTE_COOKIES_PATH || join(stateDir, 'cookies.json') };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function syncVault(cookies, vaultCookiePath, logger) {
|
|
71
|
+
if (!vaultCookiePath) return;
|
|
72
|
+
const next = JSON.stringify(cookies, null, 2);
|
|
73
|
+
if (existsSync(vaultCookiePath) && readFileSync(vaultCookiePath, 'utf8') === next) {
|
|
74
|
+
logger('Vault unchanged.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
writeCookies(vaultCookiePath, cookies);
|
|
78
|
+
logger('Vault cookies updated.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getCredentialStatus(env = DEFAULT_ENV) {
|
|
82
|
+
const mergedEnv = loadCredentialEnv(env);
|
|
83
|
+
return {
|
|
84
|
+
hasEmail: Boolean(mergedEnv.CONTINENTE_EMAIL),
|
|
85
|
+
hasPassword: Boolean(mergedEnv.CONTINENTE_PASSWORD),
|
|
86
|
+
ready: Boolean(mergedEnv.CONTINENTE_EMAIL && mergedEnv.CONTINENTE_PASSWORD),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function isLoggedIn(page) {
|
|
91
|
+
const response = await page.goto(CONTINENTE_CHECK_URL, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
92
|
+
if (!response?.ok() || page.url().includes('/login')) return false;
|
|
93
|
+
const session = await page.context().request.get(
|
|
94
|
+
'https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Cart-MiniCartShow',
|
|
95
|
+
{ headers: { 'X-Requested-With': 'XMLHttpRequest' }, timeout: 10000 }
|
|
96
|
+
);
|
|
97
|
+
if (!session.ok()) return false;
|
|
98
|
+
const payload = await session.json().catch(() => null);
|
|
99
|
+
return payload?.resources?.customerAuthenticated === true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fillFirst(page, selectors, value, label) {
|
|
103
|
+
for (const selector of selectors) {
|
|
104
|
+
const input = await page.waitForSelector(selector, { timeout: 3000 }).catch(() => null);
|
|
105
|
+
if (input) {
|
|
106
|
+
await input.fill(value);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Could not find ${label} input`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function clickFirst(page, selectors) {
|
|
114
|
+
for (const selector of selectors) {
|
|
115
|
+
const button = await page.$(selector);
|
|
116
|
+
if (button) {
|
|
117
|
+
await button.click();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await page.keyboard.press('Enter');
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function acceptCookies(page) {
|
|
126
|
+
await page.getByRole('button', { name: /PERMITIR TODOS|Permitir todos/i })
|
|
127
|
+
.click({ timeout: 5000 })
|
|
128
|
+
.catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function findLoginFrame(page) {
|
|
132
|
+
const deadline = Date.now() + 15000;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
const frame = page.frames().find((candidate) =>
|
|
135
|
+
candidate.url().includes('login.continente.pt/user-register')
|
|
136
|
+
);
|
|
137
|
+
if (frame) {
|
|
138
|
+
return frame;
|
|
139
|
+
}
|
|
140
|
+
await page.waitForTimeout(500);
|
|
141
|
+
}
|
|
142
|
+
throw new Error('Could not find Continente login frame');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function autoLogin({
|
|
146
|
+
env = DEFAULT_ENV,
|
|
147
|
+
log: logger = log,
|
|
148
|
+
} = {}) {
|
|
149
|
+
const mergedEnv = loadCredentialEnv(env);
|
|
150
|
+
const email = mergedEnv.CONTINENTE_EMAIL;
|
|
151
|
+
const password = mergedEnv.CONTINENTE_PASSWORD;
|
|
152
|
+
|
|
153
|
+
if (!getCredentialStatus(mergedEnv).ready) {
|
|
154
|
+
logger('Missing CONTINENTE_EMAIL or CONTINENTE_PASSWORD.');
|
|
155
|
+
return { success: false, error: 'missing_credentials' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const browser = await chromium.launch({
|
|
159
|
+
headless: mergedEnv.CONTINENTE_LOGIN_HEADLESS !== 'false',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const context = await browser.newContext({
|
|
164
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
165
|
+
locale: 'pt-PT',
|
|
166
|
+
});
|
|
167
|
+
const page = await context.newPage();
|
|
168
|
+
|
|
169
|
+
logger('Opening login page.');
|
|
170
|
+
await page.goto(CONTINENTE_LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
171
|
+
await acceptCookies(page);
|
|
172
|
+
const loginFrame = await findLoginFrame(page);
|
|
173
|
+
|
|
174
|
+
logger('Entering email.');
|
|
175
|
+
await fillFirst(loginFrame, [
|
|
176
|
+
'#userNameCC',
|
|
177
|
+
'input[name="userNameCC"]',
|
|
178
|
+
'input[type="email"]',
|
|
179
|
+
'input[name="email"]',
|
|
180
|
+
'input[name="username"]',
|
|
181
|
+
'input[type="text"]',
|
|
182
|
+
], email, 'email');
|
|
183
|
+
|
|
184
|
+
await clickFirst(loginFrame, [
|
|
185
|
+
'button[type="submit"]',
|
|
186
|
+
'button:has-text("Avançar")',
|
|
187
|
+
'button:has-text("Continuar")',
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
logger('Entering password.');
|
|
191
|
+
await page.waitForTimeout(1500);
|
|
192
|
+
await fillFirst(loginFrame, [
|
|
193
|
+
'#password_input',
|
|
194
|
+
'input[name="password_input"]',
|
|
195
|
+
'input[type="password"]',
|
|
196
|
+
'input[name="password"]',
|
|
197
|
+
], password, 'password');
|
|
198
|
+
|
|
199
|
+
await clickFirst(loginFrame, [
|
|
200
|
+
'button[type="submit"]',
|
|
201
|
+
'button:has-text("Avançar")',
|
|
202
|
+
'button:has-text("Entrar")',
|
|
203
|
+
'button:has-text("Login")',
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
await page.waitForTimeout(4000);
|
|
207
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
|
|
208
|
+
|
|
209
|
+
if (!(await isLoggedIn(page))) {
|
|
210
|
+
throw new Error('Login did not reach an authenticated Continente page');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cookies = await context.cookies();
|
|
214
|
+
writeCookies(resolveStatePaths(mergedEnv).cookieFile, cookies);
|
|
215
|
+
syncVault(cookies, mergedEnv.CONTINENTE_VAULT_COOKIE_PATH, logger);
|
|
216
|
+
logger(`Saved ${cookies.length} cookies.`);
|
|
217
|
+
return { success: true, cookies: cookies.length };
|
|
218
|
+
} finally {
|
|
219
|
+
await browser.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
224
|
+
autoLogin().then((result) => {
|
|
225
|
+
if (!result.success) {
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}).catch((error) => {
|
|
229
|
+
log(error.message);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
|
232
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continente-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server for Continente.pt — search products, manage your cart, and browse order history
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "MCP server for Continente.pt — search products, manage your cart, and browse order history.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"continente-mcp": "src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=18.
|
|
11
|
+
"node": ">=20.18.1"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "node src/index.js",
|
|
15
15
|
"setup": "playwright install chromium --with-deps",
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"check:release": "node scripts/check-release.js"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"mcp",
|
|
@@ -39,10 +40,7 @@
|
|
|
39
40
|
"files": [
|
|
40
41
|
"src/",
|
|
41
42
|
"skills/",
|
|
42
|
-
"continente-
|
|
43
|
-
"continente-keepalive.py",
|
|
44
|
-
"continente-backup.js",
|
|
45
|
-
"check-change.js",
|
|
43
|
+
"continente-auto-login.js",
|
|
46
44
|
".env.example",
|
|
47
45
|
"LICENSE",
|
|
48
46
|
"README.md"
|