continente-mcp 3.1.0 → 4.1.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 +21 -11
- package/README.md +34 -295
- package/continente-auto-login.js +233 -0
- package/package.json +7 -9
- package/skills/groceries/SKILL.md +11 -5
- package/src/auth-session.js +37 -0
- package/src/cart-utils.js +113 -0
- package/src/index.js +369 -132
- package/src/reliability.js +44 -0
- 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,29 @@
|
|
|
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: use a different private env file. This one cannot go in the file
|
|
12
|
+
# itself; set it in your MCP client's server "env" instead.
|
|
13
|
+
# CONTINENTE_ENV_PATH=/absolute/path/to/credentials.env
|
|
14
|
+
|
|
15
|
+
# Set to false only for local debugging, so you can watch the login browser.
|
|
16
|
+
# CONTINENTE_LOGIN_HEADLESS=true
|
|
3
17
|
|
|
4
18
|
# Path where cookies are stored (default: ~/.continente/cookies.json)
|
|
5
|
-
#
|
|
19
|
+
# Cookies are an internal session cache; you usually do not need to edit this.
|
|
20
|
+
# CONTINENTE_COOKIES_PATH=/absolute/path/to/cookies.json
|
|
6
21
|
|
|
7
22
|
# If you run the MCP server on a second machine and sync cookies via a shared
|
|
8
23
|
# folder (e.g. Obsidian vault, Syncthing, Dropbox), set this to the shared path
|
|
9
|
-
#
|
|
10
|
-
#
|
|
24
|
+
# for automatic login to also write cookies there. These cookies give access
|
|
25
|
+
# to your account: only use a private folder you control.
|
|
11
26
|
# CONTINENTE_VAULT_COOKIE_PATH=/path/to/shared/folder/cookies.json
|
|
12
27
|
|
|
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/...
|
|
28
|
+
# Override the state directory (preferences and default cookies)
|
|
29
|
+
# CONTINENTE_STATE_DIR=/absolute/path/to/state
|
package/README.md
CHANGED
|
@@ -1,325 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Continente MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Let your AI assistant search Continente.pt and fill your basket with the products you usually buy.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Search that knows your habits** — catalogue results put your saved favourites first.
|
|
10
|
+
- **Hands-free basket** — add, change and remove items; every change is checked against the basket afterwards.
|
|
11
|
+
- **Order history** — read recent orders and see your most-bought products.
|
|
12
|
+
- **Automatic login** — expired sessions sign in again on their own from a private credentials file.
|
|
13
|
+
- **You stay in control** — checkout and payment always happen on Continente.pt.
|
|
10
14
|
|
|
11
|
-
##
|
|
15
|
+
## Install
|
|
12
16
|
|
|
13
|
-
|
|
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
|
-
---
|
|
33
|
-
|
|
34
|
-
## Setup
|
|
35
|
-
|
|
36
|
-
### 1. Clone and install
|
|
37
|
-
|
|
38
|
-
The setup requires the repository — the Python cookie scripts live here and are needed before the server can authenticate.
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
git clone https://github.com/davidmarqu3s/continente-mcp
|
|
42
|
-
cd continente-mcp
|
|
43
|
-
npm install
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Install the Chromium browser that Playwright uses internally:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run setup
|
|
50
|
-
# or: npx playwright install chromium --with-deps
|
|
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
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
To specify a browser explicitly:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
python3 continente-cookie-reader.py --browser firefox
|
|
83
|
-
python3 continente-cookie-reader.py --list-browsers # see all supported browsers
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Cookies are saved to `~/.continente/cookies.json` (`%USERPROFILE%\.continente\cookies.json` on Windows) with owner-only read permissions.
|
|
87
|
-
|
|
88
|
-
### 3. Configure your MCP client
|
|
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:**
|
|
106
|
-
|
|
107
|
-
```json
|
|
108
|
-
{
|
|
109
|
-
"mcpServers": {
|
|
110
|
-
"continente": {
|
|
111
|
-
"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):**
|
|
17
|
+
Requires Node.js 20.18.1 or later. Install the browser the server uses (on Linux, add `--with-deps`):
|
|
167
18
|
|
|
168
19
|
```bash
|
|
169
|
-
|
|
170
|
-
# Add:
|
|
171
|
-
*/20 * * * * python3 /path/to/continente-keepalive.py >> ~/.continente/keepalive.log 2>&1
|
|
20
|
+
npx -y -p continente-mcp playwright install chromium
|
|
172
21
|
```
|
|
173
22
|
|
|
174
|
-
|
|
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.
|
|
23
|
+
For favourites, basket and order history, create `~/.continente/credentials.env` with your Continente login and make it private to your user (`chmod 600` on macOS/Linux). Search works without it.
|
|
193
24
|
|
|
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
25
|
```
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
26
|
+
CONTINENTE_EMAIL=you@example.com
|
|
27
|
+
CONTINENTE_PASSWORD=your-password
|
|
216
28
|
```
|
|
217
29
|
|
|
218
|
-
|
|
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):
|
|
30
|
+
Then add the server to your MCP client and restart it:
|
|
232
31
|
|
|
233
32
|
```json
|
|
234
33
|
{
|
|
235
34
|
"mcpServers": {
|
|
236
|
-
"continente": {
|
|
237
|
-
"command": "npx",
|
|
238
|
-
"args": ["continente-mcp"],
|
|
239
|
-
"env": {
|
|
240
|
-
"CONTINENTE_ALERT_WEBHOOK": "https://hooks.slack.com/services/..."
|
|
241
|
-
}
|
|
242
|
-
}
|
|
35
|
+
"continente": { "command": "npx", "args": ["-y", "continente-mcp"] }
|
|
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 | ✓ | — | — |
|
|
276
|
-
|
|
277
|
-
Powered by [browser-cookie3](https://github.com/borisbabic/browser_cookie3).
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
## Environment variables
|
|
282
|
-
|
|
283
|
-
See [`.env.example`](./.env.example) for all options. The main ones:
|
|
40
|
+
To run from source instead, clone the repository, run `npm install && npm run setup`, and use `node /absolute/path/to/continente-mcp/src/index.js` as the command.
|
|
284
41
|
|
|
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) |
|
|
42
|
+
## Use
|
|
291
43
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## Skills
|
|
297
|
-
|
|
298
|
-
The [`skills/`](./skills/) directory contains ready-made agent skills for clients that support them (e.g. Claude Code).
|
|
299
|
-
|
|
300
|
-
### groceries
|
|
301
|
-
|
|
302
|
-
Adds items to the basket by name. Matches against your favourites and order history to pick the right product variant automatically.
|
|
303
|
-
|
|
304
|
-
Install it in Claude Code:
|
|
305
|
-
|
|
306
|
-
```bash
|
|
307
|
-
# From the repo root
|
|
308
|
-
claude skill install skills/groceries
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
Then use it:
|
|
312
|
-
|
|
313
|
-
```
|
|
314
|
-
/groceries queijo flamengo, leite mimosa, ovos
|
|
315
|
-
```
|
|
44
|
+
Ask your assistant things like "add milk, eggs and bread to my Continente basket". Run `refresh_favorites` once so searches know what you buy.
|
|
316
45
|
|
|
317
|
-
|
|
46
|
+
| Tool | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `search_products` | Search the catalogue, favourites first |
|
|
49
|
+
| `get_favorites` / `refresh_favorites` | Read cached favourites / refresh them from your account |
|
|
50
|
+
| `get_cart` | Read the basket |
|
|
51
|
+
| `add_to_cart` / `update_cart_item` | Add a product / set its quantity (zero removes it) |
|
|
52
|
+
| `get_order_history` / `get_most_bought` | Read recent orders / your most-bought products |
|
|
53
|
+
| `close_session` | Close the browser session |
|
|
318
54
|
|
|
319
|
-
|
|
55
|
+
## More
|
|
320
56
|
|
|
321
|
-
|
|
57
|
+
- **Agents:** the [groceries skill](skills/groceries/SKILL.md) teaches assistants to pick the products you usually buy.
|
|
58
|
+
- **Settings:** custom credential, state and cookie paths are listed in [`.env.example`](.env.example).
|
|
59
|
+
- **Troubleshooting:** [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
60
|
+
- **Developers:** [docs/development.md](docs/development.md) and [docs/releases.md](docs/releases.md).
|
|
322
61
|
|
|
323
62
|
## License
|
|
324
63
|
|
|
325
|
-
[MIT](
|
|
64
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
export function writeCookies(path, cookies) {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
18
|
+
// Create owner-only; chmod also tightens files created by older versions.
|
|
19
|
+
writeFileSync(path, JSON.stringify(cookies, null, 2), { mode: 0o600 });
|
|
20
|
+
chmodSync(path, 0o600);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unquoteEnvValue(value) {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (
|
|
26
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
27
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
28
|
+
) {
|
|
29
|
+
return trimmed.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseEnvFile(contents) {
|
|
35
|
+
const parsed = {};
|
|
36
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
37
|
+
let raw = line.trim();
|
|
38
|
+
if (!raw || raw.startsWith('#')) continue;
|
|
39
|
+
if (raw.startsWith('export ')) raw = raw.slice('export '.length).trim();
|
|
40
|
+
const index = raw.indexOf('=');
|
|
41
|
+
if (index === -1) continue;
|
|
42
|
+
const key = raw.slice(0, index).trim();
|
|
43
|
+
const value = raw.slice(index + 1);
|
|
44
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(key)) {
|
|
45
|
+
parsed[key] = unquoteEnvValue(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function credentialEnvPath(env = DEFAULT_ENV) {
|
|
52
|
+
const home = env.HOME || env.USERPROFILE || (env === DEFAULT_ENV ? homedir() : null);
|
|
53
|
+
const stateDir = env.CONTINENTE_STATE_DIR || (home ? join(home, '.continente') : null);
|
|
54
|
+
return env.CONTINENTE_ENV_PATH || (stateDir ? join(stateDir, 'credentials.env') : null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadCredentialEnv(env = DEFAULT_ENV) {
|
|
58
|
+
const file = credentialEnvPath(env);
|
|
59
|
+
if (!file || !existsSync(file)) {
|
|
60
|
+
return { ...env };
|
|
61
|
+
}
|
|
62
|
+
return { ...parseEnvFile(readFileSync(file, 'utf8')), ...env, CONTINENTE_ENV_PATH: file };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveStatePaths(env = DEFAULT_ENV) {
|
|
66
|
+
const settings = loadCredentialEnv(env);
|
|
67
|
+
const stateDir = settings.CONTINENTE_STATE_DIR || join(settings.HOME || settings.USERPROFILE || homedir(), '.continente');
|
|
68
|
+
return { stateDir, cookieFile: settings.CONTINENTE_COOKIES_PATH || join(stateDir, 'cookies.json') };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function syncVault(cookies, vaultCookiePath, logger) {
|
|
72
|
+
if (!vaultCookiePath) return;
|
|
73
|
+
const next = JSON.stringify(cookies, null, 2);
|
|
74
|
+
if (existsSync(vaultCookiePath) && readFileSync(vaultCookiePath, 'utf8') === next) {
|
|
75
|
+
logger('Vault unchanged.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
writeCookies(vaultCookiePath, cookies);
|
|
79
|
+
logger('Vault cookies updated.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getCredentialStatus(env = DEFAULT_ENV) {
|
|
83
|
+
const mergedEnv = loadCredentialEnv(env);
|
|
84
|
+
return {
|
|
85
|
+
hasEmail: Boolean(mergedEnv.CONTINENTE_EMAIL),
|
|
86
|
+
hasPassword: Boolean(mergedEnv.CONTINENTE_PASSWORD),
|
|
87
|
+
ready: Boolean(mergedEnv.CONTINENTE_EMAIL && mergedEnv.CONTINENTE_PASSWORD),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function isLoggedIn(page) {
|
|
92
|
+
const response = await page.goto(CONTINENTE_CHECK_URL, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
93
|
+
if (!response?.ok() || page.url().includes('/login')) return false;
|
|
94
|
+
const session = await page.context().request.get(
|
|
95
|
+
'https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Cart-MiniCartShow',
|
|
96
|
+
{ headers: { 'X-Requested-With': 'XMLHttpRequest' }, timeout: 10000 }
|
|
97
|
+
);
|
|
98
|
+
if (!session.ok()) return false;
|
|
99
|
+
const payload = await session.json().catch(() => null);
|
|
100
|
+
return payload?.resources?.customerAuthenticated === true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function fillFirst(page, selectors, value, label) {
|
|
104
|
+
for (const selector of selectors) {
|
|
105
|
+
const input = await page.waitForSelector(selector, { timeout: 3000 }).catch(() => null);
|
|
106
|
+
if (input) {
|
|
107
|
+
await input.fill(value);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Could not find ${label} input`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function clickFirst(page, selectors) {
|
|
115
|
+
for (const selector of selectors) {
|
|
116
|
+
const button = await page.$(selector);
|
|
117
|
+
if (button) {
|
|
118
|
+
await button.click();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await page.keyboard.press('Enter');
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function acceptCookies(page) {
|
|
127
|
+
await page.getByRole('button', { name: /PERMITIR TODOS|Permitir todos/i })
|
|
128
|
+
.click({ timeout: 5000 })
|
|
129
|
+
.catch(() => {});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function findLoginFrame(page) {
|
|
133
|
+
const deadline = Date.now() + 15000;
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
const frame = page.frames().find((candidate) =>
|
|
136
|
+
candidate.url().includes('login.continente.pt/user-register')
|
|
137
|
+
);
|
|
138
|
+
if (frame) {
|
|
139
|
+
return frame;
|
|
140
|
+
}
|
|
141
|
+
await page.waitForTimeout(500);
|
|
142
|
+
}
|
|
143
|
+
throw new Error('Could not find Continente login frame');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function autoLogin({
|
|
147
|
+
env = DEFAULT_ENV,
|
|
148
|
+
log: logger = log,
|
|
149
|
+
} = {}) {
|
|
150
|
+
const mergedEnv = loadCredentialEnv(env);
|
|
151
|
+
const email = mergedEnv.CONTINENTE_EMAIL;
|
|
152
|
+
const password = mergedEnv.CONTINENTE_PASSWORD;
|
|
153
|
+
|
|
154
|
+
if (!getCredentialStatus(mergedEnv).ready) {
|
|
155
|
+
logger('Missing CONTINENTE_EMAIL or CONTINENTE_PASSWORD.');
|
|
156
|
+
return { success: false, error: 'missing_credentials' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const browser = await chromium.launch({
|
|
160
|
+
headless: mergedEnv.CONTINENTE_LOGIN_HEADLESS !== 'false',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const context = await browser.newContext({
|
|
165
|
+
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',
|
|
166
|
+
locale: 'pt-PT',
|
|
167
|
+
});
|
|
168
|
+
const page = await context.newPage();
|
|
169
|
+
|
|
170
|
+
logger('Opening login page.');
|
|
171
|
+
await page.goto(CONTINENTE_LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
172
|
+
await acceptCookies(page);
|
|
173
|
+
const loginFrame = await findLoginFrame(page);
|
|
174
|
+
|
|
175
|
+
logger('Entering email.');
|
|
176
|
+
await fillFirst(loginFrame, [
|
|
177
|
+
'#userNameCC',
|
|
178
|
+
'input[name="userNameCC"]',
|
|
179
|
+
'input[type="email"]',
|
|
180
|
+
'input[name="email"]',
|
|
181
|
+
'input[name="username"]',
|
|
182
|
+
'input[type="text"]',
|
|
183
|
+
], email, 'email');
|
|
184
|
+
|
|
185
|
+
await clickFirst(loginFrame, [
|
|
186
|
+
'button[type="submit"]',
|
|
187
|
+
'button:has-text("Avançar")',
|
|
188
|
+
'button:has-text("Continuar")',
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
logger('Entering password.');
|
|
192
|
+
await page.waitForTimeout(1500);
|
|
193
|
+
await fillFirst(loginFrame, [
|
|
194
|
+
'#password_input',
|
|
195
|
+
'input[name="password_input"]',
|
|
196
|
+
'input[type="password"]',
|
|
197
|
+
'input[name="password"]',
|
|
198
|
+
], password, 'password');
|
|
199
|
+
|
|
200
|
+
await clickFirst(loginFrame, [
|
|
201
|
+
'button[type="submit"]',
|
|
202
|
+
'button:has-text("Avançar")',
|
|
203
|
+
'button:has-text("Entrar")',
|
|
204
|
+
'button:has-text("Login")',
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
await page.waitForTimeout(4000);
|
|
208
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 15000 }).catch(() => {});
|
|
209
|
+
|
|
210
|
+
if (!(await isLoggedIn(page))) {
|
|
211
|
+
throw new Error('Login did not reach an authenticated Continente page');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const cookies = await context.cookies();
|
|
215
|
+
writeCookies(resolveStatePaths(mergedEnv).cookieFile, cookies);
|
|
216
|
+
syncVault(cookies, mergedEnv.CONTINENTE_VAULT_COOKIE_PATH, logger);
|
|
217
|
+
logger(`Saved ${cookies.length} cookies.`);
|
|
218
|
+
return { success: true, cookies: cookies.length };
|
|
219
|
+
} finally {
|
|
220
|
+
await browser.close();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
225
|
+
autoLogin().then((result) => {
|
|
226
|
+
if (!result.success) {
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}).catch((error) => {
|
|
230
|
+
log(error.message);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|
|
233
|
+
}
|
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.1.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,16 +40,13 @@
|
|
|
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"
|
|
49
47
|
],
|
|
50
48
|
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
52
50
|
"cheerio": "^1.2.0",
|
|
53
51
|
"playwright": "^1.59.1"
|
|
54
52
|
}
|