@frihet/mcp-server 1.16.6 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -31
- 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 +549 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +254 -0
- package/dist/client-interface.d.ts +22 -14
- package/dist/client.d.ts +114 -18
- package/dist/client.js +488 -56
- package/dist/demo-client.d.ts +16 -13
- package/dist/demo-client.js +171 -17
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/index.js +41 -56
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +192 -105
- package/dist/openai-profile.d.ts +25 -21
- package/dist/openai-profile.js +1166 -156
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +715 -41
- package/dist/openai-review-oauth.d.ts +47 -0
- package/dist/openai-review-oauth.js +73 -3
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +2 -0
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +10 -0
- package/dist/redaction.js +74 -0
- package/dist/resources/register-all.js +6 -12
- package/dist/server-composition.d.ts +16 -0
- package/dist/server-composition.js +44 -0
- package/dist/tool-exposure.d.ts +25 -22
- package/dist/tool-exposure.js +199 -77
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/backend-availability.d.ts +3 -3
- package/dist/tools/backend-availability.js +3 -3
- package/dist/tools/clients.js +9 -9
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +30 -11
- package/dist/tools/einvoice.d.ts +1 -1
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +8 -9
- package/dist/tools/intelligence.js +2 -2
- package/dist/tools/invoices.js +150 -47
- 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 +109 -24
- package/dist/tools/shared.d.ts +1431 -75
- package/dist/tools/shared.js +386 -76
- package/dist/tools/team.js +18 -14
- package/dist/tools/vendors.js +9 -9
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +28 -4
- package/docs/agent-onboarding.json +381 -0
- package/package.json +30 -7
package/dist/tools/shared.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { z } from "zod/v4";
|
|
10
10
|
import { log, logToolCall } from "../logger.js";
|
|
11
11
|
import { recordToolCall } from "../metrics.js";
|
|
12
|
+
import { sanitizeServerRemediation } from "../redaction.js";
|
|
12
13
|
/* ------------------------------------------------------------------ */
|
|
13
14
|
/* Safety annotations for MCP tool registrations */
|
|
14
15
|
/* ------------------------------------------------------------------ */
|
|
@@ -43,47 +44,73 @@ export const ERROR_CONTENT_ANNOTATIONS = {
|
|
|
43
44
|
/* Response size guard */
|
|
44
45
|
/* ------------------------------------------------------------------ */
|
|
45
46
|
const MAX_RESPONSE_CHARS = 80_000; // ~20,000 tokens safety margin
|
|
46
|
-
export function truncateResponse(text) {
|
|
47
|
+
export function truncateResponse(text, truncatedNotice = '\n\n[Response truncated. Use pagination (limit/offset) to retrieve smaller result sets.]') {
|
|
47
48
|
if (text.length <= MAX_RESPONSE_CHARS)
|
|
48
49
|
return text;
|
|
49
|
-
return text.slice(0, MAX_RESPONSE_CHARS) +
|
|
50
|
-
'\n\n[Response truncated. Use pagination (limit/offset) to retrieve smaller result sets.]';
|
|
50
|
+
return text.slice(0, MAX_RESPONSE_CHARS) + truncatedNotice;
|
|
51
51
|
}
|
|
52
52
|
function isFrihetApiError(error) {
|
|
53
53
|
return (typeof error === "object" &&
|
|
54
54
|
error !== null &&
|
|
55
55
|
"statusCode" in error &&
|
|
56
56
|
"errorCode" in error &&
|
|
57
|
-
typeof error.statusCode === "number"
|
|
57
|
+
typeof error.statusCode === "number" &&
|
|
58
|
+
typeof error.errorCode === "string" &&
|
|
59
|
+
typeof error.message === "string");
|
|
58
60
|
}
|
|
61
|
+
const FORBIDDEN_FRIENDLY_MESSAGE = "Access denied. Reconnect Frihet or use configured credentials with permission for this action. / Acceso denegado. Vuelve a conectar Frihet o usa credenciales con permiso para esta accion.";
|
|
59
62
|
/**
|
|
60
63
|
* Maps an error to a user-friendly MCP tool response with error annotations.
|
|
61
64
|
* Emits structured log entries for all errors.
|
|
62
65
|
*/
|
|
63
66
|
export function handleToolError(error, toolName) {
|
|
64
67
|
if (isFrihetApiError(error)) {
|
|
68
|
+
const loggedMessage = error.statusCode === 403 ? "Forbidden" : error.message;
|
|
69
|
+
const loggedCode = error.statusCode === 403 ? "forbidden" : error.errorCode;
|
|
65
70
|
log({
|
|
66
71
|
level: "error",
|
|
67
|
-
message: `API error: ${error.statusCode} ${
|
|
72
|
+
message: `API error: ${error.statusCode} ${loggedCode}: ${loggedMessage}`,
|
|
68
73
|
tool: toolName,
|
|
69
74
|
operation: "tool_error",
|
|
70
75
|
error: {
|
|
71
|
-
message:
|
|
72
|
-
code:
|
|
76
|
+
message: loggedMessage,
|
|
77
|
+
code: loggedCode,
|
|
73
78
|
statusCode: error.statusCode,
|
|
74
79
|
},
|
|
75
80
|
});
|
|
76
81
|
const messages = {
|
|
77
82
|
400: "Bad request. Check your input parameters. / Solicitud incorrecta. Revisa los parametros.",
|
|
78
|
-
401: "Authentication failed.
|
|
79
|
-
403:
|
|
83
|
+
401: "Authentication failed. Reconnect Frihet or check the configured credentials. / Autenticacion fallida. Vuelve a conectar Frihet o revisa las credenciales configuradas.",
|
|
84
|
+
403: FORBIDDEN_FRIENDLY_MESSAGE,
|
|
80
85
|
404: "Resource not found. / Recurso no encontrado.",
|
|
81
86
|
405: "Method not allowed. / Metodo no permitido.",
|
|
82
87
|
413: "Request body too large (max 1MB). / Cuerpo de la solicitud demasiado grande (max 1MB).",
|
|
83
88
|
429: "Rate limit exceeded. Try again later. / Limite de peticiones excedido. Intenta mas tarde.",
|
|
84
89
|
500: "Internal server error. Try again later. / Error interno del servidor.",
|
|
90
|
+
503: "Frihet could not confirm the request outcome because the network connection failed.",
|
|
85
91
|
};
|
|
86
|
-
const
|
|
92
|
+
const transportOutcomeUnknown = error.errorCode === "request_timeout" || error.errorCode === "network_error";
|
|
93
|
+
const operationOutcomeUnknown = transportOutcomeUnknown
|
|
94
|
+
|| error.errorCode === "response_timeout"
|
|
95
|
+
|| error.errorCode === "invalid_response_after_success"
|
|
96
|
+
|| (error.statusCode >= 500 && error.statusCode <= 599)
|
|
97
|
+
|| (error.statusCode === 409
|
|
98
|
+
&& error.errorCode.toUpperCase() === "IDEMPOTENCY_REQUEST_IN_PROGRESS");
|
|
99
|
+
let friendlyMessage;
|
|
100
|
+
if (error.statusCode === 413 && error.errorCode === "payload_too_large") {
|
|
101
|
+
friendlyMessage = `Document response too large: ${error.message} / Respuesta de documento demasiado grande.`;
|
|
102
|
+
}
|
|
103
|
+
else if (error.statusCode === 403) {
|
|
104
|
+
const guidance = sanitizeServerRemediation(error.detail, error.errorCode)
|
|
105
|
+
?? sanitizeServerRemediation(error.message, error.errorCode);
|
|
106
|
+
friendlyMessage = guidance
|
|
107
|
+
? `${FORBIDDEN_FRIENDLY_MESSAGE} Server guidance: ${guidance}`
|
|
108
|
+
: FORBIDDEN_FRIENDLY_MESSAGE;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
friendlyMessage = messages[error.statusCode]
|
|
112
|
+
?? `API error ${error.statusCode}. Contact support if this persists.`;
|
|
113
|
+
}
|
|
87
114
|
return {
|
|
88
115
|
content: [
|
|
89
116
|
{
|
|
@@ -93,6 +120,16 @@ export function handleToolError(error, toolName) {
|
|
|
93
120
|
},
|
|
94
121
|
],
|
|
95
122
|
isError: true,
|
|
123
|
+
...(operationOutcomeUnknown
|
|
124
|
+
? {
|
|
125
|
+
_meta: {
|
|
126
|
+
"io.frihet/operationOutcomeUnknown": true,
|
|
127
|
+
...(transportOutcomeUnknown
|
|
128
|
+
? { "io.frihet/transportOutcomeUnknown": true }
|
|
129
|
+
: {}),
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
96
133
|
};
|
|
97
134
|
}
|
|
98
135
|
const errMsg = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
@@ -132,6 +169,15 @@ export function formatPaginatedResponse(resourceName, response) {
|
|
|
132
169
|
}
|
|
133
170
|
return truncateResponse(lines.join("\n"));
|
|
134
171
|
}
|
|
172
|
+
/** Format an exhaustive list whose backend exposes no pagination controls. */
|
|
173
|
+
export function formatUnpaginatedListResponse(resourceName, response) {
|
|
174
|
+
const lines = [`Found ${response.total} ${resourceName}:`, ""];
|
|
175
|
+
for (const item of response.data) {
|
|
176
|
+
lines.push(JSON.stringify(item, null, 2));
|
|
177
|
+
lines.push("---");
|
|
178
|
+
}
|
|
179
|
+
return truncateResponse(lines.join("\n"), "\n\n[Response truncated.]");
|
|
180
|
+
}
|
|
135
181
|
/**
|
|
136
182
|
* Formats a single record for display.
|
|
137
183
|
*/
|
|
@@ -157,11 +203,22 @@ export function mutateContent(text) {
|
|
|
157
203
|
return { type: "text", text, annotations: MUTATE_CONTENT_ANNOTATIONS };
|
|
158
204
|
}
|
|
159
205
|
/* ------------------------------------------------------------------ */
|
|
160
|
-
/* Contextual enrichment —
|
|
206
|
+
/* Contextual enrichment — text appended to `content`, not structuredContent */
|
|
161
207
|
/* ------------------------------------------------------------------ */
|
|
162
208
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
209
|
+
* Returns agent-facing suggestions and warnings for a tool call, formatted
|
|
210
|
+
* as plain text to be APPENDED to the human-readable `content` block.
|
|
211
|
+
*
|
|
212
|
+
* Why text, not structured fields: `structuredContent` is the strict
|
|
213
|
+
* outputSchema contract. Spreading `{ _suggestions, _warnings }` into
|
|
214
|
+
* structuredContent would make them undeclared enrichment (Inspector /
|
|
215
|
+
* conformance failures, drift in the OpenAI reviewed surface's frozen
|
|
216
|
+
* snapshot). The hints are agent-facing and deterministic from the
|
|
217
|
+
* emitted data, so they belong in the text the model reads, not in the
|
|
218
|
+
* JSON the schema validates. Pinned by
|
|
219
|
+
* src/__tests__/paginated-strict-output.test.ts (ERP #1580): the strict
|
|
220
|
+
* contract test asserts structuredContent never carries the old
|
|
221
|
+
* `_suggestions` / `_warnings` keys.
|
|
165
222
|
*/
|
|
166
223
|
export function enrichResponse(resource, operation, data) {
|
|
167
224
|
const suggestions = [];
|
|
@@ -237,10 +294,20 @@ export function enrichResponse(resource, operation, data) {
|
|
|
237
294
|
if (operation === "delete") {
|
|
238
295
|
warnings.push("This action cannot be undone / Esta accion no se puede deshacer");
|
|
239
296
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
297
|
+
if (suggestions.length === 0 && warnings.length === 0)
|
|
298
|
+
return "";
|
|
299
|
+
const lines = [];
|
|
300
|
+
if (warnings.length > 0) {
|
|
301
|
+
lines.push("Warnings:");
|
|
302
|
+
for (const w of warnings)
|
|
303
|
+
lines.push(` - ${w}`);
|
|
304
|
+
}
|
|
305
|
+
if (suggestions.length > 0) {
|
|
306
|
+
lines.push("Suggested next steps:");
|
|
307
|
+
for (const s of suggestions)
|
|
308
|
+
lines.push(` - ${s}`);
|
|
309
|
+
}
|
|
310
|
+
return "\n" + lines.join("\n");
|
|
244
311
|
}
|
|
245
312
|
/* ------------------------------------------------------------------ */
|
|
246
313
|
/* Output schemas (MCP spec 2025-11-25: outputSchema + structuredContent) */
|
|
@@ -268,6 +335,15 @@ export function paginatedOutput(itemSchema, opts = {}) {
|
|
|
268
335
|
// so they keep the item schema's own required contract (an `{}` row there is
|
|
269
336
|
// an API regression the schema must catch, not tolerate).
|
|
270
337
|
const rowSchema = opts.projectable ? itemSchema.partial() : itemSchema;
|
|
338
|
+
// Canonical envelope contract: the emitted `structuredContent` of every
|
|
339
|
+
// paginated tool is exactly this object. The MCP SDK validates the runtime
|
|
340
|
+
// value against this schema on every call; anything not declared here is
|
|
341
|
+
// an Inspector/conformance failure. Pinned by
|
|
342
|
+
// src/__tests__/paginated-strict-output.test.ts (ERP #1580). The
|
|
343
|
+
// OpenAI reviewed surface's frozen snapshot (`openai-review-descriptor`)
|
|
344
|
+
// is a byte contract on this shape, so adding a field here would drift
|
|
345
|
+
// the snapshot — the right place for agent-facing hints is the `content`
|
|
346
|
+
// text block, not `structuredContent` (see enrichResponse).
|
|
271
347
|
return z.object({
|
|
272
348
|
data: z.array(rowSchema),
|
|
273
349
|
total: z.number(),
|
|
@@ -281,6 +357,32 @@ export const deleteResultOutput = z.object({
|
|
|
281
357
|
success: z.boolean(),
|
|
282
358
|
id: z.string(),
|
|
283
359
|
});
|
|
360
|
+
/**
|
|
361
|
+
* Schema for deletes of FISCAL DOCUMENTS (invoices, quotes) — the only two
|
|
362
|
+
* resources where "delete" is not always a delete.
|
|
363
|
+
*
|
|
364
|
+
* The backend refuses to destroy a non-draft invoice/quote (VeriFactu hash
|
|
365
|
+
* chain) and soft-CANCELS it instead, distinguishing the outcomes on the wire:
|
|
366
|
+
* 204 = row destroyed, 200 + body = document kept with status=cancelled
|
|
367
|
+
* (erp-main publicApi.ts `deleteResource`). `outcome` is what lets an agent
|
|
368
|
+
* tell the user which one actually happened (GAP-12).
|
|
369
|
+
*
|
|
370
|
+
* Deliberately NOT folded into {@link deleteResultOutput}: that one is shared by
|
|
371
|
+
* nine non-fiscal delete tools whose backend has no cancel branch, and widening
|
|
372
|
+
* it would drift their published output schema for no behavioural reason.
|
|
373
|
+
*/
|
|
374
|
+
export const documentDeleteResultOutput = z.object({
|
|
375
|
+
success: z.boolean(),
|
|
376
|
+
id: z.string(),
|
|
377
|
+
/** "deleted" = row destroyed (204). "cancelled" = document kept, status=cancelled (200). */
|
|
378
|
+
outcome: z.enum(["deleted", "cancelled"]).optional(),
|
|
379
|
+
/** Document status after a soft cancel (always "cancelled" when present). */
|
|
380
|
+
status: z.string().optional(),
|
|
381
|
+
/** Status the document held before it was cancelled (e.g. "sent", "paid"). */
|
|
382
|
+
previousStatus: z.string().optional(),
|
|
383
|
+
/** Backend path that performed the soft cancellation, when reported. */
|
|
384
|
+
cancelledVia: z.string().optional(),
|
|
385
|
+
});
|
|
284
386
|
/**
|
|
285
387
|
* Permissive structured-output schema for tools whose API returns an open
|
|
286
388
|
* `Record<string, unknown>` (summaries, single-object actions, VIES/portal
|
|
@@ -296,9 +398,12 @@ export function openObjectOutput(description) {
|
|
|
296
398
|
}
|
|
297
399
|
/* --- Per-resource item schemas ------------------------------------ */
|
|
298
400
|
const lineItemSchema = z.object({
|
|
401
|
+
id: z.string().optional(),
|
|
299
402
|
description: z.string(),
|
|
300
403
|
quantity: z.number(),
|
|
301
404
|
unitPrice: z.number(),
|
|
405
|
+
taxRate: z.number().optional(),
|
|
406
|
+
discount: z.number().optional(),
|
|
302
407
|
});
|
|
303
408
|
export const invoiceItemOutput = z.object({
|
|
304
409
|
id: z.string(),
|
|
@@ -306,6 +411,7 @@ export const invoiceItemOutput = z.object({
|
|
|
306
411
|
// draft — verified live. Kept documented but non-required so get/create/update
|
|
307
412
|
// of a draft or a client-less invoice validates. `id` stays required: every
|
|
308
413
|
// single-object read/create/update returns the doc id.
|
|
414
|
+
clientId: z.string().optional(),
|
|
309
415
|
clientName: z.string().nullable().optional(),
|
|
310
416
|
items: z.array(lineItemSchema).optional(),
|
|
311
417
|
issueDate: z.string().optional(),
|
|
@@ -313,6 +419,7 @@ export const invoiceItemOutput = z.object({
|
|
|
313
419
|
status: z.string().optional(),
|
|
314
420
|
notes: z.string().optional(),
|
|
315
421
|
taxRate: z.number().optional(),
|
|
422
|
+
discountRate: z.number().optional(),
|
|
316
423
|
total: z.number().optional(),
|
|
317
424
|
createdAt: z.string().optional(),
|
|
318
425
|
updatedAt: z.string().optional(),
|
|
@@ -323,8 +430,10 @@ export const expenseItemOutput = z.object({
|
|
|
323
430
|
amount: z.number(),
|
|
324
431
|
category: z.string().optional(),
|
|
325
432
|
date: z.string().optional(),
|
|
433
|
+
vendorId: z.string().optional(),
|
|
326
434
|
vendor: z.string().optional(),
|
|
327
435
|
taxDeductible: z.boolean().optional(),
|
|
436
|
+
paidDate: z.string().optional(),
|
|
328
437
|
createdAt: z.string().optional(),
|
|
329
438
|
updatedAt: z.string().optional(),
|
|
330
439
|
}).passthrough();
|
|
@@ -340,6 +449,7 @@ export const clientItemOutput = z.object({
|
|
|
340
449
|
name: z.string(),
|
|
341
450
|
email: z.string().optional(),
|
|
342
451
|
phone: z.string().optional(),
|
|
452
|
+
stage: z.string().optional(),
|
|
343
453
|
taxId: z.string().optional(),
|
|
344
454
|
address: addressOutputSchema,
|
|
345
455
|
createdAt: z.string().optional(),
|
|
@@ -351,18 +461,24 @@ export const productItemOutput = z.object({
|
|
|
351
461
|
unitPrice: z.number(),
|
|
352
462
|
description: z.string().optional(),
|
|
353
463
|
taxRate: z.number().optional(),
|
|
464
|
+
isActive: z.boolean().optional(),
|
|
354
465
|
createdAt: z.string().optional(),
|
|
355
466
|
updatedAt: z.string().optional(),
|
|
356
467
|
}).passthrough();
|
|
357
468
|
export const quoteItemOutput = z.object({
|
|
358
469
|
id: z.string(),
|
|
359
470
|
// Same draft/client-less tolerance as invoices (shared form engine).
|
|
471
|
+
clientId: z.string().optional(),
|
|
360
472
|
clientName: z.string().nullable().optional(),
|
|
361
473
|
items: z.array(lineItemSchema).optional(),
|
|
474
|
+
issueDate: z.string().optional(),
|
|
475
|
+
dueDate: z.string().optional(),
|
|
362
476
|
validUntil: z.string().optional(),
|
|
363
477
|
notes: z.string().optional(),
|
|
364
478
|
status: z.string().optional(),
|
|
365
479
|
total: z.number().optional(),
|
|
480
|
+
taxRate: z.number().optional(),
|
|
481
|
+
discountRate: z.number().optional(),
|
|
366
482
|
createdAt: z.string().optional(),
|
|
367
483
|
updatedAt: z.string().optional(),
|
|
368
484
|
}).passthrough();
|
|
@@ -378,13 +494,31 @@ export const vendorItemOutput = z.object({
|
|
|
378
494
|
}).passthrough();
|
|
379
495
|
export const webhookItemOutput = z.object({
|
|
380
496
|
id: z.string(),
|
|
497
|
+
userId: z.string().optional(),
|
|
498
|
+
name: z.string(),
|
|
381
499
|
url: z.string(),
|
|
382
500
|
events: z.array(z.string()),
|
|
383
|
-
|
|
384
|
-
|
|
501
|
+
status: z.enum(["active", "inactive", "paused"]),
|
|
502
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
503
|
+
hasSecret: z.boolean(),
|
|
504
|
+
pausedReason: z.string().optional(),
|
|
505
|
+
lastTriggeredAt: z.string().optional(),
|
|
385
506
|
createdAt: z.string().optional(),
|
|
386
507
|
updatedAt: z.string().optional(),
|
|
387
|
-
|
|
508
|
+
_demo: z.literal(true).optional(),
|
|
509
|
+
_demoNotice: z.string().optional(),
|
|
510
|
+
}).strict();
|
|
511
|
+
/** Create alone may echo the caller-supplied signing secret once. */
|
|
512
|
+
export const webhookCreateOutput = webhookItemOutput.extend({
|
|
513
|
+
secret: z.string().optional(),
|
|
514
|
+
}).strict();
|
|
515
|
+
/** `/webhooks` has no backend pagination contract. */
|
|
516
|
+
export const webhookListOutput = z.object({
|
|
517
|
+
data: z.array(webhookItemOutput),
|
|
518
|
+
total: z.number(),
|
|
519
|
+
_demo: z.literal(true).optional(),
|
|
520
|
+
_demoNotice: z.string().optional(),
|
|
521
|
+
}).strict();
|
|
388
522
|
/* --- CRM subcollection item schemas -------------------------------- */
|
|
389
523
|
export const contactItemOutput = z.object({
|
|
390
524
|
id: z.string(),
|
|
@@ -401,10 +535,14 @@ export const activityItemOutput = z.object({
|
|
|
401
535
|
type: z.string(),
|
|
402
536
|
title: z.string(),
|
|
403
537
|
description: z.string().optional(),
|
|
404
|
-
|
|
538
|
+
metadata: z.record(z.string(), z.string()).optional(),
|
|
539
|
+
timestamp: z.string(),
|
|
540
|
+
createdBy: z.enum(["system", "user"]).optional(),
|
|
405
541
|
createdAt: z.string().optional(),
|
|
406
542
|
updatedAt: z.string().optional(),
|
|
407
|
-
|
|
543
|
+
_demo: z.literal(true).optional(),
|
|
544
|
+
_demoNotice: z.string().optional(),
|
|
545
|
+
}).strict();
|
|
408
546
|
export const noteItemOutput = z.object({
|
|
409
547
|
id: z.string(),
|
|
410
548
|
content: z.string(),
|
|
@@ -543,6 +681,7 @@ export const actionResultOutput = z.object({
|
|
|
543
681
|
success: z.boolean().optional(),
|
|
544
682
|
id: z.string().optional(),
|
|
545
683
|
message: z.string().optional(),
|
|
684
|
+
messageId: z.string().optional(),
|
|
546
685
|
// Anti-envelope tripwire: with everything optional + passthrough, a raw
|
|
547
686
|
// {data, meta} API envelope would otherwise VALIDATE — silently re-hiding the
|
|
548
687
|
// exact bug this PR fixes if a client method ever regresses to raw request().
|
|
@@ -568,6 +707,9 @@ export const creditNoteResultOutput = z.object({
|
|
|
568
707
|
originalInvoiceId: z.string().optional(),
|
|
569
708
|
reason: z.string().optional(),
|
|
570
709
|
fullCredit: z.boolean().optional(),
|
|
710
|
+
status: z.string().optional(),
|
|
711
|
+
rectificationMethod: z.string().optional(),
|
|
712
|
+
totalCredited: z.number().optional(),
|
|
571
713
|
}).passthrough().optional(),
|
|
572
714
|
// Anti-envelope tripwire — see actionResultOutput.
|
|
573
715
|
data: z.never().optional(),
|
|
@@ -729,11 +871,32 @@ export const recurringInvoiceItemOutput = z.object({
|
|
|
729
871
|
createdAt: z.string().optional(),
|
|
730
872
|
updatedAt: z.string().optional(),
|
|
731
873
|
}).passthrough();
|
|
732
|
-
/**
|
|
874
|
+
/**
|
|
875
|
+
* Schema for the MIME-discriminated `get_invoice_einvoice` artifact.
|
|
876
|
+
*
|
|
877
|
+
* Stored UBL/Facturae artifacts return strict UTF-8 `xml`; Factur-X artifacts
|
|
878
|
+
* return PDF bytes as `base64`. Identity, MIME and byte size are always present.
|
|
879
|
+
*/
|
|
880
|
+
export const einvoiceResultOutput = z.object({
|
|
881
|
+
id: z.string(),
|
|
882
|
+
contentType: z.string(),
|
|
883
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
884
|
+
xml: z.string().optional(),
|
|
885
|
+
base64: z.string().optional(),
|
|
886
|
+
filename: z.string().optional(),
|
|
887
|
+
}).passthrough().refine((value) => (typeof value.xml === "string") !== (typeof value.base64 === "string"), { message: "Exactly one of xml or base64 is required" });
|
|
888
|
+
/**
|
|
889
|
+
* Schema for `get_invoice_pdf` results (#1393 — content-type-aware).
|
|
890
|
+
*
|
|
891
|
+
* The backend serves raw `application/pdf` bytes. MCP `structuredContent` is
|
|
892
|
+
* JSON-only, so callers receive bounded base64 plus request identity and size.
|
|
893
|
+
*/
|
|
733
894
|
export const pdfResultOutput = z.object({
|
|
734
895
|
id: z.string(),
|
|
735
|
-
|
|
736
|
-
|
|
896
|
+
contentType: z.string(),
|
|
897
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
898
|
+
base64: z.string(),
|
|
899
|
+
filename: z.string().optional(),
|
|
737
900
|
}).passthrough();
|
|
738
901
|
/* --- Time summary schema --------------------------------------------------- */
|
|
739
902
|
const timeSummaryGroupItemSchema = z.object({
|
|
@@ -756,14 +919,15 @@ export const timeSummaryOutput = z.object({
|
|
|
756
919
|
/* --- Team member item schema ----------------------------------------------- */
|
|
757
920
|
export const teamMemberItemOutput = z.object({
|
|
758
921
|
id: z.string(),
|
|
759
|
-
name: z.string().optional(),
|
|
760
|
-
email: z.string(),
|
|
761
|
-
role: z.enum(["owner", "admin", "
|
|
922
|
+
name: z.string().nullable().optional(),
|
|
923
|
+
email: z.string().nullable().optional(),
|
|
924
|
+
role: z.enum(["owner", "admin", "editor", "accountant", "viewer"]).optional(),
|
|
762
925
|
status: z.enum(["active", "pending"]).optional(),
|
|
763
|
-
invitedAt: z.string().optional(),
|
|
764
|
-
joinedAt: z.string().optional(),
|
|
765
|
-
createdAt: z.string().optional(),
|
|
766
|
-
updatedAt: z.string().optional(),
|
|
926
|
+
invitedAt: z.string().nullable().optional(),
|
|
927
|
+
joinedAt: z.string().nullable().optional(),
|
|
928
|
+
createdAt: z.string().nullable().optional(),
|
|
929
|
+
updatedAt: z.string().nullable().optional(),
|
|
930
|
+
expiresAt: z.string().nullable().optional(),
|
|
767
931
|
}).passthrough();
|
|
768
932
|
/* --- Gestoria item schemas ------------------------------------------------- */
|
|
769
933
|
/**
|
|
@@ -885,19 +1049,40 @@ export const attendanceEntryItemOutput = z.object({
|
|
|
885
1049
|
createdAt: z.string().optional(),
|
|
886
1050
|
updatedAt: z.string().optional(),
|
|
887
1051
|
}).passthrough();
|
|
888
|
-
/**
|
|
1052
|
+
/** Exact ERP DTO for `GET /v1/time-entries/overtime`. */
|
|
889
1053
|
export const overtimeReportOutput = z.object({
|
|
890
|
-
period: z.string(),
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1054
|
+
period: z.string().regex(/^\d{4}(-(0[1-9]|1[0-2]))?$/),
|
|
1055
|
+
employeeId: z.string().nullable(),
|
|
1056
|
+
recordCount: z.number().int().nonnegative(),
|
|
1057
|
+
dailyOvertime: z.array(z.object({
|
|
1058
|
+
date: z.string(),
|
|
1059
|
+
minutes: z.number().nonnegative(),
|
|
1060
|
+
exceedsDaily: z.boolean(),
|
|
1061
|
+
})),
|
|
1062
|
+
weeklyOvertime: z.array(z.object({
|
|
1063
|
+
weekStart: z.string(),
|
|
1064
|
+
totalMinutes: z.number().nonnegative(),
|
|
1065
|
+
overtimeMinutes: z.number().nonnegative(),
|
|
1066
|
+
})),
|
|
1067
|
+
monthlyTotal: z.object({
|
|
1068
|
+
workedMinutes: z.number().nonnegative(),
|
|
1069
|
+
overtimeMinutes: z.number().nonnegative(),
|
|
1070
|
+
regularMinutes: z.number().nonnegative(),
|
|
1071
|
+
}),
|
|
1072
|
+
annualOvertimeHours: z.number().nonnegative(),
|
|
1073
|
+
alerts: z.array(z.object({
|
|
1074
|
+
type: z.enum(["daily_exceeded", "weekly_exceeded", "annual_approaching", "annual_exceeded", "missing_break"]),
|
|
1075
|
+
severity: z.enum(["warning", "critical"]),
|
|
1076
|
+
message: z.string(),
|
|
1077
|
+
date: z.string(),
|
|
1078
|
+
})),
|
|
1079
|
+
data: z.never().optional(),
|
|
1080
|
+
meta: z.never().optional(),
|
|
1081
|
+
totalRegularHours: z.never().optional(),
|
|
1082
|
+
totalOvertimeHours: z.never().optional(),
|
|
1083
|
+
estimatedCostEur: z.never().optional(),
|
|
1084
|
+
byEmployee: z.never().optional(),
|
|
1085
|
+
generatedAt: z.never().optional(),
|
|
901
1086
|
}).passthrough();
|
|
902
1087
|
/** Anomaly detection record — backend `/v1/anomalies`. */
|
|
903
1088
|
export const anomalyItemOutput = z.object({
|
|
@@ -922,28 +1107,60 @@ export const webhookTestResultOutput = z.object({
|
|
|
922
1107
|
attemptedAt: z.string().optional(),
|
|
923
1108
|
error: z.string().optional(),
|
|
924
1109
|
}).passthrough();
|
|
925
|
-
/**
|
|
1110
|
+
/** Exact normalized dataset DTO from `GET /v1/payroll/prep/export`. */
|
|
926
1111
|
export const payrollExportOutput = z.object({
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1112
|
+
month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/),
|
|
1113
|
+
format: z.enum(["a3", "contasol", "sage", "siltra"]),
|
|
1114
|
+
employees: z.array(z.object({
|
|
1115
|
+
id: z.string(),
|
|
1116
|
+
name: z.string(),
|
|
1117
|
+
nss: z.string(),
|
|
1118
|
+
irpfPct: z.number(),
|
|
1119
|
+
salaryGrossAnnual: z.number(),
|
|
1120
|
+
convenioColectivo: z.string().nullable(),
|
|
1121
|
+
categoriaProfesional: z.string().nullable(),
|
|
1122
|
+
prorrateoPagasExtras: z.boolean().nullable(),
|
|
1123
|
+
formaPago: z.enum(["transferencia", "efectivo", "cheque"]),
|
|
1124
|
+
iban: z.string().nullable(),
|
|
1125
|
+
})),
|
|
1126
|
+
summary: z.object({
|
|
1127
|
+
exportedCount: z.number().int().nonnegative(),
|
|
1128
|
+
skippedNotReady: z.number().int().nonnegative(),
|
|
1129
|
+
totalGrossAnnual: z.number(),
|
|
1130
|
+
}),
|
|
1131
|
+
data: z.never().optional(),
|
|
1132
|
+
meta: z.never().optional(),
|
|
1133
|
+
fileUrl: z.never().optional(),
|
|
1134
|
+
filename: z.never().optional(),
|
|
1135
|
+
rowCount: z.never().optional(),
|
|
1136
|
+
generatedAt: z.never().optional(),
|
|
933
1137
|
}).passthrough();
|
|
934
|
-
/**
|
|
1138
|
+
/** Exact readiness DTO from `GET /v1/payroll/prep/employees`. */
|
|
935
1139
|
export const payrollChecklistOutput = z.object({
|
|
936
|
-
month: z.string(),
|
|
937
|
-
totalEmployees: z.number().int().optional(),
|
|
938
|
-
readyEmployees: z.number().int().optional(),
|
|
939
|
-
missingEmployees: z.number().int().optional(),
|
|
1140
|
+
month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/),
|
|
940
1141
|
employees: z.array(z.object({
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
status: z.enum(["
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1142
|
+
id: z.string(),
|
|
1143
|
+
name: z.string(),
|
|
1144
|
+
status: z.enum(["active", "onLeave"]),
|
|
1145
|
+
hasPayrollProfile: z.boolean(),
|
|
1146
|
+
ready: z.boolean(),
|
|
1147
|
+
missingFields: z.array(z.enum(["nss", "irpfPct", "salaryGrossAnnual", "formaPago", "iban"])),
|
|
1148
|
+
reviewedForMonth: z.string().nullable(),
|
|
1149
|
+
reviewedThisMonth: z.boolean(),
|
|
1150
|
+
reviewedAt: z.string().nullable(),
|
|
1151
|
+
})),
|
|
1152
|
+
summary: z.object({
|
|
1153
|
+
total: z.number().int().nonnegative(),
|
|
1154
|
+
ready: z.number().int().nonnegative(),
|
|
1155
|
+
notReady: z.number().int().nonnegative(),
|
|
1156
|
+
reviewedThisMonth: z.number().int().nonnegative(),
|
|
1157
|
+
}),
|
|
1158
|
+
data: z.never().optional(),
|
|
1159
|
+
meta: z.never().optional(),
|
|
1160
|
+
totalEmployees: z.never().optional(),
|
|
1161
|
+
readyEmployees: z.never().optional(),
|
|
1162
|
+
missingEmployees: z.never().optional(),
|
|
1163
|
+
generatedAt: z.never().optional(),
|
|
947
1164
|
}).passthrough();
|
|
948
1165
|
/** Onboarding workspace state — backend `/v1/onboarding/status`. */
|
|
949
1166
|
export const onboardingStatusOutput = z.object({
|
|
@@ -961,23 +1178,82 @@ export const onboardingPersonaResultOutput = z.object({
|
|
|
961
1178
|
persona: z.enum(["autonomo", "empresa", "agencia", "gestoria"]),
|
|
962
1179
|
updatedAt: z.string().optional(),
|
|
963
1180
|
}).passthrough();
|
|
964
|
-
|
|
1181
|
+
const rbacRoleOutput = z.enum(["owner", "admin", "manager", "sales", "accountant", "employee", "viewer"]);
|
|
1182
|
+
const rbacResourceOutput = z.enum([
|
|
1183
|
+
"workspace", "invoices", "quotes", "expenses", "clients", "products", "accounting",
|
|
1184
|
+
"people", "payroll", "integrations", "banking", "settings", "audit_log", "billing",
|
|
1185
|
+
]);
|
|
1186
|
+
const rbacActionOutput = z.enum(["read", "create", "update", "delete", "bulk_delete", "export"]);
|
|
1187
|
+
const rbacActionsOutput = z.array(rbacActionOutput);
|
|
1188
|
+
// Matrix rows intentionally omit denied resources. Keep every known resource
|
|
1189
|
+
// explicit and optional so sparse backend rows validate consistently.
|
|
1190
|
+
const rbacResourceGrantsOutput = z.object({
|
|
1191
|
+
workspace: rbacActionsOutput.optional(),
|
|
1192
|
+
invoices: rbacActionsOutput.optional(),
|
|
1193
|
+
quotes: rbacActionsOutput.optional(),
|
|
1194
|
+
expenses: rbacActionsOutput.optional(),
|
|
1195
|
+
clients: rbacActionsOutput.optional(),
|
|
1196
|
+
products: rbacActionsOutput.optional(),
|
|
1197
|
+
accounting: rbacActionsOutput.optional(),
|
|
1198
|
+
people: rbacActionsOutput.optional(),
|
|
1199
|
+
payroll: rbacActionsOutput.optional(),
|
|
1200
|
+
integrations: rbacActionsOutput.optional(),
|
|
1201
|
+
banking: rbacActionsOutput.optional(),
|
|
1202
|
+
settings: rbacActionsOutput.optional(),
|
|
1203
|
+
audit_log: rbacActionsOutput.optional(),
|
|
1204
|
+
billing: rbacActionsOutput.optional(),
|
|
1205
|
+
}).strict();
|
|
1206
|
+
const rbacMatrixOutput = z.object({
|
|
1207
|
+
owner: rbacResourceGrantsOutput,
|
|
1208
|
+
admin: rbacResourceGrantsOutput,
|
|
1209
|
+
manager: rbacResourceGrantsOutput,
|
|
1210
|
+
sales: rbacResourceGrantsOutput,
|
|
1211
|
+
accountant: rbacResourceGrantsOutput,
|
|
1212
|
+
employee: rbacResourceGrantsOutput,
|
|
1213
|
+
viewer: rbacResourceGrantsOutput,
|
|
1214
|
+
}).strict();
|
|
1215
|
+
/**
|
|
1216
|
+
* Documented RBAC-model snapshot — backend `/v1/permissions/matrix`.
|
|
1217
|
+
* This is not an exhaustive report of runtime authorization or Firestore rules.
|
|
1218
|
+
*/
|
|
965
1219
|
export const permissionsMatrixOutput = z.object({
|
|
966
|
-
roles: z.array(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1220
|
+
roles: z.array(rbacRoleOutput).describe("Role names represented by this documented RBAC model"),
|
|
1221
|
+
resources: z.array(rbacResourceOutput).describe("Resource names represented by this documented RBAC model"),
|
|
1222
|
+
actions: z.array(rbacActionOutput).describe("Action names represented by this documented RBAC model"),
|
|
1223
|
+
legacyAliases: z.record(z.string(), rbacRoleOutput).describe("Legacy role name to canonical RBAC-model role"),
|
|
1224
|
+
matrix: rbacMatrixOutput
|
|
1225
|
+
.describe("Documented role/resource/action grants; not a runtime authorization guarantee"),
|
|
1226
|
+
source: z.string().describe("Backend explanation of the model's source and limitations"),
|
|
972
1227
|
}).passthrough();
|
|
973
|
-
/**
|
|
1228
|
+
/**
|
|
1229
|
+
* Caller's RBAC-model row and API-key scope state — backend `/v1/permissions/me`.
|
|
1230
|
+
* Known denials are intentionally non-exhaustive; a backend 403 is authoritative.
|
|
1231
|
+
*/
|
|
974
1232
|
export const permissionsMeOutput = z.object({
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1233
|
+
role: rbacRoleOutput.describe("Backwards-compatible RBAC-model role"),
|
|
1234
|
+
isOwner: z.boolean().describe("Backwards-compatible RBAC-model owner flag"),
|
|
1235
|
+
resources: rbacResourceGrantsOutput.describe("Backwards-compatible RBAC-model resource grants"),
|
|
1236
|
+
scopes: z.array(z.string()).describe("Backwards-compatible RBAC capabilities; not API-key scopes"),
|
|
1237
|
+
legacyFieldSemantics: z.object({
|
|
1238
|
+
resources: z.literal("rbacResources"),
|
|
1239
|
+
scopes: z.literal("rbacCapabilities"),
|
|
1240
|
+
}).describe("Explicit meaning of the backwards-compatible resources/scopes fields"),
|
|
1241
|
+
rbac: z.object({
|
|
1242
|
+
role: rbacRoleOutput,
|
|
1243
|
+
isOwner: z.boolean(),
|
|
1244
|
+
resources: rbacResourceGrantsOutput,
|
|
1245
|
+
capabilities: z.array(z.string()),
|
|
1246
|
+
}).describe("Documented RBAC-model view; not an exhaustive authorization prediction"),
|
|
1247
|
+
apiKeyScopes: z.array(z.string()).describe("Actual scopes stored for this API key"),
|
|
1248
|
+
apiKeyUnrestricted: z.boolean().describe("Whether empty/absent stored scopes use unrestricted-key semantics"),
|
|
1249
|
+
denied: z.object({
|
|
1250
|
+
einvoice: z.boolean(),
|
|
1251
|
+
}).describe("Known API-key-scope denials only"),
|
|
1252
|
+
deniedSemantics: z.string().describe("Backend limitation statement for denied"),
|
|
1253
|
+
notIncluded: z.array(z.enum(["featureFlags", "deployedEndpoints"]))
|
|
1254
|
+
.describe("Authorization surfaces this endpoint cannot truthfully report"),
|
|
979
1255
|
}).passthrough();
|
|
980
|
-
/**
|
|
1256
|
+
/** Legacy demo/write result retained for period_close and period_reopen only. */
|
|
981
1257
|
export const periodStatusOutput = z.object({
|
|
982
1258
|
id: z.string(),
|
|
983
1259
|
type: z.enum(["monthly", "quarterly"]).optional(),
|
|
@@ -991,6 +1267,35 @@ export const periodStatusOutput = z.object({
|
|
|
991
1267
|
createdAt: z.string().optional(),
|
|
992
1268
|
updatedAt: z.string().optional(),
|
|
993
1269
|
}).passthrough();
|
|
1270
|
+
/** Exact ERP DTO for `GET /v1/periods/current` and `GET /v1/periods/YYYY`. */
|
|
1271
|
+
export const currentPeriodOutput = z.object({
|
|
1272
|
+
fiscalYear: z.string().regex(/^\d{4}$/),
|
|
1273
|
+
fiscalYearStart: z.string().regex(/^\d{2}-\d{2}$/),
|
|
1274
|
+
status: z.enum(["open", "closed"]),
|
|
1275
|
+
dateRange: z.object({
|
|
1276
|
+
from: z.string(),
|
|
1277
|
+
to: z.string(),
|
|
1278
|
+
}),
|
|
1279
|
+
closing: z.object({
|
|
1280
|
+
status: z.string(),
|
|
1281
|
+
closedAt: z.string().nullable(),
|
|
1282
|
+
netIncome: z.number().nullable(),
|
|
1283
|
+
totalIncome: z.number().nullable(),
|
|
1284
|
+
totalExpenses: z.number().nullable(),
|
|
1285
|
+
journalEntries: z.number().nullable(),
|
|
1286
|
+
}).nullable(),
|
|
1287
|
+
data: z.never().optional(),
|
|
1288
|
+
meta: z.never().optional(),
|
|
1289
|
+
id: z.never().optional(),
|
|
1290
|
+
type: z.never().optional(),
|
|
1291
|
+
startDate: z.never().optional(),
|
|
1292
|
+
endDate: z.never().optional(),
|
|
1293
|
+
closedAt: z.never().optional(),
|
|
1294
|
+
closedBy: z.never().optional(),
|
|
1295
|
+
reopenedAt: z.never().optional(),
|
|
1296
|
+
reopenReason: z.never().optional(),
|
|
1297
|
+
generatedAt: z.never().optional(),
|
|
1298
|
+
}).passthrough();
|
|
994
1299
|
/**
|
|
995
1300
|
* Wraps a tool handler to automatically log execution time, success/failure,
|
|
996
1301
|
* and record metrics. Catches errors and routes them through handleToolError.
|
|
@@ -1014,7 +1319,12 @@ export async function withToolLogging(toolName, fn) {
|
|
|
1014
1319
|
}
|
|
1015
1320
|
catch (error) {
|
|
1016
1321
|
const durationMs = Math.round(Date.now() - startTime);
|
|
1017
|
-
|
|
1322
|
+
const errorForLog = isFrihetApiError(error) && error.statusCode === 403
|
|
1323
|
+
? Object.assign(new Error("Forbidden"), { statusCode: 403, errorCode: "forbidden" })
|
|
1324
|
+
: error instanceof Error
|
|
1325
|
+
? error
|
|
1326
|
+
: new Error(String(error));
|
|
1327
|
+
logToolCall(toolName, startTime, false, errorForLog);
|
|
1018
1328
|
recordToolCall(toolName, durationMs, false);
|
|
1019
1329
|
return handleToolError(error, toolName);
|
|
1020
1330
|
}
|