@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
|
@@ -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 { withToolLogging, formatRecord, getContent, READ_ONLY_ANNOTATIONS, permissionsMatrixOutput, permissionsMeOutput, } from "./shared.js";
|
|
17
14
|
import { withBackendGuard } from "./backend-availability.js";
|
|
@@ -19,10 +16,9 @@ export function registerPermissionsTools(server, client) {
|
|
|
19
16
|
// -- permissions_matrix --
|
|
20
17
|
server.registerTool("permissions_matrix", {
|
|
21
18
|
title: "Permissions Matrix",
|
|
22
|
-
description: "Return the
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"/ Devuelve la matriz completa de roles y permisos del workspace.",
|
|
19
|
+
description: "Return the backend's documented RBAC-model snapshot by role, resource, and action. " +
|
|
20
|
+
"It is not derived from Firestore rules and is not a runtime authorization guarantee; " +
|
|
21
|
+
"a backend 403 is authoritative. / Devuelve el modelo RBAC documentado, no una garantia exhaustiva de autorizacion.",
|
|
26
22
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
27
23
|
inputSchema: {},
|
|
28
24
|
outputSchema: permissionsMatrixOutput,
|
|
@@ -36,9 +32,10 @@ export function registerPermissionsTools(server, client) {
|
|
|
36
32
|
// -- permissions_me --
|
|
37
33
|
server.registerTool("permissions_me", {
|
|
38
34
|
title: "My Permissions",
|
|
39
|
-
description: "Return the caller's
|
|
40
|
-
"
|
|
41
|
-
"
|
|
35
|
+
description: "Return the caller's backwards-compatible RBAC-model fields separately from actual API-key scopes, " +
|
|
36
|
+
"unrestricted-key semantics, known e-invoice scope denial, and surfaces not reported here. " +
|
|
37
|
+
"This is non-exhaustive; use the exact backend 403 as authority. " +
|
|
38
|
+
"/ Separa el modelo RBAC de los scopes reales de la API key; no es una autorizacion exhaustiva.",
|
|
42
39
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
43
40
|
inputSchema: {},
|
|
44
41
|
outputSchema: permissionsMeOutput,
|
package/dist/tools/products.js
CHANGED
|
@@ -67,9 +67,9 @@ export function registerProductTools(server, client) {
|
|
|
67
67
|
"Los productos se pueden usar al crear facturas y presupuestos para entrada rapida de datos.",
|
|
68
68
|
annotations: CREATE_ANNOTATIONS,
|
|
69
69
|
inputSchema: {
|
|
70
|
-
name: z.string().describe("Product/service name / Nombre del producto o servicio"),
|
|
71
|
-
unitPrice: z.number().describe("Unit price in EUR / Precio unitario en EUR"),
|
|
72
|
-
description: z.string().optional().describe("Product description / Descripcion"),
|
|
70
|
+
name: z.string().trim().min(1).max(200).describe("Product/service name / Nombre del producto o servicio"),
|
|
71
|
+
unitPrice: z.number().finite().min(0).max(10_000_000).describe("Unit price in EUR / Precio unitario en EUR"),
|
|
72
|
+
description: z.string().max(2000).optional().describe("Product description / Descripcion"),
|
|
73
73
|
taxRate: z
|
|
74
74
|
.number()
|
|
75
75
|
.min(0)
|
|
@@ -94,9 +94,9 @@ export function registerProductTools(server, client) {
|
|
|
94
94
|
annotations: UPDATE_ANNOTATIONS,
|
|
95
95
|
inputSchema: {
|
|
96
96
|
id: z.string().describe("Product ID / ID del producto"),
|
|
97
|
-
name: z.string().optional().describe("Name / Nombre"),
|
|
98
|
-
unitPrice: z.number().optional().describe("Unit price / Precio unitario"),
|
|
99
|
-
description: z.string().optional().describe("Description / Descripcion"),
|
|
97
|
+
name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
|
|
98
|
+
unitPrice: z.number().finite().min(0).max(10_000_000).optional().describe("Unit price / Precio unitario"),
|
|
99
|
+
description: z.string().max(2000).optional().describe("Description / Descripcion"),
|
|
100
100
|
taxRate: z.number().min(0).max(100).optional().describe("Tax rate % / IVA %"),
|
|
101
101
|
},
|
|
102
102
|
outputSchema: productItemOutput,
|
package/dist/tools/quotes.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* Quote tools for the Frihet MCP server.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput,
|
|
5
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, documentDeleteResultOutput, quoteItemOutput, actionResultOutput } from "./shared.js";
|
|
6
6
|
const quoteItemSchema = z.object({
|
|
7
|
-
description: z.string().describe("Description of the line item / Descripcion del concepto"),
|
|
8
|
-
quantity: z.number().describe("Quantity / Cantidad"),
|
|
9
|
-
unitPrice: z.number().describe("Unit price in EUR / Precio unitario en EUR"),
|
|
10
|
-
});
|
|
7
|
+
description: z.string().trim().min(1).max(500).describe("Description of the line item / Descripcion del concepto"),
|
|
8
|
+
quantity: z.number().finite().min(0.0001).max(1_000_000).describe("Quantity / Cantidad"),
|
|
9
|
+
unitPrice: z.number().finite().min(0).max(10_000_000).describe("Unit price in EUR / Precio unitario en EUR"),
|
|
10
|
+
}).strict();
|
|
11
11
|
// Optional client-identity + ES fiscal fields the Frihet API accepts on quote
|
|
12
12
|
// create+update (mirrors the invoice subset the backend supports for quotes).
|
|
13
13
|
const quoteOptionalFields = {
|
|
14
14
|
clientId: z
|
|
15
15
|
.string()
|
|
16
16
|
.optional()
|
|
17
|
-
.describe("Existing client ID —
|
|
17
|
+
.describe("Existing client ID — stored client details are used for the document / ID de cliente existente"),
|
|
18
18
|
clientTaxId: z
|
|
19
19
|
.string()
|
|
20
20
|
.optional()
|
|
@@ -24,11 +24,11 @@ const quoteOptionalFields = {
|
|
|
24
24
|
.optional()
|
|
25
25
|
.describe("Client billing address / Direccion fiscal del cliente"),
|
|
26
26
|
issueDate: z
|
|
27
|
-
.
|
|
27
|
+
.iso.date()
|
|
28
28
|
.optional()
|
|
29
29
|
.describe("Issue date in ISO 8601 (YYYY-MM-DD), defaults to today / Fecha de emision"),
|
|
30
30
|
dueDate: z
|
|
31
|
-
.
|
|
31
|
+
.iso.date()
|
|
32
32
|
.optional()
|
|
33
33
|
.describe("Due date in ISO 8601 (YYYY-MM-DD) / Fecha de vencimiento"),
|
|
34
34
|
taxRate: z
|
|
@@ -132,17 +132,17 @@ export function registerQuoteTools(server, client) {
|
|
|
132
132
|
"Los presupuestos se pueden convertir en facturas despues.",
|
|
133
133
|
annotations: CREATE_ANNOTATIONS,
|
|
134
134
|
inputSchema: {
|
|
135
|
-
clientName: z.string().describe("Client name / Nombre del cliente"),
|
|
135
|
+
clientName: z.string().trim().min(1).max(200).describe("Client name / Nombre del cliente"),
|
|
136
136
|
...quoteOptionalFields,
|
|
137
137
|
items: z
|
|
138
138
|
.array(quoteItemSchema)
|
|
139
139
|
.min(1)
|
|
140
140
|
.describe("Line items (each with description, quantity, unitPrice) / Conceptos del presupuesto"),
|
|
141
141
|
validUntil: z
|
|
142
|
-
.
|
|
142
|
+
.iso.date()
|
|
143
143
|
.optional()
|
|
144
144
|
.describe("Expiry date in ISO 8601 (YYYY-MM-DD) / Fecha de validez"),
|
|
145
|
-
notes: z.string().optional().describe("Additional notes shown on the quote / Notas adicionales"),
|
|
145
|
+
notes: z.string().max(10_000).optional().describe("Additional notes shown on the quote / Notas adicionales"),
|
|
146
146
|
status: z
|
|
147
147
|
.enum(["draft", "sent", "accepted", "rejected", "expired"])
|
|
148
148
|
.optional()
|
|
@@ -153,8 +153,8 @@ export function registerQuoteTools(server, client) {
|
|
|
153
153
|
const result = await client.createQuote(input);
|
|
154
154
|
const hints = enrichResponse("quotes", "create", result);
|
|
155
155
|
return {
|
|
156
|
-
content: [mutateContent(formatRecord("Quote created", result))],
|
|
157
|
-
structuredContent: { ...result
|
|
156
|
+
content: [mutateContent(formatRecord("Quote created", result) + hints)],
|
|
157
|
+
structuredContent: { ...result },
|
|
158
158
|
};
|
|
159
159
|
}));
|
|
160
160
|
// -- update_quote --
|
|
@@ -187,37 +187,183 @@ export function registerQuoteTools(server, client) {
|
|
|
187
187
|
// -- delete_quote --
|
|
188
188
|
server.registerTool("delete_quote", {
|
|
189
189
|
title: "Delete Quote",
|
|
190
|
-
description: "
|
|
191
|
-
"
|
|
190
|
+
description: "Delete a quote by its ID. Requires confirm=true. " +
|
|
191
|
+
"Only a clean DRAFT with no delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused and left unchanged. A sent/accepted/rejected/expired quote is NOT " +
|
|
192
|
+
"destroyed: the backend CANCELS it (status=cancelled) — the same non-destructive path invoices " +
|
|
193
|
+
"take — and it stays readable via get_quote. The result reports which happened in `outcome` " +
|
|
194
|
+
"(\"deleted\" or \"cancelled\"). " +
|
|
195
|
+
"/ Elimina un presupuesto por su ID. Requiere confirm=true. Solo un BORRADOR limpio sin evidencia de entrega, respuesta, adjuntos o conversion se " +
|
|
196
|
+
"elimina de forma permanente; un borrador protegido se rechaza sin cambios. Uno ya enviado/aceptado/rechazado NO se destruye: se CANCELA " +
|
|
197
|
+
"(status=cancelled) y sigue consultable con get_quote.",
|
|
192
198
|
annotations: DELETE_ANNOTATIONS,
|
|
193
199
|
inputSchema: {
|
|
194
200
|
id: z.string().describe("Quote ID / ID del presupuesto"),
|
|
201
|
+
confirm: z
|
|
202
|
+
.boolean()
|
|
203
|
+
.describe("Must be true to confirm deletion / Debe ser true para confirmar la eliminacion"),
|
|
195
204
|
},
|
|
196
|
-
outputSchema:
|
|
197
|
-
}, async ({ id }) => withToolLogging("delete_quote", async () => {
|
|
198
|
-
|
|
205
|
+
outputSchema: documentDeleteResultOutput,
|
|
206
|
+
}, async ({ id, confirm }) => withToolLogging("delete_quote", async () => {
|
|
207
|
+
if (!confirm) {
|
|
208
|
+
return confirmationRequiredError("Error: confirm=true is required to delete a quote. " +
|
|
209
|
+
"A clean draft without delivery, response, attachment, or conversion evidence is removed permanently; a protected draft is refused; a sent/accepted quote is CANCELLED " +
|
|
210
|
+
"(status=cancelled) instead and remains in the record. " +
|
|
211
|
+
"Set confirm=true to proceed. / " +
|
|
212
|
+
"Se requiere confirm=true para eliminar un presupuesto.");
|
|
213
|
+
}
|
|
214
|
+
// 204 → undefined (draft row destroyed). 200 → soft-cancel body. See GAP-12.
|
|
215
|
+
const outcome = await client.deleteQuote(id);
|
|
216
|
+
const cancelled = !!outcome && typeof outcome === "object";
|
|
217
|
+
const body = cancelled ? outcome : {};
|
|
218
|
+
const previous = typeof body["previousStatus"] === "string" ? ` (was ${body["previousStatus"]})` : "";
|
|
199
219
|
return {
|
|
200
|
-
content: [mutateContent(
|
|
201
|
-
|
|
220
|
+
content: [mutateContent(cancelled
|
|
221
|
+
? `Quote ${id} was CANCELLED, not deleted${previous}: it still exists with status=cancelled. / ` +
|
|
222
|
+
`Presupuesto ${id} CANCELADO, no eliminado: sigue existiendo con status=cancelled.`
|
|
223
|
+
: `Quote ${id} deleted permanently (it was a draft). / ` +
|
|
224
|
+
`Presupuesto ${id} eliminado permanentemente (era un borrador).`)],
|
|
225
|
+
structuredContent: {
|
|
226
|
+
success: true,
|
|
227
|
+
id,
|
|
228
|
+
...body,
|
|
229
|
+
outcome: cancelled ? "cancelled" : "deleted",
|
|
230
|
+
},
|
|
202
231
|
};
|
|
203
232
|
}));
|
|
204
233
|
// -- send_quote --
|
|
205
234
|
server.registerTool("send_quote", {
|
|
206
235
|
title: "Send Quote",
|
|
207
|
-
description: "Send a quote/estimate to the client via email.
|
|
236
|
+
description: "Send a quote/estimate to the client via email. Requires confirm=true. " +
|
|
237
|
+
"Optionally override the recipient email address. " +
|
|
208
238
|
"The quote must exist and should not already be expired or rejected. " +
|
|
209
|
-
"
|
|
239
|
+
"Sending reaches a third party and cannot be recalled. " +
|
|
240
|
+
"/ Envia un presupuesto al cliente por email. Requiere confirm=true. " +
|
|
241
|
+
"Opcionalmente se puede cambiar el email destinatario. El envio llega a un tercero y no se puede anular.",
|
|
210
242
|
annotations: UPDATE_ANNOTATIONS,
|
|
211
243
|
inputSchema: {
|
|
212
244
|
id: z.string().describe("Quote ID / ID del presupuesto"),
|
|
213
245
|
to: z.string().optional().describe("Override recipient email / Email destinatario alternativo"),
|
|
246
|
+
confirm: z
|
|
247
|
+
.boolean()
|
|
248
|
+
.describe("Must be true to confirm sending / Debe ser true para confirmar el envio"),
|
|
214
249
|
},
|
|
215
250
|
outputSchema: actionResultOutput,
|
|
216
|
-
}, async ({ id, to }) => withToolLogging("send_quote", async () => {
|
|
217
|
-
|
|
251
|
+
}, async ({ id, to, confirm }) => withToolLogging("send_quote", async () => {
|
|
252
|
+
if (!confirm) {
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: "Error: confirm=true is required to send a quote. " +
|
|
258
|
+
"This delivers an email to the client — a third party outside this workspace — " +
|
|
259
|
+
"and it cannot be recalled once sent. Set confirm=true to proceed. / " +
|
|
260
|
+
"Se requiere confirm=true para enviar un presupuesto por email a un tercero.",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
isError: true,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const quote = await client.getQuote(id);
|
|
267
|
+
const status = typeof quote["status"] === "string" ? quote["status"] : undefined;
|
|
268
|
+
if (status && ["expired", "rejected", "cancelled"].includes(status)) {
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: `Error: a ${status} quote cannot be sent.` }],
|
|
271
|
+
isError: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const isValidRecipientEmail = (email) => email.length <= 255 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
275
|
+
let recipientEmail;
|
|
276
|
+
if (to !== undefined) {
|
|
277
|
+
const trimmedTo = to.trim();
|
|
278
|
+
if (!trimmedTo || !isValidRecipientEmail(trimmedTo)) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${trimmedTo}".` }],
|
|
281
|
+
isError: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
recipientEmail = trimmedTo;
|
|
285
|
+
}
|
|
286
|
+
if (!recipientEmail) {
|
|
287
|
+
const clientId = typeof quote["clientId"] === "string" && quote["clientId"].trim()
|
|
288
|
+
? quote["clientId"].trim()
|
|
289
|
+
: undefined;
|
|
290
|
+
if (clientId) {
|
|
291
|
+
let linkedClient;
|
|
292
|
+
try {
|
|
293
|
+
linkedClient = await client.getClient(clientId);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const isNotFound = typeof err === "object" &&
|
|
297
|
+
err !== null &&
|
|
298
|
+
"statusCode" in err &&
|
|
299
|
+
err.statusCode === 404;
|
|
300
|
+
if (!isNotFound) {
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const clientEmail = linkedClient && typeof linkedClient["email"] === "string" && linkedClient["email"].trim()
|
|
305
|
+
? linkedClient["email"].trim()
|
|
306
|
+
: undefined;
|
|
307
|
+
if (clientEmail) {
|
|
308
|
+
if (!isValidRecipientEmail(clientEmail)) {
|
|
309
|
+
return {
|
|
310
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${clientEmail}".` }],
|
|
311
|
+
isError: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
recipientEmail = clientEmail;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!recipientEmail) {
|
|
318
|
+
const docFallbackEmail = typeof quote["clientEmail"] === "string" && quote["clientEmail"].trim()
|
|
319
|
+
? quote["clientEmail"].trim()
|
|
320
|
+
: typeof quote["email"] === "string" && quote["email"].trim()
|
|
321
|
+
? quote["email"].trim()
|
|
322
|
+
: undefined;
|
|
323
|
+
if (docFallbackEmail) {
|
|
324
|
+
if (!isValidRecipientEmail(docFallbackEmail)) {
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: `Error: invalid recipient email address format: "${docFallbackEmail}".` }],
|
|
327
|
+
isError: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const fallbackMsg = `Error: the linked client has no current email address; only a document-level fallback email ("${docFallbackEmail}") was found on the quote, which may be outdated. ` +
|
|
331
|
+
`Confirm this specific recipient with the user and re-run with to="${docFallbackEmail}" and confirm=true to send. / ` +
|
|
332
|
+
`El cliente vinculado no tiene email actual; el presupuesto conserva el correo documental "${docFallbackEmail}", que podria estar obsoleto. Confirma este destinatario concreto y vuelve a llamar con to="${docFallbackEmail}" y confirm=true.`;
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: fallbackMsg }],
|
|
335
|
+
structuredContent: {
|
|
336
|
+
success: false,
|
|
337
|
+
id,
|
|
338
|
+
message: fallbackMsg,
|
|
339
|
+
effectiveRecipient: docFallbackEmail,
|
|
340
|
+
recipientSource: "document_fallback",
|
|
341
|
+
requiresExplicitRecipientConfirmation: true,
|
|
342
|
+
},
|
|
343
|
+
isError: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
349
|
+
type: "text",
|
|
350
|
+
text: clientId
|
|
351
|
+
? "Error: the linked client has no saved email address."
|
|
352
|
+
: "Error: this quote has no linked client with a saved email address.",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
isError: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const result = await client.sendQuote(id, recipientEmail);
|
|
360
|
+
const enrichedResult = {
|
|
361
|
+
...(result && typeof result === "object" ? result : {}),
|
|
362
|
+
effectiveRecipient: recipientEmail,
|
|
363
|
+
};
|
|
218
364
|
return {
|
|
219
|
-
content: [mutateContent(formatRecord("Quote sent",
|
|
220
|
-
structuredContent:
|
|
365
|
+
content: [mutateContent(formatRecord("Quote sent", enrichedResult))],
|
|
366
|
+
structuredContent: enrichedResult,
|
|
221
367
|
};
|
|
222
368
|
}));
|
|
223
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
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-resource global search tool — Phase 10 second-wave safe parity.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `GET /v1/search/global?q=&types=&limit=&offset=` (publicApi.ts:4476).
|
|
5
|
+
* Read-only fan-out across invoices / expenses / vendors / clients / products
|
|
6
|
+
* with workspace scoping preserved by Firestore subcollection paths on the
|
|
7
|
+
* server. No mutating counterpart; no auth boundary different from any other
|
|
8
|
+
* list tool.
|
|
9
|
+
*
|
|
10
|
+
* Why this lives alone (not in clients.ts): it is not a clients/* endpoint,
|
|
11
|
+
* has its own canonical envelope (`{data, total, ..., query, types,
|
|
12
|
+
* truncated?}` instead of the standard paginated one), and is the only
|
|
13
|
+
* server-side fan-out tool — coupling it with one resource would mislead the
|
|
14
|
+
* tool listing.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod/v4";
|
|
17
|
+
import { withToolLogging, listContent, READ_ONLY_ANNOTATIONS, formatPaginatedResponse, } from "./shared.js";
|
|
18
|
+
const GLOBAL_SEARCH_TYPES = ["invoices", "expenses", "vendors", "clients", "products"];
|
|
19
|
+
const globalSearchOutputSchema = z.object({
|
|
20
|
+
data: z.array(z.record(z.string(), z.unknown())),
|
|
21
|
+
total: z.number().int().nonnegative(),
|
|
22
|
+
limit: z.number().int().positive(),
|
|
23
|
+
offset: z.number().int().nonnegative(),
|
|
24
|
+
hasMore: z.boolean(),
|
|
25
|
+
query: z.string(),
|
|
26
|
+
types: z.array(z.enum(GLOBAL_SEARCH_TYPES)),
|
|
27
|
+
truncated: z.boolean().optional(),
|
|
28
|
+
});
|
|
29
|
+
export function registerSearchTools(server, client) {
|
|
30
|
+
server.registerTool("global_search", {
|
|
31
|
+
title: "Global Search",
|
|
32
|
+
description: "Search across invoices, expenses, vendors, clients, and products in one call. " +
|
|
33
|
+
"Use this to resolve an entity name/email/number when you do not yet know the resource type. " +
|
|
34
|
+
"Returns per-hit {type, id, name, ...} so the caller can dispatch to the right list/get tool. " +
|
|
35
|
+
"Read-only and workspace-scoped. " +
|
|
36
|
+
"Example: q='acme', types=['clients','invoices'], limit=10 " +
|
|
37
|
+
"/ Busca en facturas, gastos, proveedores, clientes y productos en una sola llamada. " +
|
|
38
|
+
"Util para resolver un nombre/correo/numero cuando no se sabe aun el tipo de recurso.",
|
|
39
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
40
|
+
inputSchema: {
|
|
41
|
+
q: z
|
|
42
|
+
.string()
|
|
43
|
+
.trim()
|
|
44
|
+
.min(1)
|
|
45
|
+
.max(200)
|
|
46
|
+
.describe("Free-text query (1-200 chars) / Texto a buscar"),
|
|
47
|
+
types: z
|
|
48
|
+
.array(z.enum(GLOBAL_SEARCH_TYPES))
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Restrict to these resource types. Defaults to all 5. / Tipos a incluir (por defecto todos)"),
|
|
51
|
+
limit: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.min(1)
|
|
55
|
+
.max(50)
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Max results across all types (1-50, default 25) / Resultados maximos"),
|
|
58
|
+
offset: z
|
|
59
|
+
.number()
|
|
60
|
+
.int()
|
|
61
|
+
.min(0)
|
|
62
|
+
.max(1000)
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Offset / Desplazamiento"),
|
|
65
|
+
},
|
|
66
|
+
outputSchema: globalSearchOutputSchema,
|
|
67
|
+
}, async ({ q, types, limit, offset }) => withToolLogging("global_search", async () => {
|
|
68
|
+
// Trim defensively: the JSON Schema serialization of `z.string().trim()`
|
|
69
|
+
// drops the trim hint, so leading/trailing whitespace survives MCP's
|
|
70
|
+
// input validation and would reach the ERP's q-length check as junk.
|
|
71
|
+
const cleanQ = q.trim();
|
|
72
|
+
const result = await client.globalSearch({
|
|
73
|
+
q: cleanQ,
|
|
74
|
+
types: types,
|
|
75
|
+
limit,
|
|
76
|
+
offset,
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
content: [listContent(formatPaginatedResponse("search", result))],
|
|
80
|
+
structuredContent: result,
|
|
81
|
+
};
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=search.js.map
|