@frihet/mcp-server 1.16.6 → 2.0.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.
Files changed (108) hide show
  1. package/README.md +168 -82
  2. package/dist/agent-onboarding.d.ts +98 -0
  3. package/dist/agent-onboarding.js +275 -0
  4. package/dist/api-origin.d.ts +9 -0
  5. package/dist/api-origin.js +41 -0
  6. package/dist/canary/mcp-harness.d.ts +77 -0
  7. package/dist/canary/mcp-harness.js +552 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +262 -0
  10. package/dist/claude-candidate-contract.d.ts +482 -0
  11. package/dist/claude-candidate-contract.js +87 -0
  12. package/dist/claude-output-privacy.d.ts +17 -0
  13. package/dist/claude-output-privacy.js +240 -0
  14. package/dist/claude-profile.d.ts +18 -0
  15. package/dist/claude-profile.js +260 -0
  16. package/dist/client-interface.d.ts +47 -33
  17. package/dist/client.d.ts +172 -40
  18. package/dist/client.js +708 -165
  19. package/dist/demo-client.d.ts +41 -32
  20. package/dist/demo-client.js +202 -31
  21. package/dist/demo-fixtures.d.ts +3 -3
  22. package/dist/demo-fixtures.js +7 -7
  23. package/dist/deposit-contract.d.ts +29 -0
  24. package/dist/deposit-contract.js +76 -0
  25. package/dist/fetch-no-redirect.d.ts +3 -0
  26. package/dist/fetch-no-redirect.js +16 -0
  27. package/dist/fiscal-aliases.d.ts +44 -0
  28. package/dist/fiscal-aliases.js +62 -0
  29. package/dist/fiscal-period.d.ts +26 -0
  30. package/dist/fiscal-period.js +31 -0
  31. package/dist/google-profile.d.ts +19 -0
  32. package/dist/google-profile.js +122 -0
  33. package/dist/index.js +68 -59
  34. package/dist/logger.d.ts +2 -2
  35. package/dist/logger.js +140 -8
  36. package/dist/muse-profile.d.ts +19 -0
  37. package/dist/muse-profile.js +122 -0
  38. package/dist/observability.d.ts +18 -29
  39. package/dist/observability.js +194 -106
  40. package/dist/openai-profile.d.ts +26 -22
  41. package/dist/openai-profile.js +1193 -157
  42. package/dist/openai-review-contract.d.ts +15 -6
  43. package/dist/openai-review-contract.js +714 -41
  44. package/dist/openai-review-oauth.d.ts +149 -2
  45. package/dist/openai-review-oauth.js +220 -6
  46. package/dist/prompts/register-all.d.ts +2 -0
  47. package/dist/prompts/register-all.js +34 -78
  48. package/dist/public-capability-contract.d.ts +39 -0
  49. package/dist/public-capability-contract.js +186 -0
  50. package/dist/redaction.d.ts +15 -0
  51. package/dist/redaction.js +83 -0
  52. package/dist/resources/register-all.js +154 -64
  53. package/dist/retry-after.d.ts +19 -0
  54. package/dist/retry-after.js +84 -0
  55. package/dist/server-composition.d.ts +19 -0
  56. package/dist/server-composition.js +74 -0
  57. package/dist/tool-exposure.d.ts +31 -27
  58. package/dist/tool-exposure.js +211 -87
  59. package/dist/tools/accountingClose.d.ts +14 -5
  60. package/dist/tools/accountingClose.js +41 -23
  61. package/dist/tools/audit_gl.js +8 -8
  62. package/dist/tools/backend-availability.d.ts +8 -29
  63. package/dist/tools/backend-availability.js +31 -30
  64. package/dist/tools/bank_rules.js +66 -15
  65. package/dist/tools/banking.js +5 -13
  66. package/dist/tools/clients.js +261 -15
  67. package/dist/tools/crm.js +22 -21
  68. package/dist/tools/deposits.js +114 -85
  69. package/dist/tools/einvoice.d.ts +16 -2
  70. package/dist/tools/einvoice.js +59 -12
  71. package/dist/tools/expenses.d.ts +16 -0
  72. package/dist/tools/expenses.js +47 -27
  73. package/dist/tools/fiscal.d.ts +3 -4
  74. package/dist/tools/fiscal.js +93 -60
  75. package/dist/tools/gestoria.js +15 -7
  76. package/dist/tools/hr.d.ts +2 -3
  77. package/dist/tools/hr.js +49 -20
  78. package/dist/tools/igic.d.ts +9 -8
  79. package/dist/tools/igic.js +34 -64
  80. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  81. package/dist/tools/impuesto_sociedades.js +16 -37
  82. package/dist/tools/intelligence.js +166 -12
  83. package/dist/tools/invoices.js +224 -54
  84. package/dist/tools/kitchen.js +19 -9
  85. package/dist/tools/payroll.d.ts +5 -8
  86. package/dist/tools/payroll.js +19 -32
  87. package/dist/tools/permissions.d.ts +4 -7
  88. package/dist/tools/permissions.js +11 -14
  89. package/dist/tools/products.js +6 -6
  90. package/dist/tools/quotes.js +172 -26
  91. package/dist/tools/recurring.js +26 -25
  92. package/dist/tools/register-all.d.ts +2 -2
  93. package/dist/tools/register-all.js +9 -6
  94. package/dist/tools/search.d.ts +19 -0
  95. package/dist/tools/search.js +84 -0
  96. package/dist/tools/shared.d.ts +1445 -109
  97. package/dist/tools/shared.js +478 -102
  98. package/dist/tools/team.js +24 -28
  99. package/dist/tools/time.js +4 -12
  100. package/dist/tools/vendors.js +249 -15
  101. package/dist/tools/webhooks.js +35 -30
  102. package/dist/types.d.ts +31 -5
  103. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  104. package/dist/workflows/daily-operational-brief.js +881 -0
  105. package/docs/agent-onboarding.json +391 -0
  106. package/package.json +49 -12
  107. package/dist/tools/onboarding.d.ts +0 -19
  108. package/dist/tools/onboarding.js +0 -64
