@frihet/mcp-server 1.17.0 → 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 +84 -59
- 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 +240 -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 +27 -21
- package/dist/demo-client.js +31 -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 +5 -2
- package/dist/server-composition.js +35 -5
- package/dist/tool-exposure.d.ts +6 -5
- package/dist/tool-exposure.js +15 -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 +252 -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 +17 -37
- 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 +240 -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
|
@@ -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
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* This module is used by both the local (stdio) and remote (Cloudflare Workers)
|
|
5
5
|
* MCP servers. It must NOT import concrete classes from either client — error
|
|
6
6
|
* detection uses duck-typing (checking for `statusCode`/`errorCode` properties)
|
|
7
|
-
* so it works regardless of which FrihetApiError class threw the error.
|
|
7
|
+
* so it works regardless of which FrihetApiError class threw the error. The
|
|
8
|
+
* client-owned predicate separately verifies local validation provenance.
|
|
8
9
|
*/
|
|
9
10
|
import type { ToolAnnotations, Annotations } from "@modelcontextprotocol/sdk/types.js";
|
|
10
11
|
import { z } from "zod/v4";
|
|
@@ -13,9 +14,9 @@ export declare const READ_ONLY_ANNOTATIONS: ToolAnnotations;
|
|
|
13
14
|
export declare const CREATE_ANNOTATIONS: ToolAnnotations;
|
|
14
15
|
export declare const UPDATE_ANNOTATIONS: ToolAnnotations;
|
|
15
16
|
export declare const DELETE_ANNOTATIONS: ToolAnnotations;
|
|
16
|
-
/** List operations: useful to both user and assistant for navigation
|
|
17
|
+
/** List operations: useful to both user and assistant for navigation. */
|
|
17
18
|
export declare const LIST_CONTENT_ANNOTATIONS: Annotations;
|
|
18
|
-
/** Get/read operations: useful to both
|
|
19
|
+
/** Get/read operations: useful to both user and assistant. */
|
|
19
20
|
export declare const GET_CONTENT_ANNOTATIONS: Annotations;
|
|
20
21
|
/** Mutating operations (create/update/delete): primarily for the user, highest priority. */
|
|
21
22
|
export declare const MUTATE_CONTENT_ANNOTATIONS: Annotations;
|
|
@@ -33,6 +34,7 @@ export declare function truncateResponse(text: string, truncatedNotice?: string)
|
|
|
33
34
|
*/
|
|
34
35
|
export declare function handleToolError(error: unknown, toolName?: string): {
|
|
35
36
|
content: AnnotatedTextContent[];
|
|
37
|
+
structuredContent?: Record<string, unknown>;
|
|
36
38
|
isError: true;
|
|
37
39
|
_meta?: Record<string, unknown>;
|
|
38
40
|
};
|
|
@@ -445,9 +447,9 @@ export declare const posTerminalItemOutput: z.ZodObject<{
|
|
|
445
447
|
deviceType: z.ZodOptional<z.ZodString>;
|
|
446
448
|
locationId: z.ZodOptional<z.ZodString>;
|
|
447
449
|
status: z.ZodOptional<z.ZodEnum<{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
active: "active";
|
|
451
|
+
paused: "paused";
|
|
452
|
+
retired: "retired";
|
|
451
453
|
}>>;
|
|
452
454
|
stripeReaderId: z.ZodOptional<z.ZodString>;
|
|
453
455
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
@@ -717,16 +719,18 @@ export declare const teamMemberItemOutput: z.ZodObject<{
|
|
|
717
719
|
id: z.ZodString;
|
|
718
720
|
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
719
721
|
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
720
|
-
role: z.ZodOptional<z.ZodEnum<{
|
|
722
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
721
723
|
owner: "owner";
|
|
722
724
|
admin: "admin";
|
|
723
725
|
editor: "editor";
|
|
724
726
|
accountant: "accountant";
|
|
725
727
|
viewer: "viewer";
|
|
726
|
-
}
|
|
728
|
+
}>>>;
|
|
727
729
|
status: z.ZodOptional<z.ZodEnum<{
|
|
728
730
|
active: "active";
|
|
729
731
|
pending: "pending";
|
|
732
|
+
expired: "expired";
|
|
733
|
+
invalid: "invalid";
|
|
730
734
|
}>>;
|
|
731
735
|
invitedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
732
736
|
joinedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
@@ -1017,32 +1021,6 @@ export declare const payrollChecklistOutput: z.ZodObject<{
|
|
|
1017
1021
|
missingEmployees: z.ZodOptional<z.ZodNever>;
|
|
1018
1022
|
generatedAt: z.ZodOptional<z.ZodNever>;
|
|
1019
1023
|
}, z.core.$loose>;
|
|
1020
|
-
/** Onboarding workspace state — backend `/v1/onboarding/status`. */
|
|
1021
|
-
export declare const onboardingStatusOutput: z.ZodObject<{
|
|
1022
|
-
workspaceId: z.ZodOptional<z.ZodString>;
|
|
1023
|
-
persona: z.ZodOptional<z.ZodEnum<{
|
|
1024
|
-
autonomo: "autonomo";
|
|
1025
|
-
empresa: "empresa";
|
|
1026
|
-
agencia: "agencia";
|
|
1027
|
-
gestoria: "gestoria";
|
|
1028
|
-
}>>;
|
|
1029
|
-
completedSteps: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1030
|
-
pendingSteps: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1031
|
-
percentComplete: z.ZodOptional<z.ZodNumber>;
|
|
1032
|
-
startedAt: z.ZodOptional<z.ZodString>;
|
|
1033
|
-
completedAt: z.ZodOptional<z.ZodString>;
|
|
1034
|
-
}, z.core.$loose>;
|
|
1035
|
-
/** Onboarding persona update result. */
|
|
1036
|
-
export declare const onboardingPersonaResultOutput: z.ZodObject<{
|
|
1037
|
-
workspaceId: z.ZodOptional<z.ZodString>;
|
|
1038
|
-
persona: z.ZodEnum<{
|
|
1039
|
-
autonomo: "autonomo";
|
|
1040
|
-
empresa: "empresa";
|
|
1041
|
-
agencia: "agencia";
|
|
1042
|
-
gestoria: "gestoria";
|
|
1043
|
-
}>;
|
|
1044
|
-
updatedAt: z.ZodOptional<z.ZodString>;
|
|
1045
|
-
}, z.core.$loose>;
|
|
1046
1024
|
/**
|
|
1047
1025
|
* Documented RBAC-model snapshot — backend `/v1/permissions/matrix`.
|
|
1048
1026
|
* This is not an exhaustive report of runtime authorization or Firestore rules.
|
|
@@ -1058,13 +1036,13 @@ export declare const permissionsMatrixOutput: z.ZodObject<{
|
|
|
1058
1036
|
employee: "employee";
|
|
1059
1037
|
}>>;
|
|
1060
1038
|
resources: z.ZodArray<z.ZodEnum<{
|
|
1039
|
+
invoices: "invoices";
|
|
1061
1040
|
expenses: "expenses";
|
|
1041
|
+
clients: "clients";
|
|
1042
|
+
products: "products";
|
|
1062
1043
|
banking: "banking";
|
|
1063
|
-
invoices: "invoices";
|
|
1064
1044
|
quotes: "quotes";
|
|
1065
|
-
clients: "clients";
|
|
1066
1045
|
payroll: "payroll";
|
|
1067
|
-
products: "products";
|
|
1068
1046
|
workspace: "workspace";
|
|
1069
1047
|
accounting: "accounting";
|
|
1070
1048
|
people: "people";
|
|
@@ -2225,6 +2203,8 @@ interface ToolResult {
|
|
|
2225
2203
|
structuredContent?: Record<string, unknown>;
|
|
2226
2204
|
isError?: boolean;
|
|
2227
2205
|
}
|
|
2206
|
+
/** Mark a local confirmation refusal without parsing or exposing provider text. */
|
|
2207
|
+
export declare function confirmationRequiredError(message: string): ToolResult;
|
|
2228
2208
|
/**
|
|
2229
2209
|
* Wraps a tool handler to automatically log execution time, success/failure,
|
|
2230
2210
|
* and record metrics. Catches errors and routes them through handleToolError.
|
package/dist/tools/shared.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* This module is used by both the local (stdio) and remote (Cloudflare Workers)
|
|
5
5
|
* MCP servers. It must NOT import concrete classes from either client — error
|
|
6
6
|
* detection uses duck-typing (checking for `statusCode`/`errorCode` properties)
|
|
7
|
-
* so it works regardless of which FrihetApiError class threw the error.
|
|
7
|
+
* so it works regardless of which FrihetApiError class threw the error. The
|
|
8
|
+
* client-owned predicate separately verifies local validation provenance.
|
|
8
9
|
*/
|
|
9
10
|
import { z } from "zod/v4";
|
|
10
11
|
import { log, logToolCall } from "../logger.js";
|
|
11
12
|
import { recordToolCall } from "../metrics.js";
|
|
12
13
|
import { sanitizeServerRemediation } from "../redaction.js";
|
|
14
|
+
import { isLocalInputValidationError } from "../client.js";
|
|
13
15
|
/* ------------------------------------------------------------------ */
|
|
14
16
|
/* Safety annotations for MCP tool registrations */
|
|
15
17
|
/* ------------------------------------------------------------------ */
|
|
@@ -20,15 +22,16 @@ export const DELETE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true,
|
|
|
20
22
|
/* ------------------------------------------------------------------ */
|
|
21
23
|
/* Content annotations for tool responses */
|
|
22
24
|
/* ------------------------------------------------------------------ */
|
|
23
|
-
|
|
25
|
+
// MCP permits fractional priorities, but some clients (including Codex CLI
|
|
26
|
+
// 0.153.4) fail to decode them. Omit this optional hint on read responses while
|
|
27
|
+
// retaining their audience. See https://github.com/openai/codex/issues/38979.
|
|
28
|
+
/** List operations: useful to both user and assistant for navigation. */
|
|
24
29
|
export const LIST_CONTENT_ANNOTATIONS = {
|
|
25
30
|
audience: ["user", "assistant"],
|
|
26
|
-
priority: 0.5,
|
|
27
31
|
};
|
|
28
|
-
/** Get/read operations: useful to both
|
|
32
|
+
/** Get/read operations: useful to both user and assistant. */
|
|
29
33
|
export const GET_CONTENT_ANNOTATIONS = {
|
|
30
34
|
audience: ["user", "assistant"],
|
|
31
|
-
priority: 0.7,
|
|
32
35
|
};
|
|
33
36
|
/** Mutating operations (create/update/delete): primarily for the user, highest priority. */
|
|
34
37
|
export const MUTATE_CONTENT_ANNOTATIONS = {
|
|
@@ -58,12 +61,25 @@ function isFrihetApiError(error) {
|
|
|
58
61
|
typeof error.errorCode === "string" &&
|
|
59
62
|
typeof error.message === "string");
|
|
60
63
|
}
|
|
64
|
+
/** Error class names safe to echo from the unexpected-error path. */
|
|
65
|
+
const UNEXPECTED_ERROR_CLASSES = new Set([
|
|
66
|
+
"FrihetApiError",
|
|
67
|
+
"TypeError",
|
|
68
|
+
"SyntaxError",
|
|
69
|
+
"RangeError",
|
|
70
|
+
"AbortError",
|
|
71
|
+
"ZodError",
|
|
72
|
+
]);
|
|
61
73
|
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.";
|
|
62
74
|
/**
|
|
63
75
|
* Maps an error to a user-friendly MCP tool response with error annotations.
|
|
64
76
|
* Emits structured log entries for all errors.
|
|
65
77
|
*/
|
|
66
78
|
export function handleToolError(error, toolName) {
|
|
79
|
+
// NOTE: FrihetApiError carries the backend `requestId`, but it is NOT echoed
|
|
80
|
+
// here: `requestId` is in SENSITIVE_FIELD_NAMES (src/redaction.ts), whose
|
|
81
|
+
// values must not leave the process. Surfacing it needs an explicit policy
|
|
82
|
+
// change, not a side effect of error formatting.
|
|
67
83
|
if (isFrihetApiError(error)) {
|
|
68
84
|
const loggedMessage = error.statusCode === 403 ? "Forbidden" : error.message;
|
|
69
85
|
const loggedCode = error.statusCode === 403 ? "forbidden" : error.errorCode;
|
|
@@ -96,8 +112,22 @@ export function handleToolError(error, toolName) {
|
|
|
96
112
|
|| (error.statusCode >= 500 && error.statusCode <= 599)
|
|
97
113
|
|| (error.statusCode === 409
|
|
98
114
|
&& error.errorCode.toUpperCase() === "IDEMPOTENCY_REQUEST_IN_PROGRESS");
|
|
115
|
+
// Fixed booleans only: private output profiles must preserve definitive
|
|
116
|
+
// API denial semantics without forwarding provider text, codes or IDs.
|
|
117
|
+
const definitiveDenial = !operationOutcomeUnknown
|
|
118
|
+
&& (error.statusCode === 404 || error.statusCode === 403);
|
|
119
|
+
const localValidationRejected = !operationOutcomeUnknown && error.statusCode === 400
|
|
120
|
+
&& isLocalInputValidationError(error);
|
|
121
|
+
const requestRejected = !operationOutcomeUnknown && error.statusCode === 400
|
|
122
|
+
&& !localValidationRejected;
|
|
99
123
|
let friendlyMessage;
|
|
100
|
-
if (
|
|
124
|
+
if (operationOutcomeUnknown) {
|
|
125
|
+
friendlyMessage = "Frihet could not confirm whether this request completed. Read the current record before deciding whether to retry. / Frihet no pudo confirmar si la solicitud se completó. Consulta el estado actual antes de decidir si reintentar.";
|
|
126
|
+
}
|
|
127
|
+
else if (localValidationRejected) {
|
|
128
|
+
friendlyMessage = "Input validation failed before sending the request. No action was performed. Correct the ID and input parameters before retrying. / La validación falló antes de enviar la solicitud. No se realizó ninguna acción. Corrige el ID y los parámetros antes de reintentar.";
|
|
129
|
+
}
|
|
130
|
+
else if (error.statusCode === 413 && error.errorCode === "payload_too_large") {
|
|
101
131
|
friendlyMessage = `Document response too large: ${error.message} / Respuesta de documento demasiado grande.`;
|
|
102
132
|
}
|
|
103
133
|
else if (error.statusCode === 403) {
|
|
@@ -120,10 +150,24 @@ export function handleToolError(error, toolName) {
|
|
|
120
150
|
},
|
|
121
151
|
],
|
|
122
152
|
isError: true,
|
|
123
|
-
...(operationOutcomeUnknown
|
|
153
|
+
...(error.statusCode === 401 || operationOutcomeUnknown || definitiveDenial || localValidationRejected || requestRejected
|
|
124
154
|
? {
|
|
125
155
|
_meta: {
|
|
126
|
-
|
|
156
|
+
...(definitiveDenial
|
|
157
|
+
? { [error.statusCode === 404 ? "io.frihet/resourceNotFound" : "io.frihet/accessDenied"]: true }
|
|
158
|
+
: {}),
|
|
159
|
+
...(error.statusCode === 401
|
|
160
|
+
? { "io.frihet/authenticationRequired": true }
|
|
161
|
+
: {}),
|
|
162
|
+
...(localValidationRejected
|
|
163
|
+
? { "io.frihet/localValidationRejected": true }
|
|
164
|
+
: {}),
|
|
165
|
+
...(requestRejected
|
|
166
|
+
? { "io.frihet/requestRejected": true }
|
|
167
|
+
: {}),
|
|
168
|
+
...(operationOutcomeUnknown
|
|
169
|
+
? { "io.frihet/operationOutcomeUnknown": true }
|
|
170
|
+
: {}),
|
|
127
171
|
...(transportOutcomeUnknown
|
|
128
172
|
? { "io.frihet/transportOutcomeUnknown": true }
|
|
129
173
|
: {}),
|
|
@@ -143,8 +187,19 @@ export function handleToolError(error, toolName) {
|
|
|
143
187
|
code: error instanceof Error ? error.name : "unknown",
|
|
144
188
|
},
|
|
145
189
|
});
|
|
190
|
+
// Class name only, from a fixed allowlist — never the message, stack, or an
|
|
191
|
+
// arbitrary `name` (a thrown object can set `name` to any identifier-shaped
|
|
192
|
+
// value, including a secret).
|
|
193
|
+
const errorClass = error instanceof Error && UNEXPECTED_ERROR_CLASSES.has(error.name)
|
|
194
|
+
? error.name
|
|
195
|
+
: "UnknownError";
|
|
146
196
|
return {
|
|
147
|
-
content: [{
|
|
197
|
+
content: [{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Error: An unexpected error occurred (${errorClass}). Contact support if this persists.`,
|
|
200
|
+
annotations: ERROR_CONTENT_ANNOTATIONS,
|
|
201
|
+
}],
|
|
202
|
+
structuredContent: { error: "unexpected_error", errorClass },
|
|
148
203
|
isError: true,
|
|
149
204
|
};
|
|
150
205
|
}
|
|
@@ -160,13 +215,21 @@ export function formatPaginatedResponse(resourceName, response) {
|
|
|
160
215
|
lines.push(JSON.stringify(item, null, 2));
|
|
161
216
|
lines.push("---");
|
|
162
217
|
}
|
|
163
|
-
|
|
218
|
+
const hasMoreByTotal = response.total > response.offset + response.data.length;
|
|
219
|
+
const isTruncatedScanWindow = Boolean(response.nextCursor && response.data.length < response.limit);
|
|
220
|
+
if (isTruncatedScanWindow && response.nextCursor) {
|
|
221
|
+
lines.push(`More results available beyond the current scan window. Continue with after='${response.nextCursor}' to fetch the next page.`);
|
|
222
|
+
}
|
|
223
|
+
else if (hasMoreByTotal) {
|
|
164
224
|
const nextOffset = response.offset + response.data.length;
|
|
165
225
|
lines.push(`More results available. Use offset=${nextOffset} to see the next page.`);
|
|
166
226
|
if (response.nextCursor) {
|
|
167
227
|
lines.push(`Cursor pagination: use after='${response.nextCursor}' for efficient cursor-based pagination.`);
|
|
168
228
|
}
|
|
169
229
|
}
|
|
230
|
+
else if (response.nextCursor) {
|
|
231
|
+
lines.push(`Cursor pagination: use after='${response.nextCursor}' for efficient cursor-based pagination.`);
|
|
232
|
+
}
|
|
170
233
|
return truncateResponse(lines.join("\n"));
|
|
171
234
|
}
|
|
172
235
|
/** Format an exhaustive list whose backend exposes no pagination controls. */
|
|
@@ -645,7 +708,9 @@ export const posTerminalItemOutput = z.object({
|
|
|
645
708
|
label: z.string().optional(),
|
|
646
709
|
deviceType: z.string().optional(),
|
|
647
710
|
locationId: z.string().optional(),
|
|
648
|
-
|
|
711
|
+
// Raw stored value: Frihet-ERP apps/erp/modules/pos/schema/collections.ts
|
|
712
|
+
// (posTerminals.status), returned verbatim by publicApi/families/pos.ts.
|
|
713
|
+
status: z.enum(["active", "paused", "retired"]).optional(),
|
|
649
714
|
stripeReaderId: z.string().optional(),
|
|
650
715
|
createdAt: z.string().optional(),
|
|
651
716
|
updatedAt: z.string().optional(),
|
|
@@ -921,8 +986,11 @@ export const teamMemberItemOutput = z.object({
|
|
|
921
986
|
id: z.string(),
|
|
922
987
|
name: z.string().nullable().optional(),
|
|
923
988
|
email: z.string().nullable().optional(),
|
|
924
|
-
|
|
925
|
-
|
|
989
|
+
// Frihet-ERP functions/src/publicApi.ts team listing: invitation rows whose
|
|
990
|
+
// stored role is not assignable are rendered with `role: null` and
|
|
991
|
+
// `status: 'invalid'`; past-expiry invitations surface as `expired`.
|
|
992
|
+
role: z.enum(["owner", "admin", "editor", "accountant", "viewer"]).nullable().optional(),
|
|
993
|
+
status: z.enum(["active", "pending", "expired", "invalid"]).optional(),
|
|
926
994
|
invitedAt: z.string().nullable().optional(),
|
|
927
995
|
joinedAt: z.string().nullable().optional(),
|
|
928
996
|
createdAt: z.string().nullable().optional(),
|
|
@@ -1019,7 +1087,13 @@ export const gestoriaAgingConsolidatedOutput = z.object({
|
|
|
1019
1087
|
}).passthrough()),
|
|
1020
1088
|
generatedAt: z.string().optional(),
|
|
1021
1089
|
}).passthrough();
|
|
1022
|
-
/* --- D4-B: HR / Webhook test / Payroll /
|
|
1090
|
+
/* --- D4-B: HR / Webhook test / Payroll / Permissions / Period close ---
|
|
1091
|
+
* Onboarding (onboarding_status, onboarding_persona_set) was withdrawn via
|
|
1092
|
+
* issue #124 because Frihet-ERP has no /v1/onboarding/* family. The schemas
|
|
1093
|
+
* lived here previously; the registration in register-all.ts and the file
|
|
1094
|
+
* src/tools/onboarding.ts are also gone. If a future Frihet-ERP release
|
|
1095
|
+
* ships the family, restore src/tools/onboarding.ts, re-add the export
|
|
1096
|
+
* symbols here, and re-import them in register-all.ts. */
|
|
1023
1097
|
/** Leave/PTO request — backend `/v1/leaves`. */
|
|
1024
1098
|
export const leaveRequestItemOutput = z.object({
|
|
1025
1099
|
id: z.string(),
|
|
@@ -1162,22 +1236,6 @@ export const payrollChecklistOutput = z.object({
|
|
|
1162
1236
|
missingEmployees: z.never().optional(),
|
|
1163
1237
|
generatedAt: z.never().optional(),
|
|
1164
1238
|
}).passthrough();
|
|
1165
|
-
/** Onboarding workspace state — backend `/v1/onboarding/status`. */
|
|
1166
|
-
export const onboardingStatusOutput = z.object({
|
|
1167
|
-
workspaceId: z.string().optional(),
|
|
1168
|
-
persona: z.enum(["autonomo", "empresa", "agencia", "gestoria"]).optional(),
|
|
1169
|
-
completedSteps: z.array(z.string()).optional(),
|
|
1170
|
-
pendingSteps: z.array(z.string()).optional(),
|
|
1171
|
-
percentComplete: z.number().min(0).max(100).optional(),
|
|
1172
|
-
startedAt: z.string().optional(),
|
|
1173
|
-
completedAt: z.string().optional(),
|
|
1174
|
-
}).passthrough();
|
|
1175
|
-
/** Onboarding persona update result. */
|
|
1176
|
-
export const onboardingPersonaResultOutput = z.object({
|
|
1177
|
-
workspaceId: z.string().optional(),
|
|
1178
|
-
persona: z.enum(["autonomo", "empresa", "agencia", "gestoria"]),
|
|
1179
|
-
updatedAt: z.string().optional(),
|
|
1180
|
-
}).passthrough();
|
|
1181
1239
|
const rbacRoleOutput = z.enum(["owner", "admin", "manager", "sales", "accountant", "employee", "viewer"]);
|
|
1182
1240
|
const rbacResourceOutput = z.enum([
|
|
1183
1241
|
"workspace", "invoices", "quotes", "expenses", "clients", "products", "accounting",
|
|
@@ -1296,6 +1354,14 @@ export const currentPeriodOutput = z.object({
|
|
|
1296
1354
|
reopenReason: z.never().optional(),
|
|
1297
1355
|
generatedAt: z.never().optional(),
|
|
1298
1356
|
}).passthrough();
|
|
1357
|
+
/** Mark a local confirmation refusal without parsing or exposing provider text. */
|
|
1358
|
+
export function confirmationRequiredError(message) {
|
|
1359
|
+
return {
|
|
1360
|
+
content: [{ type: "text", text: message, annotations: ERROR_CONTENT_ANNOTATIONS }],
|
|
1361
|
+
isError: true,
|
|
1362
|
+
_meta: { "io.frihet/confirmationRequired": true },
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1299
1365
|
/**
|
|
1300
1366
|
* Wraps a tool handler to automatically log execution time, success/failure,
|
|
1301
1367
|
* and record metrics. Catches errors and routes them through handleToolError.
|
package/dist/tools/team.js
CHANGED
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
* Backend: /v1/team/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod/v4";
|
|
15
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, teamMemberItemOutput, } from "./shared.js";
|
|
15
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, teamMemberItemOutput, } from "./shared.js";
|
|
16
16
|
export function registerTeamTools(server, client) {
|
|
17
17
|
// -- list_team_members --
|
|
18
18
|
server.registerTool("list_team_members", {
|
|
19
19
|
title: "List Team Members",
|
|
20
20
|
description: "List active workspace team members and pending invitations. The workspace owner is NOT a member row and is excluded. " +
|
|
21
|
-
"Each row carries member ID, name, email, role, and
|
|
21
|
+
"Each row carries member ID, name, email, role, and status (active/pending/expired/invalid; role is null on invalid invitations). " +
|
|
22
22
|
"Useful for reviewing team access and outstanding invitations. " +
|
|
23
23
|
"/ Lista miembros activos del equipo e invitaciones pendientes. El propietario del espacio NO aparece como miembro y queda excluido. " +
|
|
24
|
-
"Cada fila lleva ID, nombre, email, rol y estado
|
|
24
|
+
"Cada fila lleva ID, nombre, email, rol y estado (active/pending/expired/invalid; rol null en invitaciones invalidas). " +
|
|
25
25
|
"Util para revisar el acceso del equipo y las invitaciones pendientes.",
|
|
26
26
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
27
27
|
inputSchema: {
|
|
@@ -30,9 +30,9 @@ export function registerTeamTools(server, client) {
|
|
|
30
30
|
.optional()
|
|
31
31
|
.describe("Filter by role / Filtrar por rol"),
|
|
32
32
|
status: z
|
|
33
|
-
.enum(["active", "pending"])
|
|
33
|
+
.enum(["active", "pending", "expired", "invalid"])
|
|
34
34
|
.optional()
|
|
35
|
-
.describe("Filter by
|
|
35
|
+
.describe("Filter by status / Filtrar por estado"),
|
|
36
36
|
limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
|
|
37
37
|
offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
|
|
38
38
|
},
|
|
@@ -123,17 +123,9 @@ export function registerTeamTools(server, client) {
|
|
|
123
123
|
outputSchema: actionResultOutput,
|
|
124
124
|
}, async ({ memberId, confirm }) => withToolLogging("remove_team_member", async () => {
|
|
125
125
|
if (!confirm) {
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
type: "text",
|
|
130
|
-
text: "Error: confirm=true is required to remove a team member. " +
|
|
131
|
-
"The member will immediately lose access to the workspace. Set confirm=true to proceed. / " +
|
|
132
|
-
"Se requiere confirm=true para eliminar un miembro del equipo.",
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
isError: true,
|
|
136
|
-
};
|
|
126
|
+
return confirmationRequiredError("Error: confirm=true is required to remove a team member. " +
|
|
127
|
+
"The member will immediately lose access to the workspace. Set confirm=true to proceed. / " +
|
|
128
|
+
"Se requiere confirm=true para eliminar un miembro del equipo.");
|
|
137
129
|
}
|
|
138
130
|
await client.removeTeamMember(memberId);
|
|
139
131
|
return {
|
package/dist/tools/time.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Backend: /v1/time/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
|
|
15
15
|
*/
|
|
16
16
|
import { z } from "zod/v4";
|
|
17
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, deleteResultOutput, timeEntryItemOutput, timeSummaryOutput, } from "./shared.js";
|
|
17
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, deleteResultOutput, timeEntryItemOutput, timeSummaryOutput, } from "./shared.js";
|
|
18
18
|
export function registerTimeTools(server, client) {
|
|
19
19
|
// -- list_time_entries --
|
|
20
20
|
server.registerTool("list_time_entries", {
|
|
@@ -132,17 +132,9 @@ export function registerTimeTools(server, client) {
|
|
|
132
132
|
outputSchema: deleteResultOutput,
|
|
133
133
|
}, async ({ id, confirm }) => withToolLogging("delete_time_entry", async () => {
|
|
134
134
|
if (!confirm) {
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
type: "text",
|
|
139
|
-
text: "Error: confirm=true is required to delete a time entry. " +
|
|
140
|
-
"Deleted entries affect billing reports. Set confirm=true to proceed. / " +
|
|
141
|
-
"Se requiere confirm=true para eliminar una entrada de tiempo.",
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
isError: true,
|
|
145
|
-
};
|
|
135
|
+
return confirmationRequiredError("Error: confirm=true is required to delete a time entry. " +
|
|
136
|
+
"Deleted entries affect billing reports. Set confirm=true to proceed. / " +
|
|
137
|
+
"Se requiere confirm=true para eliminar una entrada de tiempo.");
|
|
146
138
|
}
|
|
147
139
|
await client.deleteTimeEntry(id);
|
|
148
140
|
return {
|