@frihet/mcp-server 1.16.5 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +92 -31
  2. package/dist/agent-onboarding.d.ts +98 -0
  3. package/dist/agent-onboarding.js +275 -0
  4. package/dist/api-origin.d.ts +9 -0
  5. package/dist/api-origin.js +41 -0
  6. package/dist/canary/mcp-harness.d.ts +77 -0
  7. package/dist/canary/mcp-harness.js +549 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +254 -0
  10. package/dist/client-interface.d.ts +22 -14
  11. package/dist/client.d.ts +114 -18
  12. package/dist/client.js +520 -58
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +209 -29
  15. package/dist/demo-fixtures.d.ts +3 -3
  16. package/dist/demo-fixtures.js +7 -7
  17. package/dist/fiscal-aliases.d.ts +44 -0
  18. package/dist/fiscal-aliases.js +62 -0
  19. package/dist/index.js +41 -56
  20. package/dist/logger.d.ts +2 -2
  21. package/dist/logger.js +140 -8
  22. package/dist/observability.d.ts +18 -29
  23. package/dist/observability.js +192 -105
  24. package/dist/openai-profile.d.ts +27 -15
  25. package/dist/openai-profile.js +1191 -145
  26. package/dist/openai-review-contract.d.ts +49 -0
  27. package/dist/openai-review-contract.js +919 -0
  28. package/dist/openai-review-oauth.d.ts +98 -0
  29. package/dist/openai-review-oauth.js +136 -0
  30. package/dist/prompts/register-all.d.ts +2 -0
  31. package/dist/prompts/register-all.js +2 -0
  32. package/dist/public-capability-contract.d.ts +39 -0
  33. package/dist/public-capability-contract.js +186 -0
  34. package/dist/redaction.d.ts +10 -0
  35. package/dist/redaction.js +74 -0
  36. package/dist/resources/register-all.js +6 -12
  37. package/dist/server-composition.d.ts +16 -0
  38. package/dist/server-composition.js +44 -0
  39. package/dist/tool-exposure.d.ts +25 -22
  40. package/dist/tool-exposure.js +199 -77
  41. package/dist/tools/accountingClose.d.ts +14 -5
  42. package/dist/tools/accountingClose.js +41 -23
  43. package/dist/tools/backend-availability.d.ts +3 -3
  44. package/dist/tools/backend-availability.js +3 -3
  45. package/dist/tools/clients.js +9 -9
  46. package/dist/tools/crm.js +22 -21
  47. package/dist/tools/deposits.js +30 -11
  48. package/dist/tools/einvoice.d.ts +1 -1
  49. package/dist/tools/expenses.d.ts +16 -0
  50. package/dist/tools/expenses.js +47 -27
  51. package/dist/tools/hr.d.ts +2 -3
  52. package/dist/tools/hr.js +8 -9
  53. package/dist/tools/intelligence.js +95 -4
  54. package/dist/tools/invoices.js +150 -47
  55. package/dist/tools/payroll.d.ts +5 -8
  56. package/dist/tools/payroll.js +19 -32
  57. package/dist/tools/permissions.d.ts +4 -7
  58. package/dist/tools/permissions.js +11 -14
  59. package/dist/tools/products.js +6 -6
  60. package/dist/tools/quotes.js +109 -24
  61. package/dist/tools/shared.d.ts +1431 -75
  62. package/dist/tools/shared.js +386 -76
  63. package/dist/tools/team.js +18 -14
  64. package/dist/tools/vendors.js +9 -9
  65. package/dist/tools/webhooks.js +35 -30
  66. package/dist/types.d.ts +28 -4
  67. package/docs/agent-onboarding.json +381 -0
  68. package/package.json +32 -7
@@ -2,35 +2,48 @@
2
2
  * Period close tools for the Frihet MCP server — D4-B megasprint (3 tools).
3
3
  *
4
4
  * Tools:
5
- * 1. period_close_status — get the current accounting period state (open/closing/closed)
5
+ * 1. period_close_status — get the current or selected fiscal-year state
6
6
  * 2. period_close — close a monthly or quarterly period (TRUST AREA)
7
7
  * 3. period_reopen — reopen a closed period with required reason (TRUST AREA)
8
8
  *