@@ -13,6 +13,171 @@ const addressSchema = z
13
13
  })
14
14
  .optional()
15
15
  .describe("Client address / Direccion del cliente");
16
+ /**
17
+ * Splits a concatenated / camelCase / snake_case / kebab-case search query into
18
+ * space-separated tokens (e.g. "CanariasBlue" -> "Canarias Blue",
19
+ * "SLCanarias" -> "SL Canarias", "Canarias_Blue" -> "Canarias Blue").
20
+ */
21
+ function splitCompoundQuery(q) {
22
+ return q
23
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
24
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
25
+ .replace(/[_-]+/g, " ")
26
+ .replace(/\s+/g, " ")
27
+ .trim();
28
+ }
29
+ const OPTIONAL_CLIENT_STRING_FIELDS = [
30
+ "email",
31
+ "phone",
32
+ "stage",
33
+ "taxId",
34
+ "createdAt",
35
+ "updatedAt",
36
+ ];
37
+ /**
38
+ * Normalizes a client record returned by the Frihet API so it always satisfies
39
+ * `clientItemOutput` without losing ERP-canonical data:
40
+ * - Strips `null` on optional string fields (`email`, `phone`, `stage`, `taxId`,
41
+ * `createdAt`, `updatedAt`) so `z.string().optional()` never rejects legacy docs.
42
+ * - Backfills `taxId` from legacy `nif` / `cif` / `vatNumber` / `tax_id` when
43
+ * `taxId` is absent or null, preserving all original fields.
44
+ * - Converts legacy string `address` into `{ street }`.
45
+ * - Omits `null` or empty `address` (which otherwise fails `addressOutputSchema`).
46
+ * - Maps ERP canonical address keys (`province` -> `state`, `zip` -> `postalCode`,
47
+ * `countryCode` -> `country`) because `publicApi.ts` (`normalizeAddress`) stores
48
+ * `zip` and `province`, whereas `addressOutputSchema` only declares `postalCode`
49
+ * and `state` (and strips unknown keys in `structuredContent`).
50
+ */
51
+ function normalizeClientRecord(clientRecord) {
52
+ if (!clientRecord || typeof clientRecord !== "object" || Array.isArray(clientRecord)) {
53
+ return clientRecord;
54
+ }
55
+ const out = { ...clientRecord };
56
+ for (const field of OPTIONAL_CLIENT_STRING_FIELDS) {
57
+ if (out[field] === null) {
58
+ delete out[field];
59
+ }
60
+ }
61
+ if (typeof out.taxId !== "string" || !out.taxId.trim()) {
62
+ for (const alias of ["nif", "cif", "vatNumber", "tax_id"]) {
63
+ const candidate = out[alias];
64
+ if (typeof candidate === "string" && candidate.trim()) {
65
+ out.taxId = candidate.trim();
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ const rawAddress = out.address;
71
+ const rootStreet = typeof out.street === "string" && out.street.trim() ? out.street.trim() : undefined;
72
+ const rootCity = typeof out.city === "string" && out.city.trim() ? out.city.trim() : undefined;
73
+ const rootState = typeof out.state === "string" && out.state.trim()
74
+ ? out.state.trim()
75
+ : typeof out.province === "string" && out.province.trim()
76
+ ? out.province.trim()
77
+ : undefined;
78
+ const rootPostalCode = typeof out.postalCode === "string" && out.postalCode.trim()
79
+ ? out.postalCode.trim()
80
+ : typeof out.zip === "string" && out.zip.trim()
81
+ ? out.zip.trim()
82
+ : undefined;
83
+ const rootCountry = typeof out.country === "string" && out.country.trim()
84
+ ? out.country.trim()
85
+ : typeof out.countryCode === "string" && out.countryCode.trim()
86
+ ? out.countryCode.trim()
87
+ : undefined;
88
+ const buildFlatAddress = (streetOverride) => {
89
+ const built = {};
90
+ const st = streetOverride ?? rootStreet;
91
+ if (st !== undefined)
92
+ built.street = st;
93
+ if (rootCity !== undefined)
94
+ built.city = rootCity;
95
+ if (rootState !== undefined)
96
+ built.state = rootState;
97
+ if (rootPostalCode !== undefined)
98
+ built.postalCode = rootPostalCode;
99
+ if (rootCountry !== undefined)
100
+ built.country = rootCountry;
101
+ return Object.keys(built).length > 0 ? built : undefined;
102
+ };
103
+ if (rawAddress === undefined || rawAddress === null) {
104
+ if (rawAddress === null) {
105
+ delete out.address;
106
+ }
107
+ const flatAddr = buildFlatAddress();
108
+ if (flatAddr) {
109
+ out.address = flatAddr;
110
+ }
111
+ return out;
112
+ }
113
+ if (typeof rawAddress === "string") {
114
+ const trimmed = rawAddress.trim();
115
+ const flatAddr = buildFlatAddress(trimmed || undefined);
116
+ if (!flatAddr) {
117
+ delete out.address;
118
+ return out;
119
+ }
120
+ out.address = flatAddr;
121
+ return out;
122
+ }
123
+ if (typeof rawAddress === "object" && !Array.isArray(rawAddress)) {
124
+ const addrObj = rawAddress;
125
+ const normalizedAddress = {};
126
+ for (const [k, v] of Object.entries(addrObj)) {
127
+ if (v !== null && v !== undefined) {
128
+ normalizedAddress[k] = v;
129
+ }
130
+ }
131
+ const street = typeof addrObj.street === "string" && addrObj.street.trim()
132
+ ? addrObj.street.trim()
133
+ : rootStreet;
134
+ const city = typeof addrObj.city === "string" && addrObj.city.trim()
135
+ ? addrObj.city.trim()
136
+ : rootCity;
137
+ const state = typeof addrObj.state === "string" && addrObj.state.trim()
138
+ ? addrObj.state.trim()
139
+ : typeof addrObj.province === "string" && addrObj.province.trim()
140
+ ? addrObj.province.trim()
141
+ : rootState;
142
+ const postalCode = typeof addrObj.postalCode === "string" && addrObj.postalCode.trim()
143
+ ? addrObj.postalCode.trim()
144
+ : typeof addrObj.zip === "string" && addrObj.zip.trim()
145
+ ? addrObj.zip.trim()
146
+ : rootPostalCode;
147
+ const country = typeof addrObj.country === "string" && addrObj.country.trim()
148
+ ? addrObj.country.trim()
149
+ : typeof addrObj.countryCode === "string" && addrObj.countryCode.trim()
150
+ ? addrObj.countryCode.trim()
151
+ : rootCountry;
152
+ if (street !== undefined)
153
+ normalizedAddress.street = street;
154
+ else
155
+ delete normalizedAddress.street;
156
+ if (city !== undefined)
157
+ normalizedAddress.city = city;
158
+ else
159
+ delete normalizedAddress.city;
160
+ if (state !== undefined)
161
+ normalizedAddress.state = state;
162
+ else
163
+ delete normalizedAddress.state;
164
+ if (postalCode !== undefined)
165
+ normalizedAddress.postalCode = postalCode;
166
+ else
167
+ delete normalizedAddress.postalCode;
168
+ if (country !== undefined)
169
+ normalizedAddress.country = country;
170
+ else
171
+ delete normalizedAddress.country;
172
+ if (Object.keys(normalizedAddress).length === 0) {
173
+ delete out.address;
174
+ return out;
175
+ }
176
+ out.address = normalizedAddress;
177
+ return out;
178
+ }
179
+ return out;
180
+ }
16
181
  export function registerClientTools(server, client) {
17
182
  // -- list_clients --
18
183
  server.registerTool("list_clients", {
@@ -44,10 +209,91 @@ export function registerClientTools(server, client) {
44
209
  },
45
210
  outputSchema: paginatedOutput(clientItemOutput, { projectable: true }),
46
211
  }, async ({ limit, offset, q, stage, fields, after }) => withToolLogging("list_clients", async () => {
47
- const result = await client.listClients({ limit, offset, after, fields, q, stage });
212
+ const trimmedQ = q?.trim();
213
+ const trimmedAfter = after?.trim();
214
+ // Limitation in Frihet-ERP `listResources` (publicApi.ts L8158-8268):
215
+ // When `search` (`q`) is provided, the backend slices results by `offset`/`limit`
216
+ // and returns before evaluating `cursorParam` (`after`). Moreover, protected
217
+ // profiles (Claude `claudeToolResult` and OpenAI `applyOpenAIProfile`) rebuild
218
+ // `content` from `structuredContent` while `paginatedOutput` is frozen, so a
219
+ // text-only note would be silently discarded. Fail closed (`isError: true`
220
+ // without `structuredContent`) so no profile mistakes page 1 for a cursor page.
221
+ if (trimmedQ && trimmedAfter) {
222
+ return {
223
+ content: [{
224
+ type: "text",
225
+ text: "Error: text search ('q') cannot be combined with cursor pagination ('after') because the ERP backend slices search results by 'offset'/'limit' and ignores 'after'. Use 'offset' and 'limit' when searching with 'q', or omit 'q' when paginating with 'after'. / " +
226
+ "Error: la busqueda textual ('q') no se puede combinar con el cursor 'after' porque el backend pagina las busquedas mediante 'offset'/'limit' e ignora 'after'. Usa 'offset' y 'limit' con 'q', u omite 'q' al paginar con 'after'.",
227
+ }],
228
+ isError: true,
229
+ };
230
+ }
231
+ let result = await client.listClients({ limit, offset, after, fields, q, stage });
232
+ // When a concatenated / camelCase query (e.g. "CanariasBlue") yields zero total matches,
233
+ // retry with split tokens ("Canarias Blue") using the same offset/limit/fields/stage.
234
+ // Guardrails matching publicApi.ts `listResources`:
235
+ // 1. Never fallback when `after` cursor is supplied with `q`.
236
+ // 2. Never fallback when the first scan hit the 500-document cap (`truncated === true`),
237
+ // since total===0 only reflects the first 500 scanned records.
238
+ // 3. On page 2+ (`offset > 0`), only fallback when `result.total === 0` explicitly (not
239
+ // when `total === undefined`, which indicates an exhausted offset).
240
+ let usedFallback = false;
241
+ const splitQ = trimmedQ ? splitCompoundQuery(trimmedQ) : undefined;
242
+ const isSubsequentPage = (offset !== undefined && offset > 0) || Boolean(after);
243
+ const initialTruncated = result?.truncated === true;
244
+ const canFallback = splitQ !== undefined && splitQ !== trimmedQ && !after && !initialTruncated;
245
+ const hasZeroTotalMatches = Array.isArray(result?.data) &&
246
+ result.data.length === 0 &&
247
+ (isSubsequentPage ? result.total === 0 : result.total === 0 || result.total === undefined);
248
+ if (canFallback && hasZeroTotalMatches) {
249
+ try {
250
+ const fallbackResult = await client.listClients({ limit, offset, after, fields, q: splitQ, stage });
251
+ const fallbackTruncated = fallbackResult?.truncated === true;
252
+ if (fallbackTruncated ||
253
+ (fallbackResult?.data && (fallbackResult.data.length > 0 || (fallbackResult.total !== undefined && fallbackResult.total > 0)))) {
254
+ result = fallbackResult;
255
+ usedFallback = true;
256
+ }
257
+ }
258
+ catch {
259
+ // Preserve original response if fallback fails
260
+ }
261
+ }
262
+ const rawEnvelope = (result ?? {});
263
+ const isTruncated = rawEnvelope.truncated === true;
264
+ const normalizedData = Array.isArray(result?.data)
265
+ ? result.data.map((c) => (c && typeof c === "object" ? normalizeClientRecord(c) : c))
266
+ : [];
267
+ // Conservative refusal when the ERP 500-document scan cap was saturated (`truncated: true`).
268
+ // Because `paginatedOutput` is a frozen schema contract and both Claude (`claudeToolResult`)
269
+ // and OpenAI (`applyOpenAIProfile`) overwrite `content` whenever `structuredContent` is
270
+ // present, returning `structuredContent` here would hide the truncation warning and present
271
+ // an incomplete 500-doc scan as exhaustive.
272
+ if (isTruncated) {
273
+ const partialCount = typeof result?.total === "number" ? result.total : normalizedData.length;
274
+ return {
275
+ content: [{
276
+ type: "text",
277
+ text: `Error: client search hit the ERP 500-document scan cap (truncated=true; ${partialCount} match(es) within the first 500 scanned records) and cannot be returned as an exhaustive result across protected profiles. Narrow 'q' (or filter by 'stage') or paginate without 'q' using 'after'. / ` +
278
+ `Error: la busqueda de clientes alcanzo el limite de escaneo de 500 documentos del ERP (truncated=true; ${partialCount} coincidencia(s) en los primeros 500 registros) y no puede devolverse como resultado exhaustivo. Acota 'q' (o filtra por 'stage') o pagina sin 'q' usando 'after'.`,
279
+ }],
280
+ isError: true,
281
+ };
282
+ }
283
+ const cleanEnvelope = {
284
+ data: normalizedData,
285
+ total: typeof result?.total === "number" ? result.total : normalizedData.length,
286
+ limit: typeof result?.limit === "number" ? result.limit : (limit ?? 20),
287
+ offset: typeof result?.offset === "number" ? result.offset : (offset ?? 0),
288
+ ...(typeof result?.nextCursor === "string" ? { nextCursor: result.nextCursor } : {}),
289
+ };
290
+ const notes = [];
291
+ if (usedFallback) {
292
+ notes.push(` (criterio aplicado: "${splitQ}")`);
293
+ }
48
294
  return {
49
- content: [listContent(formatPaginatedResponse("clients", result))],
50
- structuredContent: result,
295
+ content: [listContent(formatPaginatedResponse("clients", cleanEnvelope) + notes.join(""))],
296
+ structuredContent: cleanEnvelope,
51
297
  };
52
298
  }));
53
299
  // -- get_client --
@@ -61,7 +307,7 @@ export function registerClientTools(server, client) {
61
307
  },
62
308
  outputSchema: clientItemOutput,
63
309
  }, async ({ id }) => withToolLogging("get_client", async () => {
64
- const result = await client.getClient(id);
310
+ const result = normalizeClientRecord(await client.getClient(id));
65
311
  return {
66
312
  content: [getContent(formatRecord("Client", result))],
67
313
  structuredContent: result,
@@ -77,19 +323,19 @@ export function registerClientTools(server, client) {
77
323
  "Los clientes se usan al crear facturas y presupuestos.",
78
324
  annotations: CREATE_ANNOTATIONS,
79
325
  inputSchema: {
80
- name: z.string().describe("Client/company name / Nombre del cliente o empresa"),
81
- email: z.string().optional().describe("Email address / Correo electronico"),
82
- phone: z.string().optional().describe("Phone number / Telefono"),
326
+ name: z.string().trim().min(1).max(200).describe("Client/company name / Nombre del cliente o empresa"),
327
+ email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
328
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
83
329
  taxId: z.string().optional().describe("Tax ID (NIF/CIF/VAT) / NIF o CIF"),
84
330
  address: addressSchema,
85
331
  },
86
332
  outputSchema: clientItemOutput,
87
333
  }, async (input) => withToolLogging("create_client", async () => {
88
- const result = await client.createClient(input);
334
+ const result = normalizeClientRecord(await client.createClient(input));
89
335
  const hints = enrichResponse("clients", "create", result);
90
336
  return {
91
- content: [mutateContent(formatRecord("Client created", result))],
92
- structuredContent: { ...result, ...hints },
337
+ content: [mutateContent(formatRecord("Client created", result) + hints)],
338
+ structuredContent: { ...result },
93
339
  };
94
340
  }));
95
341
  // -- update_client --
@@ -100,16 +346,16 @@ export function registerClientTools(server, client) {
100
346
  "/ Actualiza un cliente existente. Solo se modifican los campos proporcionados.",
101
347
  annotations: UPDATE_ANNOTATIONS,
102
348
  inputSchema: {
103
- id: z.string().describe("Client ID / ID del cliente"),
104
- name: z.string().optional().describe("Name / Nombre"),
105
- email: z.string().optional().describe("Email / Correo"),
106
- phone: z.string().optional().describe("Phone / Telefono"),
349
+ id: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
350
+ name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
351
+ email: z.string().trim().email().max(255).optional().describe("Email / Correo"),
352
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone / Telefono"),
107
353
  taxId: z.string().optional().describe("Tax ID / NIF/CIF"),
108
354
  address: addressSchema,
109
355
  },
110
356
  outputSchema: clientItemOutput,
111
357
  }, async ({ id, ...data }) => withToolLogging("update_client", async () => {
112
- const result = await client.updateClient(id, data);
358
+ const result = normalizeClientRecord(await client.updateClient(id, data));
113
359
  return {
114
360
  content: [mutateContent(formatRecord("Client updated", result))],
115
361
  structuredContent: result,
package/dist/tools/crm.js CHANGED
@@ -37,11 +37,11 @@ export function registerCrmTools(server, client) {
37
37
  "/ Anade un nuevo contacto a un cliente. Requiere al menos un nombre.",
38
38
  annotations: CREATE_ANNOTATIONS,
39
39
  inputSchema: {
40
- clientId: z.string().describe("Client ID / ID del cliente"),
41
- name: z.string().describe("Contact name / Nombre del contacto"),
42
- email: z.string().optional().describe("Email address / Correo electronico"),
43
- phone: z.string().optional().describe("Phone number / Telefono"),
44
- role: z.string().optional().describe("Role or title (e.g. CEO, CTO, Billing) / Cargo o rol"),
40
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
41
+ name: z.string().trim().min(1).max(200).describe("Contact name / Nombre del contacto"),
42
+ email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
43
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
44
+ role: z.string().trim().min(1).max(200).optional().describe("Role or title (e.g. CEO, CTO, Billing) / Cargo o rol"),
45
45
  isPrimary: z.boolean().optional().describe("Whether this is the primary contact / Si es el contacto principal"),
46
46
  },
47
47
  outputSchema: contactItemOutput,
@@ -49,8 +49,8 @@ export function registerCrmTools(server, client) {
49
49
  const result = await client.createClientContact(clientId, data);
50
50
  const hints = enrichResponse("contacts", "create", result);
51
51
  return {
52
- content: [mutateContent(formatRecord("Contact created", result))],
53
- structuredContent: { ...result, ...hints },
52
+ content: [mutateContent(formatRecord("Contact created", result) + hints)],
53
+ structuredContent: { ...result },
54
54
  };
55
55
  }));
56
56
  // -- delete_client_contact --
@@ -60,8 +60,8 @@ export function registerCrmTools(server, client) {
60
60
  "/ Elimina permanentemente un contacto de un cliente. Esta accion no se puede deshacer.",
61
61
  annotations: DELETE_ANNOTATIONS,
62
62
  inputSchema: {
63
- clientId: z.string().describe("Client ID / ID del cliente"),
64
- contactId: z.string().describe("Contact ID / ID del contacto"),
63
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
64
+ contactId: z.string().trim().min(1).max(128).describe("Contact ID / ID del contacto"),
65
65
  },
66
66
  outputSchema: deleteResultOutput,
67
67
  }, async ({ clientId, contactId }) => withToolLogging("delete_client_contact", async () => {
@@ -82,7 +82,7 @@ export function registerCrmTools(server, client) {
82
82
  "/ Lista todas las actividades CRM de un cliente con paginacion opcional.",
83
83
  annotations: READ_ONLY_ANNOTATIONS,
84
84
  inputSchema: {
85
- clientId: z.string().describe("Client ID / ID del cliente"),
85
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
86
86
  limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
87
87
  offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
88
88
  },
@@ -98,18 +98,18 @@ export function registerCrmTools(server, client) {
98
98
  server.registerTool("log_client_activity", {
99
99
  title: "Log Client Activity",
100
100
  description: "Log a CRM activity against a client. Use to track calls, emails, meetings, or tasks. " +
101
+ "Do not include passwords, credentials, payment-card data, health data, or official/government identifiers in free-text fields. " +
101
102
  "Example: clientId='abc123', type='call', title='Discussed Q2 proposal', description='Client interested in upgrade' " +
102
- "/ Registra una actividad CRM para un cliente. Usa para rastrear llamadas, emails, reuniones o tareas.",
103
+ "/ Registra una actividad CRM para un cliente. No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales en los campos de texto libre.",
103
104
  annotations: CREATE_ANNOTATIONS,
104
- inputSchema: {
105
+ inputSchema: z.object({
105
106
  clientId: z.string().describe("Client ID / ID del cliente"),
106
107
  type: z
107
108
  .enum(["call", "email", "meeting", "task"])
108
109
  .describe("Activity type / Tipo de actividad"),
109
- title: z.string().describe("Activity title / Titulo de la actividad"),
110
- description: z.string().optional().describe("Detailed description / Descripcion detallada"),
111
- date: z.string().optional().describe("Activity date (ISO 8601, defaults to now) / Fecha de la actividad"),
112
- },
110
+ title: z.string().trim().min(1).max(500).describe("Activity title / Titulo de la actividad"),
111
+ description: z.string().trim().min(1).max(5000).optional().describe("Detailed description; exclude secrets, credentials, card or health data, and official identifiers / Descripcion detallada; excluye secretos, credenciales, datos de tarjetas o salud e identificadores oficiales"),
112
+ }).strict(),
113
113
  outputSchema: activityItemOutput,
114
114
  }, async ({ clientId, ...data }) => withToolLogging("log_client_activity", async () => {
115
115
  const result = await client.logClientActivity(clientId, data);
@@ -144,12 +144,13 @@ export function registerCrmTools(server, client) {
144
144
  server.registerTool("create_client_note", {
145
145
  title: "Create Client Note",
146
146
  description: "Add a note to a client. Notes are free-form text entries useful for keeping context. " +
147
+ "Do not include passwords, credentials, payment-card data, health data, or official/government identifiers. " +
147
148
  "Example: clientId='abc123', content='Prefers invoices in English. Payment NET 30.' " +
148
- "/ Anade una nota a un cliente. Las notas son texto libre para mantener contexto.",
149
+ "/ Anade una nota a un cliente. No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales.",
149
150
  annotations: CREATE_ANNOTATIONS,
150
151
  inputSchema: {
151
- clientId: z.string().describe("Client ID / ID del cliente"),
152
- content: z.string().describe("Note content / Contenido de la nota"),
152
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
153
+ content: z.string().trim().min(1).max(10000).describe("Note content; exclude secrets, credentials, card or health data, and official identifiers / Contenido; excluye secretos, credenciales, datos de tarjetas o salud e identificadores oficiales"),
153
154
  },
154
155
  outputSchema: noteItemOutput,
155
156
  }, async ({ clientId, content }) => withToolLogging("create_client_note", async () => {
@@ -166,8 +167,8 @@ export function registerCrmTools(server, client) {
166
167
  "/ Elimina permanentemente una nota de un cliente. Esta accion no se puede deshacer.",
167
168
  annotations: DELETE_ANNOTATIONS,
168
169
  inputSchema: {
169
- clientId: z.string().describe("Client ID / ID del cliente"),
170
- noteId: z.string().describe("Note ID / ID de la nota"),
170
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
171
+ noteId: z.string().trim().min(1).max(128).describe("Note ID / ID de la nota"),
171
172
  },
172
173
  outputSchema: deleteResultOutput,
173
174
  }, async ({ clientId, noteId }) => withToolLogging("delete_client_note", async () => {