@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.
- package/README.md +168 -82
- package/dist/agent-onboarding.d.ts +98 -0
- package/dist/agent-onboarding.js +275 -0
- package/dist/api-origin.d.ts +9 -0
- package/dist/api-origin.js +41 -0
- package/dist/canary/mcp-harness.d.ts +77 -0
- package/dist/canary/mcp-harness.js +552 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +262 -0
- package/dist/claude-candidate-contract.d.ts +482 -0
- package/dist/claude-candidate-contract.js +87 -0
- package/dist/claude-output-privacy.d.ts +17 -0
- package/dist/claude-output-privacy.js +240 -0
- package/dist/claude-profile.d.ts +18 -0
- package/dist/claude-profile.js +260 -0
- package/dist/client-interface.d.ts +47 -33
- package/dist/client.d.ts +172 -40
- package/dist/client.js +708 -165
- package/dist/demo-client.d.ts +41 -32
- package/dist/demo-client.js +202 -31
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/deposit-contract.d.ts +29 -0
- package/dist/deposit-contract.js +76 -0
- package/dist/fetch-no-redirect.d.ts +3 -0
- package/dist/fetch-no-redirect.js +16 -0
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/fiscal-period.d.ts +26 -0
- package/dist/fiscal-period.js +31 -0
- package/dist/google-profile.d.ts +19 -0
- package/dist/google-profile.js +122 -0
- package/dist/index.js +68 -59
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +194 -106
- package/dist/openai-profile.d.ts +26 -22
- package/dist/openai-profile.js +1193 -157
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +714 -41
- package/dist/openai-review-oauth.d.ts +149 -2
- package/dist/openai-review-oauth.js +220 -6
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +34 -78
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +15 -0
- package/dist/redaction.js +83 -0
- package/dist/resources/register-all.js +154 -64
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +19 -0
- package/dist/server-composition.js +74 -0
- package/dist/tool-exposure.d.ts +31 -27
- package/dist/tool-exposure.js +211 -87
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/audit_gl.js +8 -8
- package/dist/tools/backend-availability.d.ts +8 -29
- package/dist/tools/backend-availability.js +31 -30
- package/dist/tools/bank_rules.js +66 -15
- package/dist/tools/banking.js +5 -13
- package/dist/tools/clients.js +261 -15
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +114 -85
- package/dist/tools/einvoice.d.ts +16 -2
- package/dist/tools/einvoice.js +59 -12
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/fiscal.d.ts +3 -4
- package/dist/tools/fiscal.js +93 -60
- package/dist/tools/gestoria.js +15 -7
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +49 -20
- package/dist/tools/igic.d.ts +9 -8
- package/dist/tools/igic.js +34 -64
- package/dist/tools/impuesto_sociedades.d.ts +4 -5
- package/dist/tools/impuesto_sociedades.js +16 -37
- package/dist/tools/intelligence.js +166 -12
- package/dist/tools/invoices.js +224 -54
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/payroll.d.ts +5 -8
- package/dist/tools/payroll.js +19 -32
- package/dist/tools/permissions.d.ts +4 -7
- package/dist/tools/permissions.js +11 -14
- package/dist/tools/products.js +6 -6
- package/dist/tools/quotes.js +172 -26
- package/dist/tools/recurring.js +26 -25
- package/dist/tools/register-all.d.ts +2 -2
- package/dist/tools/register-all.js +9 -6
- package/dist/tools/search.d.ts +19 -0
- package/dist/tools/search.js +84 -0
- package/dist/tools/shared.d.ts +1445 -109
- package/dist/tools/shared.js +478 -102
- package/dist/tools/team.js +24 -28
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +249 -15
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +31 -5
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +391 -0
- package/package.json +49 -12
- package/dist/tools/onboarding.d.ts +0 -19
- package/dist/tools/onboarding.js +0 -64
package/dist/tools/invoices.js
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
* Invoice 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,
|
|
5
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, invoiceItemOutput, actionResultOutput, creditNoteResultOutput, pdfResultOutput, einvoiceResultOutput } from "./shared.js";
|
|
6
6
|
const invoiceItemSchema = 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
|
+
function documentResultMetadata(result) {
|
|
12
|
+
return {
|
|
13
|
+
id: result.id,
|
|
14
|
+
contentType: result.contentType,
|
|
15
|
+
sizeBytes: result.sizeBytes,
|
|
16
|
+
...(result.filename ? { filename: result.filename } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
11
19
|
// Optional client-identity fields the Frihet API accepts on invoice/quote
|
|
12
|
-
// create+update. When clientId is supplied the server
|
|
13
|
-
//
|
|
20
|
+
// create+update. When clientId is supplied the server snapshots the stored
|
|
21
|
+
// client record; the other fields can override document-specific details.
|
|
14
22
|
const clientIdentityFields = {
|
|
15
23
|
clientId: z
|
|
16
24
|
.string()
|
|
17
25
|
.optional()
|
|
18
|
-
.describe("Existing client ID —
|
|
26
|
+
.describe("Existing client ID — stored client details are used for the document / ID de cliente existente"),
|
|
19
27
|
clientTaxId: z
|
|
20
28
|
.string()
|
|
21
29
|
.optional()
|
|
@@ -73,6 +81,10 @@ const invoiceFiscalFields = {
|
|
|
73
81
|
.enum(["service", "goods"])
|
|
74
82
|
.optional()
|
|
75
83
|
.describe("Operation type (service or goods) / Tipo de operacion"),
|
|
84
|
+
fiscalTreatment: z
|
|
85
|
+
.enum(["not_subject_location", "export_exempt"])
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Explicit fiscal treatment declared by the user; never inferred / Tratamiento fiscal declarado expresamente por el usuario; nunca inferido"),
|
|
76
88
|
};
|
|
77
89
|
export function registerInvoiceTools(server, client) {
|
|
78
90
|
// -- list_invoices --
|
|
@@ -133,8 +145,8 @@ export function registerInvoiceTools(server, client) {
|
|
|
133
145
|
const result = await client.listInvoices({ limit, offset, after, fields, status, from, to, clientId, seriesId });
|
|
134
146
|
const hints = enrichResponse("invoices", "list", result.data);
|
|
135
147
|
return {
|
|
136
|
-
content: [listContent(formatPaginatedResponse("invoices", result))],
|
|
137
|
-
structuredContent: { ...result
|
|
148
|
+
content: [listContent(formatPaginatedResponse("invoices", result) + hints)],
|
|
149
|
+
structuredContent: { ...result },
|
|
138
150
|
};
|
|
139
151
|
}));
|
|
140
152
|
// -- get_invoice --
|
|
@@ -165,18 +177,18 @@ export function registerInvoiceTools(server, client) {
|
|
|
165
177
|
"Soporta retencion IRPF (autonomos ES), recargo de equivalencia, serie, anticipo y descuento global.",
|
|
166
178
|
annotations: CREATE_ANNOTATIONS,
|
|
167
179
|
inputSchema: {
|
|
168
|
-
clientName: z.string().describe("Client/customer name / Nombre del cliente"),
|
|
180
|
+
clientName: z.string().trim().min(1).max(200).describe("Client/customer name / Nombre del cliente"),
|
|
169
181
|
...clientIdentityFields,
|
|
170
182
|
items: z
|
|
171
183
|
.array(invoiceItemSchema)
|
|
172
184
|
.min(1)
|
|
173
185
|
.describe("Line items (each with description, quantity, unitPrice) / Conceptos de la factura"),
|
|
174
186
|
issueDate: z
|
|
175
|
-
.
|
|
187
|
+
.iso.date()
|
|
176
188
|
.optional()
|
|
177
189
|
.describe("Issue date in ISO 8601 format (YYYY-MM-DD), defaults to today / Fecha de emision"),
|
|
178
190
|
dueDate: z
|
|
179
|
-
.
|
|
191
|
+
.iso.date()
|
|
180
192
|
.optional()
|
|
181
193
|
.describe("Due date in ISO 8601 format (YYYY-MM-DD) / Fecha de vencimiento"),
|
|
182
194
|
status: z
|
|
@@ -185,6 +197,7 @@ export function registerInvoiceTools(server, client) {
|
|
|
185
197
|
.describe("Invoice status (default: draft) / Estado de la factura"),
|
|
186
198
|
notes: z
|
|
187
199
|
.string()
|
|
200
|
+
.max(10_000)
|
|
188
201
|
.optional()
|
|
189
202
|
.describe("Additional notes shown on the invoice / Notas adicionales"),
|
|
190
203
|
taxRate: z
|
|
@@ -197,11 +210,16 @@ export function registerInvoiceTools(server, client) {
|
|
|
197
210
|
},
|
|
198
211
|
outputSchema: invoiceItemOutput,
|
|
199
212
|
}, async (input) => withToolLogging("create_invoice", async () => {
|
|
200
|
-
const
|
|
213
|
+
const status = input.status ?? "draft";
|
|
214
|
+
const payload = {
|
|
215
|
+
...input,
|
|
216
|
+
status,
|
|
217
|
+
};
|
|
218
|
+
const result = await client.createInvoice(payload);
|
|
201
219
|
const hints = enrichResponse("invoices", "create", result);
|
|
202
220
|
return {
|
|
203
|
-
content: [mutateContent(formatRecord("Invoice created", result))],
|
|
204
|
-
structuredContent: { ...result
|
|
221
|
+
content: [mutateContent(formatRecord("Invoice created", result) + hints)],
|
|
222
|
+
structuredContent: { ...result },
|
|
205
223
|
};
|
|
206
224
|
}));
|
|
207
225
|
// -- update_invoice --
|
|
@@ -235,26 +253,65 @@ export function registerInvoiceTools(server, client) {
|
|
|
235
253
|
const result = await client.updateInvoice(id, data);
|
|
236
254
|
const hints = enrichResponse("invoices", "update", result);
|
|
237
255
|
return {
|
|
238
|
-
content: [mutateContent(formatRecord("Invoice updated", result))],
|
|
239
|
-
structuredContent: { ...result
|
|
256
|
+
content: [mutateContent(formatRecord("Invoice updated", result) + hints)],
|
|
257
|
+
structuredContent: { ...result },
|
|
240
258
|
};
|
|
241
259
|
}));
|
|
242
260
|
// -- delete_invoice --
|
|
243
261
|
server.registerTool("delete_invoice", {
|
|
244
262
|
title: "Delete Invoice",
|
|
245
|
-
description: "
|
|
246
|
-
"
|
|
263
|
+
description: "Delete an invoice by its ID. Requires confirm=true. " +
|
|
264
|
+
"Only a DRAFT invoice is removed permanently. A sent/paid/overdue invoice is NOT destroyed: " +
|
|
265
|
+
"the backend CANCELS it (status=cancelled) so the VeriFactu hash chain stays intact, and the " +
|
|
266
|
+
"document remains readable via get_invoice. The result reports which happened in `outcome` " +
|
|
267
|
+
"(\"deleted\" or \"cancelled\"). " +
|
|
268
|
+
"/ Elimina una factura por su ID. Requiere confirm=true. Solo una factura en BORRADOR se elimina " +
|
|
269
|
+
"de forma permanente. Una factura emitida/pagada NO se destruye: se CANCELA (status=cancelled) para " +
|
|
270
|
+
"preservar la cadena de hash VeriFactu y sigue consultable con get_invoice.",
|
|
247
271
|
annotations: DELETE_ANNOTATIONS,
|
|
248
272
|
inputSchema: {
|
|
249
273
|
id: z.string().describe("Invoice ID / ID de la factura"),
|
|
274
|
+
confirm: z
|
|
275
|
+
.boolean()
|
|
276
|
+
.describe("Must be true to confirm deletion / Debe ser true para confirmar la eliminacion"),
|
|
250
277
|
},
|
|
251
|
-
outputSchema:
|
|
252
|
-
}, async ({ id }) => withToolLogging("delete_invoice", async () => {
|
|
253
|
-
|
|
278
|
+
outputSchema: documentDeleteResultOutput,
|
|
279
|
+
}, async ({ id, confirm }) => withToolLogging("delete_invoice", async () => {
|
|
280
|
+
if (!confirm) {
|
|
281
|
+
return confirmationRequiredError("Error: confirm=true is required to delete an invoice. " +
|
|
282
|
+
"A draft invoice is removed permanently; a sent/paid invoice is CANCELLED " +
|
|
283
|
+
"(status=cancelled) instead, because VeriFactu forbids breaking the hash chain. " +
|
|
284
|
+
"Either way the invoice stops counting as a live receivable. Set confirm=true to proceed. / " +
|
|
285
|
+
"Se requiere confirm=true para eliminar una factura.");
|
|
286
|
+
}
|
|
287
|
+
// 204 → undefined (draft row destroyed). 200 → soft-cancel body (document
|
|
288
|
+
// kept, status=cancelled). Reporting both as "deleted" is the GAP-12 lie.
|
|
289
|
+
const outcome = await client.deleteInvoice(id);
|
|
290
|
+
const cancelled = !!outcome && typeof outcome === "object";
|
|
291
|
+
const body = cancelled ? outcome : {};
|
|
254
292
|
const hints = enrichResponse("invoices", "delete", { id });
|
|
293
|
+
const previous = typeof body["previousStatus"] === "string" ? ` (was ${body["previousStatus"]})` : "";
|
|
294
|
+
// R3 fix (ERP #1580): extract the per-branch body text to a single
|
|
295
|
+
// variable and append `+ hints` ONCE, after the ternary closes. R2's
|
|
296
|
+
// `+ hints` was branch-local (only in the DELETED branch), so the
|
|
297
|
+
// CANCELLED branch silently dropped the "cannot be undone" warning —
|
|
298
|
+
// 15/16 behavioral paths had warnings, not 16/16. The OUTER concat
|
|
299
|
+
// guarantees both terminal branches carry the enrichment. The
|
|
300
|
+
// `paginated-strict-output.test.ts` runtime tests pin BOTH branches.
|
|
301
|
+
const bodyText = cancelled
|
|
302
|
+
? `Invoice ${id} was CANCELLED, not deleted${previous}: it still exists with ` +
|
|
303
|
+
"status=cancelled because VeriFactu forbids destroying an issued invoice. / " +
|
|
304
|
+
`Factura ${id} CANCELADA, no eliminada: sigue existiendo con status=cancelled (VeriFactu).`
|
|
305
|
+
: `Invoice ${id} deleted permanently (it was a draft). / ` +
|
|
306
|
+
`Factura ${id} eliminada permanentemente (era un borrador).`;
|
|
255
307
|
return {
|
|
256
|
-
content: [mutateContent(
|
|
257
|
-
structuredContent: {
|
|
308
|
+
content: [mutateContent(bodyText + hints)],
|
|
309
|
+
structuredContent: {
|
|
310
|
+
success: true,
|
|
311
|
+
id,
|
|
312
|
+
...body,
|
|
313
|
+
outcome: cancelled ? "cancelled" : "deleted",
|
|
314
|
+
},
|
|
258
315
|
};
|
|
259
316
|
}));
|
|
260
317
|
// -- search_invoices --
|
|
@@ -299,27 +356,135 @@ export function registerInvoiceTools(server, client) {
|
|
|
299
356
|
const label = query ? `invoices matching "${query}"` : "invoices";
|
|
300
357
|
const hints = enrichResponse("invoices", "list", result.data);
|
|
301
358
|
return {
|
|
302
|
-
content: [listContent(formatPaginatedResponse(label, result))],
|
|
303
|
-
structuredContent: { ...result
|
|
359
|
+
content: [listContent(formatPaginatedResponse(label, result) + hints)],
|
|
360
|
+
structuredContent: { ...result },
|
|
304
361
|
};
|
|
305
362
|
}));
|
|
306
363
|
// -- send_invoice --
|
|
307
364
|
server.registerTool("send_invoice", {
|
|
308
365
|
title: "Send Invoice",
|
|
309
|
-
description: "Send an invoice to the client via email.
|
|
366
|
+
description: "Send an invoice to the client via email. Requires confirm=true. " +
|
|
367
|
+
"Optionally override the recipient email address. " +
|
|
310
368
|
"The invoice must exist and should not already be cancelled. " +
|
|
311
|
-
"
|
|
369
|
+
"Sending reaches a third party and cannot be recalled. " +
|
|
370
|
+
"/ Envia una factura al cliente por email. Requiere confirm=true. " +
|
|
371
|
+
"Opcionalmente se puede cambiar el email destinatario. El envio llega a un tercero y no se puede anular.",
|
|
312
372
|
annotations: UPDATE_ANNOTATIONS,
|
|
313
373
|
inputSchema: {
|
|
314
374
|
id: z.string().describe("Invoice ID / ID de la factura"),
|
|
315
375
|
to: z.string().optional().describe("Override recipient email / Email destinatario alternativo"),
|
|
376
|
+
confirm: z
|
|
377
|
+
.boolean()
|
|
378
|
+
.describe("Must be true to confirm sending / Debe ser true para confirmar el envio"),
|
|
316
379
|
},
|
|
317
380
|
outputSchema: actionResultOutput,
|
|
318
|
-
}, async ({ id, to }) => withToolLogging("send_invoice", async () => {
|
|
319
|
-
|
|
381
|
+
}, async ({ id, to, confirm }) => withToolLogging("send_invoice", async () => {
|
|
382
|
+
if (!confirm) {
|
|
383
|
+
return confirmationRequiredError("Error: confirm=true is required to send an invoice. " +
|
|
384
|
+
"This delivers an email to the client — a third party outside this workspace — " +
|
|
385
|
+
"and it cannot be recalled once sent. Set confirm=true to proceed. / " +
|
|
386
|
+
"Se requiere confirm=true para enviar una factura por email a un tercero.");
|
|
387
|
+
}
|
|
388
|
+
const invoice = await client.getInvoice(id);
|
|
389
|
+
if (invoice["status"] === "cancelled") {
|
|
390
|
+
return {
|
|
391
|
+
content: [{ type: "text", text: "Error: a cancelled invoice cannot be sent." }],
|
|
392
|
+
isError: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const isValidRecipientEmail = (email) => email.length <= 255 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
396
|
+
let recipientEmail;
|
|
397
|
+
if (to !== undefined) {
|
|
398
|
+
const trimmedTo = to.trim();
|
|
399
|
+
if (!trimmedTo || !isValidRecipientEmail(trimmedTo)) {
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
|
|
402
|
+
isError: true,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
recipientEmail = trimmedTo;
|
|
406
|
+
}
|
|
407
|
+
if (!recipientEmail) {
|
|
408
|
+
const clientId = typeof invoice["clientId"] === "string" && invoice["clientId"].trim()
|
|
409
|
+
? invoice["clientId"].trim()
|
|
410
|
+
: undefined;
|
|
411
|
+
if (clientId) {
|
|
412
|
+
let linkedClient;
|
|
413
|
+
try {
|
|
414
|
+
linkedClient = await client.getClient(clientId);
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
const isNotFound = typeof err === "object" &&
|
|
418
|
+
err !== null &&
|
|
419
|
+
"statusCode" in err &&
|
|
420
|
+
err.statusCode === 404;
|
|
421
|
+
if (!isNotFound) {
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const clientEmail = linkedClient && typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
|
|
426
|
+
? linkedClient["email"].trim()
|
|
427
|
+
: undefined;
|
|
428
|
+
if (clientEmail) {
|
|
429
|
+
if (!isValidRecipientEmail(clientEmail)) {
|
|
430
|
+
return {
|
|
431
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${clientEmail}".` }],
|
|
432
|
+
isError: true,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
recipientEmail = clientEmail;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!recipientEmail) {
|
|
439
|
+
const docFallbackEmail = typeof invoice["clientEmail"] === "string" && invoice["clientEmail"].trim()
|
|
440
|
+
? invoice["clientEmail"].trim()
|
|
441
|
+
: typeof invoice["email"] === "string" && invoice["email"].trim()
|
|
442
|
+
? invoice["email"].trim()
|
|
443
|
+
: undefined;
|
|
444
|
+
if (docFallbackEmail) {
|
|
445
|
+
if (!isValidRecipientEmail(docFallbackEmail)) {
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${docFallbackEmail}".` }],
|
|
448
|
+
isError: true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const fallbackMsg = `Error: the linked client has no current email address; only a document-level fallback email ("${docFallbackEmail}") was found on the invoice, which may be outdated. ` +
|
|
452
|
+
`Confirm this specific recipient with the user and re-run with to="${docFallbackEmail}" and confirm=true to send. / ` +
|
|
453
|
+
`El cliente vinculado no tiene email actual; la factura conserva el correo documental "${docFallbackEmail}", que podria estar obsoleto. Confirma este destinatario concreto y vuelve a llamar con to="${docFallbackEmail}" y confirm=true.`;
|
|
454
|
+
return {
|
|
455
|
+
content: [{ type: "text", text: fallbackMsg }],
|
|
456
|
+
structuredContent: {
|
|
457
|
+
success: false,
|
|
458
|
+
id,
|
|
459
|
+
message: fallbackMsg,
|
|
460
|
+
effectiveRecipient: docFallbackEmail,
|
|
461
|
+
recipientSource: "document_fallback",
|
|
462
|
+
requiresExplicitRecipientConfirmation: true,
|
|
463
|
+
},
|
|
464
|
+
isError: true,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
content: [
|
|
469
|
+
{
|
|
470
|
+
type: "text",
|
|
471
|
+
text: clientId
|
|
472
|
+
? "Error: the linked client has no saved email address."
|
|
473
|
+
: "Error: this invoice has no linked client with a saved email address.",
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
isError: true,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const result = await client.sendInvoice(id, recipientEmail);
|
|
481
|
+
const enrichedResult = {
|
|
482
|
+
...(result && typeof result === "object" ? result : {}),
|
|
483
|
+
effectiveRecipient: recipientEmail,
|
|
484
|
+
};
|
|
320
485
|
return {
|
|
321
|
-
content: [mutateContent(formatRecord("Invoice sent",
|
|
322
|
-
structuredContent:
|
|
486
|
+
content: [mutateContent(formatRecord("Invoice sent", enrichedResult))],
|
|
487
|
+
structuredContent: enrichedResult,
|
|
323
488
|
};
|
|
324
489
|
}));
|
|
325
490
|
// -- mark_invoice_paid --
|
|
@@ -344,8 +509,10 @@ export function registerInvoiceTools(server, client) {
|
|
|
344
509
|
// -- get_invoice_pdf --
|
|
345
510
|
server.registerTool("get_invoice_pdf", {
|
|
346
511
|
title: "Get Invoice PDF",
|
|
347
|
-
description: "Get the PDF for an invoice.
|
|
348
|
-
"
|
|
512
|
+
description: "Get the PDF for an invoice. The body is served as raw `application/pdf` bytes, base64-encoded in `base64` " +
|
|
513
|
+
"with `sizeBytes` and `contentType`. PDFs larger than 25 MiB are rejected with `413 payload_too_large`. " +
|
|
514
|
+
"/ Obtiene el PDF de una factura. El cuerpo se sirve como bytes `application/pdf` en bruto, codificados en base64 " +
|
|
515
|
+
"en `base64`, con `sizeBytes` y `contentType`. PDFs mayores de 25 MiB se rechazan con `413 payload_too_large`.",
|
|
349
516
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
350
517
|
inputSchema: {
|
|
351
518
|
id: z.string().describe("Invoice ID / ID de la factura"),
|
|
@@ -354,33 +521,31 @@ export function registerInvoiceTools(server, client) {
|
|
|
354
521
|
}, async ({ id }) => withToolLogging("get_invoice_pdf", async () => {
|
|
355
522
|
const result = await client.getInvoicePdf(id);
|
|
356
523
|
return {
|
|
357
|
-
content: [getContent(formatRecord("Invoice PDF", result))],
|
|
524
|
+
content: [getContent(formatRecord("Invoice PDF", documentResultMetadata(result)))],
|
|
358
525
|
structuredContent: result,
|
|
359
526
|
};
|
|
360
527
|
}));
|
|
361
528
|
// -- get_invoice_einvoice --
|
|
362
529
|
server.registerTool("get_invoice_einvoice", {
|
|
363
|
-
title: "Get Invoice E-Invoice
|
|
364
|
-
description: "Download the e-invoice
|
|
365
|
-
"
|
|
530
|
+
title: "Get Invoice E-Invoice Artifact",
|
|
531
|
+
description: "Download the stored e-invoice artifact for an invoice. XML formats return strict UTF-8 text in `xml`; " +
|
|
532
|
+
"Factur-X returns `application/pdf` bytes encoded in `base64`. Both include `id`, `contentType`, and `sizeBytes`. " +
|
|
533
|
+
"XML larger than 5 MiB or PDF larger than 25 MiB is rejected with `413 payload_too_large`. " +
|
|
366
534
|
"Only available after the invoice has been saved/sent. " +
|
|
367
|
-
"/ Descarga el
|
|
368
|
-
"
|
|
535
|
+
"/ Descarga el artefacto de factura electronica guardado. Los formatos XML devuelven texto UTF-8 estricto en `xml`; " +
|
|
536
|
+
"Factur-X devuelve bytes `application/pdf` codificados en `base64`. Ambos incluyen `id`, `contentType` y `sizeBytes`. " +
|
|
537
|
+
"XML mayor de 5 MiB o PDF mayor de 25 MiB se rechaza con `413 payload_too_large`. " +
|
|
369
538
|
"Solo disponible despues de guardar o enviar la factura.",
|
|
370
539
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
371
540
|
inputSchema: {
|
|
372
541
|
id: z.string().describe("Invoice ID / ID de la factura"),
|
|
373
542
|
},
|
|
374
|
-
outputSchema:
|
|
375
|
-
xml: z.string().optional(),
|
|
376
|
-
filename: z.string().optional(),
|
|
377
|
-
format: z.string().optional(),
|
|
378
|
-
}).passthrough(),
|
|
543
|
+
outputSchema: einvoiceResultOutput,
|
|
379
544
|
}, async ({ id }) => withToolLogging("get_invoice_einvoice", async () => {
|
|
380
545
|
const result = await client.getInvoiceEInvoice(id);
|
|
381
546
|
return {
|
|
382
|
-
content: [
|
|
383
|
-
structuredContent:
|
|
547
|
+
content: [getContent(formatRecord("E-invoice artifact", documentResultMetadata(result)))],
|
|
548
|
+
structuredContent: result,
|
|
384
549
|
};
|
|
385
550
|
}));
|
|
386
551
|
// -- create_credit_note --
|
|
@@ -439,17 +604,22 @@ export function registerInvoiceTools(server, client) {
|
|
|
439
604
|
}, idempotencyKey);
|
|
440
605
|
const hints = enrichResponse("invoices", "create", result);
|
|
441
606
|
return {
|
|
442
|
-
content: [mutateContent(formatRecord("Credit note created", result))],
|
|
443
|
-
structuredContent: { ...result
|
|
607
|
+
content: [mutateContent(formatRecord("Credit note created", result) + hints)],
|
|
608
|
+
structuredContent: { ...result },
|
|
444
609
|
};
|
|
445
610
|
}));
|
|
446
611
|
// -- apply_late_fee --
|
|
447
612
|
server.registerTool("apply_late_fee", {
|
|
448
613
|
title: "Apply Late Fee",
|
|
449
|
-
description: "Apply late payment interest to an overdue invoice.
|
|
450
|
-
"
|
|
451
|
-
"
|
|
452
|
-
"
|
|
614
|
+
description: "Apply late payment interest flags to an overdue invoice. " +
|
|
615
|
+
"Calculates interest based on EU Late Payment Directive (8% default) or auto-calculates " +
|
|
616
|
+
"from days overdue, and flips the `hasLateFee` / `lateFeeAmount` / `lateFeeDaysOverdue` " +
|
|
617
|
+
"fields on the invoice. The endpoint does NOT create a debit note — if a separate " +
|
|
618
|
+
"rectifying invoice is required, issue it explicitly with create_invoice or create_credit_note. " +
|
|
619
|
+
"/ Aplica los indicadores de intereses de demora a una factura vencida. Calcula intereses " +
|
|
620
|
+
"segun la Directiva Europea de Morosidad (8% por defecto) o a partir de los dias de retraso, " +
|
|
621
|
+
"y actualiza los flags `hasLateFee` / `lateFeeAmount` / `lateFeeDaysOverdue`. NO crea una " +
|
|
622
|
+
"nota de debito — si hace falta una factura rectificativa aparte, emitela con create_invoice o create_credit_note.",
|
|
453
623
|
annotations: CREATE_ANNOTATIONS,
|
|
454
624
|
inputSchema: {
|
|
455
625
|
invoiceId: z.string().describe("ID of the overdue invoice / ID de la factura vencida"),
|
package/dist/tools/kitchen.js
CHANGED
|
@@ -177,21 +177,32 @@ export function registerKitchenTools(server, client) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
const agg = new Map();
|
|
180
|
+
let totalOpenTickets = 0;
|
|
180
181
|
for (const t of ticketsResult.data) {
|
|
181
182
|
const ticket = t;
|
|
182
183
|
const status = typeof ticket["status"] === "string" ? ticket["status"] : "";
|
|
183
|
-
if (status === "served" || status === "cancelled")
|
|
184
|
+
if (status === "served" || status === "cancelled" || status === "voided")
|
|
184
185
|
continue;
|
|
185
|
-
|
|
186
|
+
totalOpenTickets += 1;
|
|
187
|
+
// REST tickets can route to multiple stations; retain legacy single-station input.
|
|
188
|
+
const stationIds = Array.isArray(ticket["stationIds"])
|
|
189
|
+
? [...new Set(ticket["stationIds"].filter((id) => typeof id === "string" && id.length > 0))]
|
|
190
|
+
: typeof ticket["stationId"] === "string" ? [ticket["stationId"]] : [];
|
|
191
|
+
if (stationIds.length === 0)
|
|
192
|
+
stationIds.push("__unassigned__");
|
|
186
193
|
const createdAt = typeof ticket["createdAt"] === "string"
|
|
187
194
|
? new Date(ticket["createdAt"]).getTime()
|
|
188
195
|
: null;
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
existing.oldestCreatedAt
|
|
196
|
+
for (const stationId of stationIds) {
|
|
197
|
+
const existing = agg.get(stationId) ?? { openTickets: 0, oldestCreatedAt: null };
|
|
198
|
+
existing.openTickets += 1;
|
|
199
|
+
if (createdAt !== null && (existing.oldestCreatedAt === null || createdAt < existing.oldestCreatedAt)) {
|
|
200
|
+
existing.oldestCreatedAt = createdAt;
|
|
201
|
+
}
|
|
202
|
+
agg.set(stationId, existing);
|
|
203
|
+
if (stationId !== "__unassigned__" && !stationMap.has(stationId))
|
|
204
|
+
stationMap.set(stationId, stationId);
|
|
193
205
|
}
|
|
194
|
-
agg.set(stationId, existing);
|
|
195
206
|
}
|
|
196
207
|
// Build station summaries, also include stations with zero open tickets
|
|
197
208
|
let maxLoad = 0;
|
|
@@ -240,10 +251,9 @@ export function registerKitchenTools(server, client) {
|
|
|
240
251
|
}
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
|
-
const totalOpenTickets = summaries.reduce((acc, s) => acc + s.openTickets, 0);
|
|
244
254
|
const result = {
|
|
245
255
|
stations: summaries,
|
|
246
|
-
|
|
256
|
+
...(maxLoad > 0 ? { bottleneckStationId: bottleneckId } : {}),
|
|
247
257
|
totalOpenTickets,
|
|
248
258
|
generatedAt: new Date().toISOString(),
|
|
249
259
|
};
|
package/dist/tools/payroll.d.ts
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
* Payroll preparation tools for the Frihet MCP server — D4-B megasprint (2 tools).
|
|
3
3
|
*
|
|
4
4
|
* Tools:
|
|
5
|
-
* 1. payroll_export —
|
|
6
|
-
* 2. payroll_checklist —
|
|
5
|
+
* 1. payroll_export — read the normalized export-ready employee dataset
|
|
6
|
+
* 2. payroll_checklist — read payroll-profile readiness for payable employees
|
|
7
7
|
*
|
|
8
8
|
* REST surface: /v1/payroll/prep/export, /v1/payroll/prep/employees
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
|
|
15
|
-
* until backend ships. TODO: confirm A3 columns + SILTRA file extension.
|
|
10
|
+
* The ERP validates and echoes a destination-format label but returns the same
|
|
11
|
+
* normalized JSON dataset for each value. It does not generate a format-specific
|
|
12
|
+
* file, and Frihet does not process payroll. These read routes are live.
|
|
16
13
|
*/
|
|
17
14
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
15
|
import type { IFrihetClient } from "../client-interface.js";
|
package/dist/tools/payroll.js
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
* Payroll preparation tools for the Frihet MCP server — D4-B megasprint (2 tools).
|
|
3
3
|
*
|
|
4
4
|
* Tools:
|
|
5
|
-
* 1. payroll_export —
|
|
6
|
-
* 2. payroll_checklist —
|
|
5
|
+
* 1. payroll_export — read the normalized export-ready employee dataset
|
|
6
|
+
* 2. payroll_checklist — read payroll-profile readiness for payable employees
|
|
7
7
|
*
|
|
8
8
|
* REST surface: /v1/payroll/prep/export, /v1/payroll/prep/employees
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
|
|
15
|
-
* until backend ships. TODO: confirm A3 columns + SILTRA file extension.
|
|
10
|
+
* The ERP validates and echoes a destination-format label but returns the same
|
|
11
|
+
* normalized JSON dataset for each value. It does not generate a format-specific
|
|
12
|
+
* file, and Frihet does not process payroll. These read routes are live.
|
|
16
13
|
*/
|
|
17
14
|
import { z } from "zod/v4";
|
|
18
15
|
import { withToolLogging, formatRecord, getContent, READ_ONLY_ANNOTATIONS, payrollExportOutput, payrollChecklistOutput, } from "./shared.js";
|
|
@@ -20,24 +17,18 @@ import { withBackendGuard } from "./backend-availability.js";
|
|
|
20
17
|
export function registerPayrollTools(server, client) {
|
|
21
18
|
// -- payroll_export --
|
|
22
19
|
server.registerTool("payroll_export", {
|
|
23
|
-
title: "
|
|
24
|
-
description: "
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
" - 'holded' — Holded import format\n" +
|
|
30
|
-
" - 'siltra' — SILTRA Seguridad Social XML\n" +
|
|
31
|
-
"\n" +
|
|
32
|
-
"Frihet does NOT calculate payroll — it exports staged data for the gestoria. " +
|
|
33
|
-
"Month format: 'YYYY-MM'. " +
|
|
34
|
-
"/ Exporta datos de nominas en formato compatible con gestoria. Mes en formato 'YYYY-MM'.",
|
|
20
|
+
title: "Payroll Export-Ready Data",
|
|
21
|
+
description: "Read the normalized payroll-ready employee dataset for a month. " +
|
|
22
|
+
"The ERP accepts 'a3', 'contasol', 'sage', or 'siltra' as a destination label and echoes it; " +
|
|
23
|
+
"the response is JSON and is identical across formats apart from that label. " +
|
|
24
|
+
"No CSV, XML, PDF, or provider-specific file is generated. Frihet does not calculate payroll. " +
|
|
25
|
+
"/ Lee los datos normalizados listos para nomina; no genera un archivo especifico del proveedor.",
|
|
35
26
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
36
27
|
inputSchema: {
|
|
37
28
|
format: z
|
|
38
|
-
.enum(["a3", "contasol", "sage", "
|
|
39
|
-
.describe("
|
|
40
|
-
month: z.string().regex(/^\d{4}
|
|
29
|
+
.enum(["a3", "contasol", "sage", "siltra"])
|
|
30
|
+
.describe("Echoed destination label; does not change JSON serialization / Etiqueta de destino"),
|
|
31
|
+
month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).describe("Month in 'YYYY-MM' format / Mes formato 'YYYY-MM'"),
|
|
41
32
|
},
|
|
42
33
|
outputSchema: payrollExportOutput,
|
|
43
34
|
}, async ({ format, month }) => withToolLogging("payroll_export", () => withBackendGuard("payroll_export", "/v1/payroll/prep/export", async () => {
|
|
@@ -50,17 +41,13 @@ export function registerPayrollTools(server, client) {
|
|
|
50
41
|
// -- payroll_checklist --
|
|
51
42
|
server.registerTool("payroll_checklist", {
|
|
52
43
|
title: "Payroll Readiness Checklist",
|
|
53
|
-
description: "List
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
" - 'blocked' — manually blocked or data inconsistency detected\n" +
|
|
58
|
-
"\n" +
|
|
59
|
-
"Use BEFORE payroll_export to identify gaps. Month format: 'YYYY-MM'. " +
|
|
60
|
-
"/ Lista empleados con estado de preparacion para nomina. Usar antes de payroll_export.",
|
|
44
|
+
description: "List payable employees (active or on leave) for a month, whether each has a payroll profile, " +
|
|
45
|
+
"whether its required fields are ready, which fields are missing, and monthly review state. " +
|
|
46
|
+
"Suspended and offboarded employees are excluded. Use before payroll_export. " +
|
|
47
|
+
"/ Lista empleados pagables y la preparacion real de su perfil de nomina.",
|
|
61
48
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
62
49
|
inputSchema: {
|
|
63
|
-
month: z.string().regex(/^\d{4}
|
|
50
|
+
month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).describe("Month in 'YYYY-MM' format / Mes formato 'YYYY-MM'"),
|
|
64
51
|
},
|
|
65
52
|
outputSchema: payrollChecklistOutput,
|
|
66
53
|
}, async ({ month }) => withToolLogging("payroll_checklist", () => withBackendGuard("payroll_checklist", "/v1/payroll/prep/employees", async () => {
|
|
@@ -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 —
|
|
6
|
-
* 2. permissions_me —
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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 type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
14
|
import type { IFrihetClient } from "../client-interface.js";
|