@frihet/mcp-server 1.16.5 → 1.17.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 (68) hide show
  1. package/README.md +92 -31
  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 +549 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +254 -0
  10. package/dist/client-interface.d.ts +22 -14
  11. package/dist/client.d.ts +114 -18
  12. package/dist/client.js +520 -58
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +209 -29
  15. package/dist/demo-fixtures.d.ts +3 -3
  16. package/dist/demo-fixtures.js +7 -7
  17. package/dist/fiscal-aliases.d.ts +44 -0
  18. package/dist/fiscal-aliases.js +62 -0
  19. package/dist/index.js +41 -56
  20. package/dist/logger.d.ts +2 -2
  21. package/dist/logger.js +140 -8
  22. package/dist/observability.d.ts +18 -29
  23. package/dist/observability.js +192 -105
  24. package/dist/openai-profile.d.ts +27 -15
  25. package/dist/openai-profile.js +1191 -145
  26. package/dist/openai-review-contract.d.ts +49 -0
  27. package/dist/openai-review-contract.js +919 -0
  28. package/dist/openai-review-oauth.d.ts +98 -0
  29. package/dist/openai-review-oauth.js +136 -0
  30. package/dist/prompts/register-all.d.ts +2 -0
  31. package/dist/prompts/register-all.js +2 -0
  32. package/dist/public-capability-contract.d.ts +39 -0
  33. package/dist/public-capability-contract.js +186 -0
  34. package/dist/redaction.d.ts +10 -0
  35. package/dist/redaction.js +74 -0
  36. package/dist/resources/register-all.js +6 -12
  37. package/dist/server-composition.d.ts +16 -0
  38. package/dist/server-composition.js +44 -0
  39. package/dist/tool-exposure.d.ts +25 -22
  40. package/dist/tool-exposure.js +199 -77
  41. package/dist/tools/accountingClose.d.ts +14 -5
  42. package/dist/tools/accountingClose.js +41 -23
  43. package/dist/tools/backend-availability.d.ts +3 -3
  44. package/dist/tools/backend-availability.js +3 -3
  45. package/dist/tools/clients.js +9 -9
  46. package/dist/tools/crm.js +22 -21
  47. package/dist/tools/deposits.js +30 -11
  48. package/dist/tools/einvoice.d.ts +1 -1
  49. package/dist/tools/expenses.d.ts +16 -0
  50. package/dist/tools/expenses.js +47 -27
  51. package/dist/tools/hr.d.ts +2 -3
  52. package/dist/tools/hr.js +8 -9
  53. package/dist/tools/intelligence.js +95 -4
  54. package/dist/tools/invoices.js +150 -47
  55. package/dist/tools/payroll.d.ts +5 -8
  56. package/dist/tools/payroll.js +19 -32
  57. package/dist/tools/permissions.d.ts +4 -7
  58. package/dist/tools/permissions.js +11 -14
  59. package/dist/tools/products.js +6 -6
  60. package/dist/tools/quotes.js +109 -24
  61. package/dist/tools/shared.d.ts +1431 -75
  62. package/dist/tools/shared.js +386 -76
  63. package/dist/tools/team.js +18 -14
  64. package/dist/tools/vendors.js +9 -9
  65. package/dist/tools/webhooks.js +35 -30
  66. package/dist/types.d.ts +28 -4
  67. package/docs/agent-onboarding.json +381 -0
  68. package/package.json +32 -7
@@ -10,7 +10,98 @@
10
10
  * - duplicate_invoice: Clone an invoice for a new period
11
11
  */
12
12
  import { z } from "zod/v4";
