@frihet/mcp-server 1.17.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -348
- package/dist/agent-onboarding.d.ts +1 -1
- package/dist/agent-onboarding.js +3 -3
- package/dist/canary/mcp-harness.d.ts +1 -1
- package/dist/canary/mcp-harness.js +4 -1
- package/dist/capability-truth.js +20 -12
- 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 +249 -0
- package/dist/claude-profile.d.ts +18 -0
- package/dist/claude-profile.js +260 -0
- package/dist/client-interface.d.ts +26 -20
- package/dist/client.d.ts +65 -29
- package/dist/client.js +278 -167
- package/dist/demo-client.d.ts +28 -21
- package/dist/demo-client.js +32 -14
- 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 +2 -2
- package/dist/fiscal-aliases.js +2 -2
- 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 +31 -7
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +2 -2
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.js +3 -2
- package/dist/openai-profile.d.ts +2 -2
- package/dist/openai-profile.js +37 -11
- package/dist/openai-review-contract.d.ts +1 -1
- package/dist/openai-review-contract.js +4 -5
- package/dist/openai-review-oauth.d.ts +107 -7
- package/dist/openai-review-oauth.js +161 -17
- package/dist/prompts/register-all.js +32 -78
- package/dist/redaction.d.ts +5 -0
- package/dist/redaction.js +9 -0
- package/dist/resources/register-all.js +150 -54
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +6 -2
- package/dist/server-composition.js +46 -5
- package/dist/tool-exposure.d.ts +7 -5
- package/dist/tool-exposure.js +21 -13
- 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 +254 -6
- package/dist/tools/deposits.js +107 -97
- package/dist/tools/einvoice.d.ts +17 -3
- package/dist/tools/einvoice.js +59 -12
- 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.js +41 -11
- 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 +164 -10
- package/dist/tools/invoices.js +109 -42
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/quotes.js +86 -25
- 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 +18 -38
- package/dist/tools/shared.js +96 -30
- package/dist/tools/team.js +8 -16
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +242 -6
- package/dist/types.d.ts +3 -1
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +15 -5
- package/package.json +26 -12
- package/dist/tools/onboarding.d.ts +0 -19
- package/dist/tools/onboarding.js +0 -64
package/dist/tools/invoices.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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, documentDeleteResultOutput, invoiceItemOutput, actionResultOutput, creditNoteResultOutput, pdfResultOutput, einvoiceResultOutput } from "./shared.js";
|
|
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
7
|
description: z.string().trim().min(1).max(500).describe("Description of the line item / Descripcion del concepto"),
|
|
8
8
|
quantity: z.number().finite().min(0.0001).max(1_000_000).describe("Quantity / Cantidad"),
|
|
@@ -81,6 +81,10 @@ const invoiceFiscalFields = {
|
|
|
81
81
|
.enum(["service", "goods"])
|
|
82
82
|
.optional()
|
|
83
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"),
|
|
84
88
|
};
|
|
85
89
|
export function registerInvoiceTools(server, client) {
|
|
86
90
|
// -- list_invoices --
|
|
@@ -206,7 +210,12 @@ export function registerInvoiceTools(server, client) {
|
|
|
206
210
|
},
|
|
207
211
|
outputSchema: invoiceItemOutput,
|
|
208
212
|
}, async (input) => withToolLogging("create_invoice", async () => {
|
|
209
|
-
const
|
|
213
|
+
const status = input.status ?? "draft";
|
|
214
|
+
const payload = {
|
|
215
|
+
...input,
|
|
216
|
+
status,
|
|
217
|
+
};
|
|
218
|
+
const result = await client.createInvoice(payload);
|
|
210
219
|
const hints = enrichResponse("invoices", "create", result);
|
|
211
220
|
return {
|
|
212
221
|
content: [mutateContent(formatRecord("Invoice created", result) + hints)],
|
|
@@ -269,19 +278,11 @@ export function registerInvoiceTools(server, client) {
|
|
|
269
278
|
outputSchema: documentDeleteResultOutput,
|
|
270
279
|
}, async ({ id, confirm }) => withToolLogging("delete_invoice", async () => {
|
|
271
280
|
if (!confirm) {
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
"A draft invoice is removed permanently; a sent/paid invoice is CANCELLED " +
|
|
278
|
-
"(status=cancelled) instead, because VeriFactu forbids breaking the hash chain. " +
|
|
279
|
-
"Either way the invoice stops counting as a live receivable. Set confirm=true to proceed. / " +
|
|
280
|
-
"Se requiere confirm=true para eliminar una factura.",
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
isError: true,
|
|
284
|
-
};
|
|
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.");
|
|
285
286
|
}
|
|
286
287
|
// 204 → undefined (draft row destroyed). 200 → soft-cancel body (document
|
|
287
288
|
// kept, status=cancelled). Reporting both as "deleted" is the GAP-12 lie.
|
|
@@ -379,18 +380,10 @@ export function registerInvoiceTools(server, client) {
|
|
|
379
380
|
outputSchema: actionResultOutput,
|
|
380
381
|
}, async ({ id, to, confirm }) => withToolLogging("send_invoice", async () => {
|
|
381
382
|
if (!confirm) {
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
text: "Error: confirm=true is required to send an invoice. " +
|
|
387
|
-
"This delivers an email to the client — a third party outside this workspace — " +
|
|
388
|
-
"and it cannot be recalled once sent. Set confirm=true to proceed. / " +
|
|
389
|
-
"Se requiere confirm=true para enviar una factura por email a un tercero.",
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
isError: true,
|
|
393
|
-
};
|
|
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.");
|
|
394
387
|
}
|
|
395
388
|
const invoice = await client.getInvoice(id);
|
|
396
389
|
if (invoice["status"] === "cancelled") {
|
|
@@ -399,30 +392,99 @@ export function registerInvoiceTools(server, client) {
|
|
|
399
392
|
isError: true,
|
|
400
393
|
};
|
|
401
394
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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)) {
|
|
406
400
|
return {
|
|
407
|
-
content: [{ type: "text", text:
|
|
401
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
|
|
408
402
|
isError: true,
|
|
409
403
|
};
|
|
410
404
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
405
|
+
recipientEmail = trimmedTo;
|
|
406
|
+
}
|
|
407
|
+
if (!recipientEmail) {
|
|
408
|
+
const clientId = typeof invoice["clientId"] === "string" && invoice["clientId"].trim()
|
|
409
|
+
? invoice["clientId"].trim()
|
|
414
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
|
+
}
|
|
415
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
|
+
}
|
|
416
467
|
return {
|
|
417
|
-
content: [
|
|
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
|
+
],
|
|
418
476
|
isError: true,
|
|
419
477
|
};
|
|
420
478
|
}
|
|
421
479
|
}
|
|
422
480
|
const result = await client.sendInvoice(id, recipientEmail);
|
|
481
|
+
const enrichedResult = {
|
|
482
|
+
...(result && typeof result === "object" ? result : {}),
|
|
483
|
+
effectiveRecipient: recipientEmail,
|
|
484
|
+
};
|
|
423
485
|
return {
|
|
424
|
-
content: [mutateContent(formatRecord("Invoice sent",
|
|
425
|
-
structuredContent:
|
|
486
|
+
content: [mutateContent(formatRecord("Invoice sent", enrichedResult))],
|
|
487
|
+
structuredContent: enrichedResult,
|
|
426
488
|
};
|
|
427
489
|
}));
|
|
428
490
|
// -- mark_invoice_paid --
|
|
@@ -549,10 +611,15 @@ export function registerInvoiceTools(server, client) {
|
|
|
549
611
|
// -- apply_late_fee --
|
|
550
612
|
server.registerTool("apply_late_fee", {
|
|
551
613
|
title: "Apply Late Fee",
|
|
552
|
-
description: "Apply late payment interest to an overdue invoice.
|
|
553
|
-
"
|
|
554
|
-
"
|
|
555
|
-
"
|
|
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.",
|
|
556
623
|
annotations: CREATE_ANNOTATIONS,
|
|
557
624
|
inputSchema: {
|
|
558
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/quotes.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Quote tools for the Frihet MCP server.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
|
|
5
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
|
|
6
6
|
const quoteItemSchema = z.object({
|
|
7
7
|
description: z.string().trim().min(1).max(500).describe("Description of the line item / Descripcion del concepto"),
|
|
8
8
|
quantity: z.number().finite().min(0.0001).max(1_000_000).describe("Quantity / Cantidad"),
|
|
@@ -205,19 +205,11 @@ export function registerQuoteTools(server, client) {
|
|
|
205
205
|
outputSchema: documentDeleteResultOutput,
|
|
206
206
|
}, async ({ id, confirm }) => withToolLogging("delete_quote", async () => {
|
|
207
207
|
if (!confirm) {
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"A clean draft without delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused; a sent/accepted quote is CANCELLED " +
|
|
214
|
-
"(status=cancelled) instead and remains in the record. " +
|
|
215
|
-
"Set confirm=true to proceed. / " +
|
|
216
|
-
"Se requiere confirm=true para eliminar un presupuesto.",
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
isError: true,
|
|
220
|
-
};
|
|
208
|
+
return confirmationRequiredError("Error: confirm=true is required to delete a quote. " +
|
|
209
|
+
"A clean draft without delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused; a sent/accepted quote is CANCELLED " +
|
|
210
|
+
"(status=cancelled) instead and remains in the record. " +
|
|
211
|
+
"Set confirm=true to proceed. / " +
|
|
212
|
+
"Se requiere confirm=true para eliminar un presupuesto.");
|
|
221
213
|
}
|
|
222
214
|
// 204 → undefined (draft row destroyed). 200 → soft-cancel body. See GAP-12.
|
|
223
215
|
const outcome = await client.deleteQuote(id);
|
|
@@ -279,30 +271,99 @@ export function registerQuoteTools(server, client) {
|
|
|
279
271
|
isError: true,
|
|
280
272
|
};
|
|
281
273
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
const isValidRecipientEmail = (email) => email.length <= 255 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
275
|
+
let recipientEmail;
|
|
276
|
+
if (to !== undefined) {
|
|
277
|
+
const trimmedTo = to.trim();
|
|
278
|
+
if (!trimmedTo || !isValidRecipientEmail(trimmedTo)) {
|
|
286
279
|
return {
|
|
287
|
-
content: [{ type: "text", text:
|
|
280
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
|
|
288
281
|
isError: true,
|
|
289
282
|
};
|
|
290
283
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
284
|
+
recipientEmail = trimmedTo;
|
|
285
|
+
}
|
|
286
|
+
if (!recipientEmail) {
|
|
287
|
+
const clientId = typeof quote["clientId"] === "string" && quote["clientId"].trim()
|
|
288
|
+
? quote["clientId"].trim()
|
|
294
289
|
: undefined;
|
|
290
|
+
if (clientId) {
|
|
291
|
+
let linkedClient;
|
|
292
|
+
try {
|
|
293
|
+
linkedClient = await client.getClient(clientId);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const isNotFound = typeof err === "object" &&
|
|
297
|
+
err !== null &&
|
|
298
|
+
"statusCode" in err &&
|
|
299
|
+
err.statusCode === 404;
|
|
300
|
+
if (!isNotFound) {
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const clientEmail = linkedClient && typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
|
|
305
|
+
? linkedClient["email"].trim()
|
|
306
|
+
: undefined;
|
|
307
|
+
if (clientEmail) {
|
|
308
|
+
if (!isValidRecipientEmail(clientEmail)) {
|
|
309
|
+
return {
|
|
310
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${clientEmail}".` }],
|
|
311
|
+
isError: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
recipientEmail = clientEmail;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
295
317
|
if (!recipientEmail) {
|
|
318
|
+
const docFallbackEmail = typeof quote["clientEmail"] === "string" && quote["clientEmail"].trim()
|
|
319
|
+
? quote["clientEmail"].trim()
|
|
320
|
+
: typeof quote["email"] === "string" && quote["email"].trim()
|
|
321
|
+
? quote["email"].trim()
|
|
322
|
+
: undefined;
|
|
323
|
+
if (docFallbackEmail) {
|
|
324
|
+
if (!isValidRecipientEmail(docFallbackEmail)) {
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${docFallbackEmail}".` }],
|
|
327
|
+
isError: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const fallbackMsg = `Error: the linked client has no current email address; only a document-level fallback email ("${docFallbackEmail}") was found on the quote, which may be outdated. ` +
|
|
331
|
+
`Confirm this specific recipient with the user and re-run with to="${docFallbackEmail}" and confirm=true to send. / ` +
|
|
332
|
+
`El cliente vinculado no tiene email actual; el presupuesto conserva el correo documental "${docFallbackEmail}", que podria estar obsoleto. Confirma este destinatario concreto y vuelve a llamar con to="${docFallbackEmail}" y confirm=true.`;
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: fallbackMsg }],
|
|
335
|
+
structuredContent: {
|
|
336
|
+
success: false,
|
|
337
|
+
id,
|
|
338
|
+
message: fallbackMsg,
|
|
339
|
+
effectiveRecipient: docFallbackEmail,
|
|
340
|
+
recipientSource: "document_fallback",
|
|
341
|
+
requiresExplicitRecipientConfirmation: true,
|
|
342
|
+
},
|
|
343
|
+
isError: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
296
346
|
return {
|
|
297
|
-
content: [
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
349
|
+
type: "text",
|
|
350
|
+
text: clientId
|
|
351
|
+
? "Error: the linked client has no saved email address."
|
|
352
|
+
: "Error: this quote has no linked client with a saved email address.",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
298
355
|
isError: true,
|
|
299
356
|
};
|
|
300
357
|
}
|
|
301
358
|
}
|
|
302
359
|
const result = await client.sendQuote(id, recipientEmail);
|
|
360
|
+
const enrichedResult = {
|
|
361
|
+
...(result && typeof result === "object" ? result : {}),
|
|
362
|
+
effectiveRecipient: recipientEmail,
|
|
363
|
+
};
|
|
303
364
|
return {
|
|
304
|
-
content: [mutateContent(formatRecord("Quote sent",
|
|
305
|
-
structuredContent:
|
|
365
|
+
content: [mutateContent(formatRecord("Quote sent", enrichedResult))],
|
|
366
|
+
structuredContent: enrichedResult,
|
|
306
367
|
};
|
|
307
368
|
}));
|
|
308
369
|
}
|
package/dist/tools/recurring.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Backend: /v1/recurring/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
|
|
17
17
|
*/
|
|
18
18
|
import { z } from "zod/v4";
|
|
19
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, deleteResultOutput, recurringInvoiceItemOutput, } from "./shared.js";
|
|
19
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, deleteResultOutput, recurringInvoiceItemOutput, } from "./shared.js";
|
|
20
20
|
export function registerRecurringTools(server, client) {
|
|
21
21
|
// -- list_recurring_invoices --
|
|
22
22
|
server.registerTool("list_recurring_invoices", {
|
|
@@ -191,17 +191,9 @@ export function registerRecurringTools(server, client) {
|
|
|
191
191
|
outputSchema: deleteResultOutput,
|
|
192
192
|
}, async ({ id, confirm }) => withToolLogging("delete_recurring_invoice", async () => {
|
|
193
193
|
if (!confirm) {
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
type: "text",
|
|
198
|
-
text: "Error: confirm=true is required to delete a recurring invoice template. " +
|
|
199
|
-
"This will stop all future invoice generation from this template. Set confirm=true to proceed. / " +
|
|
200
|
-
"Se requiere confirm=true para eliminar la plantilla de factura recurrente.",
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
isError: true,
|
|
204
|
-
};
|
|
194
|
+
return confirmationRequiredError("Error: confirm=true is required to delete a recurring invoice template. " +
|
|
195
|
+
"This will stop all future invoice generation from this template. Set confirm=true to proceed. / " +
|
|
196
|
+
"Se requiere confirm=true para eliminar la plantilla de factura recurrente.");
|
|
205
197
|
}
|
|
206
198
|
await client.deleteRecurringInvoice(id);
|
|
207
199
|
return {
|
|
@@ -212,23 +204,32 @@ export function registerRecurringTools(server, client) {
|
|
|
212
204
|
// -- run_recurring_now --
|
|
213
205
|
server.registerTool("run_recurring_now", {
|
|
214
206
|
title: "Run Recurring Invoice Now",
|
|
215
|
-
description: "
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
207
|
+
description: "Generate an invoice from a recurring template and update its run count and last-run date. Requires confirm=true. " +
|
|
208
|
+
"draftOnly defaults to true. With draftOnly=false, the invoice is created with status='sent' and may trigger fiscal numbering and automatic tax submission. " +
|
|
209
|
+
"Both modes may deliver configured invoice-created webhooks and notify workspace admins/accountants; this endpoint does not email the invoice to the client. " +
|
|
210
|
+
"Each new call can create another invoice; check the template and invoices after an uncertain result before retrying. " +
|
|
211
|
+
"Example: templateId='rec_abc123', confirm=true " +
|
|
212
|
+
"/ Genera una factura recurrente y actualiza el contador y la fecha de ejecución. Requiere confirm=true. " +
|
|
213
|
+
"Por defecto crea un borrador; con draftOnly=false crea el estado 'sent' y puede activar la numeración y remisión fiscal automática. " +
|
|
214
|
+
"Ambas opciones pueden activar webhooks y avisos internos; esta operación no envía la factura por correo al cliente. " +
|
|
215
|
+
"Una nueva llamada puede crear otra factura; comprueba la plantilla y las facturas antes de repetir un resultado incierto.",
|
|
216
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
217
|
+
inputSchema: z.object({
|
|
218
|
+
templateId: z.string().min(1).describe("Recurring invoice template ID / ID de la plantilla de factura recurrente"),
|
|
225
219
|
draftOnly: z
|
|
226
220
|
.boolean()
|
|
227
221
|
.optional()
|
|
228
|
-
.describe("
|
|
229
|
-
|
|
222
|
+
.describe("Default true creates a draft; false creates status='sent' and may trigger automatic fiscal submission. Neither option emails the client. / Por defecto true crea un borrador; false crea el estado 'sent' y puede activar la remisión fiscal automática. Ninguna opción envía correo al cliente."),
|
|
223
|
+
confirm: z.boolean().describe("Must be true to authorize invoice creation, configured webhooks and possible fiscal issuance / Debe ser true para autorizar la creación, los webhooks configurados y la posible emisión fiscal"),
|
|
224
|
+
}).strict(),
|
|
230
225
|
outputSchema: actionResultOutput,
|
|
231
|
-
}, async ({ templateId, draftOnly }) => withToolLogging("run_recurring_now", async () => {
|
|
226
|
+
}, async ({ templateId, draftOnly, confirm }) => withToolLogging("run_recurring_now", async () => {
|
|
227
|
+
if (confirm !== true) {
|
|
228
|
+
return confirmationRequiredError("Error: confirm=true is required to create a recurring invoice and allow configured webhooks. " +
|
|
229
|
+
"draftOnly=false may also trigger fiscal numbering and automatic tax submission. / " +
|
|
230
|
+
"Se requiere confirm=true para crear la factura y permitir los webhooks configurados. " +
|
|
231
|
+
"draftOnly=false también puede activar la numeración y remisión fiscal automática.");
|
|
232
|
+
}
|
|
232
233
|
const result = await client.runRecurringNow(templateId, { draftOnly: draftOnly ?? true });
|
|
233
234
|
return {
|
|
234
235
|
content: [mutateContent(formatRecord("Recurring invoice triggered", result))],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Barrel module that registers
|
|
2
|
+
* Barrel module that registers every Frihet ERP tool on an McpServer.
|
|
3
3
|
*
|
|
4
4
|
* Used by both the local (stdio) and remote (Cloudflare Workers) servers
|
|
5
5
|
* so tool definitions stay in sync — one source of truth.
|
|
6
6
|
*
|
|
7
7
|
* Langfuse observability is injected by patching server.registerTool once
|
|
8
8
|
* before any tool registration. This wraps every tool callback with
|
|
9
|
-
* traceMCPTool so
|
|
9
|
+
* traceMCPTool so every registered tool is instrumented at zero per-tool cost.
|
|
10
10
|
*/
|
|
11
11
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
12
|
import type { IFrihetClient } from "../client-interface.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Barrel module that registers
|
|
2
|
+
* Barrel module that registers every Frihet ERP tool on an McpServer.
|
|
3
3
|
*
|
|
4
4
|
* Used by both the local (stdio) and remote (Cloudflare Workers) servers
|
|
5
5
|
* so tool definitions stay in sync — one source of truth.
|
|
6
6
|
*
|
|
7
7
|
* Langfuse observability is injected by patching server.registerTool once
|
|
8
8
|
* before any tool registration. This wraps every tool callback with
|
|
9
|
-
* traceMCPTool so
|
|
9
|
+
* traceMCPTool so every registered tool is instrumented at zero per-tool cost.
|
|
10
10
|
*/
|
|
11
11
|
import { traceMCPTool } from "../observability.js";
|
|
12
12
|
import { registerInvoiceTools } from "./invoices.js";
|
|
@@ -37,13 +37,13 @@ import { registerImpuestoSociedadesTools } from "./impuesto_sociedades.js";
|
|
|
37
37
|
import { registerBankRulesTools } from "./bank_rules.js";
|
|
38
38
|
import { registerHrTools } from "./hr.js";
|
|
39
39
|
import { registerPayrollTools } from "./payroll.js";
|
|
40
|
-
import { registerOnboardingTools } from "./onboarding.js";
|
|
41
40
|
import { registerPermissionsTools } from "./permissions.js";
|
|
42
41
|
import { registerAccountingCloseTools } from "./accountingClose.js";
|
|
42
|
+
import { registerSearchTools } from "./search.js";
|
|
43
43
|
/**
|
|
44
44
|
* Patches server.registerTool to wrap every tool callback with Langfuse tracing.
|
|
45
45
|
*
|
|
46
|
-
* The patch is applied once before tool registration so
|
|
46
|
+
* The patch is applied once before tool registration so every registered tool is
|
|
47
47
|
* instrumented without per-tool edits. Tool call signatures are unchanged —
|
|
48
48
|
* existing MCP clients continue to work identically.
|
|
49
49
|
*
|
|
@@ -57,7 +57,10 @@ import { registerAccountingCloseTools } from "./accountingClose.js";
|
|
|
57
57
|
* attendance_clock_in, attendance_clock_out, overtime_report, anomaly_list
|
|
58
58
|
* Webhook trust (1): test_webhook (added to registerWebhookTools)
|
|
59
59
|
* Payroll (2): payroll_export, payroll_checklist
|
|
60
|
-
* Onboarding
|
|
60
|
+
* (Onboarding tools onboarding_status / onboarding_persona_set withdrawn
|
|
61
|
+
* via #124 — Frihet-ERP /v1/onboarding/* family absent; tools were deleted,
|
|
62
|
+
* not stubbed, to keep the catalogue honest. See src/tools/igic.ts for the
|
|
63
|
+
* precedent of an inline NOT_DEPLOYED stub where the tool is retained.)
|
|
61
64
|
* Permissions (2): permissions_matrix, permissions_me
|
|
62
65
|
* Period close (3): period_close_status, period_close, period_reopen
|
|
63
66
|
*/
|
|
@@ -106,8 +109,8 @@ export function registerAllTools(server, client) {
|
|
|
106
109
|
registerBankRulesTools(server, client);
|
|
107
110
|
registerHrTools(server, client);
|
|
108
111
|
registerPayrollTools(server, client);
|
|
109
|
-
registerOnboardingTools(server, client);
|
|
110
112
|
registerPermissionsTools(server, client);
|
|
111
113
|
registerAccountingCloseTools(server, client);
|
|
114
|
+
registerSearchTools(server, client);
|
|
112
115
|
}
|
|
113
116
|
//# sourceMappingURL=register-all.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-resource global search tool — Phase 10 second-wave safe parity.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `GET /v1/search/global?q=&types=&limit=&offset=` (publicApi.ts:4476).
|
|
5
|
+
* Read-only fan-out across invoices / expenses / vendors / clients / products
|
|
6
|
+
* with workspace scoping preserved by Firestore subcollection paths on the
|
|
7
|
+
* server. No mutating counterpart; no auth boundary different from any other
|
|
8
|
+
* list tool.
|
|
9
|
+
*
|
|
10
|
+
* Why this lives alone (not in clients.ts): it is not a clients/* endpoint,
|
|
11
|
+
* has its own canonical envelope (`{data, total, ..., query, types,
|
|
12
|
+
* truncated?}` instead of the standard paginated one), and is the only
|
|
13
|
+
* server-side fan-out tool — coupling it with one resource would mislead the
|
|
14
|
+
* tool listing.
|
|
15
|
+
*/
|
|
16
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import type { IFrihetClient } from "../client-interface.js";
|
|
18
|
+
export declare function registerSearchTools(server: McpServer, client: IFrihetClient): void;
|
|
19
|
+
//# sourceMappingURL=search.d.ts.map
|