@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
@@ -2,20 +2,28 @@
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, deleteResultOutput, invoiceItemOutput, actionResultOutput, creditNoteResultOutput, pdfResultOutput } 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
- description: z.string().describe("Description of the line item / Descripcion del concepto"),
8
- quantity: z.number().describe("Quantity / Cantidad"),
9
- unitPrice: z.number().describe("Unit price in EUR / Precio unitario en EUR"),
10
- });
7
+ description: z.string().trim().min(1).max(500).describe("Description of the line item / Descripcion del concepto"),
8
+ quantity: z.number().finite().min(0.0001).max(1_000_000).describe("Quantity / Cantidad"),
9
+ unitPrice: z.number().finite().min(0).max(10_000_000).describe("Unit price in EUR / Precio unitario en EUR"),
10
+ }).strict();
11
+ function documentResultMetadata(result) {
12
+ return {
13
+ id: result.id,
14
+ contentType: result.contentType,
15
+ sizeBytes: result.sizeBytes,
16
+ ...(result.filename ? { filename: result.filename } : {}),
17
+ };
18
+ }
11
19
  // Optional client-identity fields the Frihet API accepts on invoice/quote
12
- // create+update. When clientId is supplied the server back-fills taxId/address
13
- // from the stored client; clientTaxId/clientAddress override per-document.
20
+ // create+update. When clientId is supplied the server snapshots the stored
21
+ // client record; the other fields can override document-specific details.
14
22
  const clientIdentityFields = {
15
23
  clientId: z
16
24
  .string()
17
25
  .optional()
18
- .describe("Existing client ID — server back-fills taxId/address / ID de cliente existente"),
26
+ .describe("Existing client ID — stored client details are used for the document / ID de cliente existente"),
19
27
  clientTaxId: z
20
28
  .string()
21
29
  .optional()
@@ -73,6 +81,10 @@ const invoiceFiscalFields = {
73
81
  .enum(["service", "goods"])
74
82
  .optional()
75
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"),
76
88
  };
77
89
  export function registerInvoiceTools(server, client) {
78
90
  // -- list_invoices --
@@ -133,8 +145,8 @@ export function registerInvoiceTools(server, client) {
133
145
  const result = await client.listInvoices({ limit, offset, after, fields, status, from, to, clientId, seriesId });
134
146
  const hints = enrichResponse("invoices", "list", result.data);
135
147
  return {
136
- content: [listContent(formatPaginatedResponse("invoices", result))],
137
- structuredContent: { ...result, ...hints },
148
+ content: [listContent(formatPaginatedResponse("invoices", result) + hints)],
149
+ structuredContent: { ...result },
138
150
  };
139
151
  }));
140
152
  // -- get_invoice --
@@ -165,18 +177,18 @@ export function registerInvoiceTools(server, client) {
165
177
  "Soporta retencion IRPF (autonomos ES), recargo de equivalencia, serie, anticipo y descuento global.",
166
178
  annotations: CREATE_ANNOTATIONS,
167
179
  inputSchema: {
168
- clientName: z.string().describe("Client/customer name / Nombre del cliente"),
180
+ clientName: z.string().trim().min(1).max(200).describe("Client/customer name / Nombre del cliente"),
169
181
  ...clientIdentityFields,
170
182
  items: z
171
183
  .array(invoiceItemSchema)
172
184
  .min(1)
173
185
  .describe("Line items (each with description, quantity, unitPrice) / Conceptos de la factura"),
174
186
  issueDate: z
175
- .string()
187
+ .iso.date()
176
188
  .optional()
177
189
  .describe("Issue date in ISO 8601 format (YYYY-MM-DD), defaults to today / Fecha de emision"),
178
190
  dueDate: z
179
- .string()
191
+ .iso.date()
180
192
  .optional()
181
193
  .describe("Due date in ISO 8601 format (YYYY-MM-DD) / Fecha de vencimiento"),
182
194
  status: z
@@ -185,6 +197,7 @@ export function registerInvoiceTools(server, client) {
185
197
  .describe("Invoice status (default: draft) / Estado de la factura"),
186
198
  notes: z
187
199
  .string()
200
+ .max(10_000)
188
201
  .optional()
189
202
  .describe("Additional notes shown on the invoice / Notas adicionales"),
190
203
  taxRate: z
@@ -197,11 +210,16 @@ export function registerInvoiceTools(server, client) {
197
210
  },
198
211
  outputSchema: invoiceItemOutput,
199
212
  }, async (input) => withToolLogging("create_invoice", async () => {
200
- 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);
201
219
  const hints = enrichResponse("invoices", "create", result);
202
220
  return {
203
- content: [mutateContent(formatRecord("Invoice created", result))],
204
- structuredContent: { ...result, ...hints },
221
+ content: [mutateContent(formatRecord("Invoice created", result) + hints)],
222
+ structuredContent: { ...result },
205
223
  };
206
224
  }));
