@frihet/mcp-server 1.17.0 → 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 (87) hide show
  1. package/README.md +84 -59
  2. package/dist/agent-onboarding.d.ts +1 -1
  3. package/dist/agent-onboarding.js +3 -3
  4. package/dist/canary/mcp-harness.d.ts +1 -1
  5. package/dist/canary/mcp-harness.js +4 -1
  6. package/dist/capability-truth.js +20 -12
  7. package/dist/claude-candidate-contract.d.ts +482 -0
  8. package/dist/claude-candidate-contract.js +87 -0
  9. package/dist/claude-output-privacy.d.ts +17 -0
  10. package/dist/claude-output-privacy.js +240 -0
  11. package/dist/claude-profile.d.ts +18 -0
  12. package/dist/claude-profile.js +260 -0
  13. package/dist/client-interface.d.ts +26 -20
  14. package/dist/client.d.ts +65 -29
  15. package/dist/client.js +278 -167
  16. package/dist/demo-client.d.ts +27 -21
  17. package/dist/demo-client.js +31 -14
  18. package/dist/deposit-contract.d.ts +29 -0
  19. package/dist/deposit-contract.js +76 -0
  20. package/dist/fetch-no-redirect.d.ts +3 -0
  21. package/dist/fetch-no-redirect.js +16 -0
  22. package/dist/fiscal-aliases.d.ts +2 -2
  23. package/dist/fiscal-aliases.js +2 -2
  24. package/dist/fiscal-period.d.ts +26 -0
  25. package/dist/fiscal-period.js +31 -0
  26. package/dist/google-profile.d.ts +19 -0
  27. package/dist/google-profile.js +122 -0
  28. package/dist/index.js +31 -7
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +2 -2
  31. package/dist/muse-profile.d.ts +19 -0
  32. package/dist/muse-profile.js +122 -0
  33. package/dist/observability.js +3 -2
  34. package/dist/openai-profile.d.ts +2 -2
  35. package/dist/openai-profile.js +37 -11
  36. package/dist/openai-review-contract.d.ts +1 -1
  37. package/dist/openai-review-contract.js +4 -5
  38. package/dist/openai-review-oauth.d.ts +107 -7
  39. package/dist/openai-review-oauth.js +161 -17
  40. package/dist/prompts/register-all.js +32 -78
  41. package/dist/redaction.d.ts +5 -0
  42. package/dist/redaction.js +9 -0
  43. package/dist/resources/register-all.js +150 -54
  44. package/dist/retry-after.d.ts +19 -0
  45. package/dist/retry-after.js +84 -0
  46. package/dist/server-composition.d.ts +5 -2
  47. package/dist/server-composition.js +35 -5
  48. package/dist/tool-exposure.d.ts +6 -5
  49. package/dist/tool-exposure.js +15 -13
  50. package/dist/tools/audit_gl.js +8 -8
  51. package/dist/tools/backend-availability.d.ts +8 -29
  52. package/dist/tools/backend-availability.js +31 -30
  53. package/dist/tools/bank_rules.js +66 -15
  54. package/dist/tools/banking.js +5 -13
  55. package/dist/tools/clients.js +252 -6
  56. package/dist/tools/deposits.js +107 -97
  57. package/dist/tools/einvoice.d.ts +17 -3
  58. package/dist/tools/einvoice.js +59 -12
  59. package/dist/tools/fiscal.d.ts +3 -4
  60. package/dist/tools/fiscal.js +93 -60
  61. package/dist/tools/gestoria.js +15 -7
  62. package/dist/tools/hr.js +41 -11
  63. package/dist/tools/igic.d.ts +9 -8
  64. package/dist/tools/igic.js +34 -64
  65. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  66. package/dist/tools/impuesto_sociedades.js +16 -37
  67. package/dist/tools/intelligence.js +164 -10
  68. package/dist/tools/invoices.js +109 -42
  69. package/dist/tools/kitchen.js +19 -9
  70. package/dist/tools/quotes.js +86 -25
  71. package/dist/tools/recurring.js +26 -25
  72. package/dist/tools/register-all.d.ts +2 -2
  73. package/dist/tools/register-all.js +9 -6
  74. package/dist/tools/search.d.ts +19 -0
  75. package/dist/tools/search.js +84 -0
  76. package/dist/tools/shared.d.ts +17 -37
  77. package/dist/tools/shared.js +96 -30
  78. package/dist/tools/team.js +8 -16
  79. package/dist/tools/time.js +4 -12
  80. package/dist/tools/vendors.js +240 -6
  81. package/dist/types.d.ts +3 -1
  82. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  83. package/dist/workflows/daily-operational-brief.js +881 -0
  84. package/docs/agent-onboarding.json +15 -5
  85. package/package.json +26 -12
  86. package/dist/tools/onboarding.d.ts +0 -19
  87. package/dist/tools/onboarding.js +0 -64
