@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.
Files changed (87) hide show
  1. package/README.md +84 -59
  2. package/dist/agent-onboarding.d.ts +1 -1
  3. package/dist/agent-onboarding.js +3 -3
  4. package/dist/canary/mcp-harness.d.ts +1 -1
  5. package/dist/canary/mcp-harness.js +4 -1
  6. package/dist/capability-truth.js +20 -12
  7. package/dist/claude-candidate-contract.d.ts +482 -0
  8. package/dist/claude-candidate-contract.js +87 -0
  9. package/dist/claude-output-privacy.d.ts +17 -0
  10. package/dist/claude-output-privacy.js +240 -0
  11. package/dist/claude-profile.d.ts +18 -0
  12. package/dist/claude-profile.js +260 -0
  13. package/dist/client-interface.d.ts +26 -20
  14. package/dist/client.d.ts +65 -29
  15. package/dist/client.js +278 -167
  16. package/dist/demo-client.d.ts +27 -21
  17. package/dist/demo-client.js +31 -14
  18. package/dist/deposit-contract.d.ts +29 -0
  19. package/dist/deposit-contract.js +76 -0
  20. package/dist/fetch-no-redirect.d.ts +3 -0
  21. package/dist/fetch-no-redirect.js +16 -0
  22. package/dist/fiscal-aliases.d.ts +2 -2
  23. package/dist/fiscal-aliases.js +2 -2
  24. package/dist/fiscal-period.d.ts +26 -0
  25. package/dist/fiscal-period.js +31 -0
  26. package/dist/google-profile.d.ts +19 -0
  27. package/dist/google-profile.js +122 -0
  28. package/dist/index.js +31 -7
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +2 -2
  31. package/dist/muse-profile.d.ts +19 -0
  32. package/dist/muse-profile.js +122 -0
  33. package/dist/observability.js +3 -2
  34. package/dist/openai-profile.d.ts +2 -2
  35. package/dist/openai-profile.js +37 -11
  36. package/dist/openai-review-contract.d.ts +1 -1
  37. package/dist/openai-review-contract.js +4 -5
  38. package/dist/openai-review-oauth.d.ts +107 -7
  39. package/dist/openai-review-oauth.js +161 -17
  40. package/dist/prompts/register-all.js +32 -78
  41. package/dist/redaction.d.ts +5 -0
  42. package/dist/redaction.js +9 -0
  43. package/dist/resources/register-all.js +150 -54
  44. package/dist/retry-after.d.ts +19 -0
  45. package/dist/retry-after.js +84 -0
  46. package/dist/server-composition.d.ts +5 -2
  47. package/dist/server-composition.js +35 -5
  48. package/dist/tool-exposure.d.ts +6 -5
  49. package/dist/tool-exposure.js +15 -13
  50. package/dist/tools/audit_gl.js +8 -8
  51. package/dist/tools/backend-availability.d.ts +8 -29
  52. package/dist/tools/backend-availability.js +31 -30
  53. package/dist/tools/bank_rules.js +66 -15
  54. package/dist/tools/banking.js +5 -13
  55. package/dist/tools/clients.js +252 -6
  56. package/dist/tools/deposits.js +107 -97
  57. package/dist/tools/einvoice.d.ts +17 -3
  58. package/dist/tools/einvoice.js +59 -12
  59. package/dist/tools/fiscal.d.ts +3 -4
  60. package/dist/tools/fiscal.js +93 -60
  61. package/dist/tools/gestoria.js +15 -7
  62. package/dist/tools/hr.js +41 -11
  63. package/dist/tools/igic.d.ts +9 -8
  64. package/dist/tools/igic.js +34 -64
  65. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  66. package/dist/tools/impuesto_sociedades.js +16 -37
  67. package/dist/tools/intelligence.js +164 -10
  68. package/dist/tools/invoices.js +109 -42
  69. package/dist/tools/kitchen.js +19 -9
  70. package/dist/tools/quotes.js +86 -25
  71. package/dist/tools/recurring.js +26 -25
  72. package/dist/tools/register-all.d.ts +2 -2
  73. package/dist/tools/register-all.js +9 -6
  74. package/dist/tools/search.d.ts +19 -0
  75. package/dist/tools/search.js +84 -0
  76. package/dist/tools/shared.d.ts +17 -37
  77. package/dist/tools/shared.js +96 -30
  78. package/dist/tools/team.js +8 -16
  79. package/dist/tools/time.js +4 -12
  80. package/dist/tools/vendors.js +240 -6
  81. package/dist/types.d.ts +3 -1
  82. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  83. package/dist/workflows/daily-operational-brief.js +881 -0
  84. package/docs/agent-onboarding.json +15 -5
  85. package/package.json +26 -12
  86. package/dist/tools/onboarding.d.ts +0 -19
  87. 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
@@ -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, medium priority. */
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, higher priority as specifically requested data. */
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
- unknown: "unknown";
449
- online: "online";
450
- offline: "offline";
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.
@@ -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
- /** List operations: useful to both user and assistant for navigation, medium priority. */
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, higher priority as specifically requested data. */
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 (error.statusCode === 413 && error.errorCode === "payload_too_large") {
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
- "io.frihet/operationOutcomeUnknown": true,
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: [{ type: "text", text: "Error: An unexpected error occurred. Contact support if this persists.", annotations: ERROR_CONTENT_ANNOTATIONS }],
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
- if (response.total > response.offset + response.data.length) {
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
- status: z.enum(["online", "offline", "unknown"]).optional(),
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
- role: z.enum(["owner", "admin", "editor", "accountant", "viewer"]).optional(),
925
- status: z.enum(["active", "pending"]).optional(),
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 / Onboarding / Permissions / Period close --- */
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.
@@ -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 invite status (pending/active). " +
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 de invitacion (pendiente/activo). " +
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 invite status / Filtrar por estado de invitacion"),
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
- content: [
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 {
@@ -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
- content: [
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 {