207
225
  // -- update_invoice --
@@ -235,26 +253,65 @@ export function registerInvoiceTools(server, client) {
235
253
  const result = await client.updateInvoice(id, data);
236
254
  const hints = enrichResponse("invoices", "update", result);
237
255
  return {
238
- content: [mutateContent(formatRecord("Invoice updated", result))],
239
- structuredContent: { ...result, ...hints },
256
+ content: [mutateContent(formatRecord("Invoice updated", result) + hints)],
257
+ structuredContent: { ...result },
240
258
  };
241
259
  }));
242
260
  // -- delete_invoice --
243
261
  server.registerTool("delete_invoice", {
244
262
  title: "Delete Invoice",
245
- description: "Permanently delete an invoice by its ID. This action cannot be undone. " +
246
- "/ Elimina permanentemente una factura por su ID. Esta accion no se puede deshacer.",
263
+ description: "Delete an invoice by its ID. Requires confirm=true. " +
264
+ "Only a DRAFT invoice is removed permanently. A sent/paid/overdue invoice is NOT destroyed: " +
265
+ "the backend CANCELS it (status=cancelled) so the VeriFactu hash chain stays intact, and the " +
266
+ "document remains readable via get_invoice. The result reports which happened in `outcome` " +
267
+ "(\"deleted\" or \"cancelled\"). " +
268
+ "/ Elimina una factura por su ID. Requiere confirm=true. Solo una factura en BORRADOR se elimina " +
269
+ "de forma permanente. Una factura emitida/pagada NO se destruye: se CANCELA (status=cancelled) para " +
270
+ "preservar la cadena de hash VeriFactu y sigue consultable con get_invoice.",
247
271
  annotations: DELETE_ANNOTATIONS,
248
272
  inputSchema: {
249
273
  id: z.string().describe("Invoice ID / ID de la factura"),
274
+ confirm: z
275
+ .boolean()
276
+ .describe("Must be true to confirm deletion / Debe ser true para confirmar la eliminacion"),
250
277
  },
251
- outputSchema: deleteResultOutput,
252
- }, async ({ id }) => withToolLogging("delete_invoice", async () => {
253
- await client.deleteInvoice(id);
278
+ outputSchema: documentDeleteResultOutput,
279
+ }, async ({ id, confirm }) => withToolLogging("delete_invoice", async () => {
280
+ if (!confirm) {
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.");
286
+ }
287
+ // 204 → undefined (draft row destroyed). 200 → soft-cancel body (document
288
+ // kept, status=cancelled). Reporting both as "deleted" is the GAP-12 lie.
289
+ const outcome = await client.deleteInvoice(id);
290
+ const cancelled = !!outcome && typeof outcome === "object";
291
+ const body = cancelled ? outcome : {};
254
292
  const hints = enrichResponse("invoices", "delete", { id });
293
+ const previous = typeof body["previousStatus"] === "string" ? ` (was ${body["previousStatus"]})` : "";
294
+ // R3 fix (ERP #1580): extract the per-branch body text to a single
295
+ // variable and append `+ hints` ONCE, after the ternary closes. R2's
296
+ // `+ hints` was branch-local (only in the DELETED branch), so the
297
+ // CANCELLED branch silently dropped the "cannot be undone" warning —
298
+ // 15/16 behavioral paths had warnings, not 16/16. The OUTER concat
299
+ // guarantees both terminal branches carry the enrichment. The
300
+ // `paginated-strict-output.test.ts` runtime tests pin BOTH branches.
301
+ const bodyText = cancelled
302
+ ? `Invoice ${id} was CANCELLED, not deleted${previous}: it still exists with ` +
303
+ "status=cancelled because VeriFactu forbids destroying an issued invoice. / " +
304
+ `Factura ${id} CANCELADA, no eliminada: sigue existiendo con status=cancelled (VeriFactu).`
305
+ : `Invoice ${id} deleted permanently (it was a draft). / ` +
306
+ `Factura ${id} eliminada permanentemente (era un borrador).`;
255
307
  return {
256
- content: [mutateContent(`Invoice ${id} deleted successfully. / Factura ${id} eliminada correctamente.`)],
257
- structuredContent: { success: true, id, ...hints },
308
+ content: [mutateContent(bodyText + hints)],
309
+ structuredContent: {
310
+ success: true,
311
+ id,
312
+ ...body,
313
+ outcome: cancelled ? "cancelled" : "deleted",
314
+ },
258
315
  };
259
316
  }));
260
317
  // -- search_invoices --
@@ -299,27 +356,135 @@ export function registerInvoiceTools(server, client) {
299
356
  const label = query ? `invoices matching "${query}"` : "invoices";
300
357
  const hints = enrichResponse("invoices", "list", result.data);
301
358
  return {
302
- content: [listContent(formatPaginatedResponse(label, result))],
303
- structuredContent: { ...result, ...hints },
359
+ content: [listContent(formatPaginatedResponse(label, result) + hints)],
360
+ structuredContent: { ...result },
304
361
  };
305
362
  }));