9
9
  * REST surface: /v1/periods/current, /v1/periods/{id}, /v1/periods/close, /v1/periods/{id}/reopen
10
10
  *
11
- * Closing a period freezes invoices/expenses/journal entries. Reopening requires a
12
- * compliance reason logged for audit. These are TRUST AREA operations.
11
+ * STATE OF THE WORLD (verified against erp-main functions/src/publicApi/families/
12
+ * periods.ts): only the READ route is implemented. Both WRITE routes answer
13
+ * HTTP 501 NOT_IMPLEMENTED — "deferred to the upcoming fiscal-write wave (Trust,
14
+ * serialized). Use year-end closing in the app for now." Nothing is frozen, and
15
+ * there is no idempotent no-op to rely on, because there is no handler at all.
13
16
  *
14
- * NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
15
- * until backend ships. TODO: confirm callable boundary vs REST shell for closePeriod.
17
+ * The descriptions below therefore state the 501 in the present tense. Do not
18
+ * restore aspirational prose about freezing or idempotency until a handler exists
19
+ * — GAP-13 was exactly that: an agent planning a year-end close read a mechanism
20
+ * the backend never had.
21
+ *
22
+ * `withBackendGuard` converts 404 only (backend-availability.ts isBackendNotFound),
23
+ * so a 501 surfaces through the generic error path as isError with the backend's
24
+ * own NOT_IMPLEMENTED message — an honest failure, not a fake success.
16
25
  */
17
26
  import { z } from "zod/v4";
18
- import { withToolLogging, formatRecord, getContent, mutateContent, READ_ONLY_ANNOTATIONS, periodStatusOutput, } from "./shared.js";
27
+ import { withToolLogging, formatRecord, getContent, mutateContent, READ_ONLY_ANNOTATIONS, currentPeriodOutput, periodStatusOutput, } from "./shared.js";
19
28
  import { withBackendGuard } from "./backend-availability.js";
