auchan-mcp 0.1.0 → 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/README.md CHANGED
@@ -21,35 +21,18 @@ O `robots.txt` do Auchan pede que a pesquisa não seja usada de forma automátic
21
21
 
22
22
  ## Instalar
23
23
 
24
- Precisas do Node.js 20.18.1 ou mais recente. Na pasta `servers/auchan`:
25
-
26
- ```
27
- npm install
28
- ```
29
-
30
- Para o carrinho, instala também o browser que o login usa:
31
-
32
- ```
33
- npx playwright install chromium
34
- ```
35
-
36
- e cria `%USERPROFILE%\.auchan-mcp\credentials.env` (no macOS/Linux, `~/.auchan-mcp/credentials.env`) com o teu login do Auchan. A pesquisa funciona sem este ficheiro.
37
-
38
- ```
39
- AUCHAN_EMAIL=tu@exemplo.pt
40
- AUCHAN_PASSWORD=a-tua-palavra-passe
41
- ```
42
-
43
- Depois junta o servidor ao teu cliente MCP e reinicia-o:
24
+ Precisas do Node.js 20.18.1 ou mais recente e de um cliente MCP, por exemplo o Claude Code. Junta o servidor e reinicia o cliente:
44
25
 
45
26
  ```json
46
27
  {
47
28
  "mcpServers": {
48
- "auchan": { "command": "node", "args": ["C:/caminho/para/servers/auchan/src/index.js"] }
29
+ "auchan": { "command": "npx", "args": ["-y", "auchan-mcp"] }
49
30
  }
50
31
  }
51
32
  ```
52
33
 
34
+ A pesquisa funciona logo, sem conta. Para o carrinho não crias nada: quando o assistente precisar dele, abre-se uma janela do Edge ou do Chrome na página de login do Auchan. Entras tu, a janela fecha e ficas ligado. A palavra-passe nunca passa pelo servidor nem fica guardada: só a sessão, no teu computador (`%USERPROFILE%\.auchan-mcp\cookies.json`). Se aparecer um CAPTCHA, resolves tu na janela.
35
+
53
36
  ## Usar
54
37
 
55
38
  Pede coisas como "procura leite meio-gordo no Auchan" ou "junta 2 pacotes ao carrinho do Auchan".
@@ -59,13 +42,12 @@ Pede coisas como "procura leite meio-gordo no Auchan" ou "junta 2 pacotes ao car
59
42
  | `search_products` | Pesquisar o catálogo (devolve JSON) |
60
43
  | `get_cart` | Ver o carrinho |
61
44
  | `add_to_cart` / `update_cart_item` | Juntar um produto / mudar a quantidade (0 tira-o) |
62
- | `close_session` | Fechar o browser |
63
-
64
- As ferramentas do carrinho só aparecem quando existe o ficheiro de credenciais.
45
+ | `login` | Ligar a tua conta: abre a janela de login (as ferramentas do carrinho fazem-no sozinhas se for preciso) |
46
+ | `logout` | Desligar a conta: apaga a sessão guardada neste computador |
47
+ | `close_session` | Libertar recursos |
65
48
 
66
49
  ## Mais
67
50
 
68
- - **Definições:** caminhos e opções em [`.env.example`](.env.example).
69
51
  - **Problemas:** [docs/troubleshooting.md](docs/troubleshooting.md).