306
363
  // -- send_invoice --
307
364
  server.registerTool("send_invoice", {
308
365
  title: "Send Invoice",
309
- description: "Send an invoice to the client via email. Optionally override the recipient email address. " +
366
+ description: "Send an invoice to the client via email. Requires confirm=true. " +
367
+ "Optionally override the recipient email address. " +
310
368
  "The invoice must exist and should not already be cancelled. " +
311
- "/ Envia una factura al cliente por email. Opcionalmente se puede cambiar el email destinatario.",
369
+ "Sending reaches a third party and cannot be recalled. " +
370
+ "/ Envia una factura al cliente por email. Requiere confirm=true. " +
371
+ "Opcionalmente se puede cambiar el email destinatario. El envio llega a un tercero y no se puede anular.",
312
372
  annotations: UPDATE_ANNOTATIONS,
313
373
  inputSchema: {
314
374
  id: z.string().describe("Invoice ID / ID de la factura"),
315
375
  to: z.string().optional().describe("Override recipient email / Email destinatario alternativo"),
376
+ confirm: z
377
+ .boolean()
378
+ .describe("Must be true to confirm sending / Debe ser true para confirmar el envio"),
316
379
  },
317
380
  outputSchema: actionResultOutput,
318
- }, async ({ id, to }) => withToolLogging("send_invoice", async () => {
319
- const result = await client.sendInvoice(id, to);
381
+ }, async ({ id, to, confirm }) => withToolLogging("send_invoice", async () => {
382
+ if (!confirm) {
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.");
387
+ }
388
+ const invoice = await client.getInvoice(id);
389
+ if (invoice["status"] === "cancelled") {
390
+ return {
391
+ content: [{ type: "text", text: "Error: a cancelled invoice cannot be sent." }],
392
+ isError: true,
393
+ };
394
+ }
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)) {
400
+ return {
401
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
402
+ isError: true,
403
+ };
404
+ }
405
+ recipientEmail = trimmedTo;
406
+ }
407
+ if (!recipientEmail) {
408
+ const clientId = typeof invoice["clientId"] === "string" && invoice["clientId"].trim()
409
+ ? invoice["clientId"].trim()
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
+ }
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
+ }
467
+ return {
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
+ ],
476
+ isError: true,
477
+ };
478
+ }
479
+ }
480
+ const result = await client.sendInvoice(id, recipientEmail);
481
+ const enrichedResult = {
482
+ ...(result && typeof result === "object" ? result : {}),
483
+ effectiveRecipient: recipientEmail,
484
+ };
320
485
  return {
321
- content: [mutateContent(formatRecord("Invoice sent", result))],
322
- structuredContent: result,
486
+ content: [mutateContent(formatRecord("Invoice sent", enrichedResult))],
487
+ structuredContent: enrichedResult,
323
488
  };
324
489
  }));
325
490
  // -- mark_invoice_paid --
