auchan-mcp 0.2.0 → 0.3.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 -0
- package/README.md +17 -3
- package/dist/index.js +169 -63
- package/package.json +2 -1
package/.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# auchan-mcp: variáveis de ambiente
|
|
2
|
+
# Com o plugin supermercados-mcp, o ficheiro é criado e aberto sozinho na primeira vez:
|
|
3
|
+
# Windows: %USERPROFILE%\.supermercados-mcp\credentials.env
|
|
4
|
+
# macOS/Linux: ~/.supermercados-mcp/credentials.env
|
|
5
|
+
# Sem o plugin, podes pôr as duas linhas abaixo nesse ficheiro ou em ~/.auchan-mcp/credentials.env.
|
|
6
|
+
# Esse ficheiro nunca vai para o git. A pesquisa funciona sem ele.
|
|
7
|
+
|
|
8
|
+
# Necessárias para o carrinho (get_cart, add_to_cart, update_cart_item).
|
|
9
|
+
# AUCHAN_EMAIL=tu@exemplo.pt
|
|
10
|
+
# AUCHAN_PASSWORD=a-tua-palavra-passe
|
|
11
|
+
|
|
12
|
+
# As seguintes definem-se no "env" do servidor no cliente MCP, não no ficheiro acima.
|
|
13
|
+
|
|
14
|
+
# Outro ficheiro de credenciais (por omissão: o do plugin e depois ~/.auchan-mcp/credentials.env).
|
|
15
|
+
# AUCHAN_ENV_PATH=C:\caminho\para\credentials.env
|
|
16
|
+
|
|
17
|
+
# Outra pasta para a sessão guardada e a cache (por omissão: ~/.auchan-mcp).
|
|
18
|
+
# AUCHAN_STATE_DIR=C:\caminho\para\pasta
|
|
19
|
+
|
|
20
|
+
# Põe false só para ver o browser durante o login, ao procurar um problema.
|
|
21
|
+
# AUCHAN_LOGIN_HEADLESS=true
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Feito à imagem do [continente-mcp](https://github.com/davidmarqu3s/continente-m
|
|
|
10
10
|
|
|
11
11
|
- **Pesquisa sem conta**: nome, marca, preço, preço e preço por litro, quilo ou unidade.
|
|
12
12
|
- **Carrinho**: ver, juntar e mudar quantidades; cada alteração é confirmada no carrinho a seguir.
|
|
13
|
+
- **Entra sozinho**: usa o email e a palavra-passe de um ficheiro teu, em segundo plano e sem janela, e volta a entrar sempre que o Auchan termina a sessão.
|
|
13
14
|
- **Respeita o site**: no máximo um pedido por segundo, resultados guardados durante 6 horas e um user-agent que diz quem é.
|
|
14
15
|
- **Tu decides**: a finalização da encomenda e o pagamento fazem-se sempre no Auchan.pt. O servidor não tem nenhuma ferramenta para isso.
|
|
15
16
|
|
|
@@ -31,7 +32,16 @@ Precisas do Node.js 20.18.1 ou mais recente e de um cliente MCP, por exemplo o C
|
|
|
31
32
|
}
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
A pesquisa funciona logo, sem conta. Para o carrinho
|
|
35
|
+
A pesquisa funciona logo, sem conta. Para o carrinho, cria o ficheiro `%USERPROFILE%\.auchan-mcp\credentials.env` (no macOS e Linux, `~/.auchan-mcp/credentials.env`) com duas linhas, sem aspas e sem espaços à volta do `=`:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
AUCHAN_EMAIL=o-teu-email
|
|
39
|
+
AUCHAN_PASSWORD=a-tua-palavra-passe
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Também lê `~/.supermercados-mcp/credentials.env`, o ficheiro que o plugin [supermercados-mcp](https://github.com/vasco-branco06/supermercados-mcp) cria e abre sozinho na primeira vez, e o que `AUCHAN_ENV_PATH` indicar. O Auchan só aceita produtos no carrinho depois de escolheres uma vez, no site, entrega em casa ou levantamento (com o código postal); essa escolha fica guardada na tua conta.
|
|
43
|
+
|
|
44
|
+
A palavra-passe fica em texto simples nesse ficheiro, na tua pasta de utilizador. Não o partilhes. Só é lida para preencher o formulário de login e nunca aparece nas respostas nem nos registos. A sessão (cookies e armazenamento local) fica em `%USERPROFILE%\.auchan-mcp\cookies.json`.
|
|
35
45
|
|
|
36
46
|
## Usar
|
|
37
47
|
|
|
@@ -42,12 +52,16 @@ Pede coisas como "procura leite meio-gordo no Auchan" ou "junta 2 pacotes ao car
|
|
|
42
52
|
| `search_products` | Pesquisar o catálogo (devolve JSON) |
|
|
43
53
|
| `get_cart` | Ver o carrinho |
|
|
44
54
|
| `add_to_cart` / `update_cart_item` | Juntar um produto / mudar a quantidade (0 tira-o) |
|
|
45
|
-
| `login` |
|
|
46
|
-
| `logout` | Desligar a conta: apaga a sessão guardada neste computador |
|
|
55
|
+
| `login` | Verificar ou renovar a sessão com o ficheiro de credenciais (as ferramentas do carrinho fazem-no sozinhas) |
|
|
56
|
+
| `logout` | Desligar a conta: apaga a sessão guardada neste computador (o ficheiro de credenciais fica) |
|
|
47
57
|
| `close_session` | Libertar recursos |
|
|
48
58
|
|
|
59
|
+
Com o servidor a correr, faz um pedido leve de 10 em 10 minutos para o Auchan não fechar a sessão por inatividade, e guarda o que o site renova.
|
|
60
|
+
|
|
49
61
|
## Mais
|
|
50
62
|
|
|
63
|
+
- **Sem ficheiro de credenciais:** existe o ramo [`login-por-janela`](https://github.com/vasco-branco06/supermercados-mcp/tree/login-por-janela), em que a pessoa entra numa janela do browser e a palavra-passe nunca é guardada.
|
|
64
|
+
- **Definições:** variáveis em [`.env.example`](.env.example).
|
|
51
65
|
- **Problemas:** [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
52
66
|
- **Testes:** `npm test` corre sem internet. `LIVE=1` liga um teste que faz um pedido real ao Auchan.pt (no PowerShell: `$env:LIVE=1; npm test`).
|
|
53
67
|
|
package/dist/index.js
CHANGED
|
@@ -176,21 +176,47 @@ async function fetchPage(url2, { fetchImpl = fetch, gate = throttle } = {}) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/paths.js
|
|
179
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
179
180
|
import { homedir } from "node:os";
|
|
180
181
|
import { join } from "node:path";
|
|
181
182
|
function resolveStatePaths(env = process.env) {
|
|
182
|
-
const
|
|
183
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
184
|
+
const stateDir = env.AUCHAN_STATE_DIR || join(home, ".auchan-mcp");
|
|
183
185
|
return {
|
|
184
186
|
stateDir,
|
|
185
187
|
cacheDir: join(stateDir, "cache"),
|
|
188
|
+
// Where the plugin's first run creates the template, shared by every supermarket.
|
|
189
|
+
sharedCredentialFile: env.SUPERMERCADOS_MCP_CREDENTIALS || join(env.SUPERMERCADOS_MCP_DIR || join(home, ".supermercados-mcp"), "credentials.env"),
|
|
190
|
+
credentialFile: join(stateDir, "credentials.env"),
|
|
186
191
|
cookieFile: join(stateDir, "cookies.json")
|
|
187
192
|
};
|
|
188
193
|
}
|
|
194
|
+
function parseEnvFile(contents) {
|
|
195
|
+
const parsed = {};
|
|
196
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
197
|
+
const raw = line.trim().replace(/^export\s+/, "");
|
|
198
|
+
const index = raw.indexOf("=");
|
|
199
|
+
if (!raw || raw.startsWith("#") || index === -1) continue;
|
|
200
|
+
const key = raw.slice(0, index).trim();
|
|
201
|
+
const value = raw.slice(index + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
202
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(key)) parsed[key] = value;
|
|
203
|
+
}
|
|
204
|
+
return parsed;
|
|
205
|
+
}
|
|
206
|
+
function loadCredentials(env = process.env) {
|
|
207
|
+
const { credentialFile, sharedCredentialFile } = resolveStatePaths(env);
|
|
208
|
+
for (const file2 of [env.AUCHAN_ENV_PATH, sharedCredentialFile, credentialFile]) {
|
|
209
|
+
if (!file2 || !existsSync(file2)) continue;
|
|
210
|
+
const { AUCHAN_EMAIL: email3, AUCHAN_PASSWORD: password } = parseEnvFile(readFileSync2(file2, "utf8"));
|
|
211
|
+
if (email3 && password) return { email: email3, password };
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
189
215
|
|
|
190
216
|
// src/search.js
|
|
191
217
|
import * as cheerio2 from "cheerio";
|
|
192
218
|
import { createHash } from "node:crypto";
|
|
193
|
-
import { mkdirSync, readFileSync as
|
|
219
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
194
220
|
import { join as join2 } from "node:path";
|
|
195
221
|
|
|
196
222
|
// ../../node_modules/zod/v4/classic/external.js
|
|
@@ -20010,7 +20036,7 @@ var searchUrl = (query) => `${AUCHAN_BASE}/pt/pesquisa?q=${encodeURIComponent(qu
|
|
|
20010
20036
|
var cacheFile = (cacheDir, query) => join2(cacheDir, `${createHash("sha256").update(query.trim().toLowerCase()).digest("hex").slice(0, 32)}.json`);
|
|
20011
20037
|
function readCache(file2, now) {
|
|
20012
20038
|
try {
|
|
20013
|
-
const entry = JSON.parse(
|
|
20039
|
+
const entry = JSON.parse(readFileSync3(file2, "utf8"));
|
|
20014
20040
|
if (now - Date.parse(entry.fetched_at) < CACHE_TTL_MS && Array.isArray(entry.raw)) return entry;
|
|
20015
20041
|
} catch {
|
|
20016
20042
|
}
|
|
@@ -20035,40 +20061,54 @@ async function searchProducts(query, limit = 10, { cacheDir, fetchImpl, now = Da
|
|
|
20035
20061
|
}
|
|
20036
20062
|
|
|
20037
20063
|
// src/session.js
|
|
20038
|
-
import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync as
|
|
20064
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
20039
20065
|
import { dirname } from "node:path";
|
|
20040
20066
|
var LOGIN_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Login-OAuthLogin?oauthProvider=Salesforce&oauthLoginTargetEndPoint=1`;
|
|
20041
20067
|
var DELIVERY_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Delivery-Header`;
|
|
20042
20068
|
var CART_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Cart-Show`;
|
|
20069
|
+
var EMAIL_FIELD = 'input[name="uname1"], input[type="email"]';
|
|
20070
|
+
var PASSWORD_FIELD = 'input[name="passwordLogin"], input[type="password"]';
|
|
20071
|
+
var SUBMIT_BUTTON = '#btnSubmit_login, input[name="btnSubmit_login"], button[type="submit"]:visible';
|
|
20072
|
+
var CHALLENGE = 'iframe[src*="captcha"], iframe[title*="challenge"], .g-recaptcha, .h-captcha, #px-captcha';
|
|
20043
20073
|
var BROWSER_CHANNELS = ["msedge", "chrome", void 0];
|
|
20044
20074
|
var NO_PLAYWRIGHT = "falta o Playwright; instala o pacote de novo (npm install auchan-mcp)";
|
|
20045
|
-
var NO_BROWSER = 'n\xE3o encontrei o Microsoft Edge nem o Google Chrome para
|
|
20046
|
-
|
|
20075
|
+
var NO_BROWSER = 'n\xE3o encontrei o Microsoft Edge nem o Google Chrome para fazer o login; instala um deles ou corre "npx playwright install chromium"';
|
|
20076
|
+
var FORM_CHANGED = "o formul\xE1rio de login do Auchan n\xE3o \xE9 o esperado (o site pode ter mudado); \xE9 preciso atualizar o servidor";
|
|
20077
|
+
var LOGIN_FAILED = "o login n\xE3o terminou; confirma o email e a palavra-passe no ficheiro de credenciais";
|
|
20078
|
+
var NO_DELIVERY = "o Auchan ainda n\xE3o tem a tua entrega escolhida; escolhe-a uma vez no auchan.pt (entrega em casa ou levantamento, com o c\xF3digo postal) e tenta de novo";
|
|
20079
|
+
function writeState(path, state) {
|
|
20047
20080
|
mkdirSync2(dirname(path), { recursive: true, mode: 448 });
|
|
20048
|
-
writeFileSync2(path, JSON.stringify(
|
|
20081
|
+
writeFileSync2(path, JSON.stringify(state, null, 2), { mode: 384 });
|
|
20049
20082
|
chmodSync(path, 384);
|
|
20050
20083
|
}
|
|
20051
|
-
function
|
|
20052
|
-
if (!
|
|
20084
|
+
function readState(path) {
|
|
20085
|
+
if (!existsSync2(path)) return null;
|
|
20053
20086
|
try {
|
|
20054
|
-
const
|
|
20055
|
-
|
|
20087
|
+
const saved = JSON.parse(readFileSync4(path, "utf8"));
|
|
20088
|
+
const state = Array.isArray(saved) ? { cookies: saved, origins: [] } : { cookies: saved?.cookies, origins: saved?.origins ?? [] };
|
|
20089
|
+
return Array.isArray(state.cookies) && state.cookies.length ? state : null;
|
|
20056
20090
|
} catch {
|
|
20057
20091
|
return null;
|
|
20058
20092
|
}
|
|
20059
20093
|
}
|
|
20060
20094
|
function createSession({
|
|
20095
|
+
credentials,
|
|
20096
|
+
credentialFile = "credentials.env",
|
|
20061
20097
|
cookieFile,
|
|
20062
20098
|
log = console.error,
|
|
20063
20099
|
loadPlaywright = () => import("playwright"),
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20100
|
+
headless = true,
|
|
20101
|
+
loginTimeoutMs = 3e4,
|
|
20102
|
+
keepAliveMs = 10 * 60 * 1e3
|
|
20067
20103
|
}) {
|
|
20068
20104
|
let playwright = null;
|
|
20069
20105
|
let api = null;
|
|
20106
|
+
let state = null;
|
|
20070
20107
|
let loggedIn = false;
|
|
20071
20108
|
let pending = null;
|
|
20109
|
+
let inFlight = 0;
|
|
20110
|
+
let timer = null;
|
|
20111
|
+
let beating = false;
|
|
20072
20112
|
async function pw() {
|
|
20073
20113
|
if (!playwright) {
|
|
20074
20114
|
try {
|
|
@@ -20079,13 +20119,14 @@ function createSession({
|
|
|
20079
20119
|
}
|
|
20080
20120
|
return playwright;
|
|
20081
20121
|
}
|
|
20082
|
-
async function openApi(
|
|
20122
|
+
async function openApi(next) {
|
|
20123
|
+
state = next;
|
|
20083
20124
|
await api?.dispose().catch(() => {
|
|
20084
20125
|
});
|
|
20085
20126
|
api = await (await pw()).request.newContext({
|
|
20086
20127
|
userAgent: USER_AGENT,
|
|
20087
20128
|
locale: "pt-PT",
|
|
20088
|
-
storageState: { cookies, origins: [] }
|
|
20129
|
+
storageState: { cookies: next.cookies, origins: [] }
|
|
20089
20130
|
});
|
|
20090
20131
|
}
|
|
20091
20132
|
async function isLoggedIn(requester) {
|
|
@@ -20098,65 +20139,113 @@ function createSession({
|
|
|
20098
20139
|
assertNotBlocked(res.status(), "");
|
|
20099
20140
|
return res.ok() && /data-has-context="true"/.test(await res.text());
|
|
20100
20141
|
}
|
|
20101
|
-
async function
|
|
20142
|
+
async function launch(chromium) {
|
|
20102
20143
|
for (const channel of BROWSER_CHANNELS) {
|
|
20103
20144
|
try {
|
|
20104
|
-
return await chromium.launch({ headless
|
|
20145
|
+
return await chromium.launch({ headless, ...channel && { channel } });
|
|
20105
20146
|
} catch {
|
|
20106
20147
|
}
|
|
20107
20148
|
}
|
|
20108
|
-
|
|
20149
|
+
return null;
|
|
20150
|
+
}
|
|
20151
|
+
function scrubbed(error62) {
|
|
20152
|
+
for (const secret of [credentials?.email, credentials?.password]) {
|
|
20153
|
+
if (secret) error62.message = String(error62.message).split(secret).join("***");
|
|
20154
|
+
}
|
|
20155
|
+
return error62;
|
|
20109
20156
|
}
|
|
20110
|
-
async function
|
|
20157
|
+
async function autoLogin() {
|
|
20158
|
+
if (!credentials) {
|
|
20159
|
+
throw new Error(`falta o ficheiro ${credentialFile} com as linhas AUCHAN_EMAIL=... e AUCHAN_PASSWORD=... (ver o README)`);
|
|
20160
|
+
}
|
|
20111
20161
|
const { chromium } = await pw();
|
|
20112
|
-
const browser = await
|
|
20113
|
-
|
|
20162
|
+
const browser = await launch(chromium);
|
|
20163
|
+
if (!browser) throw new Error(NO_BROWSER);
|
|
20114
20164
|
try {
|
|
20115
|
-
const context = await browser.newContext({ locale: "pt-PT" });
|
|
20116
|
-
if (savedCookies) await context.addCookies(savedCookies);
|
|
20165
|
+
const context = await browser.newContext({ locale: "pt-PT", userAgent: USER_AGENT });
|
|
20117
20166
|
const page = await context.newPage();
|
|
20118
|
-
log(
|
|
20119
|
-
await throttle(() => page.goto(
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20123
|
-
|
|
20124
|
-
}
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
|
|
20130
|
-
|
|
20131
|
-
|
|
20167
|
+
log("[auth] A iniciar sess\xE3o no Auchan...");
|
|
20168
|
+
await throttle(() => page.goto(LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 3e4 }));
|
|
20169
|
+
await page.locator("#onetrust-accept-btn-handler").click({ timeout: 2e3 }).catch(() => {
|
|
20170
|
+
});
|
|
20171
|
+
try {
|
|
20172
|
+
await page.locator(EMAIL_FIELD).first().fill(credentials.email, { timeout: 2e4 });
|
|
20173
|
+
await page.locator(PASSWORD_FIELD).first().fill(credentials.password, { timeout: 1e4 });
|
|
20174
|
+
await throttle(() => page.locator(SUBMIT_BUTTON).first().click({ timeout: 1e4 }));
|
|
20175
|
+
} catch {
|
|
20176
|
+
throw new Error(FORM_CHANGED);
|
|
20177
|
+
}
|
|
20178
|
+
try {
|
|
20179
|
+
await page.waitForURL((url2) => url2.hostname === "www.auchan.pt" && !/login/i.test(url2.pathname), { timeout: loginTimeoutMs });
|
|
20180
|
+
} catch {
|
|
20181
|
+
if (await page.locator(CHALLENGE).count()) {
|
|
20182
|
+
throw new BlockedError("o site pediu uma verifica\xE7\xE3o anti-rob\xF4 (CAPTCHA) no login; entra uma vez no auchan.pt pelo browser e tenta mais tarde");
|
|
20132
20183
|
}
|
|
20184
|
+
throw new Error(LOGIN_FAILED);
|
|
20133
20185
|
}
|
|
20134
|
-
throw new Error(
|
|
20186
|
+
if (!await isLoggedIn(context.request)) throw new Error(LOGIN_FAILED);
|
|
20187
|
+
const next = await context.storageState();
|
|
20188
|
+
writeState(cookieFile, next);
|
|
20189
|
+
log("[auth] Sess\xE3o iniciada.");
|
|
20190
|
+
return next;
|
|
20191
|
+
} catch (error62) {
|
|
20192
|
+
if (error62 instanceof BlockedError || error62.message === LOGIN_FAILED || error62.message === FORM_CHANGED || error62.message.startsWith("falta ")) throw scrubbed(error62);
|
|
20193
|
+
throw new Error(LOGIN_FAILED);
|
|
20135
20194
|
} finally {
|
|
20136
20195
|
await browser.close().catch(() => {
|
|
20137
20196
|
});
|
|
20138
20197
|
}
|
|
20139
20198
|
}
|
|
20199
|
+
let lastPersist = 0;
|
|
20200
|
+
async function persist(force = false) {
|
|
20201
|
+
if (!api || !state || !force && Date.now() - lastPersist < 5 * 60 * 1e3) return;
|
|
20202
|
+
lastPersist = Date.now();
|
|
20203
|
+
try {
|
|
20204
|
+
const { cookies } = await api.storageState();
|
|
20205
|
+
if (cookies.length) {
|
|
20206
|
+
state = { cookies, origins: state.origins };
|
|
20207
|
+
writeState(cookieFile, state);
|
|
20208
|
+
}
|
|
20209
|
+
} catch {
|
|
20210
|
+
}
|
|
20211
|
+
}
|
|
20212
|
+
function startKeepAlive() {
|
|
20213
|
+
if (timer || !keepAliveMs) return;
|
|
20214
|
+
timer = setInterval(async () => {
|
|
20215
|
+
if (!api || !loggedIn || pending || inFlight || beating) return;
|
|
20216
|
+
beating = true;
|
|
20217
|
+
try {
|
|
20218
|
+
if (await isLoggedIn(api)) {
|
|
20219
|
+
await persist(true);
|
|
20220
|
+
return;
|
|
20221
|
+
}
|
|
20222
|
+
if (credentials) await openApi(await autoLogin());
|
|
20223
|
+
else loggedIn = false;
|
|
20224
|
+
} catch {
|
|
20225
|
+
loggedIn = false;
|
|
20226
|
+
} finally {
|
|
20227
|
+
beating = false;
|
|
20228
|
+
}
|
|
20229
|
+
}, keepAliveMs);
|
|
20230
|
+
timer.unref?.();
|
|
20231
|
+
}
|
|
20140
20232
|
function ensureSession() {
|
|
20141
20233
|
if (api && loggedIn) return Promise.resolve("saved");
|
|
20142
20234
|
pending ??= (async () => {
|
|
20143
20235
|
try {
|
|
20144
|
-
|
|
20236
|
+
let how = "login";
|
|
20237
|
+
const saved = readState(cookieFile);
|
|
20145
20238
|
if (saved) {
|
|
20146
20239
|
await openApi(saved);
|
|
20147
|
-
|
|
20148
|
-
|
|
20149
|
-
loggedIn = true;
|
|
20150
|
-
return "saved";
|
|
20151
|
-
}
|
|
20152
|
-
log(logged ? "[auth] Falta escolher a entrega." : "[auth] A sess\xE3o guardada expirou.");
|
|
20153
|
-
await openApi(await interactiveLogin(logged ? saved : null));
|
|
20154
|
-
loggedIn = true;
|
|
20155
|
-
return "window";
|
|
20240
|
+
if (await isLoggedIn(api)) how = "saved";
|
|
20241
|
+
else log("[auth] A sess\xE3o guardada expirou; vou entrar outra vez.");
|
|
20156
20242
|
}
|
|
20157
|
-
await openApi(await
|
|
20243
|
+
if (how === "login") await openApi(await autoLogin());
|
|
20244
|
+
if (!await hasDeliveryContext(api)) throw new Error(NO_DELIVERY);
|
|
20158
20245
|
loggedIn = true;
|
|
20159
|
-
|
|
20246
|
+
await persist(true);
|
|
20247
|
+
startKeepAlive();
|
|
20248
|
+
return how;
|
|
20160
20249
|
} finally {
|
|
20161
20250
|
pending = null;
|
|
20162
20251
|
}
|
|
@@ -20171,15 +20260,21 @@ function createSession({
|
|
|
20171
20260
|
return { status: res.status(), text: text2, url: res.url() };
|
|
20172
20261
|
});
|
|
20173
20262
|
async function request(method, path, form) {
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
if (/login/i.test(new URL(result.url).pathname)) {
|
|
20177
|
-
loggedIn = false;
|
|
20263
|
+
inFlight++;
|
|
20264
|
+
try {
|
|
20178
20265
|
await ensureSession();
|
|
20179
|
-
result = await call(method, path, form);
|
|
20180
|
-
if (/login/i.test(new URL(result.url).pathname))
|
|
20266
|
+
let result = await call(method, path, form);
|
|
20267
|
+
if (/login/i.test(new URL(result.url).pathname)) {
|
|
20268
|
+
loggedIn = false;
|
|
20269
|
+
await ensureSession();
|
|
20270
|
+
result = await call(method, path, form);
|
|
20271
|
+
if (/login/i.test(new URL(result.url).pathname)) throw new Error("o site n\xE3o aceitou a sess\xE3o; tenta usar a ferramenta login de novo");
|
|
20272
|
+
}
|
|
20273
|
+
await persist();
|
|
20274
|
+
return { status: result.status, text: result.text };
|
|
20275
|
+
} finally {
|
|
20276
|
+
inFlight--;
|
|
20181
20277
|
}
|
|
20182
|
-
return { status: result.status, text: result.text };
|
|
20183
20278
|
}
|
|
20184
20279
|
const site = {
|
|
20185
20280
|
get: (path) => request("get", path),
|
|
@@ -20189,6 +20284,11 @@ function createSession({
|
|
|
20189
20284
|
return ensureSession();
|
|
20190
20285
|
}
|
|
20191
20286
|
async function close() {
|
|
20287
|
+
if (timer) {
|
|
20288
|
+
clearInterval(timer);
|
|
20289
|
+
timer = null;
|
|
20290
|
+
}
|
|
20291
|
+
await persist(true);
|
|
20192
20292
|
const current = api;
|
|
20193
20293
|
api = null;
|
|
20194
20294
|
loggedIn = false;
|
|
@@ -20249,12 +20349,12 @@ var CART_TOOLS = [
|
|
|
20249
20349
|
var LOGIN_TOOLS = [
|
|
20250
20350
|
{
|
|
20251
20351
|
name: "login",
|
|
20252
|
-
description: "Connect your Auchan account
|
|
20352
|
+
description: "Connect your Auchan account with the email and password in credentials.env (no window). Logs in again by itself whenever the session ends. Cart tools call this automatically.",
|
|
20253
20353
|
inputSchema: { type: "object", properties: {} }
|
|
20254
20354
|
},
|
|
20255
20355
|
{
|
|
20256
20356
|
name: "logout",
|
|
20257
|
-
description: "Disconnect your Auchan account: deletes the saved session from this computer.",
|
|
20357
|
+
description: "Disconnect your Auchan account: deletes the saved session from this computer (credentials.env is kept).",
|
|
20258
20358
|
inputSchema: { type: "object", properties: {} }
|
|
20259
20359
|
}
|
|
20260
20360
|
];
|
|
@@ -20287,7 +20387,13 @@ var AuchanServer = class {
|
|
|
20287
20387
|
const paths = resolveStatePaths(env);
|
|
20288
20388
|
this.cacheDir = paths.cacheDir;
|
|
20289
20389
|
this.fetchImpl = fetchImpl;
|
|
20290
|
-
this.
|
|
20390
|
+
this.credentialsFound = Boolean(loadCredentials(env));
|
|
20391
|
+
this.session = session ?? createSession({
|
|
20392
|
+
credentials: loadCredentials(env),
|
|
20393
|
+
credentialFile: paths.sharedCredentialFile,
|
|
20394
|
+
cookieFile: paths.cookieFile,
|
|
20395
|
+
headless: env.AUCHAN_LOGIN_HEADLESS !== "false"
|
|
20396
|
+
});
|
|
20291
20397
|
this.tools = [SEARCH_TOOL, ...CART_TOOLS, ...LOGIN_TOOLS, CLOSE_TOOL];
|
|
20292
20398
|
this.server = new Server({ name: "auchan-mcp", version: VERSION }, { capabilities: { tools: {} } });
|
|
20293
20399
|
this.server.onclose = () => {
|
|
@@ -20331,7 +20437,7 @@ var AuchanServer = class {
|
|
|
20331
20437
|
);
|
|
20332
20438
|
}
|
|
20333
20439
|
case "login":
|
|
20334
|
-
return text(await this.session.login() === "saved" ? "J\xE1 tens a sess\xE3o do Auchan iniciada." : "Sess\xE3o do Auchan iniciada. J\xE1 posso usar o teu carrinho.");
|
|
20440
|
+
return text(await this.session.login() === "saved" ? "J\xE1 tens a sess\xE3o do Auchan iniciada." : "Sess\xE3o do Auchan iniciada com o ficheiro de credenciais. J\xE1 posso usar o teu carrinho.");
|
|
20335
20441
|
case "logout":
|
|
20336
20442
|
await this.session.logout();
|
|
20337
20443
|
return text("Sess\xE3o do Auchan terminada: apaguei a sess\xE3o guardada neste computador.");
|
|
@@ -20345,7 +20451,7 @@ var AuchanServer = class {
|
|
|
20345
20451
|
}
|
|
20346
20452
|
async start() {
|
|
20347
20453
|
await this.server.connect(new StdioServerTransport());
|
|
20348
|
-
console.error(`Auchan MCP v${VERSION} started`);
|
|
20454
|
+
console.error(`Auchan MCP v${VERSION} started${this.credentialsFound ? "" : " (search only until credentials.env exists)"}`);
|
|
20349
20455
|
}
|
|
20350
20456
|
};
|
|
20351
20457
|
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auchan-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Servidor MCP para o Auchan.pt: pesquisa de produtos e gestão do carrinho.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"files": [
|
|
30
30
|
"dist/",
|
|
31
|
+
".env.example",
|
|
31
32
|
"LICENSE",
|
|
32
33
|
"README.md"
|
|
33
34
|
],
|