13
- import { withToolLogging, formatRecord, getContent, mutateContent, openObjectOutput, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, } from "./shared.js";
13
+ import { withToolLogging, formatRecord, getContent, mutateContent, invoiceItemOutput, openObjectOutput, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, } from "./shared.js";
14
+ const usageLimitOutput = z.union([z.number(), z.literal("unlimited")]);
15
+ const businessContextOutput = z.object({
16
+ business: z.object({
17
+ name: z.string(),
18
+ taxId: z.string().optional(),
19
+ fiscalZone: z.string(),
20
+ currency: z.string(),
21
+ language: z.string(),
22
+ country: z.string(),
23
+ }).passthrough(),
24
+ defaults: z.object({
25
+ taxRate: z.number(),
26
+ irpfRate: z.number(),
27
+ dueDays: z.number(),
28
+ currency: z.string(),
29
+ }).passthrough(),
30
+ plan: z.object({
31
+ name: z.string(),
32
+ invoices: z.object({ used: z.number(), limit: usageLimitOutput }),
33
+ expenses: z.object({ used: z.number(), limit: usageLimitOutput }),
34
+ aiMessages: z.object({ used: z.number(), limit: usageLimitOutput }),
35
+ }).passthrough(),
36
+ series: z.array(z.object({
37
+ id: z.string().optional(),
38
+ prefix: z.string().optional(),
39
+ current: z.number().optional(),
40
+ year: z.number().optional(),
41
+ }).passthrough()),
42
+ recentActivity: z.object({
43
+ lastInvoice: z.object({
44
+ number: z.string().optional(),
45
+ date: z.string().optional(),
46
+ client: z.string().optional(),
47
+ }).passthrough().nullable(),
48
+ lastExpense: z.object({
49
+ date: z.string().optional(),
50
+ vendor: z.string().optional(),
51
+ amount: z.number(),
52
+ }).passthrough().nullable(),
53
+ overdueCount: z.number(),
54
+ overdueAmount: z.number(),
55
+ unpaidCount: z.number(),
56
+ }).passthrough(),
57
+ topClients: z.array(z.object({
58
+ name: z.string(),
59
+ totalRevenue: z.number(),
60
+ invoiceCount: z.number(),
61
+ }).passthrough()),
62
+ currentMonth: z.object({
63
+ revenue: z.number(),
64
+ expenses: z.number(),
65
+ profit: z.number(),
66
+ invoiceCount: z.number(),
67
+ expenseCount: z.number(),
68
+ }).passthrough(),
69
+ }).passthrough().describe("Business context snapshot: workspace profile, defaults, plan usage, recent activity and current-month performance");
70
+ const monthlySummaryOutput = z.object({
71
+ period: z.string(),
72
+ revenue: z.object({
73
+ total: z.number(),
74
+ taxBase: z.number(),
75
+ tax: z.number(),
76
+ irpf: z.number(),
77
+ }).passthrough(),
78
+ expenses: z.object({
79
+ total: z.number(),
80
+ deductible: z.number(),
81
+ tax: z.number(),
82
+ }).passthrough(),
83
+ profit: z.object({
84
+ gross: z.number(),
85
+ net: z.number(),
86
+ }).passthrough(),
87
+ invoices: z.object({
88
+ created: z.number(),
89
+ sent: z.number(),
90
+ paid: z.number(),
91
+ overdue: z.number(),
92
+ }).passthrough(),
93
+ topClients: z.array(z.object({
94
+ name: z.string(),
95
+ totalRevenue: z.number(),
96
+ invoiceCount: z.number(),
97
+ }).passthrough()),
98
+ byCategory: z.record(z.string(), z.number()),
99
+ taxLiability: z.object({
100
+ vatPayable: z.number(),
101
+ irpfRetained: z.number(),
102
+ estimatedModel303: z.number(),
103
+ }).passthrough(),
104
+ }).passthrough().describe("Monthly business summary with revenue, expenses, profit, invoice status, top clients and expense categories");
14
105
  export function registerIntelligenceTools(server, client) {
15
106
  // -- get_business_context --
16
107
  server.registerTool("get_business_context", {
@@ -22,7 +113,7 @@ export function registerIntelligenceTools(server, client) {
22
113
  "principales clientes y resumen del mes actual. Llama a esto PRIMERO en cualquier sesion.",
23
114
  annotations: READ_ONLY_ANNOTATIONS,
24
115
  inputSchema: {},
25
- outputSchema: openObjectOutput("Business context snapshot: workspace profile, fiscal setup, recent activity / Contexto de negocio: perfil, configuración fiscal y actividad reciente"),
116
+ outputSchema: businessContextOutput,
26
117
  }, async () => withToolLogging("get_business_context", async () => {
27
118
  const result = await client.getBusinessContext();
28
119
  return {
@@ -46,7 +137,7 @@ export function registerIntelligenceTools(server, client) {
46
137
  .describe("Month in YYYY-MM format (defaults to current month). Example: '2026-03' " +
47
138
  "/ Mes en formato YYYY-MM (por defecto el mes actual)"),
48
139
  },
49
- outputSchema: openObjectOutput("Monthly P&L summary: revenue, expenses, profit, tax liability, invoice stats / Resumen mensual: ingresos, gastos, beneficio, impuestos"),
140
+ outputSchema: monthlySummaryOutput,
50
141
  }, async ({ month }) => withToolLogging("get_monthly_summary", async () => {
51
142
  const result = await client.getMonthlySummary(month);
52
143
  const label = month ? `Monthly Summary (${month})` : "Monthly Summary (current month)";
@@ -101,7 +192,7 @@ export function registerIntelligenceTools(server, client) {
101
192
  .optional()
102
193
  .describe("Due date for the new invoice (YYYY-MM-DD) / Fecha de vencimiento de la nueva factura"),
103
194
  },
104
- outputSchema: openObjectOutput("The newly created draft invoice, cloned from the original / La nueva factura borrador, clonada de la original"),
195
+ outputSchema: invoiceItemOutput.describe("The newly created draft invoice, cloned from the original / La nueva factura borrador, clonada de la original"),
105
196
  }, async ({ id, newIssueDate, newDueDate }) => withToolLogging("duplicate_invoice", async () => {
106
197
  // 1. Fetch the original invoice (returns the FULL raw stored document —
107
198
  // payments, verifactu, eInvoice, operationType, createdBy, etc.)
@@ -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, 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()
@@ -133,8 +141,8 @@ export function registerInvoiceTools(server, client) {
133
141
  const result = await client.listInvoices({ limit, offset, after, fields, status, from, to, clientId, seriesId });
134
142
  const hints = enrichResponse("invoices", "list", result.data);
135
143
  return {
136
- content: [listContent(formatPaginatedResponse("invoices", result))],
137
- structuredContent: { ...result, ...hints },
144
+ content: [listContent(formatPaginatedResponse("invoices", result) + hints)],
145
+ structuredContent: { ...result },
138
146
  };
139
147
  }));
140
148
  // -- get_invoice --
@@ -165,18 +173,18 @@ export function registerInvoiceTools(server, client) {
165
173
  "Soporta retencion IRPF (autonomos ES), recargo de equivalencia, serie, anticipo y descuento global.",
166
174
  annotations: CREATE_ANNOTATIONS,
167
175
  inputSchema: {
168
- clientName: z.string().describe("Client/customer name / Nombre del cliente"),
176
+ clientName: z.string().trim().min(1).max(200).describe("Client/customer name / Nombre del cliente"),
169
177
  ...clientIdentityFields,
170
178
  items: z
171
179
  .array(invoiceItemSchema)
172
180
  .min(1)
173
181
  .describe("Line items (each with description, quantity, unitPrice) / Conceptos de la factura"),
174
182
  issueDate: z
175
- .string()
183
+ .iso.date()
176
184
  .optional()
177
185
  .describe("Issue date in ISO 8601 format (YYYY-MM-DD), defaults to today / Fecha de emision"),
178
186
  dueDate: z
179
- .string()
187
+ .iso.date()
180
188
  .optional()
181
189
  .describe("Due date in ISO 8601 format (YYYY-MM-DD) / Fecha de vencimiento"),
182
190
  status: z
@@ -185,6 +193,7 @@ export function registerInvoiceTools(server, client) {
185
193
  .describe("Invoice status (default: draft) / Estado de la factura"),
186
194
  notes: z
187
195
  .string()
196
+ .max(10_000)
188
197
  .optional()
189
198
  .describe("Additional notes shown on the invoice / Notas adicionales"),
190
199
  taxRate: z
@@ -200,8 +209,8 @@ export function registerInvoiceTools(server, client) {
200
209
  const result = await client.createInvoice(input);
201
210
  const hints = enrichResponse("invoices", "create", result);
202
211
  return {
203
- content: [mutateContent(formatRecord("Invoice created", result))],
204
- structuredContent: { ...result, ...hints },
212
+ content: [mutateContent(formatRecord("Invoice created", result) + hints)],
213
+ structuredContent: { ...result },
205
214
  };
206
215
  }));
207
216
  // -- update_invoice --
@@ -235,26 +244,73 @@ export function registerInvoiceTools(server, client) {
235
244
  const result = await client.updateInvoice(id, data);
236
245
  const hints = enrichResponse("invoices", "update", result);
237
246
  return {
238
- content: [mutateContent(formatRecord("Invoice updated", result))],
239
- structuredContent: { ...result, ...hints },
247
+ content: [mutateContent(formatRecord("Invoice updated", result) + hints)],
248
+ structuredContent: { ...result },
240
249
  };
241
250
  }));
242
251
  // -- delete_invoice --
243
252
  server.registerTool("delete_invoice", {
244
253
  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.",
254
+ description: "Delete an invoice by its ID. Requires confirm=true. " +
255
+ "Only a DRAFT invoice is removed permanently. A sent/paid/overdue invoice is NOT destroyed: " +
256
+ "the backend CANCELS it (status=cancelled) so the VeriFactu hash chain stays intact, and the " +
257
+ "document remains readable via get_invoice. The result reports which happened in `outcome` " +
258
+ "(\"deleted\" or \"cancelled\"). " +
259
+ "/ Elimina una factura por su ID. Requiere confirm=true. Solo una factura en BORRADOR se elimina " +
260
+ "de forma permanente. Una factura emitida/pagada NO se destruye: se CANCELA (status=cancelled) para " +
261
+ "preservar la cadena de hash VeriFactu y sigue consultable con get_invoice.",
247
262
  annotations: DELETE_ANNOTATIONS,
248
263
  inputSchema: {
249
264
  id: z.string().describe("Invoice ID / ID de la factura"),
265
+ confirm: z
266
+ .boolean()
267
+ .describe("Must be true to confirm deletion / Debe ser true para confirmar la eliminacion"),
250
268
  },
251
- outputSchema: deleteResultOutput,
252
- }, async ({ id }) => withToolLogging("delete_invoice", async () => {
253
- await client.deleteInvoice(id);
269
+ outputSchema: documentDeleteResultOutput,
270
+ }, async ({ id, confirm }) => withToolLogging("delete_invoice", async () => {
271
+ 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
+ };
285
+ }
286
+ // 204 → undefined (draft row destroyed). 200 → soft-cancel body (document
287
+ // kept, status=cancelled). Reporting both as "deleted" is the GAP-12 lie.
288
+ const outcome = await client.deleteInvoice(id);
289
+ const cancelled = !!outcome && typeof outcome === "object";
290
+ const body = cancelled ? outcome : {};
254
291
  const hints = enrichResponse("invoices", "delete", { id });
292
+ const previous = typeof body["previousStatus"] === "string" ? ` (was ${body["previousStatus"]})` : "";
293
+ // R3 fix (ERP #1580): extract the per-branch body text to a single
294
+ // variable and append `+ hints` ONCE, after the ternary closes. R2's
295
+ // `+ hints` was branch-local (only in the DELETED branch), so the
296
+ // CANCELLED branch silently dropped the "cannot be undone" warning —
297
+ // 15/16 behavioral paths had warnings, not 16/16. The OUTER concat
298
+ // guarantees both terminal branches carry the enrichment. The
299
+ // `paginated-strict-output.test.ts` runtime tests pin BOTH branches.
300
+ const bodyText = cancelled
301
+ ? `Invoice ${id} was CANCELLED, not deleted${previous}: it still exists with ` +
302
+ "status=cancelled because VeriFactu forbids destroying an issued invoice. / " +
303
+ `Factura ${id} CANCELADA, no eliminada: sigue existiendo con status=cancelled (VeriFactu).`
304
+ : `Invoice ${id} deleted permanently (it was a draft). / ` +
305
+ `Factura ${id} eliminada permanentemente (era un borrador).`;
255
306
  return {
256
- content: [mutateContent(`Invoice ${id} deleted successfully. / Factura ${id} eliminada correctamente.`)],
257
- structuredContent: { success: true, id, ...hints },
307
+ content: [mutateContent(bodyText + hints)],
308
+ structuredContent: {
309
+ success: true,
310
+ id,
311
+ ...body,
312
+ outcome: cancelled ? "cancelled" : "deleted",
313
+ },
258
314
  };
259
315
  }));
260
316
  // -- search_invoices --
@@ -299,24 +355,71 @@ export function registerInvoiceTools(server, client) {
299
355
  const label = query ? `invoices matching "${query}"` : "invoices";
300
356
  const hints = enrichResponse("invoices", "list", result.data);
301
357
  return {
302
- content: [listContent(formatPaginatedResponse(label, result))],
303
- structuredContent: { ...result, ...hints },
358
+ content: [listContent(formatPaginatedResponse(label, result) + hints)],
359
+ structuredContent: { ...result },
304
360
  };
305
361
  }));
306
362
  // -- send_invoice --
307
363
  server.registerTool("send_invoice", {
308
364
  title: "Send Invoice",
309
- description: "Send an invoice to the client via email. Optionally override the recipient email address. " +
365
+ description: "Send an invoice to the client via email. Requires confirm=true. " +
366
+ "Optionally override the recipient email address. " +
310
367
  "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.",
368
+ "Sending reaches a third party and cannot be recalled. " +
369
+ "/ Envia una factura al cliente por email. Requiere confirm=true. " +
370
+ "Opcionalmente se puede cambiar el email destinatario. El envio llega a un tercero y no se puede anular.",
312
371
  annotations: UPDATE_ANNOTATIONS,
313
372
  inputSchema: {
314
373
  id: z.string().describe("Invoice ID / ID de la factura"),
315
374
  to: z.string().optional().describe("Override recipient email / Email destinatario alternativo"),
375
+ confirm: z
376
+ .boolean()
377
+ .describe("Must be true to confirm sending / Debe ser true para confirmar el envio"),
316
378
  },
317
379
  outputSchema: actionResultOutput,
318
- }, async ({ id, to }) => withToolLogging("send_invoice", async () => {
319
- const result = await client.sendInvoice(id, to);
380
+ }, async ({ id, to, confirm }) => withToolLogging("send_invoice", async () => {
381
+ 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
+ };
394
+ }
395
+ const invoice = await client.getInvoice(id);
396
+ if (invoice["status"] === "cancelled") {
397
+ return {
398
+ content: [{ type: "text", text: "Error: a cancelled invoice cannot be sent." }],
399
+ isError: true,
400
+ };
401
+ }
402
+ let recipientEmail = to;
403
+ if (!recipientEmail) {
404
+ const clientId = typeof invoice["clientId"] === "string" ? invoice["clientId"] : undefined;
405
+ if (!clientId) {
406
+ return {
407
+ content: [{ type: "text", text: "Error: this invoice has no linked client with a saved email address." }],
408
+ isError: true,
409
+ };
410
+ }
411
+ const linkedClient = await client.getClient(clientId);
412
+ recipientEmail = typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
413
+ ? linkedClient["email"].trim()
414
+ : undefined;
415
+ if (!recipientEmail) {
416
+ return {
417
+ content: [{ type: "text", text: "Error: the linked client has no saved email address." }],
418
+ isError: true,
419
+ };
420
+ }
421
+ }
422
+ const result = await client.sendInvoice(id, recipientEmail);
320
423
  return {
321
424
  content: [mutateContent(formatRecord("Invoice sent", result))],
322
425
  structuredContent: result,
@@ -344,8 +447,10 @@ export function registerInvoiceTools(server, client) {
344
447
  // -- get_invoice_pdf --
345
448
  server.registerTool("get_invoice_pdf", {
346
449
  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.",
450
+ description: "Get the PDF for an invoice. The body is served as raw `application/pdf` bytes, base64-encoded in `base64` " +
451
+ "with `sizeBytes` and `contentType`. PDFs larger than 25 MiB are rejected with `413 payload_too_large`. " +
452
+ "/ Obtiene el PDF de una factura. El cuerpo se sirve como bytes `application/pdf` en bruto, codificados en base64 " +
453
+ "en `base64`, con `sizeBytes` y `contentType`. PDFs mayores de 25 MiB se rechazan con `413 payload_too_large`.",
349
454
  annotations: READ_ONLY_ANNOTATIONS,
350
455
  inputSchema: {
351
456
  id: z.string().describe("Invoice ID / ID de la factura"),
@@ -354,33 +459,31 @@ export function registerInvoiceTools(server, client) {
354
459
  }, async ({ id }) => withToolLogging("get_invoice_pdf", async () => {
355
460
  const result = await client.getInvoicePdf(id);
356
461
  return {
357
- content: [getContent(formatRecord("Invoice PDF", result))],
462
+ content: [getContent(formatRecord("Invoice PDF", documentResultMetadata(result)))],
358
463
  structuredContent: result,
359
464
  };
360
465
  }));
361
466
  // -- get_invoice_einvoice --
362
467
  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). " +
468
+ title: "Get Invoice E-Invoice Artifact",
469
+ description: "Download the stored e-invoice artifact for an invoice. XML formats return strict UTF-8 text in `xml`; " +
470
+ "Factur-X returns `application/pdf` bytes encoded in `base64`. Both include `id`, `contentType`, and `sizeBytes`. " +
471
+ "XML larger than 5 MiB or PDF larger than 25 MiB is rejected with `413 payload_too_large`. " +
366
472
  "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). " +
473
+ "/ Descarga el artefacto de factura electronica guardado. Los formatos XML devuelven texto UTF-8 estricto en `xml`; " +
474
+ "Factur-X devuelve bytes `application/pdf` codificados en `base64`. Ambos incluyen `id`, `contentType` y `sizeBytes`. " +
475
+ "XML mayor de 5 MiB o PDF mayor de 25 MiB se rechaza con `413 payload_too_large`. " +
369
476
  "Solo disponible despues de guardar o enviar la factura.",
370
477
  annotations: READ_ONLY_ANNOTATIONS,
371
478
  inputSchema: {
372
479
  id: z.string().describe("Invoice ID / ID de la factura"),
373
480
  },
374
- outputSchema: z.object({
375
- xml: z.string().optional(),
376
- filename: z.string().optional(),
377
- format: z.string().optional(),
378
- }).passthrough(),
481
+ outputSchema: einvoiceResultOutput,
379
482
  }, async ({ id }) => withToolLogging("get_invoice_einvoice", async () => {
380
483
  const result = await client.getInvoiceEInvoice(id);
381
484
  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 }),
485
+ content: [getContent(formatRecord("E-invoice artifact", documentResultMetadata(result)))],
486
+ structuredContent: result,
384
487
  };
385
488
  }));
386
489
  // -- create_credit_note --
@@ -439,8 +542,8 @@ export function registerInvoiceTools(server, client) {
439
542
  }, idempotencyKey);
440
543
  const hints = enrichResponse("invoices", "create", result);
441
544
  return {
442
- content: [mutateContent(formatRecord("Credit note created", result))],
443
- structuredContent: { ...result, ...hints },
545
+ content: [mutateContent(formatRecord("Credit note created", result) + hints)],
546
+ structuredContent: { ...result },
444
547
  };
445
548
  }));
