@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,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,
@@ -67,9 +67,9 @@ export function registerProductTools(server, client) {
67
67
  "Los productos se pueden usar al crear facturas y presupuestos para entrada rapida de datos.",
68
68
  annotations: CREATE_ANNOTATIONS,
69
69
  inputSchema: {
70
- name: z.string().describe("Product/service name / Nombre del producto o servicio"),
71
- unitPrice: z.number().describe("Unit price in EUR / Precio unitario en EUR"),
72
- description: z.string().optional().describe("Product description / Descripcion"),
70
+ name: z.string().trim().min(1).max(200).describe("Product/service name / Nombre del producto o servicio"),
71
+ unitPrice: z.number().finite().min(0).max(10_000_000).describe("Unit price in EUR / Precio unitario en EUR"),
72
+ description: z.string().max(2000).optional().describe("Product description / Descripcion"),
73
73
  taxRate: z
74
74
  .number()
75
75
  .min(0)
@@ -94,9 +94,9 @@ export function registerProductTools(server, client) {
94
94
  annotations: UPDATE_ANNOTATIONS,
95
95
  inputSchema: {
96
96
  id: z.string().describe("Product ID / ID del producto"),
97
- name: z.string().optional().describe("Name / Nombre"),
98
- unitPrice: z.number().optional().describe("Unit price / Precio unitario"),
99
- description: z.string().optional().describe("Description / Descripcion"),
97
+ name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
98
+ unitPrice: z.number().finite().min(0).max(10_000_000).optional().describe("Unit price / Precio unitario"),
99
+ description: z.string().max(2000).optional().describe("Description / Descripcion"),
100
100
  taxRate: z.number().min(0).max(100).optional().describe("Tax rate % / IVA %"),
101
101
  },
102
102
  outputSchema: productItemOutput,
@@ -2,19 +2,19 @@
2
2
  * Quote tools for the Frihet MCP server.
3
3
  */
4
4
  import { z } from "zod/v4";
5
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, deleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
5
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
6
6
  const quoteItemSchema = z.object({
7
- 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
11
  // Optional client-identity + ES fiscal fields the Frihet API accepts on quote
12
12
  // create+update (mirrors the invoice subset the backend supports for quotes).
13
13
  const quoteOptionalFields = {
14
14
  clientId: z
15
15
  .string()
16
16
  .optional()
17
- .describe("Existing client ID — server back-fills taxId/address / ID de cliente existente"),
17
+ .describe("Existing client ID — stored client details are used for the document / ID de cliente existente"),
18
18
  clientTaxId: z
19
19
  .string()
20
20
  .optional()
@@ -24,11 +24,11 @@ const quoteOptionalFields = {
24
24
  .optional()
25
25
  .describe("Client billing address / Direccion fiscal del cliente"),
26
26
  issueDate: z
27
- .string()
27
+ .iso.date()
28
28
  .optional()
29
29
  .describe("Issue date in ISO 8601 (YYYY-MM-DD), defaults to today / Fecha de emision"),
30
30
  dueDate: z
31
- .string()
31
+ .iso.date()
32
32
  .optional()
33
33
  .describe("Due date in ISO 8601 (YYYY-MM-DD) / Fecha de vencimiento"),
34
34
  taxRate: z
@@ -132,17 +132,17 @@ export function registerQuoteTools(server, client) {
132
132
  "Los presupuestos se pueden convertir en facturas despues.",
133
133
  annotations: CREATE_ANNOTATIONS,
134
134
  inputSchema: {
135
- clientName: z.string().describe("Client name / Nombre del cliente"),
135
+ clientName: z.string().trim().min(1).max(200).describe("Client name / Nombre del cliente"),
136
136
  ...quoteOptionalFields,
137
137
  items: z
138
138
  .array(quoteItemSchema)
139
139
  .min(1)
140
140
  .describe("Line items (each with description, quantity, unitPrice) / Conceptos del presupuesto"),
141
141
  validUntil: z
142
- .string()
142
+ .iso.date()
143
143
  .optional()
144
144
  .describe("Expiry date in ISO 8601 (YYYY-MM-DD) / Fecha de validez"),
145
- notes: z.string().optional().describe("Additional notes shown on the quote / Notas adicionales"),
145
+ notes: z.string().max(10_000).optional().describe("Additional notes shown on the quote / Notas adicionales"),
146
146
  status: z
147
147
  .enum(["draft", "sent", "accepted", "rejected", "expired"])
148
148
  .optional()
@@ -153,8 +153,8 @@ export function registerQuoteTools(server, client) {
153
153
  const result = await client.createQuote(input);
154
154
  const hints = enrichResponse("quotes", "create", result);
155
155
  return {
156
- content: [mutateContent(formatRecord("Quote created", result))],
157
- structuredContent: { ...result, ...hints },
156
+ content: [mutateContent(formatRecord("Quote created", result) + hints)],
157
+ structuredContent: { ...result },
158
158
  };
159
159
  }));
160
160
  // -- update_quote --
@@ -187,37 +187,183 @@ export function registerQuoteTools(server, client) {
187
187
  // -- delete_quote --
188
188
  server.registerTool("delete_quote", {
189
189
  title: "Delete Quote",
190
- description: "Permanently delete a quote by its ID. This action cannot be undone. " +
191
- "/ Elimina permanentemente un presupuesto por su ID. Esta accion no se puede deshacer.",
190
+ description: "Delete a quote by its ID. Requires confirm=true. " +
191
+ "Only a clean DRAFT with no delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused and left unchanged. A sent/accepted/rejected/expired quote is NOT " +
192
+ "destroyed: the backend CANCELS it (status=cancelled) — the same non-destructive path invoices " +
193
+ "take — and it stays readable via get_quote. The result reports which happened in `outcome` " +
194
+ "(\"deleted\" or \"cancelled\"). " +
195
+ "/ Elimina un presupuesto por su ID. Requiere confirm=true. Solo un BORRADOR limpio sin evidencia de entrega, respuesta, adjuntos o conversion se " +
196
+ "elimina de forma permanente; un borrador protegido se rechaza sin cambios. Uno ya enviado/aceptado/rechazado NO se destruye: se CANCELA " +
197
+ "(status=cancelled) y sigue consultable con get_quote.",
192
198
  annotations: DELETE_ANNOTATIONS,
193
199
  inputSchema: {
194
200
  id: z.string().describe("Quote ID / ID del presupuesto"),
201
+ confirm: z
202
+ .boolean()
203
+ .describe("Must be true to confirm deletion / Debe ser true para confirmar la eliminacion"),
195
204
  },
196
- outputSchema: deleteResultOutput,
197
- }, async ({ id }) => withToolLogging("delete_quote", async () => {
198
- await client.deleteQuote(id);
205
+ outputSchema: documentDeleteResultOutput,
206
+ }, async ({ id, confirm }) => withToolLogging("delete_quote", async () => {
207
+ if (!confirm) {
208
+ return confirmationRequiredError("Error: confirm=true is required to delete a quote. " +
209
+ "A clean draft without delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused; a sent/accepted quote is CANCELLED " +
210
+ "(status=cancelled) instead and remains in the record. " +
211
+ "Set confirm=true to proceed. / " +
212
+ "Se requiere confirm=true para eliminar un presupuesto.");
213
+ }
214
+ // 204 → undefined (draft row destroyed). 200 → soft-cancel body. See GAP-12.
215
+ const outcome = await client.deleteQuote(id);
216
+ const cancelled = !!outcome && typeof outcome === "object";
217
+ const body = cancelled ? outcome : {};
218
+ const previous = typeof body["previousStatus"] === "string" ? ` (was ${body["previousStatus"]})` : "";
199
219
  return {
200
- content: [mutateContent(`Quote ${id} deleted successfully. / Presupuesto ${id} eliminado correctamente.`)],
201
- structuredContent: { success: true, id },
220
+ content: [mutateContent(cancelled
221
+ ? `Quote ${id} was CANCELLED, not deleted${previous}: it still exists with status=cancelled. / ` +
222
+ `Presupuesto ${id} CANCELADO, no eliminado: sigue existiendo con status=cancelled.`
223
+ : `Quote ${id} deleted permanently (it was a draft). / ` +
224
+ `Presupuesto ${id} eliminado permanentemente (era un borrador).`)],
225
+ structuredContent: {
226
+ success: true,
227
+ id,
228
+ ...body,
229
+ outcome: cancelled ? "cancelled" : "deleted",
230
+ },
202
231
  };
203
232
  }));
204
233
  // -- send_quote --
205
234
  server.registerTool("send_quote", {
206
235
  title: "Send Quote",
207
- description: "Send a quote/estimate to the client via email. Optionally override the recipient email address. " +
236
+ description: "Send a quote/estimate to the client via email. Requires confirm=true. " +
237
+ "Optionally override the recipient email address. " +
208
238
  "The quote must exist and should not already be expired or rejected. " +
209
- "/ Envia un presupuesto al cliente por email. Opcionalmente se puede cambiar el email destinatario.",
239
+ "Sending reaches a third party and cannot be recalled. " +
240
+ "/ Envia un presupuesto al cliente por email. Requiere confirm=true. " +
241
+ "Opcionalmente se puede cambiar el email destinatario. El envio llega a un tercero y no se puede anular.",
210
242
  annotations: UPDATE_ANNOTATIONS,
211
243
  inputSchema: {
212
244
  id: z.string().describe("Quote ID / ID del presupuesto"),
213
245
  to: z.string().optional().describe("Override recipient email / Email destinatario alternativo"),
246
+ confirm: z
247
+ .boolean()
248
+ .describe("Must be true to confirm sending / Debe ser true para confirmar el envio"),
214
249
  },
215
250
  outputSchema: actionResultOutput,
216
- }, async ({ id, to }) => withToolLogging("send_quote", async () => {
217
- const result = await client.sendQuote(id, to);
251
+ }, async ({ id, to, confirm }) => withToolLogging("send_quote", async () => {
252
+ if (!confirm) {
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: "Error: confirm=true is required to send a quote. " +
258
+ "This delivers an email to the client — a third party outside this workspace — " +
259
+ "and it cannot be recalled once sent. Set confirm=true to proceed. / " +
260
+ "Se requiere confirm=true para enviar un presupuesto por email a un tercero.",
261
+ },
262
+ ],
263
+ isError: true,
264
+ };
265
+ }
266
+ const quote = await client.getQuote(id);
267
+ const status = typeof quote["status"] === "string" ? quote["status"] : undefined;
268
+ if (status && ["expired", "rejected", "cancelled"].includes(status)) {
269
+ return {
270
+ content: [{ type: "text", text: `Error: a ${status} quote cannot be sent.` }],
271
+ isError: true,
272
+ };
273
+ }
274
+ const isValidRecipientEmail = (email) => email.length <= 255 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
275
+ let recipientEmail;
276
+ if (to !== undefined) {
277
+ const trimmedTo = to.trim();
278
+ if (!trimmedTo || !isValidRecipientEmail(trimmedTo)) {
279
+ return {
280
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
281
+ isError: true,
282
+ };
283
+ }
284
+ recipientEmail = trimmedTo;
285
+ }
286
+ if (!recipientEmail) {
287
+ const clientId = typeof quote["clientId"] === "string" && quote["clientId"].trim()
288
+ ? quote["clientId"].trim()
289
+ : undefined;
290
+ if (clientId) {
291
+ let linkedClient;
292
+ try {
293
+ linkedClient = await client.getClient(clientId);
294
+ }
295
+ catch (err) {
296
+ const isNotFound = typeof err === "object" &&
297
+ err !== null &&
298
+ "statusCode" in err &&
299
+ err.statusCode === 404;
300
+ if (!isNotFound) {
301
+ throw err;
302
+ }
303
+ }
304
+ const clientEmail = linkedClient && typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
305
+ ? linkedClient["email"].trim()
306
+ : undefined;
307
+ if (clientEmail) {
308
+ if (!isValidRecipientEmail(clientEmail)) {
309
+ return {
310
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${clientEmail}".` }],
311
+ isError: true,
312
+ };
313
+ }
314
+ recipientEmail = clientEmail;
315
+ }
316
+ }
317
+ if (!recipientEmail) {
318
+ const docFallbackEmail = typeof quote["clientEmail"] === "string" && quote["clientEmail"].trim()
319
+ ? quote["clientEmail"].trim()
320
+ : typeof quote["email"] === "string" && quote["email"].trim()
321
+ ? quote["email"].trim()
322
+ : undefined;
323
+ if (docFallbackEmail) {
324
+ if (!isValidRecipientEmail(docFallbackEmail)) {
325
+ return {
326
+ content: [{ type: "text", text: `Error: invalid recipient email address format: "${docFallbackEmail}".` }],
327
+ isError: true,
328
+ };
329
+ }
330
+ const fallbackMsg = `Error: the linked client has no current email address; only a document-level fallback email ("${docFallbackEmail}") was found on the quote, which may be outdated. ` +
331
+ `Confirm this specific recipient with the user and re-run with to="${docFallbackEmail}" and confirm=true to send. / ` +
332
+ `El cliente vinculado no tiene email actual; el presupuesto conserva el correo documental "${docFallbackEmail}", que podria estar obsoleto. Confirma este destinatario concreto y vuelve a llamar con to="${docFallbackEmail}" y confirm=true.`;
333
+ return {
334
+ content: [{ type: "text", text: fallbackMsg }],
335
+ structuredContent: {
336
+ success: false,
337
+ id,
338
+ message: fallbackMsg,
339
+ effectiveRecipient: docFallbackEmail,
340
+ recipientSource: "document_fallback",
341
+ requiresExplicitRecipientConfirmation: true,
342
+ },
343
+ isError: true,
344
+ };
345
+ }
346
+ return {
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: clientId
351
+ ? "Error: the linked client has no saved email address."
352
+ : "Error: this quote has no linked client with a saved email address.",
353
+ },
354
+ ],
355
+ isError: true,
356
+ };
357
+ }
358
+ }
359
+ const result = await client.sendQuote(id, recipientEmail);
360
+ const enrichedResult = {
361
+ ...(result && typeof result === "object" ? result : {}),
362
+ effectiveRecipient: recipientEmail,
363
+ };
218
364
  return {
219
- content: [mutateContent(formatRecord("Quote sent", result))],
220
- structuredContent: result,
365
+ content: [mutateContent(formatRecord("Quote sent", enrichedResult))],
366
+ structuredContent: enrichedResult,
221
367
  };
222
368
  }));
223
369
  }
@@ -16,7 +16,7 @@
16
16
  * Backend: /v1/recurring/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
17
17
  */
18
18
  import { z } from "zod/v4";
19
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, deleteResultOutput, recurringInvoiceItemOutput, } from "./shared.js";
19
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, deleteResultOutput, recurringInvoiceItemOutput, } from "./shared.js";
20
20
  export function registerRecurringTools(server, client) {
21
21
  // -- list_recurring_invoices --
22
22
  server.registerTool("list_recurring_invoices", {
@@ -191,17 +191,9 @@ export function registerRecurringTools(server, client) {
191
191
  outputSchema: deleteResultOutput,
192
192
  }, async ({ id, confirm }) => withToolLogging("delete_recurring_invoice", async () => {
193
193
  if (!confirm) {
194
- return {
195
- content: [
196
- {
197
- type: "text",
198
- text: "Error: confirm=true is required to delete a recurring invoice template. " +
199
- "This will stop all future invoice generation from this template. Set confirm=true to proceed. / " +
200
- "Se requiere confirm=true para eliminar la plantilla de factura recurrente.",
201
- },
202
- ],
203
- isError: true,
204
- };
194
+ return confirmationRequiredError("Error: confirm=true is required to delete a recurring invoice template. " +
195
+ "This will stop all future invoice generation from this template. Set confirm=true to proceed. / " +
196
+ "Se requiere confirm=true para eliminar la plantilla de factura recurrente.");
205
197
  }
206
198
  await client.deleteRecurringInvoice(id);
207
199
  return {
@@ -212,23 +204,32 @@ export function registerRecurringTools(server, client) {
212
204
  // -- run_recurring_now --
213
205
  server.registerTool("run_recurring_now", {
214
206
  title: "Run Recurring Invoice Now",
215
- description: "Manually trigger immediate generation of the next invoice instance from a recurring template. " +
216
- "Useful for billing ahead of schedule or recovering from a missed automated run. " +
217
- "The generated invoice is created as a draft; review and send separately. " +
218
- "Example: templateId='rec_abc123' " +
219
- "/ Genera manualmente la siguiente instancia de una factura recurrente. " +
220
- "Util para facturar antes de lo programado o recuperar un ciclo perdido. " +
221
- "La factura generada se crea como borrador; revisar y enviar por separado.",
222
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
223
- inputSchema: {
224
- templateId: z.string().describe("Recurring invoice template ID / ID de la plantilla de factura recurrente"),
207
+ description: "Generate an invoice from a recurring template and update its run count and last-run date. Requires confirm=true. " +
208
+ "draftOnly defaults to true. With draftOnly=false, the invoice is created with status='sent' and may trigger fiscal numbering and automatic tax submission. " +
209
+ "Both modes may deliver configured invoice-created webhooks and notify workspace admins/accountants; this endpoint does not email the invoice to the client. " +
210
+ "Each new call can create another invoice; check the template and invoices after an uncertain result before retrying. " +
211
+ "Example: templateId='rec_abc123', confirm=true " +
212
+ "/ Genera una factura recurrente y actualiza el contador y la fecha de ejecución. Requiere confirm=true. " +
213
+ "Por defecto crea un borrador; con draftOnly=false crea el estado 'sent' y puede activar la numeración y remisión fiscal automática. " +
214
+ "Ambas opciones pueden activar webhooks y avisos internos; esta operación no envía la factura por correo al cliente. " +
215
+ "Una nueva llamada puede crear otra factura; comprueba la plantilla y las facturas antes de repetir un resultado incierto.",
216
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
217
+ inputSchema: z.object({
218
+ templateId: z.string().min(1).describe("Recurring invoice template ID / ID de la plantilla de factura recurrente"),
225
219
  draftOnly: z
226
220
  .boolean()
227
221
  .optional()
228
- .describe("If true, create as draft only (default true). Set false to create and mark as sent immediately. / Si true, crea como borrador. Set false para crear y marcar como enviada."),
229
- },
222
+ .describe("Default true creates a draft; false creates status='sent' and may trigger automatic fiscal submission. Neither option emails the client. / Por defecto true crea un borrador; false crea el estado 'sent' y puede activar la remisión fiscal automática. Ninguna opción envía correo al cliente."),
223
+ confirm: z.boolean().describe("Must be true to authorize invoice creation, configured webhooks and possible fiscal issuance / Debe ser true para autorizar la creación, los webhooks configurados y la posible emisión fiscal"),
224
+ }).strict(),
230
225
  outputSchema: actionResultOutput,
231
- }, async ({ templateId, draftOnly }) => withToolLogging("run_recurring_now", async () => {
226
+ }, async ({ templateId, draftOnly, confirm }) => withToolLogging("run_recurring_now", async () => {
227
+ if (confirm !== true) {
228
+ return confirmationRequiredError("Error: confirm=true is required to create a recurring invoice and allow configured webhooks. " +
229
+ "draftOnly=false may also trigger fiscal numbering and automatic tax submission. / " +
230
+ "Se requiere confirm=true para crear la factura y permitir los webhooks configurados. " +
231
+ "draftOnly=false también puede activar la numeración y remisión fiscal automática.");
232
+ }
232
233
  const result = await client.runRecurringNow(templateId, { draftOnly: draftOnly ?? true });
233
234
  return {
234
235
  content: [mutateContent(formatRecord("Recurring invoice triggered", result))],
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Barrel module that registers all 157 Frihet ERP tools on an McpServer.
2
+ * Barrel module that registers every Frihet ERP tool on an McpServer.
3
3
  *
4
4
  * Used by both the local (stdio) and remote (Cloudflare Workers) servers
5
5
  * so tool definitions stay in sync — one source of truth.
6
6
  *
7
7
  * Langfuse observability is injected by patching server.registerTool once
8
8
  * before any tool registration. This wraps every tool callback with
9
- * traceMCPTool so all 157 tools are instrumented at zero per-tool cost.
9
+ * traceMCPTool so every registered tool is instrumented at zero per-tool cost.
10
10
  */
11
11
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import type { IFrihetClient } from "../client-interface.js";
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Barrel module that registers all 157 Frihet ERP tools on an McpServer.
2
+ * Barrel module that registers every Frihet ERP tool on an McpServer.
3
3
  *
4
4
  * Used by both the local (stdio) and remote (Cloudflare Workers) servers
5
5
  * so tool definitions stay in sync — one source of truth.
6
6
  *
7
7
  * Langfuse observability is injected by patching server.registerTool once
8
8
  * before any tool registration. This wraps every tool callback with
9
- * traceMCPTool so all 157 tools are instrumented at zero per-tool cost.
9
+ * traceMCPTool so every registered tool is instrumented at zero per-tool cost.
10
10
  */
11
11
  import { traceMCPTool } from "../observability.js";
12
12
  import { registerInvoiceTools } from "./invoices.js";
@@ -37,13 +37,13 @@ import { registerImpuestoSociedadesTools } from "./impuesto_sociedades.js";
37
37
  import { registerBankRulesTools } from "./bank_rules.js";
38
38
  import { registerHrTools } from "./hr.js";
39
39
  import { registerPayrollTools } from "./payroll.js";
40
- import { registerOnboardingTools } from "./onboarding.js";
41
40
  import { registerPermissionsTools } from "./permissions.js";
42
41
  import { registerAccountingCloseTools } from "./accountingClose.js";
42
+ import { registerSearchTools } from "./search.js";
43
43
  /**
44
44
  * Patches server.registerTool to wrap every tool callback with Langfuse tracing.
45
45
  *
46
- * The patch is applied once before tool registration so all 157 tools are
46
+ * The patch is applied once before tool registration so every registered tool is
47
47
  * instrumented without per-tool edits. Tool call signatures are unchanged —
48
48
  * existing MCP clients continue to work identically.
49
49
  *
@@ -57,7 +57,10 @@ import { registerAccountingCloseTools } from "./accountingClose.js";
57
57
  * attendance_clock_in, attendance_clock_out, overtime_report, anomaly_list
58
58
  * Webhook trust (1): test_webhook (added to registerWebhookTools)
59
59
  * Payroll (2): payroll_export, payroll_checklist
60
- * Onboarding (2): onboarding_status, onboarding_persona_set
60
+ * (Onboarding tools onboarding_status / onboarding_persona_set withdrawn
61
+ * via #124 — Frihet-ERP /v1/onboarding/* family absent; tools were deleted,
62
+ * not stubbed, to keep the catalogue honest. See src/tools/igic.ts for the
63
+ * precedent of an inline NOT_DEPLOYED stub where the tool is retained.)
61
64
  * Permissions (2): permissions_matrix, permissions_me
62
65
  * Period close (3): period_close_status, period_close, period_reopen
63
66
  */
@@ -106,8 +109,8 @@ export function registerAllTools(server, client) {
106
109
  registerBankRulesTools(server, client);
107
110
  registerHrTools(server, client);
108
111
  registerPayrollTools(server, client);
109
- registerOnboardingTools(server, client);
110
112
  registerPermissionsTools(server, client);
111
113
  registerAccountingCloseTools(server, client);
114
+ registerSearchTools(server, client);
112
115
  }
113
116
  //# sourceMappingURL=register-all.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cross-resource global search tool — Phase 10 second-wave safe parity.
3
+ *
4
+ * Wraps `GET /v1/search/global?q=&types=&limit=&offset=` (publicApi.ts:4476).
5
+ * Read-only fan-out across invoices / expenses / vendors / clients / products
6
+ * with workspace scoping preserved by Firestore subcollection paths on the
7
+ * server. No mutating counterpart; no auth boundary different from any other
8
+ * list tool.
9
+ *
10
+ * Why this lives alone (not in clients.ts): it is not a clients/* endpoint,
11
+ * has its own canonical envelope (`{data, total, ..., query, types,
12
+ * truncated?}` instead of the standard paginated one), and is the only
13
+ * server-side fan-out tool — coupling it with one resource would mislead the
14
+ * tool listing.
15
+ */
16
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import type { IFrihetClient } from "../client-interface.js";
18
+ export declare function registerSearchTools(server: McpServer, client: IFrihetClient): void;
19
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Cross-resource global search tool — Phase 10 second-wave safe parity.
3
+ *
4
+ * Wraps `GET /v1/search/global?q=&types=&limit=&offset=` (publicApi.ts:4476).
5
+ * Read-only fan-out across invoices / expenses / vendors / clients / products
6
+ * with workspace scoping preserved by Firestore subcollection paths on the
7
+ * server. No mutating counterpart; no auth boundary different from any other
8
+ * list tool.
9
+ *
10
+ * Why this lives alone (not in clients.ts): it is not a clients/* endpoint,
11
+ * has its own canonical envelope (`{data, total, ..., query, types,
12
+ * truncated?}` instead of the standard paginated one), and is the only
13
+ * server-side fan-out tool — coupling it with one resource would mislead the
14
+ * tool listing.
15
+ */
16
+ import { z } from "zod/v4";
17
+ import { withToolLogging, listContent, READ_ONLY_ANNOTATIONS, formatPaginatedResponse, } from "./shared.js";
18
+ const GLOBAL_SEARCH_TYPES = ["invoices", "expenses", "vendors", "clients", "products"];
19
+ const globalSearchOutputSchema = z.object({
20
+ data: z.array(z.record(z.string(), z.unknown())),
21
+ total: z.number().int().nonnegative(),
22
+ limit: z.number().int().positive(),
23
+ offset: z.number().int().nonnegative(),
24
+ hasMore: z.boolean(),
25
+ query: z.string(),
26
+ types: z.array(z.enum(GLOBAL_SEARCH_TYPES)),
27
+ truncated: z.boolean().optional(),
28
+ });
29
+ export function registerSearchTools(server, client) {
30
+ server.registerTool("global_search", {
31
+ title: "Global Search",
32
+ description: "Search across invoices, expenses, vendors, clients, and products in one call. " +
33
+ "Use this to resolve an entity name/email/number when you do not yet know the resource type. " +
34
+ "Returns per-hit {type, id, name, ...} so the caller can dispatch to the right list/get tool. " +
35
+ "Read-only and workspace-scoped. " +
36
+ "Example: q='acme', types=['clients','invoices'], limit=10 " +
37
+ "/ Busca en facturas, gastos, proveedores, clientes y productos en una sola llamada. " +
38
+ "Util para resolver un nombre/correo/numero cuando no se sabe aun el tipo de recurso.",
39
+ annotations: READ_ONLY_ANNOTATIONS,
40
+ inputSchema: {
41
+ q: z
42
+ .string()
43
+ .trim()
44
+ .min(1)
45
+ .max(200)
46
+ .describe("Free-text query (1-200 chars) / Texto a buscar"),
47
+ types: z
48
+ .array(z.enum(GLOBAL_SEARCH_TYPES))
49
+ .optional()
50
+ .describe("Restrict to these resource types. Defaults to all 5. / Tipos a incluir (por defecto todos)"),
51
+ limit: z
52
+ .number()
53
+ .int()
54
+ .min(1)
55
+ .max(50)
56
+ .optional()
57
+ .describe("Max results across all types (1-50, default 25) / Resultados maximos"),
58
+ offset: z
59
+ .number()
60
+ .int()
61
+ .min(0)
62
+ .max(1000)
63
+ .optional()
64
+ .describe("Offset / Desplazamiento"),
65
+ },
66
+ outputSchema: globalSearchOutputSchema,
67
+ }, async ({ q, types, limit, offset }) => withToolLogging("global_search", async () => {
68
+ // Trim defensively: the JSON Schema serialization of `z.string().trim()`
69
+ // drops the trim hint, so leading/trailing whitespace survives MCP's
70
+ // input validation and would reach the ERP's q-length check as junk.
71
+ const cleanQ = q.trim();
72
+ const result = await client.globalSearch({
73
+ q: cleanQ,
74
+ types: types,
75
+ limit,
76
+ offset,
77
+ });
78
+ return {
79
+ content: [listContent(formatPaginatedResponse("search", result))],
80
+ structuredContent: result,
81
+ };
82
+ }));
83
+ }
84
+ //# sourceMappingURL=search.js.map