20
29
  export function registerAccountingCloseTools(server, client) {
21
30
  // -- period_close_status --
22
31
  server.registerTool("period_close_status", {
23
32
  title: "Period Close Status",
24
- description: "Get the current accounting period state (open / closing / closed / reopened). " +
25
- "Without periodId returns the current open or most recently closed period. " +
26
- "/ Devuelve el estado del periodo contable actual (abierto/cerrando/cerrado/reabierto).",
33
+ description: "Get the open or closed state of the current fiscal year, including its fiscal-year start, " +
34
+ "inclusive date range, and nullable closing details. Pass a four-digit fiscal-year label to read it explicitly. " +
35
+ "/ Devuelve el estado abierto o cerrado del ejercicio fiscal actual o indicado.",
27
36
  annotations: READ_ONLY_ANNOTATIONS,
28
37
  inputSchema: {
29
- periodId: z.string().optional().describe("Specific period ID (default: current) / ID periodo especifico"),
38
+ periodId: z
39
+ .string()
40
+ .regex(/^\d{4}$/)
41
+ .optional()
42
+ .describe("Compatibility field: fiscal-year label YYYY, not an arbitrary period ID (default: current) / Ejercicio fiscal YYYY"),
30
43
  },
31
- outputSchema: periodStatusOutput,
32
- }, async ({ periodId }) => withToolLogging("period_close_status", () => withBackendGuard("period_close_status", "/v1/periods/current", async () => {
33
- const result = await client.getCurrentPeriod({ periodId });
44
+ outputSchema: currentPeriodOutput,
45
+ }, async ({ periodId }) => withToolLogging("period_close_status", () => withBackendGuard("period_close_status", periodId ? `/v1/periods/${periodId}` : "/v1/periods/current", async () => {
46
+ const result = await client.getCurrentPeriod({ fiscalYear: periodId });
34
47
  return {
35
48
  content: [getContent(formatRecord("Period status", result))],
36
49
  structuredContent: result,
@@ -39,12 +52,15 @@ export function registerAccountingCloseTools(server, client) {
39
52
  // -- period_close --
40
53
  server.registerTool("period_close", {
41
54
  title: "Close Accounting Period",
42
- description: "TRUST AREA — FISCAL CLOSE. Close a monthly or quarterly accounting period. " +
43
- "Freezes invoices, expenses, journal entries and bank reconciliations for the period. " +
44
- "Requires confirm=true. Idempotent: re-closing an already closed period is a no-op. " +
45
- "Closed periods can be reopened with period_reopen + audit reason. " +
46
- "/ AREA DE CONFIANZA — CIERRE FISCAL. Cierra un periodo contable mensual o trimestral. " +
47
- "Congela facturas, gastos, asientos. Requiere confirm=true.",
55
+ description: "TRUST AREA — FISCAL CLOSE. NOT IMPLEMENTED SERVER-SIDE TODAY: the backend answers " +
56
+ "HTTP 501 NOT_IMPLEMENTED to POST /v1/periods/close, so calling this tool closes nothing. " +
57
+ "No invoices, expenses, journal entries or bank reconciliations are frozen, and there is no " +
58
+ "idempotent no-op to rely on. Close the period in the Frihet app instead. " +
59
+ "The call is still gated behind confirm=true and will surface the 501 as an error. " +
60
+ "Use period_close_status (implemented) to read the current period state. " +
61
+ "/ AREA DE CONFIANZA — CIERRE FISCAL. HOY NO IMPLEMENTADO EN EL SERVIDOR: el backend responde " +
62
+ "HTTP 501 a POST /v1/periods/close, asi que esta herramienta no cierra nada ni congela nada. " +
63
+ "El cierre debe hacerse en la app de Frihet.",
48
64
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
49
65
  inputSchema: {
50
66
  type: z
@@ -81,10 +97,12 @@ export function registerAccountingCloseTools(server, client) {
81
97
  // -- period_reopen --
82
98
  server.registerTool("period_reopen", {
83
99
  title: "Reopen Accounting Period",
84
- description: "TRUST AREA — FISCAL REOPEN. Reopen a closed accounting period. " +
85
- "Requires a compliance reason (audit log) and confirm=true. " +
86
- "Reopening allows backdated edits to invoices/expenses — use with extreme caution. " +
87
- "/ AREA DE CONFIANZA — REAPERTURA FISCAL. Reabre un periodo cerrado. Requiere motivo (auditoria) y confirm=true.",
100
+ description: "TRUST AREA — FISCAL REOPEN. NOT IMPLEMENTED SERVER-SIDE TODAY: the backend answers " +
101
+ "HTTP 501 NOT_IMPLEMENTED to POST /v1/periods/{id}/reopen, so calling this tool reopens nothing " +
102
+ "and writes no audit entry. Reopen the period in the Frihet app instead. " +
103
+ "The call is still gated behind confirm=true plus a reason, and will surface the 501 as an error. " +
104
+ "/ AREA DE CONFIANZA — REAPERTURA FISCAL. HOY NO IMPLEMENTADO EN EL SERVIDOR: el backend responde " +
105
+ "HTTP 501, asi que no se reabre nada ni se registra auditoria. Hazlo en la app de Frihet.",
88
106
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
89
107
  inputSchema: {
90
108
  periodId: z.string().describe("Closed period ID to reopen / ID del periodo cerrado"),
@@ -4,9 +4,9 @@
4
4
  * PROBLEM (the bug this prevents): several tool families are wired here AHEAD of
5
5
  * their Frihet-ERP backend Cloud Functions shipping (fiscal `/v1/fiscal/*`, bank
6
6
  * rules `/v1/banking/rules`, gestoria `/v1/gestoria/*`, GL audit `/v1/gl/*`,
7
- * portal domain/onboard, IGIC, IS, VIES onboarding, payroll prep, permissions,
8
- * HR leaves/anomalies, accounting periods, webhook test). Until the CF deploys,
9
- * the API returns a genuine HTTP 404.
7
+ * portal domain/onboard, IGIC, IS, VIES onboarding, and webhook test). Until a
8
+ * staged CF deploys, the API returns a genuine HTTP 404. Live HR, payroll-read,
9
+ * and period-read tools also retain this guard as workspace-rollout safety.
10
10
  *
11
11
  * If that raw 404 reaches `handleToolError` it is mapped to the generic message
12
12
  * "Resource not found. / Recurso no encontrado." — which an LLM reads as "the
@@ -4,9 +4,9 @@
4
4
  * PROBLEM (the bug this prevents): several tool families are wired here AHEAD of
5
5
  * their Frihet-ERP backend Cloud Functions shipping (fiscal `/v1/fiscal/*`, bank
6
6
  * rules `/v1/banking/rules`, gestoria `/v1/gestoria/*`, GL audit `/v1/gl/*`,
7
- * portal domain/onboard, IGIC, IS, VIES onboarding, payroll prep, permissions,
8
- * HR leaves/anomalies, accounting periods, webhook test). Until the CF deploys,
9
- * the API returns a genuine HTTP 404.
7
+ * portal domain/onboard, IGIC, IS, VIES onboarding, and webhook test). Until a
8
+ * staged CF deploys, the API returns a genuine HTTP 404. Live HR, payroll-read,
9
+ * and period-read tools also retain this guard as workspace-rollout safety.
10
10
  *
11
11
  * If that raw 404 reaches `handleToolError` it is mapped to the generic message
12
12
  * "Resource not found. / Recurso no encontrado." — which an LLM reads as "the
@@ -77,9 +77,9 @@ export function registerClientTools(server, client) {
77
77
  "Los clientes se usan al crear facturas y presupuestos.",
78
78
  annotations: CREATE_ANNOTATIONS,
79
79
  inputSchema: {
80
- name: z.string().describe("Client/company name / Nombre del cliente o empresa"),
81
- email: z.string().optional().describe("Email address / Correo electronico"),
82
- phone: z.string().optional().describe("Phone number / Telefono"),
80
+ name: z.string().trim().min(1).max(200).describe("Client/company name / Nombre del cliente o empresa"),
81
+ email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
82
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
83
83
  taxId: z.string().optional().describe("Tax ID (NIF/CIF/VAT) / NIF o CIF"),
84
84
  address: addressSchema,
85
85
  },
@@ -88,8 +88,8 @@ export function registerClientTools(server, client) {
88
88
  const result = await client.createClient(input);
89
89
  const hints = enrichResponse("clients", "create", result);
90
90
  return {
91
- content: [mutateContent(formatRecord("Client created", result))],
92
- structuredContent: { ...result, ...hints },
91
+ content: [mutateContent(formatRecord("Client created", result) + hints)],
92
+ structuredContent: { ...result },
93
93
  };
94
94
  }));
95
95
  // -- update_client --
@@ -100,10 +100,10 @@ export function registerClientTools(server, client) {
100
100
  "/ Actualiza un cliente existente. Solo se modifican los campos proporcionados.",
101
101
  annotations: UPDATE_ANNOTATIONS,
102
102
  inputSchema: {
103
- id: z.string().describe("Client ID / ID del cliente"),
104
- name: z.string().optional().describe("Name / Nombre"),
105
- email: z.string().optional().describe("Email / Correo"),
106
- phone: z.string().optional().describe("Phone / Telefono"),
103
+ id: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
104
+ name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
105
+ email: z.string().trim().email().max(255).optional().describe("Email / Correo"),
106
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone / Telefono"),
107
107
  taxId: z.string().optional().describe("Tax ID / NIF/CIF"),
108
108
  address: addressSchema,
109
109
  },
package/dist/tools/crm.js CHANGED
@@ -37,11 +37,11 @@ export function registerCrmTools(server, client) {
37
37
  "/ Anade un nuevo contacto a un cliente. Requiere al menos un nombre.",
38
38
  annotations: CREATE_ANNOTATIONS,
39
39
  inputSchema: {
40
- clientId: z.string().describe("Client ID / ID del cliente"),
41
- name: z.string().describe("Contact name / Nombre del contacto"),
42
- email: z.string().optional().describe("Email address / Correo electronico"),
43
- phone: z.string().optional().describe("Phone number / Telefono"),
44
- role: z.string().optional().describe("Role or title (e.g. CEO, CTO, Billing) / Cargo o rol"),
40
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
41
+ name: z.string().trim().min(1).max(200).describe("Contact name / Nombre del contacto"),
42
+ email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
43
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
44
+ role: z.string().trim().min(1).max(200).optional().describe("Role or title (e.g. CEO, CTO, Billing) / Cargo o rol"),
45
45
  isPrimary: z.boolean().optional().describe("Whether this is the primary contact / Si es el contacto principal"),
46
46
  },
47
47
  outputSchema: contactItemOutput,
@@ -49,8 +49,8 @@ export function registerCrmTools(server, client) {
49
49
  const result = await client.createClientContact(clientId, data);
50
50
  const hints = enrichResponse("contacts", "create", result);
51
51
  return {
52
- content: [mutateContent(formatRecord("Contact created", result))],
53
- structuredContent: { ...result, ...hints },
52
+ content: [mutateContent(formatRecord("Contact created", result) + hints)],
53
+ structuredContent: { ...result },
54
54
  };
55
55
  }));
56
56
  // -- delete_client_contact --
@@ -60,8 +60,8 @@ export function registerCrmTools(server, client) {
60
60
  "/ Elimina permanentemente un contacto de un cliente. Esta accion no se puede deshacer.",
61
61
  annotations: DELETE_ANNOTATIONS,
62
62
  inputSchema: {
63
- clientId: z.string().describe("Client ID / ID del cliente"),
64
- contactId: z.string().describe("Contact ID / ID del contacto"),
63
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
64
+ contactId: z.string().trim().min(1).max(128).describe("Contact ID / ID del contacto"),
65
65
  },
66
66
  outputSchema: deleteResultOutput,
67
67
  }, async ({ clientId, contactId }) => withToolLogging("delete_client_contact", async () => {
@@ -82,7 +82,7 @@ export function registerCrmTools(server, client) {
82
82
  "/ Lista todas las actividades CRM de un cliente con paginacion opcional.",
83
83
  annotations: READ_ONLY_ANNOTATIONS,
84
84
  inputSchema: {
85
- clientId: z.string().describe("Client ID / ID del cliente"),
85
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
86
86
  limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
87
87
  offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
88
88
  },
@@ -98,18 +98,18 @@ export function registerCrmTools(server, client) {
98
98
  server.registerTool("log_client_activity", {
99
99
  title: "Log Client Activity",
100
100
  description: "Log a CRM activity against a client. Use to track calls, emails, meetings, or tasks. " +
101
+ "Do not include passwords, credentials, payment-card data, health data, or official/government identifiers in free-text fields. " +
101
102
  "Example: clientId='abc123', type='call', title='Discussed Q2 proposal', description='Client interested in upgrade' " +
102
- "/ Registra una actividad CRM para un cliente. Usa para rastrear llamadas, emails, reuniones o tareas.",
103
+ "/ Registra una actividad CRM para un cliente. No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales en los campos de texto libre.",
103
104
  annotations: CREATE_ANNOTATIONS,
104
- inputSchema: {
105
+ inputSchema: z.object({
105
106
  clientId: z.string().describe("Client ID / ID del cliente"),
106
107
  type: z
107
108
  .enum(["call", "email", "meeting", "task"])
108
109
  .describe("Activity type / Tipo de actividad"),
109
- title: z.string().describe("Activity title / Titulo de la actividad"),
110
- description: z.string().optional().describe("Detailed description / Descripcion detallada"),
111
- date: z.string().optional().describe("Activity date (ISO 8601, defaults to now) / Fecha de la actividad"),
112
- },
110
+ title: z.string().trim().min(1).max(500).describe("Activity title / Titulo de la actividad"),
111
+ description: z.string().trim().min(1).max(5000).optional().describe("Detailed description; exclude secrets, credentials, card or health data, and official identifiers / Descripcion detallada; excluye secretos, credenciales, datos de tarjetas o salud e identificadores oficiales"),
112
+ }).strict(),
113
113
  outputSchema: activityItemOutput,
114
114
  }, async ({ clientId, ...data }) => withToolLogging("log_client_activity", async () => {
115
115
  const result = await client.logClientActivity(clientId, data);
@@ -144,12 +144,13 @@ export function registerCrmTools(server, client) {
144
144
  server.registerTool("create_client_note", {
145
145
  title: "Create Client Note",
146
146
  description: "Add a note to a client. Notes are free-form text entries useful for keeping context. " +
147
+ "Do not include passwords, credentials, payment-card data, health data, or official/government identifiers. " +
147
148
  "Example: clientId='abc123', content='Prefers invoices in English. Payment NET 30.' " +
148
- "/ Anade una nota a un cliente. Las notas son texto libre para mantener contexto.",
149
+ "/ Anade una nota a un cliente. No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales.",
149
150
  annotations: CREATE_ANNOTATIONS,
150
151
  inputSchema: {
151
- clientId: z.string().describe("Client ID / ID del cliente"),
152
- content: z.string().describe("Note content / Contenido de la nota"),
152
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
153
+ content: z.string().trim().min(1).max(10000).describe("Note content; exclude secrets, credentials, card or health data, and official identifiers / Contenido; excluye secretos, credenciales, datos de tarjetas o salud e identificadores oficiales"),
153
154
  },
154
155
  outputSchema: noteItemOutput,
155
156
  }, async ({ clientId, content }) => withToolLogging("create_client_note", async () => {
@@ -166,8 +167,8 @@ export function registerCrmTools(server, client) {
166
167
  "/ Elimina permanentemente una nota de un cliente. Esta accion no se puede deshacer.",
167
168
  annotations: DELETE_ANNOTATIONS,
168
169
  inputSchema: {
169
- clientId: z.string().describe("Client ID / ID del cliente"),
170
- noteId: z.string().describe("Note ID / ID de la nota"),
170
+ clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
171
+ noteId: z.string().trim().min(1).max(128).describe("Note ID / ID de la nota"),
171
172
  },
172
173
  outputSchema: deleteResultOutput,
173
174
  }, async ({ clientId, noteId }) => withToolLogging("delete_client_note", async () => {
@@ -45,8 +45,8 @@ export function registerDepositTools(server, client) {
45
45
  const result = await client.listDeposits({ limit, offset, after, fields, from, to, clientId, status });
46
46
  const hints = enrichResponse("deposits", "list", result.data);
47
47
  return {
48
- content: [listContent(formatPaginatedResponse("deposits", result))],
49
- structuredContent: { ...result, ...hints },
48
+ content: [listContent(formatPaginatedResponse("deposits", result) + hints)],
49
+ structuredContent: { ...result },
50
50
  };
51
51
  }));
52
52
  // -- get_deposit --
@@ -101,8 +101,8 @@ export function registerDepositTools(server, client) {
101
101
  const result = await client.createDeposit(input);
102
102
  const hints = enrichResponse("deposits", "create", result);
103
103
  return {
104
- content: [mutateContent(formatRecord("Deposit created", result))],
105
- structuredContent: { ...result, ...hints },
104
+ content: [mutateContent(formatRecord("Deposit created", result) + hints)],
105
+ structuredContent: { ...result },
106
106
  };
107
107
  }));
108
108
  // -- update_deposit --
@@ -147,8 +147,8 @@ export function registerDepositTools(server, client) {
147
147
  await client.deleteDeposit(id);
148
148
  const hints = enrichResponse("deposits", "delete", { id });
149
149
  return {
150
- content: [mutateContent(`Deposit ${id} deleted successfully. / Deposito ${id} eliminado correctamente.`)],
151
- structuredContent: { success: true, id, ...hints },
150
+ content: [mutateContent(`Deposit ${id} deleted successfully. / Deposito ${id} eliminado correctamente.` + hints)],
151
+ structuredContent: { success: true, id },
152
152
  };
153
153
  }));
154
154
  // -- apply_deposit --
@@ -178,18 +178,37 @@ export function registerDepositTools(server, client) {
178
178
  // -- refund_deposit --
179
179
  server.registerTool("refund_deposit", {
180
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.",
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.",
185
187
  annotations: UPDATE_ANNOTATIONS,
186
188
  inputSchema: {
187
189
  id: z.string().describe("Deposit ID / ID del deposito"),
188
190
  reason: z.string().optional().describe("Reason for the refund / Motivo de la devolucion"),
189
191
  notes: z.string().optional().describe("Refund notes / Notas de la devolucion"),
192
+ confirm: z
193
+ .boolean()
194
+ .describe("Must be true to confirm the refund / Debe ser true para confirmar la devolucion"),
190
195
  },
191
196
  outputSchema: actionResultOutput,
192
- }, async ({ id, ...data }) => withToolLogging("refund_deposit", async () => {
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
+ };
211
+ }
193
212
  const result = await client.refundDeposit(id, data);
194
213
  return {
195
214
  content: [mutateContent(formatRecord("Deposit refunded", result))],
@@ -48,7 +48,6 @@ 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";
52
51
  "xrechnung-cii": "xrechnung-cii";
53
52
  "xrechnung-ubl": "xrechnung-ubl";
54
53
  "facturx-en16931": "facturx-en16931";
@@ -59,6 +58,7 @@ export declare const eInvoiceFormatSchema: z.ZodEnum<{
59
58
  ubl: "ubl";
60
59
  cii: "cii";
61
60
  "peppol-bis-3": "peppol-bis-3";
61
+ facturae: "facturae";
62
62
  }>;
63
63
  export type EInvoiceFormat = z.infer<typeof eInvoiceFormatSchema>;
64
64
  export declare const sendEInvoiceOutput: z.ZodObject<{
@@ -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
  }
@@ -14,9 +14,8 @@
14
14
  *
15
15
  * REST surface: /v1/leaves, /v1/time-entries, /v1/anomalies
16
16
  *
17
- * NOTE: ERP backend endpoints land in parallel D4-A wave. Tools wired —
18
- * 404s propagate as isError until backend ships. TODO: wire to CF
19
- * logLeaveDecision callable when REST shell lands.
17
+ * These ERP REST routes are live. The backend guard remains a rollout/workspace
18
+ * safety net so an unavailable route is never misreported as an empty result.
20
19
  */
21
20
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
21
  import type { IFrihetClient } from "../client-interface.js";
package/dist/tools/hr.js CHANGED
@@ -14,9 +14,8 @@
14
14
  *
15
15
  * REST surface: /v1/leaves, /v1/time-entries, /v1/anomalies
16
16
  *
17
- * NOTE: ERP backend endpoints land in parallel D4-A wave. Tools wired —
18
- * 404s propagate as isError until backend ships. TODO: wire to CF
19
- * logLeaveDecision callable when REST shell lands.
17
+ * These ERP REST routes are live. The backend guard remains a rollout/workspace
18
+ * safety net so an unavailable route is never misreported as an empty result.
20
19
  */
21
20
  import { z } from "zod/v4";
22
21
  import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, leaveRequestItemOutput, attendanceEntryItemOutput, overtimeReportOutput, anomalyItemOutput, } from "./shared.js";
@@ -171,14 +170,14 @@ export function registerHrTools(server, client) {
171
170
  // -- overtime_report --
172
171
  server.registerTool("overtime_report", {
173
172
  title: "Overtime Report",
174
- description: "Generate an overtime report for a period. Aggregates regular vs overtime hours " +
175
- "per employee + total estimated cost in EUR. Useful for payroll prep and labor-law audits. " +
176
- "Period format: 'YYYY-MM' (monthly) or 'YYYY-QN' (quarterly) or 'YYYY' (annual). " +
177
- "/ Informe de horas extra por periodo. Agrega horas regulares vs extra por empleado + coste estimado.",
173
+ description: "Read the ERP overtime computation for a calendar month or year: daily and weekly overtime, " +
174
+ "worked/regular/overtime minutes, overtime hours, and compliance alerts computed from records in the selected period. " +
175
+ "Optionally filter the attendance records by employee. Period format: 'YYYY-MM' or 'YYYY'. " +
176
+ "/ Lee desgloses diarios y semanales, y minutos/horas agregados calculados con los registros del periodo seleccionado, con alertas.",
178
177
  annotations: READ_ONLY_ANNOTATIONS,
179
178
  inputSchema: {
180
- period: z.string().describe("Period (YYYY-MM, YYYY-QN, or YYYY) / Periodo"),
181
- employeeId: z.string().optional().describe("Optional filter by employee / Filtrar por empleado opcional"),
179
+ period: z.string().regex(/^\d{4}(-(0[1-9]|1[0-2]))?$/).describe("Period (YYYY-MM or YYYY) / Periodo"),
180
+ employeeId: z.string().min(1).optional().describe("Optional filter by employee / Filtrar por empleado opcional"),
182
181
  },
183
182
  outputSchema: overtimeReportOutput,
184
183
  }, async ({ period, employeeId }) => withToolLogging("overtime_report", () => withBackendGuard("overtime_report", "/v1/time-entries/overtime", async () => {