446
549
  // -- apply_late_fee --
@@ -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";
@@ -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 { withToolLogging, formatRecord, getContent, READ_ONLY_ANNOTATIONS, permissionsMatrixOutput, permissionsMeOutput, } from "./shared.js";
17
14
  import { withBackendGuard } from "./backend-availability.js";
@@ -19,10 +16,9 @@ export function registerPermissionsTools(server, client) {
19
16
  // -- permissions_matrix --
20
17
  server.registerTool("permissions_matrix", {
21
18
  title: "Permissions Matrix",
22
- description: "Return the full role-to-permission matrix for the workspace. " +
23
- "Lists every role with the permissions it grants and every protected resource. " +
24
- "Useful for security audits, role design, and compliance reporting. " +
25
- "/ Devuelve la matriz completa de roles y permisos del workspace.",
19
+ description: "Return the backend's documented RBAC-model snapshot by role, resource, and action. " +
20
+ "It is not derived from Firestore rules and is not a runtime authorization guarantee; " +
21
+ "a backend 403 is authoritative. / Devuelve el modelo RBAC documentado, no una garantia exhaustiva de autorizacion.",
26
22
  annotations: READ_ONLY_ANNOTATIONS,
27
23
  inputSchema: {},
28
24
  outputSchema: permissionsMatrixOutput,
@@ -36,9 +32,10 @@ export function registerPermissionsTools(server, client) {
36
32
  // -- permissions_me --
37
33
  server.registerTool("permissions_me", {
38
34
  title: "My Permissions",
39
- description: "Return the caller's effective role + permissions in the current workspace. " +
40
- "Useful for client-side UI gating, debugging access errors, and capability discovery. " +
41
- "/ Devuelve el rol efectivo y los permisos del llamante en el workspace actual.",
35
+ description: "Return the caller's backwards-compatible RBAC-model fields separately from actual API-key scopes, " +
36
+ "unrestricted-key semantics, known e-invoice scope denial, and surfaces not reported here. " +
37
+ "This is non-exhaustive; use the exact backend 403 as authority. " +
38
+ "/ Separa el modelo RBAC de los scopes reales de la API key; no es una autorizacion exhaustiva.",
42
39
  annotations: READ_ONLY_ANNOTATIONS,
43
40
  inputSchema: {},
44
41
  outputSchema: permissionsMeOutput,