cooperto-mcp 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/dist/index.js +110 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -80,6 +80,26 @@ To avoid placing the token in chat, set it as a server environment variable in y
80
80
 
81
81
  > **Note:** a token written into chat or project instructions remains in plain text within the conversation context. For sensitive use, prefer the environment-variable approach.
82
82
 
83
+ ## Privacy: personal data redaction
84
+
85
+ By default, the server **hides personal data** in responses (first name, last name, phone, email, address, date of birth, etc.) to minimise exposure of sensitive information to the language model. Operational fields such as dates, party size, table and venue names, codes and statuses are kept.
86
+
87
+ To include personal data when you actually need it:
88
+
89
+ - **per call** — pass `includi_dati_personali: true` in the tool arguments, or
90
+ - **globally** — set `COOPERTO_INCLUDE_PII=1` in your client config.
91
+
92
+ ## Configuration (environment variables)
93
+
94
+ | Variable | Default | Description |
95
+ |----------|---------|-------------|
96
+ | `COOPERTO_API_TOKEN` | — | Default Bearer token (optional; can also be set per session or per call). |
97
+ | `COOPERTO_INCLUDE_PII` | `0` | Set to `1` to include personal data in responses. |
98
+ | `COOPERTO_TIMEOUT_MS` | `30000` | Per-request timeout, in milliseconds. |
99
+ | `COOPERTO_MAX_RETRIES` | `2` | Automatic retries on timeouts, rate limits (429) and server errors (5xx). |
100
+ | `COOPERTO_BASE_URL` | API default | Override the API base URL. |
101
+ | `COOPERTO_IPV4_ONLY` | `0` | Set to `1` to force IPv4 (useful on networks without IPv6 routing). |
102
+
83
103
  ## Available endpoint groups
84
104
 
85
105
  | Group | Example tools |
package/dist/index.js CHANGED
@@ -52,6 +52,80 @@ const HOST = spec.host ?? "api.cooperto.it";
52
52
  const BASE_PATH = spec.basePath ?? "";
53
53
  const DEFAULT_BASE = `${SCHEMES.includes("https") ? "https" : SCHEMES[0]}://${HOST}${BASE_PATH}`;
54
54
  const BASE_URL = (process.env.COOPERTO_BASE_URL?.trim() || DEFAULT_BASE).replace(/\/$/, "");