@@ -344,8 +509,10 @@ export function registerInvoiceTools(server, client) {
344
509
  // -- get_invoice_pdf --
345
510
  server.registerTool("get_invoice_pdf", {
346
511
  title: "Get Invoice PDF",
347
- description: "Get the PDF for an invoice. Returns a URL to download the PDF or binary info. " +
348
- "/ Obtiene el PDF de una factura. Devuelve una URL de descarga o informacion del binario.",
512
+ description: "Get the PDF for an invoice. The body is served as raw `application/pdf` bytes, base64-encoded in `base64` " +
513
+ "with `sizeBytes` and `contentType`. PDFs larger than 25 MiB are rejected with `413 payload_too_large`. " +
514
+ "/ Obtiene el PDF de una factura. El cuerpo se sirve como bytes `application/pdf` en bruto, codificados en base64 " +
515
+ "en `base64`, con `sizeBytes` y `contentType`. PDFs mayores de 25 MiB se rechazan con `413 payload_too_large`.",
349
516
  annotations: READ_ONLY_ANNOTATIONS,
350
517
  inputSchema: {
351
518
  id: z.string().describe("Invoice ID / ID de la factura"),
@@ -354,33 +521,31 @@ export function registerInvoiceTools(server, client) {
354
521
  }, async ({ id }) => withToolLogging("get_invoice_pdf", async () => {
355
522
  const result = await client.getInvoicePdf(id);
356
523
  return {
357
- content: [getContent(formatRecord("Invoice PDF", result))],
524
+ content: [getContent(formatRecord("Invoice PDF", documentResultMetadata(result)))],
358
525
  structuredContent: result,
359
526
  };
360
527
  }));
361
528
  // -- get_invoice_einvoice --
362
529
  server.registerTool("get_invoice_einvoice", {
363
- title: "Get Invoice E-Invoice XML",
364
- description: "Download the e-invoice XML for an invoice. Returns EN16931-compliant XML in the auto-detected format " +
365
- "(UBL, CII, XRechnung, Factur-X, FatturaPA, PEPPOL). " +
530
+ title: "Get Invoice E-Invoice Artifact",
531
+ description: "Download the stored e-invoice artifact for an invoice. XML formats return strict UTF-8 text in `xml`; " +
532
+ "Factur-X returns `application/pdf` bytes encoded in `base64`. Both include `id`, `contentType`, and `sizeBytes`. " +
533
+ "XML larger than 5 MiB or PDF larger than 25 MiB is rejected with `413 payload_too_large`. " +
366
534
  "Only available after the invoice has been saved/sent. " +
367
- "/ Descarga el XML de factura electronica para una factura. Devuelve XML conforme a EN16931 en el formato " +
368
- "auto-detectado (UBL, CII, XRechnung, Factur-X, FatturaPA, PEPPOL). " +
535
+ "/ Descarga el artefacto de factura electronica guardado. Los formatos XML devuelven texto UTF-8 estricto en `xml`; " +
536
+ "Factur-X devuelve bytes `application/pdf` codificados en `base64`. Ambos incluyen `id`, `contentType` y `sizeBytes`. " +
537
+ "XML mayor de 5 MiB o PDF mayor de 25 MiB se rechaza con `413 payload_too_large`. " +
369
538
  "Solo disponible despues de guardar o enviar la factura.",
370
539
  annotations: READ_ONLY_ANNOTATIONS,
371
540
  inputSchema: {
372
541
  id: z.string().describe("Invoice ID / ID de la factura"),
373
542
  },
374
- outputSchema: z.object({
375
- xml: z.string().optional(),
376
- filename: z.string().optional(),
377
- format: z.string().optional(),
378
- }).passthrough(),
543
+ outputSchema: einvoiceResultOutput,
379
544
  }, async ({ id }) => withToolLogging("get_invoice_einvoice", async () => {
380
545
  const result = await client.getInvoiceEInvoice(id);
381
546
  return {
382
- content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
383
- structuredContent: (typeof result === "object" && result !== null ? result : { xml: result }),
547
+ content: [getContent(formatRecord("E-invoice artifact", documentResultMetadata(result)))],
548
+ structuredContent: result,
384
549
  };
385
550
  }));
386
551
  // -- create_credit_note --
@@ -439,17 +604,22 @@ export function registerInvoiceTools(server, client) {
439
604
  }, idempotencyKey);
440
605
  const hints = enrichResponse("invoices", "create", result);
441
606
  return {
442
- content: [mutateContent(formatRecord("Credit note created", result))],
443
- structuredContent: { ...result, ...hints },
607
+ content: [mutateContent(formatRecord("Credit note created", result) + hints)],
608
+ structuredContent: { ...result },
444
609
  };
445
610
  }));
