@frihet/mcp-server 1.17.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +106 -348
  2. package/dist/agent-onboarding.d.ts +1 -1
  3. package/dist/agent-onboarding.js +3 -3
  4. package/dist/canary/mcp-harness.d.ts +1 -1
  5. package/dist/canary/mcp-harness.js +4 -1
  6. package/dist/capability-truth.js +20 -12
  7. package/dist/claude-candidate-contract.d.ts +482 -0
  8. package/dist/claude-candidate-contract.js +87 -0
  9. package/dist/claude-output-privacy.d.ts +17 -0
  10. package/dist/claude-output-privacy.js +249 -0
  11. package/dist/claude-profile.d.ts +18 -0
  12. package/dist/claude-profile.js +260 -0
  13. package/dist/client-interface.d.ts +26 -20
  14. package/dist/client.d.ts +65 -29
  15. package/dist/client.js +278 -167
  16. package/dist/demo-client.d.ts +28 -21
  17. package/dist/demo-client.js +32 -14
  18. package/dist/deposit-contract.d.ts +29 -0
  19. package/dist/deposit-contract.js +76 -0
  20. package/dist/fetch-no-redirect.d.ts +3 -0
  21. package/dist/fetch-no-redirect.js +16 -0
  22. package/dist/fiscal-aliases.d.ts +2 -2
  23. package/dist/fiscal-aliases.js +2 -2
  24. package/dist/fiscal-period.d.ts +26 -0
  25. package/dist/fiscal-period.js +31 -0
  26. package/dist/google-profile.d.ts +19 -0
  27. package/dist/google-profile.js +122 -0
  28. package/dist/index.js +31 -7
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +2 -2
  31. package/dist/muse-profile.d.ts +19 -0
  32. package/dist/muse-profile.js +122 -0
  33. package/dist/observability.js +3 -2
  34. package/dist/openai-profile.d.ts +2 -2
  35. package/dist/openai-profile.js +37 -11
  36. package/dist/openai-review-contract.d.ts +1 -1
  37. package/dist/openai-review-contract.js +4 -5
  38. package/dist/openai-review-oauth.d.ts +107 -7
  39. package/dist/openai-review-oauth.js +161 -17
  40. package/dist/prompts/register-all.js +32 -78
  41. package/dist/redaction.d.ts +5 -0
  42. package/dist/redaction.js +9 -0
  43. package/dist/resources/register-all.js +150 -54
  44. package/dist/retry-after.d.ts +19 -0
  45. package/dist/retry-after.js +84 -0
  46. package/dist/server-composition.d.ts +6 -2
  47. package/dist/server-composition.js +46 -5
  48. package/dist/tool-exposure.d.ts +7 -5
  49. package/dist/tool-exposure.js +21 -13
  50. package/dist/tools/audit_gl.js +8 -8
  51. package/dist/tools/backend-availability.d.ts +8 -29
  52. package/dist/tools/backend-availability.js +31 -30
  53. package/dist/tools/bank_rules.js +66 -15
  54. package/dist/tools/banking.js +5 -13
  55. package/dist/tools/clients.js +254 -6
  56. package/dist/tools/deposits.js +107 -97
  57. package/dist/tools/einvoice.d.ts +17 -3
  58. package/dist/tools/einvoice.js +59 -12
  59. package/dist/tools/fiscal.d.ts +3 -4
  60. package/dist/tools/fiscal.js +93 -60
  61. package/dist/tools/gestoria.js +15 -7
  62. package/dist/tools/hr.js +41 -11
  63. package/dist/tools/igic.d.ts +9 -8
  64. package/dist/tools/igic.js +34 -64
  65. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  66. package/dist/tools/impuesto_sociedades.js +16 -37
  67. package/dist/tools/intelligence.js +164 -10
  68. package/dist/tools/invoices.js +109 -42
  69. package/dist/tools/kitchen.js +19 -9
  70. package/dist/tools/quotes.js +86 -25
  71. package/dist/tools/recurring.js +26 -25
  72. package/dist/tools/register-all.d.ts +2 -2
  73. package/dist/tools/register-all.js +9 -6
  74. package/dist/tools/search.d.ts +19 -0
  75. package/dist/tools/search.js +84 -0
  76. package/dist/tools/shared.d.ts +18 -38
  77. package/dist/tools/shared.js +96 -30
  78. package/dist/tools/team.js +8 -16
  79. package/dist/tools/time.js +4 -12
  80. package/dist/tools/vendors.js +242 -6
  81. package/dist/types.d.ts +3 -1
  82. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  83. package/dist/workflows/daily-operational-brief.js +881 -0
  84. package/docs/agent-onboarding.json +15 -5
  85. package/package.json +26 -12
  86. package/dist/tools/onboarding.d.ts +0 -19
  87. package/dist/tools/onboarding.js +0 -64
