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.
- package/README.md +20 -0
- package/dist/index.js +110 -6
- 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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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",
|