@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,7 +2,14 @@
2
2
  * Deposit 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, depositItemOutput, actionResultOutput } from "./shared.js";
5
+ import { applyDepositFields, depositFields, DepositInputError } from "../deposit-contract.js";
6
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, ERROR_CONTENT_ANNOTATIONS, paginatedOutput, deleteResultOutput, depositItemOutput, actionResultOutput } from "./shared.js";
7
+ function depositValidationResponse(error) {
8
+ return {
9
+ isError: true,
10
+ content: [{ type: "text", text: `Error: ${error.message}`, annotations: ERROR_CONTENT_ANNOTATIONS }],
11
+ };
12
+ }
6
13
  export function registerDepositTools(server, client) {
7
14
  // -- list_deposits --
8
15
  server.registerTool("list_deposits", {
@@ -45,8 +52,8 @@ export function registerDepositTools(server, client) {
45
52
  const result = await client.listDeposits({ limit, offset, after, fields, from, to, clientId, status });
46
53
  const hints = enrichResponse("deposits", "list", result.data);
47
54
  return {
48
- content: [listContent(formatPaginatedResponse("deposits", result))],
49
- structuredContent: { ...result, ...hints },
55
+ content: [listContent(formatPaginatedResponse("deposits", result) + hints)],
56
+ structuredContent: { ...result },
50
57
  };
51
58
  }));
52
59
  // -- get_deposit --
@@ -69,69 +76,71 @@ export function registerDepositTools(server, client) {
69
76
  // -- create_deposit --
70
77
  server.registerTool("create_deposit", {
71
78
  title: "Create Deposit",
72
- description: "Record a new deposit from a client. Requires clientId and amount. " +
73
- "Useful for tracking advance payments, retainers, and security deposits. " +
74
- "Example: clientId='abc123', amount=500, currency='EUR', description='Project retainer' " +
75
- "/ Registra un nuevo deposito de un cliente. Requiere clientId e importe. " +
76
- "Util para adelantos, retenciones y depositos de garantia.",
79
+ description: "Record a deposit in Frihet's bookkeeping; no money is transferred. Requires clientId, clientName, amount, date and description. " +
80
+ "The API initializes status='active'; status and notes are not writable. " +
81
+ "Example: clientId='abc123', clientName='Example Client', amount=500, date='2026-10-21', description='Project retainer' " +
82
+ "/ Registra un depósito contable sin transferir dinero. Requiere clientId, clientName, amount, date y description. " +
83
+ "La API inicia el estado 'active'; status y notes no son campos editables.",
77
84
  annotations: CREATE_ANNOTATIONS,
78
- inputSchema: {
79
- clientId: z.string().describe("Client ID / ID del cliente"),
80
- clientName: z.string().optional().describe("Client name (denormalized) / Nombre del cliente"),
81
- amount: z.number().describe("Deposit amount / Importe del deposito"),
82
- currency: z.string().optional().describe("Currency code (e.g. 'EUR', 'USD') / Codigo de moneda"),
83
- date: z
84
- .string()
85
- .optional()
86
- .describe("Deposit date in ISO 8601 (YYYY-MM-DD) / Fecha del deposito"),
87
- description: z.string().optional().describe("Deposit description / Descripcion del deposito"),
88
- status: z
89
- .string()
90
- .optional()
91
- .describe("Status (e.g. 'pending', 'applied', 'refunded') / Estado"),
92
- paymentMethod: z
93
- .string()
94
- .optional()
95
- .describe("Payment method (e.g. 'bank_transfer', 'card') / Metodo de pago"),
96
- reference: z.string().optional().describe("External reference or receipt number / Referencia externa"),
97
- notes: z.string().optional().describe("Internal notes / Notas internas"),
98
- },
85
+ inputSchema: z.object({
86
+ clientId: depositFields.clientId.describe("Client ID / ID del cliente"),
87
+ clientName: depositFields.clientName.describe("Client name / Nombre del cliente"),
88
+ amount: depositFields.amount.describe("Positive deposit amount / Importe positivo del depósito"),
89
+ currency: depositFields.currency.optional().describe("Currency code, defaults to EUR / Moneda, EUR por defecto"),
90
+ date: depositFields.receivedDate.describe("Received date (YYYY-MM-DD), sent as receivedDate / Fecha de recepción"),
91
+ description: depositFields.description.describe("Deposit description / Descripción del depósito"),
92
+ paymentMethod: depositFields.paymentMethod.describe("Payment method (e.g. bank_transfer) / Método de pago"),
93
+ reference: depositFields.paymentReference.describe("Payment reference, sent as paymentReference / Referencia del pago"),
94
+ }).strict(),
99
95
  outputSchema: depositItemOutput,
100
96
  }, async (input) => withToolLogging("create_deposit", async () => {
101
- const result = await client.createDeposit(input);
102
- const hints = enrichResponse("deposits", "create", result);
103
- return {
104
- content: [mutateContent(formatRecord("Deposit created", result))],
105
- structuredContent: { ...result, ...hints },
106
- };
97
+ try {
98
+ const result = await client.createDeposit(input);
99
+ const hints = enrichResponse("deposits", "create", result);
100
+ return {
101
+ content: [mutateContent(formatRecord("Deposit created", result) + hints)],
102
+ structuredContent: { ...result },
103
+ };
104
+ }
105
+ catch (error) {
106
+ if (error instanceof DepositInputError)
107
+ return depositValidationResponse(error);
108
+ throw error;
109
+ }
107
110
  }));
108
111
  // -- update_deposit --
109
112
  server.registerTool("update_deposit", {
110
113
  title: "Update Deposit",
111
114
  description: "Update an existing deposit using PATCH semantics. Only the provided fields will be changed. " +
112
- "Example: id='abc123', amount=750, notes='Updated retainer amount' " +
113
- "/ Actualiza un deposito existente. Solo se modifican los campos proporcionados.",
115
+ "At least one writable field is required; status and notes are not writable. " +
116
+ "Example: id='abc123', reference='RECEIPT-42' " +
117
+ "/ Actualiza un depósito existente. Requiere al menos un campo editable; status y notes no se pueden guardar.",
114
118
  annotations: UPDATE_ANNOTATIONS,
115
- inputSchema: {
116
- id: z.string().describe("Deposit ID / ID del deposito"),
117
- clientId: z.string().optional().describe("Client ID / ID del cliente"),
118
- clientName: z.string().optional().describe("Client name / Nombre del cliente"),
119
- amount: z.number().optional().describe("Amount / Importe"),
120
- currency: z.string().optional().describe("Currency code / Moneda"),
121
- date: z.string().optional().describe("Date (YYYY-MM-DD) / Fecha"),
122
- description: z.string().optional().describe("Description / Descripcion"),
123
- status: z.string().optional().describe("Status / Estado"),
124
- paymentMethod: z.string().optional().describe("Payment method / Metodo de pago"),
125
- reference: z.string().optional().describe("Reference / Referencia"),
126
- notes: z.string().optional().describe("Notes / Notas"),
127
- },
119
+ inputSchema: z.object({
120
+ id: z.string().min(1).describe("Deposit ID / ID del depósito"),
121
+ clientId: depositFields.clientId.optional().describe("Client ID / ID del cliente"),
122
+ clientName: depositFields.clientName.optional().describe("Client name / Nombre del cliente"),
123
+ amount: depositFields.amount.optional().describe("Positive amount / Importe positivo"),
124
+ currency: depositFields.currency.optional().describe("Currency code / Moneda"),
125
+ date: depositFields.receivedDate.optional().describe("Received date (YYYY-MM-DD) / Fecha de recepción"),
126
+ description: depositFields.description.optional().describe("Description / Descripción"),
127
+ paymentMethod: depositFields.paymentMethod.describe("Payment method / Método de pago"),
128
+ reference: depositFields.paymentReference.describe("Payment reference / Referencia del pago"),
129
+ }).strict().refine(({ id: _id, ...fields }) => Object.values(fields).some((value) => value !== undefined), { message: "Provide at least one writable deposit field / Indica al menos un campo del depósito" }),
128
130
  outputSchema: depositItemOutput,
129
131
  }, async ({ id, ...data }) => withToolLogging("update_deposit", async () => {
130
- const result = await client.updateDeposit(id, data);
131
- return {
132
- content: [mutateContent(formatRecord("Deposit updated", result))],
133
- structuredContent: result,
134
- };
132
+ try {
133
+ const result = await client.updateDeposit(id, data);
134
+ return {
135
+ content: [mutateContent(formatRecord("Deposit updated", result))],
136
+ structuredContent: result,
137
+ };
138
+ }
139
+ catch (error) {
140
+ if (error instanceof DepositInputError)
141
+ return depositValidationResponse(error);
142
+ throw error;
143
+ }
135
144
  }));
136
145
  // -- delete_deposit --
137
146
  server.registerTool("delete_deposit", {
@@ -147,8 +156,8 @@ export function registerDepositTools(server, client) {
147
156
  await client.deleteDeposit(id);
148
157
  const hints = enrichResponse("deposits", "delete", { id });
149
158
  return {
150
- content: [mutateContent(`Deposit ${id} deleted successfully. / Deposito ${id} eliminado correctamente.`)],
151
- structuredContent: { success: true, id, ...hints },
159
+ content: [mutateContent(`Deposit ${id} deleted successfully. / Deposito ${id} eliminado correctamente.` + hints)],
160
+ structuredContent: { success: true, id },
152
161
  };
153
162
  }));
154
163
  // -- apply_deposit --
@@ -156,43 +165,63 @@ export function registerDepositTools(server, client) {
156
165
  title: "Apply Deposit",
157
166
  description: "Apply a deposit to an invoice or mark it as used. " +
158
167
  "Transitions the deposit status to 'applied'. " +
159
- "Example: id='dep_abc123', invoiceId='inv_xyz' " +
160
- "/ Aplica un deposito a una factura o lo marca como utilizado.",
168
+ "ERP requires invoiceId + invoiceNumber + amount on the body; id is the deposit ID; notes is not writable. " +
169
+ "Example: id='dep_abc123', invoiceId='inv_xyz', invoiceNumber='F-2026-007', amount=150 " +
170
+ "/ Aplica un deposito a una factura o lo marca como utilizado. Requiere invoiceId, invoiceNumber y amount; notes no se puede guardar.",
161
171
  annotations: UPDATE_ANNOTATIONS,
162
- inputSchema: {
163
- id: z.string().describe("Deposit ID / ID del deposito"),
164
- invoiceId: z
165
- .string()
166
- .optional()
172
+ inputSchema: z.object({
173
+ id: z.string().min(1).describe("Deposit ID / ID del deposito"),
174
+ invoiceId: applyDepositFields.invoiceId
167
175
  .describe("Invoice ID to apply the deposit to / ID de la factura a la que aplicar el deposito"),
168
- notes: z.string().optional().describe("Application notes / Notas de aplicacion"),
169
- },
176
+ invoiceNumber: applyDepositFields.invoiceNumber
177
+ .describe("Invoice number (document number) the deposit applies to / Numero de la factura"),
178
+ amount: applyDepositFields.amount
179
+ .describe("Amount of the deposit to apply in EUR / Importe del deposito a aplicar en EUR"),
180
+ }).strict(),
170
181
  outputSchema: actionResultOutput,
171
182
  }, async ({ id, ...data }) => withToolLogging("apply_deposit", async () => {
172
- const result = await client.applyDeposit(id, data);
173
- return {
174
- content: [mutateContent(formatRecord("Deposit applied", result))],
175
- structuredContent: result,
176
- };
183
+ try {
184
+ const result = await client.applyDeposit(id, data);
185
+ return {
186
+ content: [mutateContent(formatRecord("Deposit applied", result))],
187
+ structuredContent: result,
188
+ };
189
+ }
190
+ catch (error) {
191
+ if (error instanceof DepositInputError)
192
+ return depositValidationResponse(error);
193
+ throw error;
194
+ }
177
195
  }));
178
196
  // -- refund_deposit --
179
197
  server.registerTool("refund_deposit", {
180
- title: "Refund Deposit",
181
- description: "Refund a deposit back to the client. " +
182
- "Transitions the deposit status to 'refunded'. " +
183
- "Example: id='dep_abc123', reason='Project cancelled' " +
184
- "/ Devuelve un deposito al cliente.",
185
- annotations: UPDATE_ANNOTATIONS,
186
- inputSchema: {
187
- id: z.string().describe("Deposit ID / ID del deposito"),
188
- reason: z.string().optional().describe("Reason for the refund / Motivo de la devolucion"),
189
- notes: z.string().optional().describe("Refund notes / Notas de la devolucion"),
190
- },
198
+ title: "Record Deposit Refund",
199
+ description: "Record a full or partial deposit refund in Frihet's bookkeeping; no money is transferred. Requires confirm=true. " +
200
+ "Omit amount to record a refund of the remaining balance. Only a full refund sets status='refunded'. " +
201
+ "A new call can record another partial refund; after an uncertain result, check get_deposit before retrying. " +
202
+ "Example: id='dep_abc123', amount=50, confirm=true " +
203
+ "/ Registra una devolución total o parcial del depósito sin transferir dinero. Requiere confirm=true. " +
204
+ "Omite amount para registrar la devolución del saldo restante. Solo la devolución total marca el estado 'refunded'. " +
205
+ "Una nueva llamada puede registrar otra devolución parcial; ante un resultado incierto, consulta get_deposit antes de repetir.",
206
+ annotations: { ...UPDATE_ANNOTATIONS, destructiveHint: true, idempotentHint: false },
207
+ inputSchema: z.object({
208
+ id: z.string().min(1).describe("Deposit ID / ID del depósito"),
209
+ amount: z.number().positive().optional().describe("Positive refund amount in the deposit currency; omit for the remaining balance / Importe positivo en la moneda del depósito; omite para el saldo restante"),
210
+ confirm: z
211
+ .boolean()
212
+ .describe("Must be true to record the refund; no money is transferred / Debe ser true para registrar la devolución; no se transfiere dinero"),
213
+ }).strict(),
191
214
  outputSchema: actionResultOutput,
192
- }, async ({ id, ...data }) => withToolLogging("refund_deposit", async () => {
193
- const result = await client.refundDeposit(id, data);
215
+ }, async ({ id, confirm, amount }) => withToolLogging("refund_deposit", async () => {
216
+ if (confirm !== true) {
217
+ return confirmationRequiredError("Error: confirm=true is required to record a deposit refund. " +
218
+ "This reduces the recorded remaining balance; no money is transferred and this tool cannot undo the entry. / " +
219
+ "Se requiere confirm=true para registrar la devolución y reducir el saldo contable. " +
220
+ "No se transfiere dinero y esta herramienta no permite deshacer el registro.");
221
+ }
222
+ const result = await client.refundDeposit(id, amount === undefined ? {} : { amount });
194
223
  return {
195
- content: [mutateContent(formatRecord("Deposit refunded", result))],
224
+ content: [mutateContent(formatRecord("Deposit refund recorded (no money transferred)", result))],
196
225
  structuredContent: result,
197
226
  };
198
227
  }));
@@ -52,15 +52,29 @@ export declare const eInvoiceFormatSchema: z.ZodEnum<{
52
52
  "xrechnung-cii": "xrechnung-cii";
53
53
  "xrechnung-ubl": "xrechnung-ubl";
54
54
  "facturx-en16931": "facturx-en16931";
55
+ fatturapa: "fatturapa";
56
+ "peppol-bis-3": "peppol-bis-3";
57
+ ubl: "ubl";
58
+ cii: "cii";
55
59
  "facturx-extended": "facturx-extended";
56
60
  "facturx-basic": "facturx-basic";
57
61
  "facturx-minimum": "facturx-minimum";
62
+ }>;
63
+ export type EInvoiceFormat = z.infer<typeof eInvoiceFormatSchema>;
64
+ /** Export is narrower than dispatch/validation. The ERP public export API
65
+ * exposes Factur-X EN16931 only; accepting Extended/Basic/Minimum here and
66
+ * silently mapping them to EN16931 would produce the wrong fiscal profile. */
67
+ export declare const eInvoiceExportFormatSchema: z.ZodEnum<{
68
+ facturae: "facturae";
69
+ "xrechnung-cii": "xrechnung-cii";
70
+ "xrechnung-ubl": "xrechnung-ubl";
71
+ "facturx-en16931": "facturx-en16931";
58
72
  fatturapa: "fatturapa";
73
+ "peppol-bis-3": "peppol-bis-3";
74
+ "fa-2-ksef": "fa-2-ksef";
59
75
  ubl: "ubl";
60
76
  cii: "cii";
61
- "peppol-bis-3": "peppol-bis-3";
62
77
  }>;
63
- export type EInvoiceFormat = z.infer<typeof eInvoiceFormatSchema>;
64
78
  export declare const sendEInvoiceOutput: z.ZodObject<{
65
79
  workflowRunId: z.ZodString;
66
80
  status: z.ZodLiteral<"queued">;
@@ -28,7 +28,7 @@
28
28
  * POST /v1/invoices/:id/ksef/submit — Poland KSeF (stub — endpoint not yet exposed; returns unavailable)
29
29
  */
30
30
  import { z } from "zod/v4";
31
- import { withToolLogging, mutateContent, getContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, } from "./shared.js";
31
+ import { withToolLogging, mutateContent, getContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, } from "./shared.js";
32
32
  /* ------------------------------------------------------------------ */
33
33
  /* Honest "backend endpoint not yet available" result */
34
34
  /* ------------------------------------------------------------------ */
@@ -99,6 +99,20 @@ export const eInvoiceFormatSchema = z.enum([
99
99
  "peppol-bis-3",
100
100
  "facturae",
101
101
  ]);
102
+ /** Export is narrower than dispatch/validation. The ERP public export API
103
+ * exposes Factur-X EN16931 only; accepting Extended/Basic/Minimum here and
104
+ * silently mapping them to EN16931 would produce the wrong fiscal profile. */
105
+ export const eInvoiceExportFormatSchema = z.enum([
106
+ "xrechnung-cii",
107
+ "xrechnung-ubl",
108
+ "facturx-en16931",
109
+ "fatturapa",
110
+ "ubl",
111
+ "cii",
112
+ "peppol-bis-3",
113
+ "fa-2-ksef",
114
+ "facturae",
115
+ ]);
102
116
  /* ------------------------------------------------------------------ */
103
117
  /* Output schemas */
104
118
  /* ------------------------------------------------------------------ */
@@ -410,19 +424,23 @@ export function registerEInvoiceTools(server, _client) {
410
424
  // -- einvoice_export --
411
425
  server.registerTool("einvoice_export", {
412
426
  title: "Export E-Invoice XML",
413
- description: "Export an invoice as e-invoicing XML in a specific format. " +
427
+ description: "Export an invoice as inline e-invoicing XML in a specific format. " +
414
428
  "Supports Facturae (ES B2G), XRechnung-CII (DE), XRechnung-UBL (DE), " +
415
- "Factur-X profiles (FR), FatturaPA (IT), PEPPOL-BIS-3 (EU network), UBL and CII (generic). " +
429
+ "Factur-X EN16931 (FR), FatturaPA (IT), PEPPOL-BIS-3 (EU network), FA-2-KSeF (PL), UBL and CII (generic). " +
430
+ "Factur-X Extended, Basic and Minimum are not exposed by this endpoint and are refused rather than collapsed to a different profile. " +
416
431
  "For Facturae, set signed=true to get XAdES-enveloped XML for FACe/AEAT submission. " +
417
- "Returns a signed download URL valid for 24 hours. " +
432
+ "Returns the generated XML inline with its MIME type and filename. " +
418
433
  "\n\n" +
419
434
  "/ Exporta una factura como XML de facturacion electronica en el formato especificado. " +
420
435
  "Para Facturae con signed=true devuelve XML firmado XAdES para envio a FACe/AEAT.",
421
- annotations: READ_ONLY_ANNOTATIONS,
436
+ // Conservative static classification: signed=true exercises a workspace
437
+ // signing certificate, so the tool cannot truthfully be advertised as a
438
+ // read-only operation even though unsigned generation is side-effect free.
439
+ annotations: UPDATE_ANNOTATIONS,
422
440
  inputSchema: {
423
441
  invoiceId: z.string().describe("Frihet invoice ID to export / ID de la factura a exportar"),
424
- format: eInvoiceFormatSchema.describe("E-invoice format. Choose based on recipient country: " +
425
- "ES B2G→facturae, DE→xrechnung-cii/ubl, FR→facturx-*, IT→fatturapa, EU PEPPOL→peppol-bis-3, " +
442
+ format: eInvoiceExportFormatSchema.describe("E-invoice format. Choose based on recipient country: " +
443
+ "ES B2G→facturae, DE→xrechnung-cii/ubl, FR→facturx-en16931, IT→fatturapa, PL→fa-2-ksef, EU PEPPOL→peppol-bis-3, " +
426
444
  "generic→ubl or cii / Formato de factura electronica."),
427
445
  signed: z.boolean().optional().describe("If true, returns XAdES-enveloped signed XML (Facturae only). Requires workspace signing certificate configured. " +
428
446
  "/ Si true, devuelve XML firmado XAdES (solo Facturae). Requiere certificado de firma configurado."),
@@ -439,7 +457,8 @@ export function registerEInvoiceTools(server, _client) {
439
457
  ` Format: ${format}\n` +
440
458
  ` Signed: ${signed ?? false}\n` +
441
459
  ` Filename: ${result.filename}\n` +
442
- ` Download URL: ${result.xmlUrl}`),
460
+ ` Content-Type: ${result.contentType}\n` +
461
+ ` XML: returned inline (${result.xml.length} characters)`),
443
462
  ],
444
463
  structuredContent: result,
445
464
  };
@@ -460,7 +479,8 @@ export function registerEInvoiceTools(server, _client) {
460
479
  // -- face_submit --
461
480
  server.registerTool("face_submit", {
462
481
  title: "Submit to FACe (Spain B2G)",
463
- description: "Submit a Facturae-formatted invoice to the Spanish FACe (Punto General de Entrada de Facturas Electrónicas) B2G portal. " +
482
+ description: "Requires confirm=true as a local client/agent interlock before submitting a Facturae invoice to Spain's FACe B2G portal. " +
483
+ "This flag is not server-verifiable human attestation and is never forwarded to the API. " +
464
484
  "The invoice must have a recipient with DIR3 administrative unit codes (órgano gestor, unidad tramitadora, oficina contable). " +
465
485
  "Returns a submission reference (registro) that can be used with face_status to poll the portal. " +
466
486
  "\n\n" +
@@ -477,9 +497,22 @@ export function registerEInvoiceTools(server, _client) {
477
497
  invoiceId: z.string().describe("Frihet invoice ID to submit / ID de la factura a enviar a FACe"),
478
498
  mode: z.enum(["mock", "sandbox", "production"]).optional().describe("Submission mode: mock (local sim), sandbox (FACe pre-prod), production (live). Default: production. " +
479
499
  "/ Modo de envío: mock (simulación local), sandbox (pre-producción FACe), production (real). Por defecto: production."),
500
+ confirm: z.boolean().describe("Must be true to pass the local submission interlock in every mode, including mock and sandbox. " +
501
+ "This is not actor/resource/payload/expiry-bound attestation. / Debe ser true para superar el interlock local en todos los modos."),
480
502
  },
481
503
  outputSchema: faceSubmitOutput,
482
- }, async ({ invoiceId, mode }) => withToolLogging("face_submit", async () => {
504
+ }, async ({ invoiceId, mode, confirm }) => withToolLogging("face_submit", async () => {
505
+ if (!confirm) {
506
+ return {
507
+ content: [{
508
+ type: "text",
509
+ text: "Error: face_submit requires confirm=true before any FACe submission or simulation. " +
510
+ "It is a local client/agent interlock, not server-verifiable human attestation. " +
511
+ "/ Se requiere confirm=true antes de cualquier envío o simulación FACe.",
512
+ }],
513
+ isError: true,
514
+ };
515
+ }
483
516
  const plannedEndpoint = `/v1/invoices/${invoiceId}/face/submit`;
484
517
  const resolvedMode = mode ?? "production";
485
518
  try {
@@ -562,7 +595,8 @@ export function registerEInvoiceTools(server, _client) {
562
595
  // -- ticketbai_submit --
563
596
  server.registerTool("ticketbai_submit", {
564
597
  title: "Submit TicketBAI (Basque Country)",
565
- description: "Submit an invoice to the Basque Country TicketBAI e-invoicing system. " +
598
+ description: "Requires confirm=true as a local client/agent interlock before submitting an invoice to TicketBAI. " +
599
+ "This flag is not server-verifiable human attestation and is never forwarded to the API. " +
566
600
  "Territory is auto-detected from the workspace address (Bizkaia → BATUZ/LROE, Gipuzkoa → Gipuzkoa TicketBAI, Araba → Araba TicketBAI). " +
567
601
  "Returns the submission TicketBAI ID (TBAI identifier) and QR code URL for printing on the invoice. " +
568
602
  "\n\n" +
@@ -577,9 +611,22 @@ export function registerEInvoiceTools(server, _client) {
577
611
  invoiceId: z.string().describe("Frihet invoice ID to submit / ID de la factura a enviar"),
578
612
  sandbox: z.boolean().optional().describe("If true, submits to the hacienda test endpoint instead of production. Default: false. " +
579
613
  "/ Si true, envía al entorno de pruebas de la hacienda. Por defecto: false."),
614
+ confirm: z.boolean().describe("Must be true to pass the local submission interlock in both sandbox and production. " +
615
+ "This is not actor/resource/payload/expiry-bound attestation. / Debe ser true para superar el interlock local."),
580
616
  },
581
617
  outputSchema: ticketbaiSubmitOutput,
582
- }, async ({ invoiceId, sandbox }) => withToolLogging("ticketbai_submit", async () => {
618
+ }, async ({ invoiceId, sandbox, confirm }) => withToolLogging("ticketbai_submit", async () => {
619
+ if (!confirm) {
620
+ return {
621
+ content: [{
622
+ type: "text",
623
+ text: "Error: ticketbai_submit requires confirm=true before any TicketBAI submission. " +
624
+ "It is a local client/agent interlock, not server-verifiable human attestation. " +
625
+ "/ Se requiere confirm=true antes de cualquier envío TicketBAI.",
626
+ }],
627
+ isError: true,
628
+ };
629
+ }
583
630
  const plannedEndpoint = `/v1/invoices/${invoiceId}/ticketbai/submit`;
584
631
  const isSandbox = sandbox ?? false;
585
632
  try {
@@ -2,6 +2,22 @@
2
2
  * Expense tools for the Frihet MCP server.
3
3
  */
4
4
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { z } from "zod/v4";
5
6
  import type { IFrihetClient } from "../client-interface.js";
7
+ /**
8
+ * Public Zod schema for `create_expense` input — exported for tests so the
9
+ * validation contract can be exercised directly without spinning up a stub
10
+ * MCP server. The registered tool's inputSchema MUST wrap this exact object
11
+ * (MCP serializes the live Zod object to JSON Schema).
12
+ */
13
+ export declare const createExpenseInputSchema: z.ZodObject<{
14
+ description: z.ZodString;
15
+ amount: z.ZodNumber;
16
+ category: z.ZodOptional<z.ZodString>;
17
+ date: z.ZodOptional<z.ZodISODate>;
18
+ vendor: z.ZodOptional<z.ZodString>;
19
+ taxDeductible: z.ZodOptional<z.ZodBoolean>;
20
+ paidDate: z.ZodOptional<z.ZodISODate>;
21
+ }, z.core.$strict>;
6
22
  export declare function registerExpenseTools(server: McpServer, client: IFrihetClient): void;
7
23
  //# sourceMappingURL=expenses.d.ts.map
@@ -3,6 +3,38 @@
3
3
  */
4
4
  import { z } from "zod/v4";
5
5
  import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, deleteResultOutput, expenseItemOutput } from "./shared.js";
6
+ /**
7
+ * Public Zod schema for `create_expense` input — exported for tests so the
8
+ * validation contract can be exercised directly without spinning up a stub
9
+ * MCP server. The registered tool's inputSchema MUST wrap this exact object
10
+ * (MCP serializes the live Zod object to JSON Schema).
11
+ */
12
+ export const createExpenseInputSchema = z
13
+ .object({
14
+ description: z.string().trim().min(1).max(1000).describe("Expense description / Descripcion del gasto"),
15
+ amount: z.number().finite().min(0.01).max(10_000_000).describe("Amount in EUR / Importe en EUR"),
16
+ category: z
17
+ .string()
18
+ .max(10_000)
19
+ .optional()
20
+ .describe("Expense category (e.g. 'office', 'travel', 'software') / Categoria"),
21
+ date: z
22
+ .iso.date()
23
+ .optional()
24
+ .describe("Expense date in ISO 8601 (YYYY-MM-DD) / Fecha del gasto"),
25
+ vendor: z.string().trim().max(200).optional().describe("Vendor/supplier name / Nombre del proveedor"),
26
+ taxDeductible: z
27
+ .boolean()
28
+ .optional()
29
+ .describe("Whether the expense is tax deductible / Si el gasto es deducible fiscalmente"),
30
+ paidDate: z
31
+ .iso.date()
32
+ .optional()
33
+ .describe("Optional payment date in ISO 8601 (YYYY-MM-DD). Determines when the expense is treated as paid. " +
34
+ "Must be a real calendar date (2028-02-29 is valid, 2026-02-29 is not). " +
35
+ "/ Fecha de pago opcional en ISO 8601 (YYYY-MM-DD). Determina cuando se considera pagado el gasto."),
36
+ })
37
+ .strict();
6
38
  export function registerExpenseTools(server, client) {
7
39
  // -- list_expenses --
8
40
  server.registerTool("list_expenses", {
@@ -45,8 +77,8 @@ export function registerExpenseTools(server, client) {
45
77
  const result = await client.listExpenses({ limit, offset, after, fields, from, to, vendorId, category });
46
78
  const hints = enrichResponse("expenses", "list", result.data);
47
79
  return {
48
- content: [listContent(formatPaginatedResponse("expenses", result))],
49
- structuredContent: { ...result, ...hints },
80
+ content: [listContent(formatPaginatedResponse("expenses", result) + hints)],
81
+ structuredContent: { ...result },
50
82
  };
51
83
  }));
52
84
  // -- get_expense --
@@ -67,38 +99,26 @@ export function registerExpenseTools(server, client) {
67
99
  };
68
100
  }));
69
101
  // -- create_expense --
102
+ // paidDate (#1062): optional ISO calendar date. Strict shape (zero-padded
103
+ // YYYY-MM-DD) AND real-calendar validity (rejects 2026-02-29, 2026-04-31,
104
+ // non-zero-padded forms). Forwards verbatim to the REST paidDate field —
105
+ // the existing pass-through in client.createExpense handles the wire shape.
70
106
  server.registerTool("create_expense", {
71
107
  title: "Create Expense",
72
108
  description: "Record a new expense. Requires a description and amount. " +
73
109
  "Useful for tracking business costs, deductible expenses, and vendor payments. " +
74
- "Example: description='Office supplies', amount=49.99, category='office', vendor='Amazon', taxDeductible=true " +
110
+ "Example: description='Office supplies', amount=49.99, category='office', vendor='Amazon', taxDeductible=true, paidDate='2026-03-15' " +
75
111
  "/ Registra un nuevo gasto. Requiere descripcion e importe. " +
76
112
  "Util para seguimiento de costes, gastos deducibles y pagos a proveedores.",
77
113
  annotations: CREATE_ANNOTATIONS,
78
- inputSchema: {
79
- description: z.string().describe("Expense description / Descripcion del gasto"),
80
- amount: z.number().describe("Amount in EUR / Importe en EUR"),
81
- category: z
82
- .string()
83
- .optional()
84
- .describe("Expense category (e.g. 'office', 'travel', 'software') / Categoria"),
85
- date: z
86
- .string()
87
- .optional()
88
- .describe("Expense date in ISO 8601 (YYYY-MM-DD) / Fecha del gasto"),
89
- vendor: z.string().optional().describe("Vendor/supplier name / Nombre del proveedor"),
90
- taxDeductible: z
91
- .boolean()
92
- .optional()
93
- .describe("Whether the expense is tax deductible / Si el gasto es deducible fiscalmente"),
94
- },
114
+ inputSchema: createExpenseInputSchema,
95
115
  outputSchema: expenseItemOutput,
96
116
  }, async (input) => withToolLogging("create_expense", async () => {
97
117
  const result = await client.createExpense(input);
98
118
  const hints = enrichResponse("expenses", "create", result);
99
119
  return {
100
- content: [mutateContent(formatRecord("Expense created", result))],
101
- structuredContent: { ...result, ...hints },
120
+ content: [mutateContent(formatRecord("Expense created", result) + hints)],
121
+ structuredContent: { ...result },
102
122
  };
103
123
  }));
104
124
  // -- update_expense --
@@ -110,10 +130,10 @@ export function registerExpenseTools(server, client) {
110
130
  annotations: UPDATE_ANNOTATIONS,
111
131
  inputSchema: {
112
132
  id: z.string().describe("Expense ID / ID del gasto"),
113
- description: z.string().optional().describe("Description / Descripcion"),
133
+ description: z.string().max(10_000).optional().describe("Description / Descripcion"),
114
134
  amount: z.number().optional().describe("Amount in EUR / Importe"),
115
- category: z.string().optional().describe("Category / Categoria"),
116
- date: z.string().optional().describe("Date (YYYY-MM-DD) / Fecha"),
135
+ category: z.string().max(10_000).optional().describe("Category / Categoria"),
136
+ date: z.iso.date().optional().describe("Date (YYYY-MM-DD) / Fecha"),
117
137
  vendor: z.string().optional().describe("Vendor / Proveedor"),
118
138
  taxDeductible: z.boolean().optional().describe("Tax deductible / Deducible"),
119
139
  },
@@ -139,8 +159,8 @@ export function registerExpenseTools(server, client) {
139
159
  await client.deleteExpense(id);
140
160
  const hints = enrichResponse("expenses", "delete", { id });
141
161
  return {
142
- content: [mutateContent(`Expense ${id} deleted successfully. / Gasto ${id} eliminado correctamente.`)],
143
- structuredContent: { success: true, id, ...hints },
162
+ content: [mutateContent(`Expense ${id} deleted successfully. / Gasto ${id} eliminado correctamente.` + hints)],
163
+ structuredContent: { success: true, id },
144
164
  };
145
165
  }));
146
166
  }
@@ -12,10 +12,9 @@
12
12
  *
13
13
  * NOTE: ticketbai_status is registered by einvoice.ts (canonical, more complete impl).
14
14
  *
15
- * REST surface: /v1/fiscal/* (documented: pending — backend ships separately)
16
- *
17
- * NOTE: ERP backend endpoints /v1/fiscal/* are planned. Tools are wired
18
- * and will surface 404 errors until the backend ships.
15
+ * REST surface: Frihet-ERP functions/src/publicApi.ts serves READ-ONLY
16
+ * GET /v1/fiscal/modelo/{303,130,390,347}. Modelo 180 has no route; its tool
17
+ * returns NOT_DEPLOYED without calling. Period params: src/fiscal-period.ts.
19
18
  */
20
19
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
20
  import type { IFrihetClient } from "../client-interface.js";