@@ -13,6 +13,171 @@ const addressSchema = z
13
13
  })
14
14
  .optional()
15
15
  .describe("Client address / Direccion del cliente");
16
+ /**
17
+ * Splits a concatenated / camelCase / snake_case / kebab-case search query into
18
+ * space-separated tokens (e.g. "CanariasBlue" -> "Canarias Blue",
19
+ * "SLCanarias" -> "SL Canarias", "Canarias_Blue" -> "Canarias Blue").
20
+ */
21
+ function splitCompoundQuery(q) {
22
+ return q
23
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
24
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
25
+ .replace(/[_-]+/g, " ")
26
+ .replace(/\s+/g, " ")
27
+ .trim();
28
+ }
29
+ const OPTIONAL_CLIENT_STRING_FIELDS = [
30
+ "email",
31
+ "phone",
32
+ "stage",
33
+ "taxId",
34
+ "createdAt",
35
+ "updatedAt",
36
+ ];
37
+ /**
38
+ * Normalizes a client record returned by the Frihet API so it always satisfies
39
+ * `clientItemOutput` without losing ERP-canonical data:
40
+ * - Strips `null` on optional string fields (`email`, `phone`, `stage`, `taxId`,
41
+ * `createdAt`, `updatedAt`) so `z.string().optional()` never rejects legacy docs.
42
+ * - Backfills `taxId` from legacy `nif` / `cif` / `vatNumber` / `tax_id` when
43
+ * `taxId` is absent or null, preserving all original fields.
44
+ * - Converts legacy string `address` into `{ street }`.
45
+ * - Omits `null` or empty `address` (which otherwise fails `addressOutputSchema`).
46
+ * - Maps ERP canonical address keys (`province` -> `state`, `zip` -> `postalCode`,
47
+ * `countryCode` -> `country`) because `publicApi.ts` (`normalizeAddress`) stores
48
+ * `zip` and `province`, whereas `addressOutputSchema` only declares `postalCode`
49
+ * and `state` (and strips unknown keys in `structuredContent`).
50
+ */
51
+ function normalizeClientRecord(clientRecord) {
52
+ if (!clientRecord || typeof clientRecord !== "object" || Array.isArray(clientRecord)) {
53
+ return clientRecord;
54
+ }
55
+ const out = { ...clientRecord };
56
+ for (const field of OPTIONAL_CLIENT_STRING_FIELDS) {
57
+ if (out[field] === null) {
58
+ delete out[field];
59
+ }
60
+ }
61
+ if (typeof out.taxId !== "string" || !out.taxId.trim()) {
62
+ for (const alias of ["nif", "cif", "vatNumber", "tax_id"]) {
63
+ const candidate = out[alias];
64
+ if (typeof candidate === "string" && candidate.trim()) {
65
+ out.taxId = candidate.trim();
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ const rawAddress = out.address;
71
+ const rootStreet = typeof out.street === "string" && out.street.trim() ? out.street.trim() : undefined;
72
+ const rootCity = typeof out.city === "string" && out.city.trim() ? out.city.trim() : undefined;
73
+ const rootState = typeof out.state === "string" && out.state.trim()
74
+ ? out.state.trim()
75
+ : typeof out.province === "string" && out.province.trim()
76
+ ? out.province.trim()
77
+ : undefined;
78
+ const rootPostalCode = typeof out.postalCode === "string" && out.postalCode.trim()
79
+ ? out.postalCode.trim()
80
+ : typeof out.zip === "string" && out.zip.trim()
81
+ ? out.zip.trim()
82
+ : undefined;
83
+ const rootCountry = typeof out.country === "string" && out.country.trim()
84
+ ? out.country.trim()
85
+ : typeof out.countryCode === "string" && out.countryCode.trim()
86
+ ? out.countryCode.trim()
87
+ : undefined;
88
+ const buildFlatAddress = (streetOverride) => {
89
+ const built = {};
90
+ const st = streetOverride ?? rootStreet;
91
+ if (st !== undefined)
92
+ built.street = st;
93
+ if (rootCity !== undefined)
94
+ built.city = rootCity;
95
+ if (rootState !== undefined)
96
+ built.state = rootState;
97
+ if (rootPostalCode !== undefined)
98
+ built.postalCode = rootPostalCode;
99
+ if (rootCountry !== undefined)
100
+ built.country = rootCountry;
101
+ return Object.keys(built).length > 0 ? built : undefined;
102
+ };
103
+ if (rawAddress === undefined || rawAddress === null) {
104
+ if (rawAddress === null) {
105
+ delete out.address;
106
+ }
107
+ const flatAddr = buildFlatAddress();
108
+ if (flatAddr) {
109
+ out.address = flatAddr;
110
+ }
111
+ return out;
112
+ }
113
+ if (typeof rawAddress === "string") {
114
+ const trimmed = rawAddress.trim();
115
+ const flatAddr = buildFlatAddress(trimmed || undefined);
116
+ if (!flatAddr) {
117
+ delete out.address;
118
+ return out;
119
+ }
120
+ out.address = flatAddr;
121
+ return out;
122
+ }
123
+ if (typeof rawAddress === "object" && !Array.isArray(rawAddress)) {
124
+ const addrObj = rawAddress;
125
+ const normalizedAddress = {};
126
+ for (const [k, v] of Object.entries(addrObj)) {
127
+ if (v !== null && v !== undefined) {
128
+ normalizedAddress[k] = v;
129
+ }
130
+ }
131
+ const street = typeof addrObj.street === "string" && addrObj.street.trim()
132
+ ? addrObj.street.trim()
133
+ : rootStreet;
134
+ const city = typeof addrObj.city === "string" && addrObj.city.trim()
135
+ ? addrObj.city.trim()
136
+ : rootCity;
137
+ const state = typeof addrObj.state === "string" && addrObj.state.trim()
138
+ ? addrObj.state.trim()
139
+ : typeof addrObj.province === "string" && addrObj.province.trim()
140
+ ? addrObj.province.trim()
141
+ : rootState;
142
+ const postalCode = typeof addrObj.postalCode === "string" && addrObj.postalCode.trim()
143
+ ? addrObj.postalCode.trim()
144
+ : typeof addrObj.zip === "string" && addrObj.zip.trim()
145
+ ? addrObj.zip.trim()
146
+ : rootPostalCode;
147
+ const country = typeof addrObj.country === "string" && addrObj.country.trim()
148
+ ? addrObj.country.trim()
149
+ : typeof addrObj.countryCode === "string" && addrObj.countryCode.trim()
150
+ ? addrObj.countryCode.trim()
151
+ : rootCountry;
152
+ if (street !== undefined)
153
+ normalizedAddress.street = street;
154
+ else
155
+ delete normalizedAddress.street;
156
+ if (city !== undefined)
157
+ normalizedAddress.city = city;
158
+ else
159
+ delete normalizedAddress.city;
160
+ if (state !== undefined)
161
+ normalizedAddress.state = state;
162
+ else
163
+ delete normalizedAddress.state;
164
+ if (postalCode !== undefined)
165
+ normalizedAddress.postalCode = postalCode;
166
+ else
167
+ delete normalizedAddress.postalCode;
168
+ if (country !== undefined)
169
+ normalizedAddress.country = country;
170
+ else
171
+ delete normalizedAddress.country;
172
+ if (Object.keys(normalizedAddress).length === 0) {
173
+ delete out.address;
174
+ return out;
175
+ }
176
+ out.address = normalizedAddress;
177
+ return out;
178
+ }
179
+ return out;
180
+ }
16
181
  export function registerClientTools(server, client) {
17
182
  // -- list_clients --
18
183
  server.registerTool("list_clients", {
@@ -44,10 +209,93 @@ export function registerClientTools(server, client) {
44
209
  },
45
210
  outputSchema: paginatedOutput(clientItemOutput, { projectable: true }),
46
211
  }, async ({ limit, offset, q, stage, fields, after }) => withToolLogging("list_clients", async () => {
47
- const result = await client.listClients({ limit, offset, after, fields, q, stage });
212
+ const trimmedQ = q?.trim();
213
+ const trimmedAfter = after?.trim();
214
+ // Limitation in Frihet-ERP `listResources` (publicApi.ts L8158-8268):
215
+ // When `search` (`q`) is provided, the backend slices results by `offset`/`limit`
216
+ // and returns before evaluating `cursorParam` (`after`). Moreover, protected
217
+ // profiles (Claude `claudeToolResult` and OpenAI `applyOpenAIProfile`) rebuild
218
+ // `content` from `structuredContent` while `paginatedOutput` is frozen, so a
219
+ // text-only note would be silently discarded. Fail closed (`isError: true`
220
+ // without `structuredContent`) so no profile mistakes page 1 for a cursor page.
221
+ if (trimmedQ && trimmedAfter) {
222
+ return {
223
+ content: [{
224
+ type: "text",
225
+ text: "Error: text search ('q') cannot be combined with cursor pagination ('after') because the ERP backend slices search results by 'offset'/'limit' and ignores 'after'. Use 'offset' and 'limit' when searching with 'q', or omit 'q' when paginating with 'after'. / " +
226
+ "Error: la busqueda textual ('q') no se puede combinar con el cursor 'after' porque el backend pagina las busquedas mediante 'offset'/'limit' e ignora 'after'. Usa 'offset' y 'limit' con 'q', u omite 'q' al paginar con 'after'.",
227
+ }],
228
+ isError: true,
229
+ _meta: { "io.frihet/cursorWithQueryUnsupported": true },
230
+ };
231
+ }
232
+ let result = await client.listClients({ limit, offset, after, fields, q, stage });
233
+ // When a concatenated / camelCase query (e.g. "CanariasBlue") yields zero total matches,
234
+ // retry with split tokens ("Canarias Blue") using the same offset/limit/fields/stage.
235
+ // Guardrails matching publicApi.ts `listResources`:
236
+ // 1. Never fallback when `after` cursor is supplied with `q`.
237
+ // 2. Never fallback when the first scan hit the 500-document cap (`truncated === true`),
238
+ // since total===0 only reflects the first 500 scanned records.
239
+ // 3. On page 2+ (`offset > 0`), only fallback when `result.total === 0` explicitly (not
240
+ // when `total === undefined`, which indicates an exhausted offset).
241
+ let usedFallback = false;
242
+ const splitQ = trimmedQ ? splitCompoundQuery(trimmedQ) : undefined;
243
+ const isSubsequentPage = (offset !== undefined && offset > 0) || Boolean(after);
244
+ const initialTruncated = result?.truncated === true;
245
+ const canFallback = splitQ !== undefined && splitQ !== trimmedQ && !after && !initialTruncated;
246
+ const hasZeroTotalMatches = Array.isArray(result?.data) &&
247
+ result.data.length === 0 &&
248
+ (isSubsequentPage ? result.total === 0 : result.total === 0 || result.total === undefined);
249
+ if (canFallback && hasZeroTotalMatches) {
250
+ try {
251
+ const fallbackResult = await client.listClients({ limit, offset, after, fields, q: splitQ, stage });
252
+ const fallbackTruncated = fallbackResult?.truncated === true;
253
+ if (fallbackTruncated ||
254
+ (fallbackResult?.data && (fallbackResult.data.length > 0 || (fallbackResult.total !== undefined && fallbackResult.total > 0)))) {
255
+ result = fallbackResult;
256
+ usedFallback = true;
257
+ }
258
+ }
259
+ catch {
260
+ // Preserve original response if fallback fails
261
+ }
262
+ }
263
+ const rawEnvelope = (result ?? {});
264
+ const isTruncated = rawEnvelope.truncated === true;
265
+ const normalizedData = Array.isArray(result?.data)
266
+ ? result.data.map((c) => (c && typeof c === "object" ? normalizeClientRecord(c) : c))
267
+ : [];
268
+ // Conservative refusal when the ERP 500-document scan cap was saturated (`truncated: true`).
269
+ // Because `paginatedOutput` is a frozen schema contract and both Claude (`claudeToolResult`)
270
+ // and OpenAI (`applyOpenAIProfile`) overwrite `content` whenever `structuredContent` is
271
+ // present, returning `structuredContent` here would hide the truncation warning and present
272
+ // an incomplete 500-doc scan as exhaustive.
273
+ if (isTruncated) {
274
+ const partialCount = typeof result?.total === "number" ? result.total : normalizedData.length;
275
+ return {
276
+ content: [{
277
+ type: "text",
278
+ text: `Error: client search hit the ERP 500-document scan cap (truncated=true; ${partialCount} match(es) within the first 500 scanned records) and cannot be returned as an exhaustive result across protected profiles. Narrow 'q' (or filter by 'stage') or paginate without 'q' using 'after'. / ` +
279
+ `Error: la busqueda de clientes alcanzo el limite de escaneo de 500 documentos del ERP (truncated=true; ${partialCount} coincidencia(s) en los primeros 500 registros) y no puede devolverse como resultado exhaustivo. Acota 'q' (o filtra por 'stage') o pagina sin 'q' usando 'after'.`,
280
+ }],
281
+ isError: true,
282
+ _meta: { "io.frihet/searchScanTruncated": true },
283
+ };
284
+ }
285
+ const cleanEnvelope = {
286
+ data: normalizedData,
287
+ total: typeof result?.total === "number" ? result.total : normalizedData.length,
288
+ limit: typeof result?.limit === "number" ? result.limit : (limit ?? 20),
289
+ offset: typeof result?.offset === "number" ? result.offset : (offset ?? 0),
290
+ ...(typeof result?.nextCursor === "string" ? { nextCursor: result.nextCursor } : {}),
291
+ };
292
+ const notes = [];
293
+ if (usedFallback) {
294
+ notes.push(` (criterio aplicado: "${splitQ}")`);
295
+ }
48
296
  return {
49
- content: [listContent(formatPaginatedResponse("clients", result))],
50
- structuredContent: result,
297
+ content: [listContent(formatPaginatedResponse("clients", cleanEnvelope) + notes.join(""))],
298
+ structuredContent: cleanEnvelope,
51
299
  };
52
300
  }));
53
301
  // -- get_client --
@@ -61,7 +309,7 @@ export function registerClientTools(server, client) {
61
309
  },
62
310
  outputSchema: clientItemOutput,
63
311
  }, async ({ id }) => withToolLogging("get_client", async () => {
64
- const result = await client.getClient(id);
312
+ const result = normalizeClientRecord(await client.getClient(id));
65
313
  return {
66
314
  content: [getContent(formatRecord("Client", result))],
67
315
  structuredContent: result,
@@ -85,7 +333,7 @@ export function registerClientTools(server, client) {
85
333
  },
86
334
  outputSchema: clientItemOutput,
87
335
  }, async (input) => withToolLogging("create_client", async () => {
88
- const result = await client.createClient(input);
336
+ const result = normalizeClientRecord(await client.createClient(input));
89
337
  const hints = enrichResponse("clients", "create", result);
90
338
  return {
91
339
  content: [mutateContent(formatRecord("Client created", result) + hints)],
@@ -109,7 +357,7 @@ export function registerClientTools(server, client) {
109
357
  },
110
358
  outputSchema: clientItemOutput,
111
359
  }, async ({ id, ...data }) => withToolLogging("update_client", async () => {
112
- const result = await client.updateClient(id, data);
360
+ const result = normalizeClientRecord(await client.updateClient(id, data));
113
361
  return {
114
362
  content: [mutateContent(formatRecord("Client updated", result))],
115
363
  structuredContent: result,
@@ -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", {
@@ -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) + hints)],
105
- structuredContent: { ...result },
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", {
@@ -156,62 +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. Requires confirm=true. " +
182
- "Transitions the deposit status to 'refunded' — a money movement that is not reversible " +
183
- "from this tool. " +
184
- "Example: id='dep_abc123', reason='Project cancelled', confirm=true " +
185
- "/ Devuelve un deposito al cliente. Requiere confirm=true. " +
186
- "Es un movimiento de dinero que no se puede revertir desde esta herramienta.",
187
- annotations: UPDATE_ANNOTATIONS,
188
- inputSchema: {
189
- id: z.string().describe("Deposit ID / ID del deposito"),
190
- reason: z.string().optional().describe("Reason for the refund / Motivo de la devolucion"),
191
- notes: z.string().optional().describe("Refund notes / Notas de la devolucion"),
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"),
192
210
  confirm: z
193
211
  .boolean()
194
- .describe("Must be true to confirm the refund / Debe ser true para confirmar la devolucion"),
195
- },
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(),
196
214
  outputSchema: actionResultOutput,
197
- }, async ({ id, confirm, ...data }) => withToolLogging("refund_deposit", async () => {
198
- if (!confirm) {
199
- return {
200
- content: [
201
- {
202
- type: "text",
203
- text: "Error: confirm=true is required to refund a deposit. " +
204
- "This returns the client's money and sets the deposit to status=refunded; " +
205
- "there is no un-refund tool. Set confirm=true when you are certain. / " +
206
- "Se requiere confirm=true para devolver un deposito. Es un movimiento de dinero irreversible.",
207
- },
208
- ],
209
- isError: true,
210
- };
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.");
211
221
  }
212
- const result = await client.refundDeposit(id, data);
222
+ const result = await client.refundDeposit(id, amount === undefined ? {} : { amount });
213
223
  return {
214
- content: [mutateContent(formatRecord("Deposit refunded", result))],
224
+ content: [mutateContent(formatRecord("Deposit refund recorded (no money transferred)", result))],
215
225
  structuredContent: result,
216
226
  };
217
227
  }));
@@ -48,19 +48,33 @@ import type { IFrihetClient } from "../client-interface.js";
48
48
  * | facturae | Facturae | Spain (ES B2G mandatory) | Facturae 3.2.x — AEAT/FACe submission |
49
49
  */
50
50
  export declare const eInvoiceFormatSchema: z.ZodEnum<{
51
+ facturae: "facturae";
51
52
  "xrechnung-cii": "xrechnung-cii";
52
53
  "xrechnung-ubl": "xrechnung-ubl";
53
54
  "facturx-en16931": "facturx-en16931";
55
+ fatturapa: "fatturapa";
56
+ "peppol-bis-3": "peppol-bis-3";
57
+ ubl: "ubl";
58
+ cii: "cii";
54
59
  "facturx-extended": "facturx-extended";
55
60
  "facturx-basic": "facturx-basic";
56
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";
57
72
  fatturapa: "fatturapa";
73
+ "peppol-bis-3": "peppol-bis-3";
74
+ "fa-2-ksef": "fa-2-ksef";
58
75
  ubl: "ubl";
59
76
  cii: "cii";
60
- "peppol-bis-3": "peppol-bis-3";
61
- facturae: "facturae";
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">;