@@ -2,7 +2,7 @@
2
2
  * Invoice tools for the Frihet MCP server.
3
3
  */
4
4
  import { z } from "zod/v4";
5
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, invoiceItemOutput, actionResultOutput, creditNoteResultOutput, pdfResultOutput, einvoiceResultOutput } from "./shared.js";
5
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, invoiceItemOutput, actionResultOutput, creditNoteResultOutput, pdfResultOutput, einvoiceResultOutput } from "./shared.js";
6
6
  const invoiceItemSchema = z.object({
7
7
  description: z.string().trim().min(1).max(500).describe("Description of the line item / Descripcion del concepto"),
8
8
  quantity: z.number().finite().min(0.0001).max(1_000_000).describe("Quantity / Cantidad"),
@@ -81,6 +81,10 @@ const invoiceFiscalFields = {
81
81
  .enum(["service", "goods"])
82
82
  .optional()
83
83
  .describe("Operation type (service or goods) / Tipo de operacion"),
84
+ fiscalTreatment: z
85
+ .enum(["not_subject_location", "export_exempt"])
86
+ .optional()
87
+ .describe("Explicit fiscal treatment declared by the user; never inferred / Tratamiento fiscal declarado expresamente por el usuario; nunca inferido"),
84
88
  };
85
89
  export function registerInvoiceTools(server, client) {
86
90
  // -- list_invoices --
@@ -206,7 +210,12 @@ export function registerInvoiceTools(server, client) {
206
210
  },
207
211
  outputSchema: invoiceItemOutput,
208
212
  }, async (input) => withToolLogging("create_invoice", async () => {
209
- const result = await client.createInvoice(input);
213
+ const status = input.status ?? "draft";
214
+ const payload = {
215
+ ...input,
216
+ status,
217
+ };
218
+ const result = await client.createInvoice(payload);
210
219
  const hints = enrichResponse("invoices", "create", result);
211
220
  return {
212
221
  content: [mutateContent(formatRecord("Invoice created", result) + hints)],
@@ -269,19 +278,11 @@ export function registerInvoiceTools(server, client) {
269
278
  outputSchema: documentDeleteResultOutput,
270
279
  }, async ({ id, confirm }) => withToolLogging("delete_invoice", async () => {
271
280
  if (!confirm) {
272
- return {
273
- content: [
274
- {
275
- type: "text",
276
- text: "Error: confirm=true is required to delete an invoice. " +
277
- "A draft invoice is removed permanently; a sent/paid invoice is CANCELLED " +
278
- "(status=cancelled) instead, because VeriFactu forbids breaking the hash chain. " +
279
- "Either way the invoice stops counting as a live receivable. Set confirm=true to proceed. / " +
280
- "Se requiere confirm=true para eliminar una factura.",
281
- },
282
- ],
283
- isError: true,
284
- };
281
+ return confirmationRequiredError("Error: confirm=true is required to delete an invoice. " +
282
+ "A draft invoice is removed permanently; a sent/paid invoice is CANCELLED " +
283
+ "(status=cancelled) instead, because VeriFactu forbids breaking the hash chain. " +
284
+ "Either way the invoice stops counting as a live receivable. Set confirm=true to proceed. / " +
285
+ "Se requiere confirm=true para eliminar una factura.");
285
286
  }
286
287
  // 204 → undefined (draft row destroyed). 200 → soft-cancel body (document
287
288
  // kept, status=cancelled). Reporting both as "deleted" is the GAP-12 lie.
@@ -379,18 +380,10 @@ export function registerInvoiceTools(server, client) {
379
380
  outputSchema: actionResultOutput,
380
381
  }, async ({ id, to, confirm }) => withToolLogging("send_invoice", async () => {
381
382
  if (!confirm) {
382
- return {
383
- content: [
384
- {
385
- type: "text",
386
- text: "Error: confirm=true is required to send an invoice. " +
387
- "This delivers an email to the client — a third party outside this workspace — " +
388
- "and it cannot be recalled once sent. Set confirm=true to proceed. / " +
389
- "Se requiere confirm=true para enviar una factura por email a un tercero.",
390
- },
391
- ],
392
- isError: true,
393
- };
383
+ return confirmationRequiredError("Error: confirm=true is required to send an invoice. " +
384
+ "This delivers an email to the client — a third party outside this workspace — " +
385
+ "and it cannot be recalled once sent. Set confirm=true to proceed. / " +
386
+ "Se requiere confirm=true para enviar una factura por email a un tercero.");
394
387
  }
395
388
  const invoice = await client.getInvoice(id);
396
389
  if (invoice["status"] === "cancelled") {
@@ -399,30 +392,99 @@ export function registerInvoiceTools(server, client) {
399
392
  isError: true,
400
393
  };
401
394
  }
402
- let recipientEmail = to;
403
- if (!recipientEmail) {
404
- const clientId = typeof invoice["clientId"] === "string" ? invoice["clientId"] : undefined;
405
- if (!clientId) {
395
+ const isValidRecipientEmail = (email) => email.length <= 255 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
396
+ let recipientEmail;
397
+ if (to !== undefined) {
398
+ const trimmedTo = to.trim();
399
+ if (!trimmedTo || !isValidRecipientEmail(trimmedTo)) {
406
400
  return {
407
- content: [{ type: "text", text: "Error: this invoice has no linked client with a saved email address." }],
401
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
408
402
  isError: true,
409
403
  };
410
404
  }
411
- const linkedClient = await client.getClient(clientId);
412
- recipientEmail = typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
413
- ? linkedClient["email"].trim()
405
+ recipientEmail = trimmedTo;
406
+ }
407
+ if (!recipientEmail) {
408
+ const clientId = typeof invoice["clientId"] === "string" && invoice["clientId"].trim()
409
+ ? invoice["clientId"].trim()
414
410
  : undefined;
411
+ if (clientId) {
412
+ let linkedClient;
413
+ try {
414
+ linkedClient = await client.getClient(clientId);
415
+ }
416
+ catch (err) {
417
+ const isNotFound = typeof err === "object" &&
418
+ err !== null &&
419
+ "statusCode" in err &&
420
+ err.statusCode === 404;
421
+ if (!isNotFound) {
422
+ throw err;
423
+ }
424
+ }
425
+ const clientEmail = linkedClient && typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
426
+ ? linkedClient["email"].trim()
427
+ : undefined;
428
+ if (clientEmail) {
429
+ if (!isValidRecipientEmail(clientEmail)) {
430
+ return {
431
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${clientEmail}".` }],
432
+ isError: true,
433
+ };
434
+ }
435
+ recipientEmail = clientEmail;
436
+ }
437
+ }
415
438
  if (!recipientEmail) {
439
+ const docFallbackEmail = typeof invoice["clientEmail"] === "string" && invoice["clientEmail"].trim()
440
+ ? invoice["clientEmail"].trim()
441
+ : typeof invoice["email"] === "string" && invoice["email"].trim()
442
+ ? invoice["email"].trim()
443
+ : undefined;
444
+ if (docFallbackEmail) {
445
+ if (!isValidRecipientEmail(docFallbackEmail)) {
446
+ return {
447
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${docFallbackEmail}".` }],
448
+ isError: true,
449
+ };
450
+ }
451
+ const fallbackMsg = `Error: the linked client has no current email address; only a document-level fallback email ("${docFallbackEmail}") was found on the invoice, which may be outdated. ` +
452
+ `Confirm this specific recipient with the user and re-run with to="${docFallbackEmail}" and confirm=true to send. / ` +
453
+ `El cliente vinculado no tiene email actual; la factura conserva el correo documental "${docFallbackEmail}", que podria estar obsoleto. Confirma este destinatario concreto y vuelve a llamar con to="${docFallbackEmail}" y confirm=true.`;
454
+ return {
455
+ content: [{ type: "text", text: fallbackMsg }],
456
+ structuredContent: {
457
+ success: false,
458
+ id,
459
+ message: fallbackMsg,
460
+ effectiveRecipient: docFallbackEmail,
461
+ recipientSource: "document_fallback",
462
+ requiresExplicitRecipientConfirmation: true,
463
+ },
464
+ isError: true,
465
+ };
466
+ }
416
467
  return {
417
- content: [{ type: "text", text: "Error: the linked client has no saved email address." }],
468
+ content: [
469
+ {
470
+ type: "text",
471
+ text: clientId
472
+ ? "Error: the linked client has no saved email address."
473
+ : "Error: this invoice has no linked client with a saved email address.",
474
+ },
475
+ ],
418
476
  isError: true,
419
477
  };
420
478
  }
421
479
  }
422
480
  const result = await client.sendInvoice(id, recipientEmail);
481
+ const enrichedResult = {
482
+ ...(result && typeof result === "object" ? result : {}),
483
+ effectiveRecipient: recipientEmail,
484
+ };
423
485
  return {
424
- content: [mutateContent(formatRecord("Invoice sent", result))],
425
- structuredContent: result,
486
+ content: [mutateContent(formatRecord("Invoice sent", enrichedResult))],
487
+ structuredContent: enrichedResult,
426
488
  };
427
489
  }));
428
490
  // -- mark_invoice_paid --
@@ -549,10 +611,15 @@ export function registerInvoiceTools(server, client) {
549
611
  // -- apply_late_fee --
550
612
  server.registerTool("apply_late_fee", {
551
613
  title: "Apply Late Fee",
552
- description: "Apply late payment interest to an overdue invoice. Calculates interest based on EU Late Payment Directive (8% default) " +
553
- "or auto-calculates from days overdue. Creates a debit note linked to the original invoice. " +
554
- "/ Aplica intereses de demora a una factura vencida. Calcula intereses segun la Directiva Europea de Morosidad (8% por defecto) " +
555
- "o los calcula automaticamente a partir de los dias de retraso. Crea una nota de debito vinculada a la factura original.",
614
+ description: "Apply late payment interest flags to an overdue invoice. " +
615
+ "Calculates interest based on EU Late Payment Directive (8% default) or auto-calculates " +
616
+ "from days overdue, and flips the `hasLateFee` / `lateFeeAmount` / `lateFeeDaysOverdue` " +
617
+ "fields on the invoice. The endpoint does NOT create a debit note — if a separate " +
618
+ "rectifying invoice is required, issue it explicitly with create_invoice or create_credit_note. " +
619
+ "/ Aplica los indicadores de intereses de demora a una factura vencida. Calcula intereses " +
620
+ "segun la Directiva Europea de Morosidad (8% por defecto) o a partir de los dias de retraso, " +
621
+ "y actualiza los flags `hasLateFee` / `lateFeeAmount` / `lateFeeDaysOverdue`. NO crea una " +
622
+ "nota de debito — si hace falta una factura rectificativa aparte, emitela con create_invoice o create_credit_note.",
556
623
  annotations: CREATE_ANNOTATIONS,
557
624
  inputSchema: {
558
625
  invoiceId: z.string().describe("ID of the overdue invoice / ID de la factura vencida"),
@@ -177,21 +177,32 @@ export function registerKitchenTools(server, client) {
177
177
  }
178
178
  }
179
179
  const agg = new Map();
180
+ let totalOpenTickets = 0;
180
181
  for (const t of ticketsResult.data) {
181
182
  const ticket = t;
182
183
  const status = typeof ticket["status"] === "string" ? ticket["status"] : "";
183
- if (status === "served" || status === "cancelled")
184
+ if (status === "served" || status === "cancelled" || status === "voided")
184
185
  continue;
185
- const stationId = typeof ticket["stationId"] === "string" ? ticket["stationId"] : "__unassigned__";
186
+ totalOpenTickets += 1;
187
+ // REST tickets can route to multiple stations; retain legacy single-station input.
188
+ const stationIds = Array.isArray(ticket["stationIds"])
189
+ ? [...new Set(ticket["stationIds"].filter((id) => typeof id === "string" && id.length > 0))]
190
+ : typeof ticket["stationId"] === "string" ? [ticket["stationId"]] : [];
191
+ if (stationIds.length === 0)
192
+ stationIds.push("__unassigned__");
186
193
  const createdAt = typeof ticket["createdAt"] === "string"
187
194
  ? new Date(ticket["createdAt"]).getTime()
188
195
  : null;
189
- const existing = agg.get(stationId) ?? { openTickets: 0, oldestCreatedAt: null };
190
- existing.openTickets += 1;
191
- if (createdAt !== null && (existing.oldestCreatedAt === null || createdAt < existing.oldestCreatedAt)) {
192
- existing.oldestCreatedAt = createdAt;
196
+ for (const stationId of stationIds) {
197
+ const existing = agg.get(stationId) ?? { openTickets: 0, oldestCreatedAt: null };
198
+ existing.openTickets += 1;
199
+ if (createdAt !== null && (existing.oldestCreatedAt === null || createdAt < existing.oldestCreatedAt)) {
200
+ existing.oldestCreatedAt = createdAt;
201
+ }
202
+ agg.set(stationId, existing);
203
+ if (stationId !== "__unassigned__" && !stationMap.has(stationId))
204
+ stationMap.set(stationId, stationId);
193
205
  }
194
- agg.set(stationId, existing);
195
206
  }
196
207
  // Build station summaries, also include stations with zero open tickets
197
208
  let maxLoad = 0;
@@ -240,10 +251,9 @@ export function registerKitchenTools(server, client) {
240
251
  }
241
252
  }
242
253
  }
243
- const totalOpenTickets = summaries.reduce((acc, s) => acc + s.openTickets, 0);
244
254
  const result = {
245
255
  stations: summaries,
246
- bottleneckStationId: maxLoad > 0 ? bottleneckId : undefined,
256
+ ...(maxLoad > 0 ? { bottleneckStationId: bottleneckId } : {}),
247
257
  totalOpenTickets,
248
258
  generatedAt: new Date().toISOString(),
249
259
  };
@@ -2,7 +2,7 @@
2
2
  * Quote tools for the Frihet MCP server.
3
3
  */
4
4
  import { z } from "zod/v4";
5
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
5
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
6
6
  const quoteItemSchema = z.object({
7
7
  description: z.string().trim().min(1).max(500).describe("Description of the line item / Descripcion del concepto"),
8
8
  quantity: z.number().finite().min(0.0001).max(1_000_000).describe("Quantity / Cantidad"),
@@ -205,19 +205,11 @@ export function registerQuoteTools(server, client) {
205
205
  outputSchema: documentDeleteResultOutput,
206
206
  }, async ({ id, confirm }) => withToolLogging("delete_quote", async () => {
207
207
  if (!confirm) {
208
- return {
209
- content: [
210
- {
211
- type: "text",
212
- text: "Error: confirm=true is required to delete a quote. " +
213
- "A clean draft without delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused; a sent/accepted quote is CANCELLED " +
214
- "(status=cancelled) instead and remains in the record. " +
215
- "Set confirm=true to proceed. / " +
216
- "Se requiere confirm=true para eliminar un presupuesto.",
217
- },
218
- ],
219
- isError: true,
220
- };
208
+ return confirmationRequiredError("Error: confirm=true is required to delete a quote. " +
209
+ "A clean draft without delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused; a sent/accepted quote is CANCELLED " +
210
+ "(status=cancelled) instead and remains in the record. " +
211
+ "Set confirm=true to proceed. / " +
212
+ "Se requiere confirm=true para eliminar un presupuesto.");
221
213
  }
222
214
  // 204 → undefined (draft row destroyed). 200 → soft-cancel body. See GAP-12.
223
215
  const outcome = await client.deleteQuote(id);
@@ -279,30 +271,99 @@ export function registerQuoteTools(server, client) {
279
271
  isError: true,
280
272
  };
281
273
  }
282
- let recipientEmail = to;
283
- if (!recipientEmail) {
284
- const clientId = typeof quote["clientId"] === "string" ? quote["clientId"] : undefined;
285
- if (!clientId) {
274
+ const isValidRecipientEmail = (email) => email.length <= 255 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
275
+ let recipientEmail;
276
+ if (to !== undefined) {
277
+ const trimmedTo = to.trim();
278
+ if (!trimmedTo || !isValidRecipientEmail(trimmedTo)) {
286
279
  return {
287
- content: [{ type: "text", text: "Error: this quote has no linked client with a saved email address." }],
280
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
288
281
  isError: true,
289
282
  };
290
283
  }
291
- const linkedClient = await client.getClient(clientId);
292
- recipientEmail = typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
293
- ? linkedClient["email"].trim()
284
+ recipientEmail = trimmedTo;
285
+ }
286
+ if (!recipientEmail) {
287
+ const clientId = typeof quote["clientId"] === "string" && quote["clientId"].trim()
288
+ ? quote["clientId"].trim()
294
289
  : undefined;
290
+ if (clientId) {
291
+ let linkedClient;
292
+ try {
293
+ linkedClient = await client.getClient(clientId);
294
+ }
295
+ catch (err) {
296
+ const isNotFound = typeof err === "object" &&
297
+ err !== null &&
298
+ "statusCode" in err &&
299
+ err.statusCode === 404;
300
+ if (!isNotFound) {
301
+ throw err;
302
+ }
303
+ }
304
+ const clientEmail = linkedClient && typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
305
+ ? linkedClient["email"].trim()
306
+ : undefined;
307
+ if (clientEmail) {
308
+ if (!isValidRecipientEmail(clientEmail)) {
309
+ return {
310
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${clientEmail}".` }],
311
+ isError: true,
312
+ };
313
+ }
314
+ recipientEmail = clientEmail;
315
+ }
316
+ }
295
317
  if (!recipientEmail) {
318
+ const docFallbackEmail = typeof quote["clientEmail"] === "string" && quote["clientEmail"].trim()
319
+ ? quote["clientEmail"].trim()
320
+ : typeof quote["email"] === "string" && quote["email"].trim()
321
+ ? quote["email"].trim()
322
+ : undefined;
323
+ if (docFallbackEmail) {
324
+ if (!isValidRecipientEmail(docFallbackEmail)) {
325
+ return {
326
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${docFallbackEmail}".` }],
327
+ isError: true,
328
+ };
329
+ }
330
+ const fallbackMsg = `Error: the linked client has no current email address; only a document-level fallback email ("${docFallbackEmail}") was found on the quote, which may be outdated. ` +
331
+ `Confirm this specific recipient with the user and re-run with to="${docFallbackEmail}" and confirm=true to send. / ` +
332
+ `El cliente vinculado no tiene email actual; el presupuesto conserva el correo documental "${docFallbackEmail}", que podria estar obsoleto. Confirma este destinatario concreto y vuelve a llamar con to="${docFallbackEmail}" y confirm=true.`;
333
+ return {
334
+ content: [{ type: "text", text: fallbackMsg }],
335
+ structuredContent: {
336
+ success: false,
337
+ id,
338
+ message: fallbackMsg,
339
+ effectiveRecipient: docFallbackEmail,
340
+ recipientSource: "document_fallback",
341
+ requiresExplicitRecipientConfirmation: true,
342
+ },
343
+ isError: true,
344
+ };
345
+ }
296
346
  return {
297
- content: [{ type: "text", text: "Error: the linked client has no saved email address." }],
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: clientId
351
+ ? "Error: the linked client has no saved email address."
352
+ : "Error: this quote has no linked client with a saved email address.",
353
+ },
354
+ ],
298
355
  isError: true,
299
356
  };
300
357
  }
301
358
  }
302
359
  const result = await client.sendQuote(id, recipientEmail);
360
+ const enrichedResult = {
361
+ ...(result && typeof result === "object" ? result : {}),
362
+ effectiveRecipient: recipientEmail,
363
+ };
303
364
  return {
304
- content: [mutateContent(formatRecord("Quote sent", result))],
305
- structuredContent: result,
365
+ content: [mutateContent(formatRecord("Quote sent", enrichedResult))],
366
+ structuredContent: enrichedResult,
306
367
  };
307
368
  }));
308
369
  }
@@ -16,7 +16,7 @@
16
16
  * Backend: /v1/recurring/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
17
17
  */
18
18
  import { z } from "zod/v4";
19
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, deleteResultOutput, recurringInvoiceItemOutput, } from "./shared.js";
19
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, deleteResultOutput, recurringInvoiceItemOutput, } from "./shared.js";
20
20
  export function registerRecurringTools(server, client) {
21
21
  // -- list_recurring_invoices --
22
22
  server.registerTool("list_recurring_invoices", {
@@ -191,17 +191,9 @@ export function registerRecurringTools(server, client) {
191
191
  outputSchema: deleteResultOutput,
192
192
  }, async ({ id, confirm }) => withToolLogging("delete_recurring_invoice", async () => {
193
193
  if (!confirm) {
194
- return {
195
- content: [
196
- {
197
- type: "text",
198
- text: "Error: confirm=true is required to delete a recurring invoice template. " +
199
- "This will stop all future invoice generation from this template. Set confirm=true to proceed. / " +
200
- "Se requiere confirm=true para eliminar la plantilla de factura recurrente.",
201
- },
202
- ],
203
- isError: true,
204
- };
194
+ return confirmationRequiredError("Error: confirm=true is required to delete a recurring invoice template. " +
195
+ "This will stop all future invoice generation from this template. Set confirm=true to proceed. / " +
196
+ "Se requiere confirm=true para eliminar la plantilla de factura recurrente.");
205
197
  }
206
198
  await client.deleteRecurringInvoice(id);
207
199
  return {
@@ -212,23 +204,32 @@ export function registerRecurringTools(server, client) {
212
204
  // -- run_recurring_now --
213
205
  server.registerTool("run_recurring_now", {
214
206
  title: "Run Recurring Invoice Now",
215
- description: "Manually trigger immediate generation of the next invoice instance from a recurring template. " +
216
- "Useful for billing ahead of schedule or recovering from a missed automated run. " +
217
- "The generated invoice is created as a draft; review and send separately. " +
218
- "Example: templateId='rec_abc123' " +
219
- "/ Genera manualmente la siguiente instancia de una factura recurrente. " +
220
- "Util para facturar antes de lo programado o recuperar un ciclo perdido. " +
221
- "La factura generada se crea como borrador; revisar y enviar por separado.",
222
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
223
- inputSchema: {
224
- templateId: z.string().describe("Recurring invoice template ID / ID de la plantilla de factura recurrente"),
207
+ description: "Generate an invoice from a recurring template and update its run count and last-run date. Requires confirm=true. " +
208
+ "draftOnly defaults to true. With draftOnly=false, the invoice is created with status='sent' and may trigger fiscal numbering and automatic tax submission. " +
209
+ "Both modes may deliver configured invoice-created webhooks and notify workspace admins/accountants; this endpoint does not email the invoice to the client. " +
210
+ "Each new call can create another invoice; check the template and invoices after an uncertain result before retrying. " +
211
+ "Example: templateId='rec_abc123', confirm=true " +
212
+ "/ Genera una factura recurrente y actualiza el contador y la fecha de ejecución. Requiere confirm=true. " +
213
+ "Por defecto crea un borrador; con draftOnly=false crea el estado 'sent' y puede activar la numeración y remisión fiscal automática. " +
214
+ "Ambas opciones pueden activar webhooks y avisos internos; esta operación no envía la factura por correo al cliente. " +
215
+ "Una nueva llamada puede crear otra factura; comprueba la plantilla y las facturas antes de repetir un resultado incierto.",
216
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
217
+ inputSchema: z.object({
218
+ templateId: z.string().min(1).describe("Recurring invoice template ID / ID de la plantilla de factura recurrente"),
225
219
  draftOnly: z
226
220
  .boolean()
227
221
  .optional()
228
- .describe("If true, create as draft only (default true). Set false to create and mark as sent immediately. / Si true, crea como borrador. Set false para crear y marcar como enviada."),
229
- },
222
+ .describe("Default true creates a draft; false creates status='sent' and may trigger automatic fiscal submission. Neither option emails the client. / Por defecto true crea un borrador; false crea el estado 'sent' y puede activar la remisión fiscal automática. Ninguna opción envía correo al cliente."),
223
+ confirm: z.boolean().describe("Must be true to authorize invoice creation, configured webhooks and possible fiscal issuance / Debe ser true para autorizar la creación, los webhooks configurados y la posible emisión fiscal"),
224
+ }).strict(),
230
225
  outputSchema: actionResultOutput,
231
- }, async ({ templateId, draftOnly }) => withToolLogging("run_recurring_now", async () => {
226
+ }, async ({ templateId, draftOnly, confirm }) => withToolLogging("run_recurring_now", async () => {
227
+ if (confirm !== true) {
228
+ return confirmationRequiredError("Error: confirm=true is required to create a recurring invoice and allow configured webhooks. " +
229
+ "draftOnly=false may also trigger fiscal numbering and automatic tax submission. / " +
230
+ "Se requiere confirm=true para crear la factura y permitir los webhooks configurados. " +
231
+ "draftOnly=false también puede activar la numeración y remisión fiscal automática.");
232
+ }
232
233
  const result = await client.runRecurringNow(templateId, { draftOnly: draftOnly ?? true });
233
234
  return {
234
235
  content: [mutateContent(formatRecord("Recurring invoice triggered", result))],
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Barrel module that registers all 157 Frihet ERP tools on an McpServer.
2
+ * Barrel module that registers every Frihet ERP tool on an McpServer.
3
3
  *
4
4
  * Used by both the local (stdio) and remote (Cloudflare Workers) servers
5
5
  * so tool definitions stay in sync — one source of truth.
6
6
  *
7
7
  * Langfuse observability is injected by patching server.registerTool once
8
8
  * before any tool registration. This wraps every tool callback with
9
- * traceMCPTool so all 157 tools are instrumented at zero per-tool cost.
9
+ * traceMCPTool so every registered tool is instrumented at zero per-tool cost.
10
10
  */
11
11
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import type { IFrihetClient } from "../client-interface.js";
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Barrel module that registers all 157 Frihet ERP tools on an McpServer.
2
+ * Barrel module that registers every Frihet ERP tool on an McpServer.
3
3
  *
4
4
  * Used by both the local (stdio) and remote (Cloudflare Workers) servers
5
5
  * so tool definitions stay in sync — one source of truth.
6
6
  *
7
7
  * Langfuse observability is injected by patching server.registerTool once
8
8
  * before any tool registration. This wraps every tool callback with
9
- * traceMCPTool so all 157 tools are instrumented at zero per-tool cost.
9
+ * traceMCPTool so every registered tool is instrumented at zero per-tool cost.
10
10
  */
11
11
  import { traceMCPTool } from "../observability.js";
12
12
  import { registerInvoiceTools } from "./invoices.js";
@@ -37,13 +37,13 @@ import { registerImpuestoSociedadesTools } from "./impuesto_sociedades.js";
37
37
  import { registerBankRulesTools } from "./bank_rules.js";
38
38
  import { registerHrTools } from "./hr.js";
39
39
  import { registerPayrollTools } from "./payroll.js";
40
- import { registerOnboardingTools } from "./onboarding.js";
41
40
  import { registerPermissionsTools } from "./permissions.js";
42
41
  import { registerAccountingCloseTools } from "./accountingClose.js";
42
+ import { registerSearchTools } from "./search.js";
43
43
  /**
44
44
  * Patches server.registerTool to wrap every tool callback with Langfuse tracing.
45
45
  *
46
- * The patch is applied once before tool registration so all 157 tools are
46
+ * The patch is applied once before tool registration so every registered tool is
47
47
  * instrumented without per-tool edits. Tool call signatures are unchanged —
48
48
  * existing MCP clients continue to work identically.
49
49
  *
@@ -57,7 +57,10 @@ import { registerAccountingCloseTools } from "./accountingClose.js";
57
57
  * attendance_clock_in, attendance_clock_out, overtime_report, anomaly_list
58
58
  * Webhook trust (1): test_webhook (added to registerWebhookTools)
59
59
  * Payroll (2): payroll_export, payroll_checklist
60
- * Onboarding (2): onboarding_status, onboarding_persona_set
60
+ * (Onboarding tools onboarding_status / onboarding_persona_set withdrawn
61
+ * via #124 — Frihet-ERP /v1/onboarding/* family absent; tools were deleted,
62
+ * not stubbed, to keep the catalogue honest. See src/tools/igic.ts for the
63
+ * precedent of an inline NOT_DEPLOYED stub where the tool is retained.)
61
64
  * Permissions (2): permissions_matrix, permissions_me
62
65
  * Period close (3): period_close_status, period_close, period_reopen
63
66
  */
@@ -106,8 +109,8 @@ export function registerAllTools(server, client) {
106
109
  registerBankRulesTools(server, client);
107
110
  registerHrTools(server, client);
108
111
  registerPayrollTools(server, client);
109
- registerOnboardingTools(server, client);
110
112
  registerPermissionsTools(server, client);
111
113
  registerAccountingCloseTools(server, client);
114
+ registerSearchTools(server, client);
112
115
  }
113
116
  //# sourceMappingURL=register-all.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cross-resource global search tool — Phase 10 second-wave safe parity.
3
+ *
4
+ * Wraps `GET /v1/search/global?q=&types=&limit=&offset=` (publicApi.ts:4476).
5
+ * Read-only fan-out across invoices / expenses / vendors / clients / products
6
+ * with workspace scoping preserved by Firestore subcollection paths on the
7
+ * server. No mutating counterpart; no auth boundary different from any other
8
+ * list tool.
9
+ *
10
+ * Why this lives alone (not in clients.ts): it is not a clients/* endpoint,
11
+ * has its own canonical envelope (`{data, total, ..., query, types,
12
+ * truncated?}` instead of the standard paginated one), and is the only
13
+ * server-side fan-out tool — coupling it with one resource would mislead the
14
+ * tool listing.
15
+ */
16
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import type { IFrihetClient } from "../client-interface.js";
18
+ export declare function registerSearchTools(server: McpServer, client: IFrihetClient): void;
19
+ //# sourceMappingURL=search.d.ts.map