70
52
  - **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`).
71
53
 
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { pathToFileURL } from "node:url";
16
16
  // src/cart.js
17
17
  import * as cheerio from "cheerio";
18
18
  var PIPELINE = "/on/demandware.store/Sites-AuchanPT-Site/pt_PT/";
19
- var CART_ACTIONS = ["Cart-Show", "Cart-AddProduct", "Cart-UpdateQuantity", "Cart-RemoveProductLineItem"];
19
+ var CART_ACTIONS = ["Cart-Show", "Cart-AddProduct", "Cart-RemoveProductLineItem"];
20
20
  function cartPath(action, params = {}) {
21
21
  if (!CART_ACTIONS.includes(action)) throw new Error(`a\xE7\xE3o n\xE3o permitida: ${action}`);
22
22
  const query = new URLSearchParams(params).toString();
@@ -35,22 +35,28 @@ function validateCartInput(name, args) {
35
35
  }
36
36
  function parseCart(html) {
37
37
  const $ = cheerio.load(html);
38
- const lines = /* @__PURE__ */ new Map();
39
- $("[data-pid][data-uuid]").each((_, el) => {
38
+ const items = [];
39
+ $(".auc-cart__product-card[data-pid]").each((_, el) => {
40
40
  const $el = $(el);
41
- const uuid3 = $el.attr("data-uuid");
42
- const line = lines.get(uuid3) ?? { product_id: $el.attr("data-pid"), uuid: uuid3, name: null, quantity: null, price: null };
43
- const value = $el.is("select") ? $el.find("option[selected]").attr("value") ?? $el.val() : $el.attr("value");
44
- if ($el.is("input, select") && line.quantity === null && value !== void 0) line.quantity = Number(value);
45
- const $line = $el.closest(".product-info, .line-item, .card");
46
- line.name ??= $el.attr("data-name") || $line.find(".line-item-name").first().text().replace(/\s+/g, " ").trim() || null;
47
- const price = Number($line.find(".sales .value[content]").first().attr("content") || NaN);
48
- if (line.price === null && Number.isFinite(price) && price > 0) line.price = price;
49
- lines.set(uuid3, line);
41
+ const input2 = $el.find("input.auc-js-update-product-quantity").first();
42
+ const uuid3 = input2.attr("data-uuid") || $el.find("[data-uuid]").first().attr("data-uuid") || null;
43
+ let gtm = {};
44
+ try {
45
+ gtm = JSON.parse($el.attr("data-gtm") || "{}");
46
+ } catch {
47
+ }
48
+ const price = Number(gtm.price);
49
+ items.push({
50
+ product_id: $el.attr("data-pid"),
51
+ uuid: uuid3,
52
+ name: gtm.name || $el.find(".auc-product-tile__name a, .line-item-name").first().text().replace(/s+/g, " ").trim() || null,
53
+ quantity: Number(input2.attr("value")),
54
+ price: Number.isFinite(price) && price > 0 ? price : null
55
+ });
50
56
  });
51
57
  const csrf = $('input[name="csrf_token"]').attr("value") || $("[data-csrf-protection-token]").attr("data-csrf-protection-token") || null;
52
- const total = $(".grand-total, .grand-total-sum").first().text().replace(/\s+/g, " ").trim() || null;
53
- return { items: [...lines.values()].filter((item) => Number.isFinite(item.quantity)), total, csrf };
58
+ const total = $(".basket-total").first().text().replace(/s+/g, " ").trim() || null;
59
+ return { items: items.filter((item) => item.uuid && Number.isFinite(item.quantity)), total, csrf };
54
60
  }
55
61
  var publicCart = (cart) => ({
56
62
  store: "auchan",
@@ -71,6 +77,9 @@ async function sendChange(site, request) {
71
77
  payload = JSON.parse(res.text);
72
78
  } catch {
73
79
  }
80
+ if (payload?.showDeliveryOptionsModal === true) {
81
+ return { ok: false, error: "o Auchan ainda precisa de saber se queres entrega ou levantamento; usa a ferramenta login e escolhe-o na janela" };
82
+ }
74
83
  if (res.status >= 400 || !payload || payload.error === true || payload.success === false) {
75
84
  return { ok: false, error: payload?.message || `o site recusou a altera\xE7\xE3o (HTTP ${res.status})` };
76
85
  }
@@ -93,14 +102,27 @@ async function addToCart(site, productId, quantity) {
93
102
  if (!sent.ok) return { success: false, error: sent.error, writeAttempted: true };
94
103
  return confirmQuantity(site, productId, previous + quantity);
95
104
  }
105
+ var addForm = (productId, quantity, csrf) => ({ pid: productId, quantity: String(quantity), options: "[]", ...csrf && { csrf_token: csrf } });
96
106
  async function updateCartItem(site, productId, quantity) {
97
107
  const before = await getCart(site);
98
108
  const line = before.items.find((item) => item.product_id === productId);
99
109
  if (!line) return { success: false, error: "not_in_cart", product_id: productId, writeAttempted: false };
110
+ const current = quantityOf(before, productId);
111
+ if (quantity === current) return { success: true, product_id: productId, cart_quantity: current };
112
+ if (quantity > current) {
113
+ const sent = await sendChange(site, () => site.post(cartPath("Cart-AddProduct"), addForm(productId, quantity - current, before.csrf)));
114
+ if (!sent.ok) return { success: false, error: sent.error, writeAttempted: true };
115
+ return confirmQuantity(site, productId, quantity);
116
+ }
100
117
  const params = { pid: productId, uuid: line.uuid };
101
118
  if (before.csrf) params.csrf_token = before.csrf;
102
- const sent = quantity === 0 ? await sendChange(site, () => site.get(cartPath("Cart-RemoveProductLineItem", params))) : await sendChange(site, () => site.get(cartPath("Cart-UpdateQuantity", { ...params, quantity: String(quantity) })));
103
- if (!sent.ok) return { success: false, error: sent.error, writeAttempted: true };
119
+ const removed = await sendChange(site, () => site.get(cartPath("Cart-RemoveProductLineItem", params)));
120
+ if (!removed.ok) return { success: false, error: removed.error, writeAttempted: true };
121
+ if (quantity > 0) {
122
+ const after = await getCart(site);
123
+ const sent = await sendChange(site, () => site.post(cartPath("Cart-AddProduct"), addForm(productId, quantity, after.csrf)));
124
+ if (!sent.ok) return { success: false, error: sent.error, writeAttempted: true };
125
+ }
104
126
  return confirmQuantity(site, productId, quantity);
105
127
  }
106
128
 
@@ -110,6 +132,7 @@ var AUCHAN_BASE = "https://www.auchan.pt";
110
132
  var VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
111
133
  var USER_AGENT = `supermercados-mcp/${VERSION} (auchan-mcp; +https://github.com/vasco-branco06/supermercados-mcp)`;
112
134
  var BLOCK_MARKERS = /captcha|cf-challenge|just a moment\.\.\.|_incapsula_|datadome|perimeterx|px-captcha|access denied/i;
135
+ var CHALLENGE_MAX_BYTES = 3e4;
113
136
  var BlockedError = class extends Error {
114
137
  };
115
138
  function createThrottle(intervalMs = 1e3, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
@@ -130,7 +153,7 @@ function createThrottle(intervalMs = 1e3, now = Date.now, sleep = (ms) => new Pr
130
153
  var throttle = createThrottle();
131
154
  function assertNotBlocked(status, body = "") {
132
155
  if (status === 403 || status === 429) throw new BlockedError(`o site bloqueou o pedido (HTTP ${status}); tenta mais tarde`);
133
- if (BLOCK_MARKERS.test(body) && !body.includes("data-pid=")) {
156
+ if (body.length < CHALLENGE_MAX_BYTES && BLOCK_MARKERS.test(body) && !body.includes("data-pid=")) {
134
157
  throw new BlockedError("o site pediu uma verifica\xE7\xE3o anti-rob\xF4 (CAPTCHA); abre o auchan.pt no browser e tenta mais tarde");
135
158
  }
136
159
  }
@@ -153,7 +176,6 @@ async function fetchPage(url2, { fetchImpl = fetch, gate = throttle } = {}) {
153
176
  }
154
177
 
155
178
  // src/paths.js
156
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
157
179
  import { homedir } from "node:os";
158
180
  import { join } from "node:path";
159
181
  function resolveStatePaths(env = process.env) {
@@ -161,33 +183,14 @@ function resolveStatePaths(env = process.env) {
161
183
  return {
162
184
  stateDir,
163
185
  cacheDir: join(stateDir, "cache"),
164
- credentialFile: join(stateDir, "credentials.env"),
165
186
  cookieFile: join(stateDir, "cookies.json")
166
187
  };
167
188
  }
168
- function parseEnvFile(contents) {
169
- const parsed = {};
170
- for (const line of contents.split(/\r?\n/)) {
171
- const raw = line.trim().replace(/^export\s+/, "");
172
- const index = raw.indexOf("=");
173
- if (!raw || raw.startsWith("#") || index === -1) continue;
174
- const key = raw.slice(0, index).trim();
175
- const value = raw.slice(index + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
176
- if (/^[A-Z_][A-Z0-9_]*$/.test(key)) parsed[key] = value;
177
- }
178
- return parsed;
179
- }
180
- function loadCredentials(env = process.env) {
181
- const { credentialFile } = resolveStatePaths(env);
182
- if (!existsSync(credentialFile)) return null;
183
- const { AUCHAN_EMAIL: email3, AUCHAN_PASSWORD: password } = parseEnvFile(readFileSync2(credentialFile, "utf8"));
184
- return email3 && password ? { email: email3, password } : null;
185
- }
186
189
 
187
190
  // src/search.js
188
191
  import * as cheerio2 from "cheerio";
189
192
  import { createHash } from "node:crypto";
190
- import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
193
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
191
194
  import { join as join2 } from "node:path";
192
195
 
193
196
  // ../../node_modules/zod/v4/classic/external.js
@@ -20007,7 +20010,7 @@ var searchUrl = (query) => `${AUCHAN_BASE}/pt/pesquisa?q=${encodeURIComponent(qu
20007
20010
  var cacheFile = (cacheDir, query) => join2(cacheDir, `${createHash("sha256").update(query.trim().toLowerCase()).digest("hex").slice(0, 32)}.json`);
20008
20011
  function readCache(file2, now) {
20009
20012
  try {
20010
- const entry = JSON.parse(readFileSync3(file2, "utf8"));
20013
+ const entry = JSON.parse(readFileSync2(file2, "utf8"));
20011
20014
  if (now - Date.parse(entry.fetched_at) < CACHE_TTL_MS && Array.isArray(entry.raw)) return entry;
20012
20015
  } catch {
20013
20016
  }
@@ -20032,113 +20035,171 @@ async function searchProducts(query, limit = 10, { cacheDir, fetchImpl, now = Da
20032
20035
  }
20033
20036
 
20034
20037
  // src/session.js
20035
- import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
20038
+ import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
20036
20039
  import { dirname } from "node:path";
20037
20040
  var LOGIN_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Login-OAuthLogin?oauthProvider=Salesforce&oauthLoginTargetEndPoint=1`;
20038
- var ACCOUNT_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Account-Show`;
20039
- var EMAIL_FIELD = 'input[type="email"], input[name="username"], input[name="email"]';
20040
- var PASSWORD_FIELD = 'input[type="password"]';
20041
- var SUBMIT = 'button[type="submit"], input[type="submit"]';
20041
+ var DELIVERY_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Delivery-Header`;
20042
+ var CART_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Cart-Show`;
20043
+ var BROWSER_CHANNELS = ["msedge", "chrome", void 0];
20044
+ 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 abrir a janela de login; instala um deles ou corre "npx playwright install chromium"';
20042
20046
  function writeCookies(path, cookies) {
20043
20047
  mkdirSync2(dirname(path), { recursive: true, mode: 448 });
20044
20048
  writeFileSync2(path, JSON.stringify(cookies, null, 2), { mode: 384 });
20045
20049
  chmodSync(path, 384);
20046
20050
  }
20047
- function createSession({ credentials, cookieFile, log = console.error }) {
20048
- let browser = null;
20049
- let context = null;
20050
- async function login(page) {
20051
- log("[auth] A iniciar sess\xE3o no Auchan...");
20052
- await throttle(() => page.goto(LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 3e4 }));
20053
- await checkChallenge(page);
20054
- await page.locator(EMAIL_FIELD).first().fill(credentials.email, { timeout: 15e3 });
20055
- const password = page.locator(PASSWORD_FIELD).first();
20056
- if (!await password.isVisible().catch(() => false)) {
20057
- await throttle(() => page.locator(SUBMIT).first().click());
20058
- }
20059
- await password.fill(credentials.password, { timeout: 15e3 });
20060
- await throttle(() => page.locator(SUBMIT).first().click());
20061
- await page.waitForURL((url2) => url2.hostname === "www.auchan.pt", { timeout: 3e4 }).catch(async () => {
20062
- await checkChallenge(page);
20063
- throw new Error("o login n\xE3o terminou; confirma o email e a palavra-passe em credentials.env");
20064
- });
20065
- writeCookies(cookieFile, await context.cookies());
20066
- log("[auth] Sess\xE3o iniciada.");
20067
- }
20068
- async function checkChallenge(page) {
20069
- const captcha = await page.locator('iframe[src*="captcha"], .g-recaptcha, .h-captcha, #px-captcha').count();
20070
- if (captcha) 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");
20071
- assertNotBlocked(200, await page.content());
20051
+ function readCookies(path) {
20052
+ if (!existsSync(path)) return null;
20053
+ try {
20054
+ const cookies = JSON.parse(readFileSync3(path, "utf8"));
20055
+ return Array.isArray(cookies) && cookies.length ? cookies : null;
20056
+ } catch {
20057
+ return null;
20072
20058
  }
20073
- async function open2() {
20074
- if (context) return;
20075
- let chromium;
20076
- try {
20077
- ({ chromium } = await import("playwright"));
20078
- } catch {
20079
- throw new Error('falta o Playwright; corre "npm install" e "npx playwright install chromium" na pasta servers/auchan');
20059
+ }
20060
+ function createSession({
20061
+ cookieFile,
20062
+ log = console.error,
20063
+ loadPlaywright = () => import("playwright"),
20064
+ loginTimeoutMs = 5 * 60 * 1e3,
20065
+ pollMs = 3e3,
20066
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
20067
+ }) {
20068
+ let playwright = null;
20069
+ let api = null;
20070
+ let loggedIn = false;
20071
+ let pending = null;
20072
+ async function pw() {
20073
+ if (!playwright) {
20074
+ try {
20075
+ playwright = await loadPlaywright();
20076
+ } catch {
20077
+ throw new Error(NO_PLAYWRIGHT);
20078
+ }
20080
20079
  }
20081
- browser = await chromium.launch({ headless: process.env.AUCHAN_LOGIN_HEADLESS !== "false" });
20082
- browser.on("disconnected", () => {
20083
- browser = context = null;
20080
+ return playwright;
20081
+ }
20082
+ async function openApi(cookies) {
20083
+ await api?.dispose().catch(() => {
20084
+ });
20085
+ api = await (await pw()).request.newContext({
20086
+ userAgent: USER_AGENT,
20087
+ locale: "pt-PT",
20088
+ storageState: { cookies, origins: [] }
20084
20089
  });
20085
- context = await browser.newContext({ userAgent: USER_AGENT, locale: "pt-PT" });
20086
- if (existsSync2(cookieFile)) {
20090
+ }
20091
+ async function isLoggedIn(requester) {
20092
+ const res = await throttle(() => requester.get(CART_URL, { timeout: 15e3 }));
20093
+ assertNotBlocked(res.status(), "");
20094
+ return res.ok() && /data-authenticated="true"/.test(await res.text());
20095
+ }
20096
+ async function hasDeliveryContext(requester) {
20097
+ const res = await throttle(() => requester.get(DELIVERY_URL, { timeout: 15e3 }));
20098
+ assertNotBlocked(res.status(), "");
20099
+ return res.ok() && /data-has-context="true"/.test(await res.text());
20100
+ }
20101
+ async function launchWindow(chromium) {
20102
+ for (const channel of BROWSER_CHANNELS) {
20087
20103
  try {
20088
- await context.addCookies(JSON.parse(readFileSync4(cookieFile, "utf8")));
20104
+ return await chromium.launch({ headless: false, ...channel && { channel } });
20089
20105
  } catch {
20090
- log("[auth] N\xE3o consegui ler as cookies guardadas; vou iniciar sess\xE3o de novo.");
20091
20106
  }
20092
20107
  }
20093
- if (await isLoggedIn()) return;
20094
- const page = await context.newPage();
20108
+ throw new Error(NO_BROWSER);
20109
+ }
20110
+ async function interactiveLogin(savedCookies = null) {
20111
+ const { chromium } = await pw();
20112
+ const browser = await launchWindow(chromium);
20113
+ const closed = new Promise((resolve) => browser.on("disconnected", () => resolve("closed")));
20095
20114
  try {
20096
- await login(page);
20115
+ const context = await browser.newContext({ locale: "pt-PT" });
20116
+ if (savedCookies) await context.addCookies(savedCookies);
20117
+ const page = await context.newPage();
20118
+ log(savedCookies ? "[auth] Abri uma janela do browser: escolhe a entrega ou o levantamento no Auchan." : "[auth] Abri uma janela do browser: faz o login no Auchan e escolhe a entrega ou o levantamento.");
20119
+ await throttle(() => page.goto(savedCookies ? `${AUCHAN_BASE}/pt/` : LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 3e4 }));
20120
+ const deadline = Date.now() + loginTimeoutMs;
20121
+ while (Date.now() < deadline) {
20122
+ if (await Promise.race([closed, sleep(pollMs).then(() => "tick")]) === "closed") {
20123
+ throw new Error("a janela de login foi fechada antes de o login terminar; tenta de novo");
20124
+ }
20125
+ const url2 = new URL(page.url());
20126
+ if (url2.hostname !== "www.auchan.pt" || /login/i.test(url2.pathname)) continue;
20127
+ if (await isLoggedIn(context.request) && await hasDeliveryContext(context.request)) {
20128
+ const cookies = await context.cookies();
20129
+ writeCookies(cookieFile, cookies);
20130
+ log("[auth] Sess\xE3o iniciada.");
20131
+ return cookies;
20132
+ }
20133
+ }
20134
+ throw new Error("o login n\xE3o terminou em 5 minutos; tenta de novo");
20097
20135
  } finally {
20098
- await page.close().catch(() => {
20136
+ await browser.close().catch(() => {
20099
20137
  });
20100
20138
  }
20101
- if (!await isLoggedIn()) {
20102
- await close();
20103
- throw new Error("o login n\xE3o ficou ativo; confirma o email e a palavra-passe em credentials.env");
20104
- }
20105
20139
  }
20106
- async function isLoggedIn() {
20107
- const res = await throttle(() => context.request.get(ACCOUNT_URL, { timeout: 15e3 }));
20108
- assertNotBlocked(res.status(), "");
20109
- return res.ok() && !/login/i.test(new URL(res.url()).pathname);
20140
+ function ensureSession() {
20141
+ if (api && loggedIn) return Promise.resolve("saved");
20142
+ pending ??= (async () => {
20143
+ try {
20144
+ const saved = readCookies(cookieFile);
20145
+ if (saved) {
20146
+ await openApi(saved);
20147
+ const logged = await isLoggedIn(api);
20148
+ if (logged && await hasDeliveryContext(api)) {
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";
20156
+ }
20157
+ await openApi(await interactiveLogin());
20158
+ loggedIn = true;
20159
+ return "window";
20160
+ } finally {
20161
+ pending = null;
20162
+ }
20163
+ })();
20164
+ return pending;
20110
20165
  }
20111
- const call = (fn) => throttle(async () => {
20112
- const res = await fn();
20166
+ const call = (method, path, form) => throttle(async () => {
20167
+ const options = form ? { form, timeout: 15e3 } : { timeout: 15e3 };
20168
+ const res = await api[method](new URL(path, AUCHAN_BASE).href, options);
20113
20169
  const text2 = await res.text();
20114
20170
  assertNotBlocked(res.status(), text2);
20115
- return { status: res.status(), text: text2 };
20171
+ return { status: res.status(), text: text2, url: res.url() };
20116
20172
  });
20173
+ async function request(method, path, form) {
20174
+ await ensureSession();
20175
+ let result = await call(method, path, form);
20176
+ if (/login/i.test(new URL(result.url).pathname)) {
20177
+ loggedIn = false;
20178
+ await ensureSession();
20179
+ result = await call(method, path, form);
20180
+ 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");
20181
+ }
20182
+ return { status: result.status, text: result.text };
20183
+ }
20117
20184
  const site = {
20118
- get: (path) => scrubbed(async () => {
20119
- await open2();
20120
- return call(() => context.request.get(new URL(path, AUCHAN_BASE).href, { timeout: 15e3 }));
20121
- }),
20122
- post: (path, form) => scrubbed(async () => {
20123
- await open2();
20124
- return call(() => context.request.post(new URL(path, AUCHAN_BASE).href, { form, timeout: 15e3 }));
20125
- })
20185
+ get: (path) => request("get", path),
20186
+ post: (path, form) => request("post", path, form)
20126
20187
  };
20127
- async function scrubbed(fn) {
20128
- try {
20129
- return await fn();
20130
- } catch (error62) {
20131
- for (const secret of [credentials.email, credentials.password]) error62.message = String(error62.message).split(secret).join("***");
20132
- throw error62;
20133
- }
20188
+ async function login() {
20189
+ return ensureSession();
20134
20190
  }
20135
20191
  async function close() {
20136
- const current = browser;
20137
- browser = context = null;
20138
- if (current) await current.close().catch(() => {
20192
+ const current = api;
20193
+ api = null;
20194
+ loggedIn = false;
20195
+ await current?.dispose().catch(() => {
20139
20196
  });
20140
20197
  }
20141
- return { site, close };
20198
+ async function logout() {
20199
+ await close();
20200
+ rmSync(cookieFile, { force: true });
20201
+ }
20202
+ return { site, login, logout, close };
20142
20203
  }
20143
20204
 
20144
20205
  // src/index.js
@@ -20185,6 +20246,18 @@ var CART_TOOLS = [
20185
20246
  }
20186
20247
  }
20187
20248
  ];
20249
+ var LOGIN_TOOLS = [
20250
+ {
20251
+ name: "login",
20252
+ description: "Connect your Auchan account. Opens a browser window on the Auchan.pt login page; the person logs in there and chooses home delivery or pickup with a postal code (Auchan needs this before any cart change), then the window closes. Tell the person this before calling. The password is never seen or stored. Cart tools call this automatically when there is no session.",
20253
+ inputSchema: { type: "object", properties: {} }
20254
+ },
20255
+ {
20256
+ name: "logout",
20257
+ description: "Disconnect your Auchan account: deletes the saved session from this computer.",
20258
+ inputSchema: { type: "object", properties: {} }
20259
+ }
20260
+ ];
20188
20261
  var CLOSE_TOOL = {
20189
20262
  name: "close_session",
20190
20263
  description: "Close the browser session. Call when done to free resources.",
@@ -20212,12 +20285,10 @@ var AuchanServer = class {
20212
20285
  // `session` and `fetchImpl` are injectable so tests never touch the network or a browser.
20213
20286
  constructor({ env = process.env, session, fetchImpl } = {}) {
20214
20287
  const paths = resolveStatePaths(env);
20215
- const credentials = loadCredentials(env);
20216
20288
  this.cacheDir = paths.cacheDir;
20217
20289
  this.fetchImpl = fetchImpl;
20218
- this.cartEnabled = Boolean(credentials);
20219
- this.session = session ?? (credentials ? createSession({ credentials, cookieFile: paths.cookieFile }) : null);
20220
- this.tools = [SEARCH_TOOL, ...this.cartEnabled ? CART_TOOLS : [], CLOSE_TOOL];
20290
+ this.session = session ?? createSession({ cookieFile: paths.cookieFile });
20291
+ this.tools = [SEARCH_TOOL, ...CART_TOOLS, ...LOGIN_TOOLS, CLOSE_TOOL];
20221
20292
  this.server = new Server({ name: "auchan-mcp", version: VERSION }, { capabilities: { tools: {} } });
20222
20293
  this.server.onclose = () => {
20223
20294
  void this.session?.close();
@@ -20259,6 +20330,11 @@ var AuchanServer = class {
20259
20330
  () => quantity === 0 ? `Removido do carrinho: ${product_id}. ${CHECKOUT_NOTE}` : `Quantidade de ${product_id} no carrinho: ${quantity}. ${CHECKOUT_NOTE}`
20260
20331
  );
20261
20332
  }
20333
+ 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.");
20335
+ case "logout":
20336
+ await this.session.logout();
20337
+ return text("Sess\xE3o do Auchan terminada: apaguei a sess\xE3o guardada neste computador.");
20262
20338
  case "close_session":
20263
20339
  await this.session?.close();
20264
20340
  return text("Session closed.");
@@ -20269,7 +20345,7 @@ var AuchanServer = class {
20269
20345
  }
20270
20346
  async start() {
20271
20347
  await this.server.connect(new StdioServerTransport());
20272
- console.error(`Auchan MCP v${VERSION} started${this.cartEnabled ? "" : " (search only: no credentials file)"}`);
20348
+ console.error(`Auchan MCP v${VERSION} started`);
20273
20349
  }
20274
20350
  };
20275
20351
  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.1.0",
3
+ "version": "0.2.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,7 +28,6 @@
28
28
  "license": "MIT",
29
29
  "files": [
30
30
  "dist/",
31
- ".env.example",
32
31
  "LICENSE",
33
32
  "README.md"
34
33
  ],
package/.env.example DELETED
@@ -1,17 +0,0 @@
1
- # auchan-mcp: variáveis de ambiente
2
- # Copia as duas primeiras linhas para um ficheiro privado:
3
- # Windows: %USERPROFILE%\.auchan-mcp\credentials.env
4
- # macOS/Linux: ~/.auchan-mcp/credentials.env
5
- # Esse ficheiro nunca vai para o git. A pesquisa funciona sem ele.
6
-
7
- # Necessárias para o carrinho (get_cart, add_to_cart, update_cart_item).
8
- # AUCHAN_EMAIL=tu@exemplo.pt
9
- # AUCHAN_PASSWORD=a-tua-palavra-passe
10
-
11
- # As seguintes definem-se no "env" do servidor no cliente MCP, não no ficheiro acima.
12
-
13
- # Outra pasta para credenciais, cookies e cache (por omissão: ~/.auchan-mcp).
14
- # AUCHAN_STATE_DIR=C:\caminho\para\pasta
15
-
16
- # Põe false só para ver o browser durante o login, ao procurar um problema.
17
- # AUCHAN_LOGIN_HEADLESS=true