55
+ // ---------------------------------------------------------------------------
56
+ // Timeout + retry (gestisce timeout e chiamate ripetute/transitorie)
57
+ // ---------------------------------------------------------------------------
58
+ const TIMEOUT_MS = Number(process.env.COOPERTO_TIMEOUT_MS) > 0
59
+ ? Number(process.env.COOPERTO_TIMEOUT_MS)
60
+ : 30_000;
61
+ const MAX_RETRIES = Number.isFinite(Number(process.env.COOPERTO_MAX_RETRIES))
62
+ ? Math.max(0, Number(process.env.COOPERTO_MAX_RETRIES))
63
+ : 2;
64
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
65
+ /**
66
+ * fetch con timeout per-richiesta e retry esponenziale sugli errori transitori
67
+ * (timeout di rete, 429 rate limit, 5xx). Gli errori 4xx "veri" non vengono
68
+ * ritentati. Questo riduce i fallimenti quando i tool vengono chiamati molte
69
+ * volte di fila o l'API risponde lentamente.
70
+ */
71
+ async function fetchWithRetry(url, init) {
72
+ let lastErr;
73
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
74
+ const ctrl = new AbortController();
75
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
76
+ try {
77
+ const res = await fetch(url, { ...init, signal: ctrl.signal });
78
+ clearTimeout(timer);
79
+ if ((res.status === 429 || res.status >= 500) && attempt < MAX_RETRIES) {
80
+ const retryAfter = Number(res.headers.get("retry-after"));
81
+ const wait = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt;
82
+ await sleep(wait);
83
+ continue;
84
+ }
85
+ return res;
86
+ }
87
+ catch (err) {
88
+ clearTimeout(timer);
89
+ lastErr = err;
90
+ if (attempt < MAX_RETRIES) {
91
+ await sleep(500 * 2 ** attempt);
92
+ continue;
93
+ }
94
+ throw err;
95
+ }
96
+ }
97
+ throw lastErr;
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Redazione dati personali (privacy by default)
101
+ // ---------------------------------------------------------------------------
102
+ // Per impostazione predefinita i campi personali nelle risposte vengono nascosti.
103
+ // Disattivabile globalmente con COOPERTO_INCLUDE_PII=1, oppure per singola
104
+ // chiamata passando includi_dati_personali=true.
105
+ const INCLUDE_PII_DEFAULT = /^(1|true|yes)$/i.test(process.env.COOPERTO_INCLUDE_PII?.trim() ?? "");
106
+ const PII_FIELDS = new Set([
107
+ "nome", "cognome", "telefono", "cellulare", "email", "datadinascita", "datanascita",
108
+ "indirizzo", "citta", "città", "cap", "provincia", "nazione", "codicefiscale",
109
+ ]);
110
+ const PII_PLACEHOLDER = "[dato personale nascosto]";
111
+ /** Maschera (in modo ricorsivo) i valori dei campi personali noti. */
112
+ function redactPII(node) {
113
+ if (Array.isArray(node))
114
+ return node.map(redactPII);
115
+ if (node && typeof node === "object") {
116
+ const out = {};
117
+ for (const [k, v] of Object.entries(node)) {
118
+ if (PII_FIELDS.has(k.toLowerCase()) && v !== null && v !== undefined && v !== "") {
119
+ out[k] = PII_PLACEHOLDER;
120
+ }
121
+ else {
122
+ out[k] = redactPII(v);
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+ return node;
128
+ }
55
129
  // --- gestione $ref (Swagger usa #/definitions, JSON Schema usa #/$defs) ------
56
130
  function collectRefs(node, used) {
57
131
  if (Array.isArray(node)) {
@@ -192,6 +266,14 @@ for (const [path, methods] of Object.entries(spec.paths)) {
192
266
  "Se omesso, viene usato il token impostato con cooperto_imposta_token, o quello di default del server.",
193
267
  };
194
268
  }
269
+ // Flag opzionale per includere i dati personali (di default nascosti).
270
+ if (!("includi_dati_personali" in properties)) {
271
+ properties.includi_dati_personali = {
272
+ type: "boolean",
273
+ description: "(Opzionale) Se true, include nella risposta i dati personali (nome, cognome, telefono, email, " +
274
+ "indirizzo, data di nascita, ecc.). Di default questi campi sono nascosti per privacy.",
275
+ };
276
+ }
195
277
  const inputSchema = { type: "object", properties };
196
278
  if (required.length)
197
279
  inputSchema.required = required;
@@ -216,7 +298,7 @@ const toolByName = new Map(tools.map((t) => [t.name, t]));
216
298
  // ---------------------------------------------------------------------------
217
299
  // Server MCP
218
300
  // ---------------------------------------------------------------------------
219
- const server = new Server({ name: "cooperto-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
301
+ const server = new Server({ name: "cooperto-mcp", version: "0.1.3" }, { capabilities: { tools: {} } });
220
302
  const setTokenToolDef = {
221
303
  name: SET_TOKEN_TOOL,
222
304
  description: "Imposta il token Bearer Cooperto da usare per le chiamate successive di questa sessione. " +
@@ -322,26 +404,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
322
404
  body = JSON.stringify(args.body);
323
405
  headers["Content-Type"] = "application/json";
324
406
  }
407
+ // Decide se includere i dati personali: override per-chiamata, poi default globale.
408
+ const includePii = args.includi_dati_personali === true
409
+ ? true
410
+ : args.includi_dati_personali === false
411
+ ? false
412
+ : INCLUDE_PII_DEFAULT;
325
413
  try {
326
- const res = await fetch(url, { method: tool.method.toUpperCase(), headers, body });
414
+ const res = await fetchWithRetry(url, { method: tool.method.toUpperCase(), headers, body });
327
415
  const text = await res.text();
328
416
  let pretty = text;
417
+ let redacted = false;
329
418
  try {
330
- pretty = JSON.stringify(JSON.parse(text), null, 2);
419
+ let parsed = JSON.parse(text);
420
+ if (!includePii) {
421
+ parsed = redactPII(parsed);
422
+ redacted = true;
423
+ }
424
+ pretty = JSON.stringify(parsed, null, 2);
331
425
  }
332
426
  catch {
333
427
  /* non-JSON: lascio com'è */
334
428
  }
429
+ const note = redacted
430
+ ? "\n\n(Nota: i dati personali sono nascosti per privacy. Per includerli, passa " +
431
+ "includi_dati_personali=true in questa chiamata oppure imposta COOPERTO_INCLUDE_PII=1 nel server.)"
432
+ : "";
335
433
  return {
336
- content: [{ type: "text", text: `HTTP ${res.status} ${res.statusText}\n${pretty}` }],
434
+ content: [{ type: "text", text: `HTTP ${res.status} ${res.statusText}\n${pretty}${note}` }],
337
435
  isError: !res.ok,
338
436
  };
339
437
  }
340
438
  catch (err) {
341
439
  const e = err;
440
+ const isTimeout = e?.name === "AbortError" || e?.code === "ABORT_ERR";
342
441
  const cause = e?.cause ? ` (causa: ${e.cause.code || e.cause.message})` : "";
442
+ const msg = isTimeout
443
+ ? `Timeout dopo ${TIMEOUT_MS} ms e ${MAX_RETRIES + 1} tentativi chiamando ${url}. ` +
444
+ "Riprova, oppure aumenta COOPERTO_TIMEOUT_MS / COOPERTO_MAX_RETRIES nel server."
445
+ : `Errore di rete chiamando ${url}: ${e?.message}${cause}`;
343
446
  return {
344
- content: [{ type: "text", text: `Errore di rete chiamando ${url}: ${e?.message}${cause}` }],
447
+ content: [{ type: "text", text: msg }],
345
448
  isError: true,
346
449
  };
347
450
  }
@@ -354,7 +457,8 @@ async function main() {
354
457
  await server.connect(transport);
355
458
  // stderr non interferisce col protocollo stdio
356
459
  console.error(`[cooperto-mcp] pronto — ${tools.length} tool generati (+ ${SET_TOKEN_TOOL}), base URL ${BASE_URL}, ` +
357
- `token di default ${ENV_TOKEN ? "presente" : "assente"} — token multi-cliente via tool/per-chiamata`);
460
+ `token di default ${ENV_TOKEN ? "presente" : "assente"} — token multi-cliente via tool/per-chiamata, ` +
461
+ `timeout ${TIMEOUT_MS}ms / ${MAX_RETRIES} retry, dati personali ${INCLUDE_PII_DEFAULT ? "INCLUSI" : "nascosti di default"}`);
358
462
  }
359
463
  main().catch((e) => {
360
464
  console.error("[cooperto-mcp] errore fatale:", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cooperto-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Model Context Protocol (MCP) server for the Cooperto.it API — bookings, contacts/loyalty, queues and venues. Spec-driven: one tool per OpenAPI operation.",
5
5
  "type": "module",
6
6
  "license": "MIT",