446
611
  // -- apply_late_fee --
447
612
  server.registerTool("apply_late_fee", {
448
613
  title: "Apply Late Fee",
449
- description: "Apply late payment interest to an overdue invoice. Calculates interest based on EU Late Payment Directive (8% default) " +
450
- "or auto-calculates from days overdue. Creates a debit note linked to the original invoice. " +
451
- "/ Aplica intereses de demora a una factura vencida. Calcula intereses segun la Directiva Europea de Morosidad (8% por defecto) " +
452
- "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.",
453
623
  annotations: CREATE_ANNOTATIONS,
454
624
  inputSchema: {
455
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,17 +2,14 @@
2
2
  * Payroll preparation tools for the Frihet MCP server — D4-B megasprint (2 tools).
3
3
  *
4
4
  * Tools:
5
- * 1. payroll_export — export payroll for a month in gestoria format (A3, Contasol, Sage, Holded, Siltra)
6
- * 2. payroll_checklist — list employees with status (ready / missing_data / blocked) for a month
5
+ * 1. payroll_export — read the normalized export-ready employee dataset
6
+ * 2. payroll_checklist — read payroll-profile readiness for payable employees
7
7
  *
8
8
  * REST surface: /v1/payroll/prep/export, /v1/payroll/prep/employees
9
9
  *
10
- * Frihet does NOT process payroll. It exports to gestoria-compatible formats so
11
- * the asesoria handles SS/IRPF/contracts. Output formats track the most common
12
- * Spanish payroll software clusters.
13
- *
14
- * NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
15
- * until backend ships. TODO: confirm A3 columns + SILTRA file extension.
10
+ * The ERP validates and echoes a destination-format label but returns the same
11
+ * normalized JSON dataset for each value. It does not generate a format-specific
12
+ * file, and Frihet does not process payroll. These read routes are live.
16
13
  */
17
14
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
15
  import type { IFrihetClient } from "../client-interface.js";
@@ -2,17 +2,14 @@
2
2
  * Payroll preparation tools for the Frihet MCP server — D4-B megasprint (2 tools).
3
3
  *
4
4
  * Tools:
5
- * 1. payroll_export — export payroll for a month in gestoria format (A3, Contasol, Sage, Holded, Siltra)
6
- * 2. payroll_checklist — list employees with status (ready / missing_data / blocked) for a month
5
+ * 1. payroll_export — read the normalized export-ready employee dataset
6
+ * 2. payroll_checklist — read payroll-profile readiness for payable employees
7
7
  *
8
8
  * REST surface: /v1/payroll/prep/export, /v1/payroll/prep/employees
9
9
  *
10
- * Frihet does NOT process payroll. It exports to gestoria-compatible formats so
11
- * the asesoria handles SS/IRPF/contracts. Output formats track the most common
12
- * Spanish payroll software clusters.
13
- *
14
- * NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
15
- * until backend ships. TODO: confirm A3 columns + SILTRA file extension.
10
+ * The ERP validates and echoes a destination-format label but returns the same
11
+ * normalized JSON dataset for each value. It does not generate a format-specific
12
+ * file, and Frihet does not process payroll. These read routes are live.
16
13
  */
17
14
  import { z } from "zod/v4";
18
15
  import { withToolLogging, formatRecord, getContent, READ_ONLY_ANNOTATIONS, payrollExportOutput, payrollChecklistOutput, } from "./shared.js";
@@ -20,24 +17,18 @@ import { withBackendGuard } from "./backend-availability.js";
20
17
  export function registerPayrollTools(server, client) {
21
18
  // -- payroll_export --
22
19
  server.registerTool("payroll_export", {
23
- title: "Export Payroll (Gestoria Format)",
24
- description: "Export payroll data for a month in gestoria-compatible format. " +
25
- "Supported formats:\n" +
26
- " - 'a3' — Wolters Kluwer A3 (CSV with Spanish payroll columns)\n" +
27
- " - 'contasol' — Sage Contasol\n" +
28
- " - 'sage' — Sage 50/200\n" +
29
- " - 'holded' — Holded import format\n" +
30
- " - 'siltra' — SILTRA Seguridad Social XML\n" +
31
- "\n" +
32
- "Frihet does NOT calculate payroll — it exports staged data for the gestoria. " +
33
- "Month format: 'YYYY-MM'. " +
34
- "/ Exporta datos de nominas en formato compatible con gestoria. Mes en formato 'YYYY-MM'.",
20
+ title: "Payroll Export-Ready Data",
21
+ description: "Read the normalized payroll-ready employee dataset for a month. " +
22
+ "The ERP accepts 'a3', 'contasol', 'sage', or 'siltra' as a destination label and echoes it; " +
23
+ "the response is JSON and is identical across formats apart from that label. " +
24
+ "No CSV, XML, PDF, or provider-specific file is generated. Frihet does not calculate payroll. " +
25
+ "/ Lee los datos normalizados listos para nomina; no genera un archivo especifico del proveedor.",
35
26
  annotations: READ_ONLY_ANNOTATIONS,
36
27
  inputSchema: {
37
28
  format: z
38
- .enum(["a3", "contasol", "sage", "holded", "siltra"])
39
- .describe("Gestoria payroll software format / Formato del software de gestoria"),
40
- month: z.string().regex(/^\d{4}-\d{2}$/).describe("Month in 'YYYY-MM' format / Mes formato 'YYYY-MM'"),
29
+ .enum(["a3", "contasol", "sage", "siltra"])
30
+ .describe("Echoed destination label; does not change JSON serialization / Etiqueta de destino"),
31
+ month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).describe("Month in 'YYYY-MM' format / Mes formato 'YYYY-MM'"),
41
32
  },
42
33
  outputSchema: payrollExportOutput,
43
34
  }, async ({ format, month }) => withToolLogging("payroll_export", () => withBackendGuard("payroll_export", "/v1/payroll/prep/export", async () => {
@@ -50,17 +41,13 @@ export function registerPayrollTools(server, client) {
50
41
  // -- payroll_checklist --
51
42
  server.registerTool("payroll_checklist", {
52
43
  title: "Payroll Readiness Checklist",
53
- description: "List all employees for a given month with their payroll readiness status. " +
54
- "Status values:\n" +
55
- " - 'ready' — all required data present, ready to export\n" +
56
- " - 'missing_data' — some required fields missing (see missingFields[])\n" +
57
- " - 'blocked' — manually blocked or data inconsistency detected\n" +
58
- "\n" +
59
- "Use BEFORE payroll_export to identify gaps. Month format: 'YYYY-MM'. " +
60
- "/ Lista empleados con estado de preparacion para nomina. Usar antes de payroll_export.",
44
+ description: "List payable employees (active or on leave) for a month, whether each has a payroll profile, " +
45
+ "whether its required fields are ready, which fields are missing, and monthly review state. " +
46
+ "Suspended and offboarded employees are excluded. Use before payroll_export. " +
47
+ "/ Lista empleados pagables y la preparacion real de su perfil de nomina.",
61
48
  annotations: READ_ONLY_ANNOTATIONS,
62
49
  inputSchema: {
63
- month: z.string().regex(/^\d{4}-\d{2}$/).describe("Month in 'YYYY-MM' format / Mes formato 'YYYY-MM'"),
50
+ month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).describe("Month in 'YYYY-MM' format / Mes formato 'YYYY-MM'"),
64
51
  },
65
52
  outputSchema: payrollChecklistOutput,
66
53
  }, async ({ month }) => withToolLogging("payroll_checklist", () => withBackendGuard("payroll_checklist", "/v1/payroll/prep/employees", async () => {
@@ -2,16 +2,13 @@
2
2
  * Permissions tools for the Frihet MCP server — D4-B megasprint (2 tools).
3
3
  *
4
4
  * Tools:
5
- * 1. permissions_matrix — full role/permission matrix for the workspace
6
- * 2. permissions_me — caller's own role + permissions
5
+ * 1. permissions_matrix — documented RBAC-model snapshot
6
+ * 2. permissions_me — RBAC model + actual API-key scope reporting
7
7
  *
8
8
  * REST surface: /v1/permissions/matrix, /v1/permissions/me
9
9
  *
10
- * Permissions are derived from role + workspace overrides. Tools are read-only —
11
- * mutating roles is done via team_invite / team_role_update (existing).
12
- *
13
- * NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
14
- * until backend ships.
10
+ * These read-only reports are useful for discovery, but neither is an exhaustive
11
+ * runtime-authorization prediction. A backend 403 remains authoritative.
15
12
  */
16
13
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
14
  import type { IFrihetClient } from "../client-interface.js";