@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
@@ -12,27 +12,27 @@
12
12
  * Backend: /v1/team/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
13
13
  */
14
14
  import { z } from "zod/v4";
15
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, teamMemberItemOutput, } from "./shared.js";
15
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, teamMemberItemOutput, } from "./shared.js";
16
16
  export function registerTeamTools(server, client) {
17
17
  // -- list_team_members --
18
18
  server.registerTool("list_team_members", {
19
19
  title: "List Team Members",
20
- description: "List all members in the workspace. " +
21
- "Returns member ID, name, email, role, and invite status (pending/active). " +
22
- "Useful for access management and auditing who has access to the account. " +
23
- "/ Lista todos los miembros del espacio de trabajo. " +
24
- "Devuelve ID, nombre, email, rol y estado de invitacion (pendiente/activo). " +
25
- "Util para gestion de accesos y auditoria de quien tiene acceso a la cuenta.",
20
+ description: "List active workspace team members and pending invitations. The workspace owner is NOT a member row and is excluded. " +
21
+ "Each row carries member ID, name, email, role, and status (active/pending/expired/invalid; role is null on invalid invitations). " +
22
+ "Useful for reviewing team access and outstanding invitations. " +
23
+ "/ Lista miembros activos del equipo e invitaciones pendientes. El propietario del espacio NO aparece como miembro y queda excluido. " +
24
+ "Cada fila lleva ID, nombre, email, rol y estado (active/pending/expired/invalid; rol null en invitaciones invalidas). " +
25
+ "Util para revisar el acceso del equipo y las invitaciones pendientes.",
26
26
  annotations: READ_ONLY_ANNOTATIONS,
27
27
  inputSchema: {
28
28
  role: z
29
- .enum(["owner", "admin", "member", "viewer"])
29
+ .enum(["owner", "admin", "editor", "accountant", "viewer"])
30
30
  .optional()
31
31
  .describe("Filter by role / Filtrar por rol"),
32
32
  status: z
33
- .enum(["active", "pending"])
33
+ .enum(["active", "pending", "expired", "invalid"])
34
34
  .optional()
35
- .describe("Filter by invite status / Filtrar por estado de invitacion"),
35
+ .describe("Filter by status / Filtrar por estado"),
36
36
  limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
37
37
  offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
38
38
  },
@@ -49,16 +49,18 @@ export function registerTeamTools(server, client) {
49
49
  title: "Invite Team Member",
50
50
  description: "Invite a new member to the workspace by email address. " +
51
51
  "An invitation email is sent — the member must accept before gaining access. " +
52
- "Roles: owner (full access), admin (manage account, no billing), member (operational access), viewer (read-only). " +
53
- "Example: email='ana@example.com', role='member' " +
52
+ "Assignable roles: admin, editor, accountant, viewer. Exact access follows the workspace's current role policy. " +
53
+ "Owner is excluded from invitations and must be transferred via a dedicated flow. " +
54
+ "Example: email='ana@example.com', role='accountant' " +
54
55
  "/ Invita a un nuevo miembro al espacio de trabajo por correo electronico. " +
55
56
  "Se envia un email de invitacion — el miembro debe aceptar antes de acceder. " +
56
- "Roles: owner (acceso total), admin (gestion sin facturacion), member (acceso operativo), viewer (solo lectura).",
57
+ "Roles asignables: admin, editor, accountant, viewer. El acceso exacto depende de la politica de roles vigente del espacio. " +
58
+ "Owner queda excluido de las invitaciones y debe transferirse por un flujo dedicado.",
57
59
  annotations: CREATE_ANNOTATIONS,
58
60
  inputSchema: {
59
61
  email: z.string().email().describe("Email address of the invitee / Email del invitado"),
60
62
  role: z
61
- .enum(["admin", "member", "viewer"])
63
+ .enum(["admin", "editor", "accountant", "viewer"])
62
64
  .describe("Role to assign (owner cannot be invited, must be transferred) / Rol a asignar (owner no se puede invitar, debe transferirse)"),
63
65
  name: z
64
66
  .string()
@@ -79,15 +81,17 @@ export function registerTeamTools(server, client) {
79
81
  description: "Change the role of an existing team member. " +
80
82
  "Only workspace admins or owners can change roles. " +
81
83
  "Cannot change the owner's role — use a dedicated ownership transfer flow. " +
82
- "Example: memberId='mbr_abc123', role='admin' " +
84
+ "Allowed roles: admin, editor, accountant, viewer. " +
85
+ "Example: memberId='mbr_abc123', role='accountant' " +
83
86
  "/ Cambia el rol de un miembro existente del espacio de trabajo. " +
84
87
  "Solo administradores o propietarios pueden cambiar roles. " +
85
- "No se puede cambiar el rol del propietario — usa el flujo de transferencia de propiedad.",
88
+ "No se puede cambiar el rol del propietario — usa el flujo de transferencia de propiedad. " +
89
+ "Roles permitidos: admin, editor, accountant, viewer.",
86
90
  annotations: UPDATE_ANNOTATIONS,
87
91
  inputSchema: {
88
92
  memberId: z.string().describe("Team member ID / ID del miembro del equipo"),
89
93
  role: z
90
- .enum(["admin", "member", "viewer"])
94
+ .enum(["admin", "editor", "accountant", "viewer"])
91
95
  .describe("New role to assign / Nuevo rol a asignar"),
92
96
  },
93
97
  outputSchema: actionResultOutput,
@@ -119,17 +123,9 @@ export function registerTeamTools(server, client) {
119
123
  outputSchema: actionResultOutput,
120
124
  }, async ({ memberId, confirm }) => withToolLogging("remove_team_member", async () => {
121
125
  if (!confirm) {
122
- return {
123
- content: [
124
- {
125
- type: "text",
126
- text: "Error: confirm=true is required to remove a team member. " +
127
- "The member will immediately lose access to the workspace. Set confirm=true to proceed. / " +
128
- "Se requiere confirm=true para eliminar un miembro del equipo.",
129
- },
130
- ],
131
- isError: true,
132
- };
126
+ return confirmationRequiredError("Error: confirm=true is required to remove a team member. " +
127
+ "The member will immediately lose access to the workspace. Set confirm=true to proceed. / " +
128
+ "Se requiere confirm=true para eliminar un miembro del equipo.");
133
129
  }
134
130
  await client.removeTeamMember(memberId);
135
131
  return {
@@ -14,7 +14,7 @@
14
14
  * Backend: /v1/time/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
15
15
  */
16
16
  import { z } from "zod/v4";
17
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, deleteResultOutput, timeEntryItemOutput, timeSummaryOutput, } from "./shared.js";
17
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, deleteResultOutput, timeEntryItemOutput, timeSummaryOutput, } from "./shared.js";
18
18
  export function registerTimeTools(server, client) {
19
19
  // -- list_time_entries --
20
20
  server.registerTool("list_time_entries", {
@@ -132,17 +132,9 @@ export function registerTimeTools(server, client) {
132
132
  outputSchema: deleteResultOutput,
133
133
  }, async ({ id, confirm }) => withToolLogging("delete_time_entry", async () => {
134
134
  if (!confirm) {
135
- return {
136
- content: [
137
- {
138
- type: "text",
139
- text: "Error: confirm=true is required to delete a time entry. " +
140
- "Deleted entries affect billing reports. Set confirm=true to proceed. / " +
141
- "Se requiere confirm=true para eliminar una entrada de tiempo.",
142
- },
143
- ],
144
- isError: true,
145
- };
135
+ return confirmationRequiredError("Error: confirm=true is required to delete a time entry. " +
136
+ "Deleted entries affect billing reports. Set confirm=true to proceed. / " +
137
+ "Se requiere confirm=true para eliminar una entrada de tiempo.");
146
138
  }
147
139
  await client.deleteTimeEntry(id);
148
140
  return {
@@ -13,6 +13,168 @@ const addressSchema = z
13
13
  })
14
14
  .optional()
15
15
  .describe("Vendor address / Direccion del proveedor");
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_VENDOR_STRING_FIELDS = [
30
+ "email",
31
+ "phone",
32
+ "taxId",
33
+ "createdAt",
34
+ "updatedAt",
35
+ ];
36
+ /**
37
+ * Normalizes a vendor record returned by the Frihet API so it always satisfies
38
+ * `vendorItemOutput` without losing ERP-canonical data:
39
+ * - Strips `null` on optional string fields (`email`, `phone`, `taxId`,
40
+ * `createdAt`, `updatedAt`) so `z.string().optional()` never rejects legacy docs.
41
+ * - Backfills `taxId` from legacy `nif` / `cif` / `vatNumber` / `tax_id` when
42
+ * `taxId` is absent or null, preserving all original fields.
43
+ * - Converts legacy string `address` into `{ street }`.
44
+ * - Omits `null` or empty `address`.
45
+ * - Maps ERP canonical address keys (`province` -> `state`, `zip` -> `postalCode`,
46
+ * `countryCode` -> `country`).
47
+ */
48
+ function normalizeVendorRecord(vendorRecord) {
49
+ if (!vendorRecord || typeof vendorRecord !== "object" || Array.isArray(vendorRecord)) {
50
+ return vendorRecord;
51
+ }
52
+ const out = { ...vendorRecord };
53
+ for (const field of OPTIONAL_VENDOR_STRING_FIELDS) {
54
+ if (out[field] === null) {
55
+ delete out[field];
56
+ }
57
+ }
58
+ if (typeof out.taxId !== "string" || !out.taxId.trim()) {
59
+ for (const alias of ["nif", "cif", "vatNumber", "tax_id"]) {
60
+ const candidate = out[alias];
61
+ if (typeof candidate === "string" && candidate.trim()) {
62
+ out.taxId = candidate.trim();
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ const rawAddress = out.address;
68
+ const rootStreet = typeof out.street === "string" && out.street.trim() ? out.street.trim() : undefined;
69
+ const rootCity = typeof out.city === "string" && out.city.trim() ? out.city.trim() : undefined;
70
+ const rootState = typeof out.state === "string" && out.state.trim()
71
+ ? out.state.trim()
72
+ : typeof out.province === "string" && out.province.trim()
73
+ ? out.province.trim()
74
+ : undefined;
75
+ const rootPostalCode = typeof out.postalCode === "string" && out.postalCode.trim()
76
+ ? out.postalCode.trim()
77
+ : typeof out.zip === "string" && out.zip.trim()
78
+ ? out.zip.trim()
79
+ : undefined;
80
+ const rootCountry = typeof out.country === "string" && out.country.trim()
81
+ ? out.country.trim()
82
+ : typeof out.countryCode === "string" && out.countryCode.trim()
83
+ ? out.countryCode.trim()
84
+ : undefined;
85
+ const buildFlatAddress = (streetOverride) => {
86
+ const built = {};
87
+ const st = streetOverride ?? rootStreet;
88
+ if (st !== undefined)
89
+ built.street = st;
90
+ if (rootCity !== undefined)
91
+ built.city = rootCity;
92
+ if (rootState !== undefined)
93
+ built.state = rootState;
94
+ if (rootPostalCode !== undefined)
95
+ built.postalCode = rootPostalCode;
96
+ if (rootCountry !== undefined)
97
+ built.country = rootCountry;
98
+ return Object.keys(built).length > 0 ? built : undefined;
99
+ };
100
+ if (rawAddress === undefined || rawAddress === null) {
101
+ if (rawAddress === null) {
102
+ delete out.address;
103
+ }
104
+ const flatAddr = buildFlatAddress();
105
+ if (flatAddr) {
106
+ out.address = flatAddr;
107
+ }
108
+ return out;
109
+ }
110
+ if (typeof rawAddress === "string") {
111
+ const trimmed = rawAddress.trim();
112
+ const flatAddr = buildFlatAddress(trimmed || undefined);
113
+ if (!flatAddr) {
114
+ delete out.address;
115
+ return out;
116
+ }
117
+ out.address = flatAddr;
118
+ return out;
119
+ }
120
+ if (typeof rawAddress === "object" && !Array.isArray(rawAddress)) {
121
+ const addrObj = rawAddress;
122
+ const normalizedAddress = {};
123
+ for (const [k, v] of Object.entries(addrObj)) {
124
+ if (v !== null && v !== undefined) {
125
+ normalizedAddress[k] = v;
126
+ }
127
+ }
128
+ const street = typeof addrObj.street === "string" && addrObj.street.trim()
129
+ ? addrObj.street.trim()
130
+ : rootStreet;
131
+ const city = typeof addrObj.city === "string" && addrObj.city.trim()
132
+ ? addrObj.city.trim()
133
+ : rootCity;
134
+ const state = typeof addrObj.state === "string" && addrObj.state.trim()
135
+ ? addrObj.state.trim()
136
+ : typeof addrObj.province === "string" && addrObj.province.trim()
137
+ ? addrObj.province.trim()
138
+ : rootState;
139
+ const postalCode = typeof addrObj.postalCode === "string" && addrObj.postalCode.trim()
140
+ ? addrObj.postalCode.trim()
141
+ : typeof addrObj.zip === "string" && addrObj.zip.trim()
142
+ ? addrObj.zip.trim()
143
+ : rootPostalCode;
144
+ const country = typeof addrObj.country === "string" && addrObj.country.trim()
145
+ ? addrObj.country.trim()
146
+ : typeof addrObj.countryCode === "string" && addrObj.countryCode.trim()
147
+ ? addrObj.countryCode.trim()
148
+ : rootCountry;
149
+ if (street !== undefined)
150
+ normalizedAddress.street = street;
151
+ else
152
+ delete normalizedAddress.street;
153
+ if (city !== undefined)
154
+ normalizedAddress.city = city;
155
+ else
156
+ delete normalizedAddress.city;
157
+ if (state !== undefined)
158
+ normalizedAddress.state = state;
159
+ else
160
+ delete normalizedAddress.state;
161
+ if (postalCode !== undefined)
162
+ normalizedAddress.postalCode = postalCode;
163
+ else
164
+ delete normalizedAddress.postalCode;
165
+ if (country !== undefined)
166
+ normalizedAddress.country = country;
167
+ else
168
+ delete normalizedAddress.country;
169
+ if (Object.keys(normalizedAddress).length === 0) {
170
+ delete out.address;
171
+ return out;
172
+ }
173
+ out.address = normalizedAddress;
174
+ return out;
175
+ }
176
+ return out;
177
+ }
16
178
  export function registerVendorTools(server, client) {
17
179
  // -- list_vendors --
18
180
  server.registerTool("list_vendors", {
@@ -31,10 +193,82 @@ export function registerVendorTools(server, client) {
31
193
  },
32
194
  outputSchema: paginatedOutput(vendorItemOutput, { projectable: true }),
33
195
  }, async ({ q, limit, offset, after, fields }) => withToolLogging("list_vendors", async () => {
34
- const result = await client.listVendors({ q, limit, offset, after, fields });
196
+ const trimmedQ = q?.trim();
197
+ const trimmedAfter = after?.trim();
198
+ // Limitation in Frihet-ERP `listResources` (publicApi.ts L8158-8268):
199
+ // When `search` (`q`) is provided, the backend slices results by `offset`/`limit`
200
+ // and returns before evaluating `cursorParam` (`after`). Protected profiles
201
+ // (Claude `claudeToolResult` and OpenAI `applyOpenAIProfile`) rebuild `content`
202
+ // from `structuredContent` while `paginatedOutput` is frozen, so fail closed
203
+ // (`isError: true` without `structuredContent`).
204
+ if (trimmedQ && trimmedAfter) {
205
+ return {
206
+ content: [{
207
+ type: "text",
208
+ 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'. / " +
209
+ "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'.",
210
+ }],
211
+ isError: true,
212
+ };
213
+ }
214
+ let result = await client.listVendors({ q, limit, offset, after, fields });
215
+ let usedFallback = false;
216
+ const splitQ = trimmedQ ? splitCompoundQuery(trimmedQ) : undefined;
217
+ const isSubsequentPage = (offset !== undefined && offset > 0) || Boolean(after);
218
+ const initialTruncated = result?.truncated === true;
219
+ const canFallback = splitQ !== undefined && splitQ !== trimmedQ && !after && !initialTruncated;
220
+ const hasZeroTotalMatches = Array.isArray(result?.data) &&
221
+ result.data.length === 0 &&
222
+ (isSubsequentPage ? result.total === 0 : result.total === 0 || result.total === undefined);
223
+ if (canFallback && hasZeroTotalMatches) {
224
+ try {
225
+ const fallbackResult = await client.listVendors({ q: splitQ, limit, offset, after, fields });
226
+ const fallbackTruncated = fallbackResult?.truncated === true;
227
+ if (fallbackTruncated ||
228
+ (fallbackResult?.data && (fallbackResult.data.length > 0 || (fallbackResult.total !== undefined && fallbackResult.total > 0)))) {
229
+ result = fallbackResult;
230
+ usedFallback = true;
231
+ }
232
+ }
233
+ catch {
234
+ // Preserve original response if fallback fails
235
+ }
236
+ }
237
+ const rawEnvelope = (result ?? {});
238
+ const isTruncated = rawEnvelope.truncated === true;
239
+ const normalizedData = Array.isArray(result?.data)
240
+ ? result.data.map((v) => (v && typeof v === "object" ? normalizeVendorRecord(v) : v))
241
+ : [];
242
+ // Conservative refusal when the ERP 500-document scan cap was saturated (`truncated: true`).
243
+ // Because `paginatedOutput` is a frozen schema contract and both Claude (`claudeToolResult`)
244
+ // and OpenAI (`applyOpenAIProfile`) overwrite `content` whenever `structuredContent` is
245
+ // present, returning `structuredContent` here would hide the truncation warning and present
246
+ // an incomplete 500-doc scan as exhaustive.
247
+ if (isTruncated) {
248
+ const partialCount = typeof result?.total === "number" ? result.total : normalizedData.length;
249
+ return {
250
+ content: [{
251
+ type: "text",
252
+ text: `Error: vendor 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 paginate without 'q' using 'after'. / ` +
253
+ `Error: la busqueda de proveedores 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 pagina sin 'q' usando 'after'.`,
254
+ }],
255
+ isError: true,
256
+ };
257
+ }
258
+ const cleanEnvelope = {
259
+ data: normalizedData,
260
+ total: typeof result?.total === "number" ? result.total : normalizedData.length,
261
+ limit: typeof result?.limit === "number" ? result.limit : (limit ?? 20),
262
+ offset: typeof result?.offset === "number" ? result.offset : (offset ?? 0),
263
+ ...(typeof result?.nextCursor === "string" ? { nextCursor: result.nextCursor } : {}),
264
+ };
265
+ const notes = [];
266
+ if (usedFallback) {
267
+ notes.push(` (criterio aplicado: "${splitQ}")`);
268
+ }
35
269
  return {
36
- content: [listContent(formatPaginatedResponse("vendors", result))],
37
- structuredContent: result,
270
+ content: [listContent(formatPaginatedResponse("vendors", cleanEnvelope) + notes.join(""))],
271
+ structuredContent: cleanEnvelope,
38
272
  };
39
273
  }));
40
274
  // -- get_vendor --
@@ -48,7 +282,7 @@ export function registerVendorTools(server, client) {
48
282
  },
49
283
  outputSchema: vendorItemOutput,
50
284
  }, async ({ id }) => withToolLogging("get_vendor", async () => {
51
- const result = await client.getVendor(id);
285
+ const result = normalizeVendorRecord(await client.getVendor(id));
52
286
  return {
53
287
  content: [getContent(formatRecord("Vendor", result))],
54
288
  structuredContent: result,
@@ -64,19 +298,19 @@ export function registerVendorTools(server, client) {
64
298
  "Los proveedores se usan al registrar gastos y pedidos de compra.",
65
299
  annotations: CREATE_ANNOTATIONS,
66
300
  inputSchema: {
67
- name: z.string().describe("Vendor/company name / Nombre del proveedor o empresa"),
68
- email: z.string().optional().describe("Email address / Correo electronico"),
69
- phone: z.string().optional().describe("Phone number / Telefono"),
301
+ name: z.string().trim().min(1).max(200).describe("Vendor/company name / Nombre del proveedor o empresa"),
302
+ email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
303
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
70
304
  taxId: z.string().optional().describe("Tax ID (NIF/CIF/VAT) / NIF o CIF"),
71
305
  address: addressSchema,
72
306
  },
73
307
  outputSchema: vendorItemOutput,
74
308
  }, async (input) => withToolLogging("create_vendor", async () => {
75
- const result = await client.createVendor(input);
309
+ const result = normalizeVendorRecord(await client.createVendor(input));
76
310
  const hints = enrichResponse("vendors", "create", result);
77
311
  return {
78
- content: [mutateContent(formatRecord("Vendor created", result))],
79
- structuredContent: { ...result, ...hints },
312
+ content: [mutateContent(formatRecord("Vendor created", result) + hints)],
313
+ structuredContent: { ...result },
80
314
  };
81
315
  }));
82
316
  // -- update_vendor --
@@ -87,16 +321,16 @@ export function registerVendorTools(server, client) {
87
321
  "/ Actualiza un proveedor existente. Solo se modifican los campos proporcionados.",
88
322
  annotations: UPDATE_ANNOTATIONS,
89
323
  inputSchema: {
90
- id: z.string().describe("Vendor ID / ID del proveedor"),
91
- name: z.string().optional().describe("Name / Nombre"),
92
- email: z.string().optional().describe("Email / Correo"),
93
- phone: z.string().optional().describe("Phone / Telefono"),
324
+ id: z.string().trim().min(1).max(128).describe("Vendor ID / ID del proveedor"),
325
+ name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
326
+ email: z.string().trim().email().max(255).optional().describe("Email / Correo"),
327
+ phone: z.string().trim().min(1).max(50).optional().describe("Phone / Telefono"),
94
328
  taxId: z.string().optional().describe("Tax ID / NIF/CIF"),
95
329
  address: addressSchema,
96
330
  },
97
331
  outputSchema: vendorItemOutput,
98
332
  }, async ({ id, ...data }) => withToolLogging("update_vendor", async () => {
99
- const result = await client.updateVendor(id, data);
333
+ const result = normalizeVendorRecord(await client.updateVendor(id, data));
100
334
  return {
101
335
  content: [mutateContent(formatRecord("Vendor updated", result))],
102
336
  structuredContent: result,
@@ -2,7 +2,7 @@
2
2
  * Webhook management tools for the Frihet MCP server.
3
3
  */
4
4
  import { z } from "zod/v4";
5
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, deleteResultOutput, webhookItemOutput, webhookTestResultOutput } from "./shared.js";
5
+ import { withToolLogging, formatRecord, formatUnpaginatedListResponse, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, deleteResultOutput, webhookCreateOutput, webhookItemOutput, webhookListOutput, webhookTestResultOutput } from "./shared.js";
6
6
  export function registerWebhookTools(server, client) {
7
7
  // -- list_webhooks --
8
8
  server.registerTool("list_webhooks", {
@@ -10,15 +10,12 @@ export function registerWebhookTools(server, client) {
10
10
  description: "List all configured webhooks. Webhooks send HTTP POST notifications when events occur in Frihet. " +
11
11
  "/ Lista todos los webhooks configurados. Los webhooks envian notificaciones HTTP POST cuando ocurren eventos en Frihet.",
12
12
  annotations: READ_ONLY_ANNOTATIONS,
13
- inputSchema: {
14
- limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
15
- offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
16
- },
17
- outputSchema: paginatedOutput(webhookItemOutput),
18
- }, async ({ limit, offset }) => withToolLogging("list_webhooks", async () => {
19
- const result = await client.listWebhooks({ limit, offset });
13
+ inputSchema: z.object({}).strict(),
14
+ outputSchema: webhookListOutput,
15
+ }, async () => withToolLogging("list_webhooks", async () => {
16
+ const result = await client.listWebhooks();
20
17
  return {
21
- content: [listContent(formatPaginatedResponse("webhooks", result))],
18
+ content: [listContent(formatUnpaginatedListResponse("webhooks", result))],
22
19
  structuredContent: result,
23
20
  };
24
21
  }));
@@ -28,9 +25,9 @@ export function registerWebhookTools(server, client) {
28
25
  description: "Get a single webhook configuration by its ID. " +
29
26
  "/ Obtiene la configuracion de un webhook por su ID.",
30
27
  annotations: READ_ONLY_ANNOTATIONS,
31
- inputSchema: {
28
+ inputSchema: z.object({
32
29
  id: z.string().describe("Webhook ID / ID del webhook"),
33
- },
30
+ }).strict(),
34
31
  outputSchema: webhookItemOutput,
35
32
  }, async ({ id }) => withToolLogging("get_webhook", async () => {
36
33
  const result = await client.getWebhook(id);
@@ -42,31 +39,37 @@ export function registerWebhookTools(server, client) {
42
39
  // -- create_webhook --
43
40
  server.registerTool("create_webhook", {
44
41
  title: "Create Webhook",
45
- description: "Register a new webhook endpoint. You must specify the URL to receive notifications " +
42
+ description: "Register a named webhook endpoint. You must specify the name, URL, and events to receive notifications " +
46
43
  "and which events to subscribe to. " +
47
- "Available events: invoice.created, invoice.updated, invoice.paid, invoice.deleted, " +
48
- "expense.created, expense.updated, expense.deleted, client.created, client.updated, " +
44
+ "Available events: invoice.created, invoice.updated, invoice.paid, " +
45
+ "expense.created, expense.updated, client.created, client.updated, " +
49
46
  "quote.created, quote.updated, quote.accepted. " +
50
- "Example: url='https://example.com/webhook', events=['invoice.created','invoice.paid'], secret='my-secret' " +
47
+ "Example: name='Invoice events', url='https://example.com/webhook', events=['invoice.created','invoice.paid'], secret='my-secret' " +
51
48
  "/ Registra un nuevo endpoint de webhook. Especifica la URL y los eventos a suscribir.",
52
49
  annotations: CREATE_ANNOTATIONS,
53
- inputSchema: {
54
- url: z.string().url().describe("Webhook endpoint URL / URL del endpoint del webhook"),
50
+ inputSchema: z.object({
51
+ name: z.string().max(200).describe("Webhook name / Nombre del webhook"),
52
+ url: z.string().url().max(2000).describe("Webhook endpoint URL / URL del endpoint del webhook"),
55
53
  events: z
56
- .array(z.string())
54
+ .array(z.string().max(100))
57
55
  .min(1)
58
56
  .describe("Events to subscribe to (e.g. ['invoice.created', 'invoice.paid']) " +
59
57
  "/ Eventos a suscribir"),
60
- active: z
61
- .boolean()
58
+ status: z
59
+ .enum(["active", "inactive"])
62
60
  .optional()
63
- .describe("Whether the webhook is active (default: true) / Si el webhook esta activo"),
61
+ .describe("Webhook status (default: active) / Estado del webhook"),
62
+ metadata: z
63
+ .record(z.string(), z.unknown())
64
+ .optional()
65
+ .describe("Arbitrary webhook metadata / Metadatos del webhook"),
64
66
  secret: z
65
67
  .string()
68
+ .max(500)
66
69
  .optional()
67
70
  .describe("Signing secret for payload verification / Secreto para verificar las notificaciones"),
68
- },
69
- outputSchema: webhookItemOutput,
71
+ }).strict(),
72
+ outputSchema: webhookCreateOutput,
70
73
  }, async (input) => withToolLogging("create_webhook", async () => {
71
74
  const result = await client.createWebhook(input);
72
75
  return {
@@ -78,16 +81,18 @@ export function registerWebhookTools(server, client) {
78
81
  server.registerTool("update_webhook", {
79
82
  title: "Update Webhook",
80
83
  description: "Update an existing webhook configuration using PATCH semantics. Only provided fields change. " +
81
- "Example: id='abc123', active=false to disable a webhook. " +
84
+ "Example: id='abc123', status='paused' to pause a webhook. " +
82
85
  "/ Actualiza la configuracion de un webhook. Solo se modifican los campos proporcionados.",
83
86
  annotations: UPDATE_ANNOTATIONS,
84
- inputSchema: {
87
+ inputSchema: z.object({
85
88
  id: z.string().describe("Webhook ID / ID del webhook"),
86
- url: z.string().url().optional().describe("Endpoint URL / URL"),
87
- events: z.array(z.string()).min(1).optional().describe("Events / Eventos"),
88
- active: z.boolean().optional().describe("Active / Activo"),
89
- secret: z.string().optional().describe("Signing secret / Secreto"),
90
- },
89
+ name: z.string().max(200).optional().describe("Webhook name / Nombre del webhook"),
90
+ url: z.string().url().max(2000).optional().describe("Endpoint URL / URL"),
91
+ events: z.array(z.string().max(100)).min(1).optional().describe("Events / Eventos"),
92
+ status: z.enum(["active", "inactive", "paused"]).optional().describe("Status / Estado"),
93
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Metadata / Metadatos"),
94
+ secret: z.string().max(500).optional().describe("Signing secret / Secreto"),
95
+ }).strict(),
91
96
  outputSchema: webhookItemOutput,
92
97
  }, async ({ id, ...data }) => withToolLogging("update_webhook", async () => {
93
98
  const result = await client.updateWebhook(id, data);
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface PaginatedResponse<T> {
11
11
  export interface ApiError {
12
12
  error: string;
13
13
  message?: string;
14
+ detail?: string;
14
15
  }
15
16
  export interface PaginationParams {
16
17
  limit?: number;
@@ -37,11 +38,13 @@ export interface Invoice {
37
38
  status?: string;
38
39
  notes?: string;
39
40
  taxRate?: number;
41
+ operationType?: "service" | "goods";
42
+ fiscalTreatment?: "not_subject_location" | "export_exempt";
40
43
  total?: number;
41
44
  createdAt?: string;
42
45
  updatedAt?: string;
43
46
  }
44
- export type CreateInvoiceInput = Pick<Invoice, "clientName" | "items"> & Partial<Pick<Invoice, "issueDate" | "dueDate" | "status" | "notes" | "taxRate">>;
47
+ export type CreateInvoiceInput = Pick<Invoice, "clientName" | "items"> & Partial<Pick<Invoice, "issueDate" | "dueDate" | "status" | "notes" | "taxRate" | "operationType" | "fiscalTreatment">>;
45
48
  export type UpdateInvoiceInput = Partial<CreateInvoiceInput>;
46
49
  export interface Expense {
47
50
  id: string;
@@ -99,13 +102,36 @@ export type CreateQuoteInput = Pick<Quote, "clientName" | "items"> & Partial<Pic
99
102
  export type UpdateQuoteInput = Partial<CreateQuoteInput>;
100
103
  export interface Webhook {
101
104
  id: string;
105
+ userId?: string;
106
+ name: string;
102
107
  url: string;
103
108
  events: string[];
104
- active?: boolean;
105
- secret?: string;
109
+ status: "active" | "inactive" | "paused";
110
+ metadata?: Record<string, unknown>;
111
+ hasSecret: boolean;
112
+ pausedReason?: string;
113
+ lastTriggeredAt?: string;
106
114
  createdAt?: string;
107
115
  updatedAt?: string;
108
116
  }
109
- export type CreateWebhookInput = Pick<Webhook, "url" | "events"> & Partial<Pick<Webhook, "active" | "secret">>;
110
- export type UpdateWebhookInput = Partial<CreateWebhookInput>;
117
+ export interface CreateWebhookInput {
118
+ name: string;
119
+ url: string;
120
+ events: string[];
121
+ status?: "active" | "inactive";
122
+ metadata?: Record<string, unknown>;
123
+ secret?: string;
124
+ }
125
+ export interface UpdateWebhookInput {
126
+ name?: string;
127
+ url?: string;
128
+ events?: string[];
129
+ status?: "active" | "inactive" | "paused";
130
+ metadata?: Record<string, unknown>;
131
+ secret?: string;
132
+ }
133
+ /** Create may echo only the caller-supplied signing secret, once. */
134
+ export type CreateWebhookResult = Webhook & {
135
+ secret?: string;
136
+ };
111
137
  //# sourceMappingURL=types.d.ts.map