@frihet/mcp-server 1.16.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +168 -82
  2. package/dist/agent-onboarding.d.ts +98 -0
  3. package/dist/agent-onboarding.js +275 -0
  4. package/dist/api-origin.d.ts +9 -0
  5. package/dist/api-origin.js +41 -0
  6. package/dist/canary/mcp-harness.d.ts +77 -0
  7. package/dist/canary/mcp-harness.js +552 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +262 -0
  10. package/dist/claude-candidate-contract.d.ts +482 -0
  11. package/dist/claude-candidate-contract.js +87 -0
  12. package/dist/claude-output-privacy.d.ts +17 -0
  13. package/dist/claude-output-privacy.js +240 -0
  14. package/dist/claude-profile.d.ts +18 -0
  15. package/dist/claude-profile.js +260 -0
  16. package/dist/client-interface.d.ts +47 -33
  17. package/dist/client.d.ts +172 -40
  18. package/dist/client.js +708 -165
  19. package/dist/demo-client.d.ts +41 -32
  20. package/dist/demo-client.js +202 -31
  21. package/dist/demo-fixtures.d.ts +3 -3
  22. package/dist/demo-fixtures.js +7 -7
  23. package/dist/deposit-contract.d.ts +29 -0
  24. package/dist/deposit-contract.js +76 -0
  25. package/dist/fetch-no-redirect.d.ts +3 -0
  26. package/dist/fetch-no-redirect.js +16 -0
  27. package/dist/fiscal-aliases.d.ts +44 -0
  28. package/dist/fiscal-aliases.js +62 -0
  29. package/dist/fiscal-period.d.ts +26 -0
  30. package/dist/fiscal-period.js +31 -0
  31. package/dist/google-profile.d.ts +19 -0
  32. package/dist/google-profile.js +122 -0
  33. package/dist/index.js +68 -59
  34. package/dist/logger.d.ts +2 -2
  35. package/dist/logger.js +140 -8
  36. package/dist/muse-profile.d.ts +19 -0
  37. package/dist/muse-profile.js +122 -0
  38. package/dist/observability.d.ts +18 -29
  39. package/dist/observability.js +194 -106
  40. package/dist/openai-profile.d.ts +26 -22
  41. package/dist/openai-profile.js +1193 -157
  42. package/dist/openai-review-contract.d.ts +15 -6
  43. package/dist/openai-review-contract.js +714 -41
  44. package/dist/openai-review-oauth.d.ts +149 -2
  45. package/dist/openai-review-oauth.js +220 -6
  46. package/dist/prompts/register-all.d.ts +2 -0
  47. package/dist/prompts/register-all.js +34 -78
  48. package/dist/public-capability-contract.d.ts +39 -0
  49. package/dist/public-capability-contract.js +186 -0
  50. package/dist/redaction.d.ts +15 -0
  51. package/dist/redaction.js +83 -0
  52. package/dist/resources/register-all.js +154 -64
  53. package/dist/retry-after.d.ts +19 -0
  54. package/dist/retry-after.js +84 -0
  55. package/dist/server-composition.d.ts +19 -0
  56. package/dist/server-composition.js +74 -0
  57. package/dist/tool-exposure.d.ts +31 -27
  58. package/dist/tool-exposure.js +211 -87
  59. package/dist/tools/accountingClose.d.ts +14 -5
  60. package/dist/tools/accountingClose.js +41 -23
  61. package/dist/tools/audit_gl.js +8 -8
  62. package/dist/tools/backend-availability.d.ts +8 -29
  63. package/dist/tools/backend-availability.js +31 -30
  64. package/dist/tools/bank_rules.js +66 -15
  65. package/dist/tools/banking.js +5 -13
  66. package/dist/tools/clients.js +261 -15
  67. package/dist/tools/crm.js +22 -21
  68. package/dist/tools/deposits.js +114 -85
  69. package/dist/tools/einvoice.d.ts +16 -2
  70. package/dist/tools/einvoice.js +59 -12
  71. package/dist/tools/expenses.d.ts +16 -0
  72. package/dist/tools/expenses.js +47 -27
  73. package/dist/tools/fiscal.d.ts +3 -4
  74. package/dist/tools/fiscal.js +93 -60
  75. package/dist/tools/gestoria.js +15 -7
  76. package/dist/tools/hr.d.ts +2 -3
  77. package/dist/tools/hr.js +49 -20
  78. package/dist/tools/igic.d.ts +9 -8
  79. package/dist/tools/igic.js +34 -64
  80. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  81. package/dist/tools/impuesto_sociedades.js +16 -37
  82. package/dist/tools/intelligence.js +166 -12
  83. package/dist/tools/invoices.js +224 -54
  84. package/dist/tools/kitchen.js +19 -9
  85. package/dist/tools/payroll.d.ts +5 -8
  86. package/dist/tools/payroll.js +19 -32
  87. package/dist/tools/permissions.d.ts +4 -7
  88. package/dist/tools/permissions.js +11 -14
  89. package/dist/tools/products.js +6 -6
  90. package/dist/tools/quotes.js +172 -26
  91. package/dist/tools/recurring.js +26 -25
  92. package/dist/tools/register-all.d.ts +2 -2
  93. package/dist/tools/register-all.js +9 -6
  94. package/dist/tools/search.d.ts +19 -0
  95. package/dist/tools/search.js +84 -0
  96. package/dist/tools/shared.d.ts +1445 -109
  97. package/dist/tools/shared.js +478 -102
  98. package/dist/tools/team.js +24 -28
  99. package/dist/tools/time.js +4 -12
  100. package/dist/tools/vendors.js +249 -15
  101. package/dist/tools/webhooks.js +35 -30
  102. package/dist/types.d.ts +31 -5
  103. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  104. package/dist/workflows/daily-operational-brief.js +881 -0
  105. package/docs/agent-onboarding.json +391 -0
  106. package/package.json +49 -12
  107. package/dist/tools/onboarding.d.ts +0 -19
  108. package/dist/tools/onboarding.js +0 -64
@@ -2,35 +2,48 @@
2
2
  * Period close tools for the Frihet MCP server — D4-B megasprint (3 tools).
3
3
  *
4
4
  * Tools:
5
- * 1. period_close_status — get the current accounting period state (open/closing/closed)
5
+ * 1. period_close_status — get the current or selected fiscal-year state
6
6
  * 2. period_close — close a monthly or quarterly period (TRUST AREA)
7
7
  * 3. period_reopen — reopen a closed period with required reason (TRUST AREA)
8
8
  *
9
9
  * REST surface: /v1/periods/current, /v1/periods/{id}, /v1/periods/close, /v1/periods/{id}/reopen
10
10
  *
11
- * Closing a period freezes invoices/expenses/journal entries. Reopening requires a
12
- * compliance reason logged for audit. These are TRUST AREA operations.
11
+ * STATE OF THE WORLD (verified against erp-main functions/src/publicApi/families/
12
+ * periods.ts): only the READ route is implemented. Both WRITE routes answer
13
+ * HTTP 501 NOT_IMPLEMENTED — "deferred to the upcoming fiscal-write wave (Trust,
14
+ * serialized). Use year-end closing in the app for now." Nothing is frozen, and
15
+ * there is no idempotent no-op to rely on, because there is no handler at all.
13
16
  *
14
- * NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
15
- * until backend ships. TODO: confirm callable boundary vs REST shell for closePeriod.
17
+ * The descriptions below therefore state the 501 in the present tense. Do not
18
+ * restore aspirational prose about freezing or idempotency until a handler exists
19
+ * — GAP-13 was exactly that: an agent planning a year-end close read a mechanism
20
+ * the backend never had.
21
+ *
22
+ * `withBackendGuard` converts 404 only (backend-availability.ts isBackendNotFound),
23
+ * so a 501 surfaces through the generic error path as isError with the backend's
24
+ * own NOT_IMPLEMENTED message — an honest failure, not a fake success.
16
25
  */
17
26
  import { z } from "zod/v4";
18
- import { withToolLogging, formatRecord, getContent, mutateContent, READ_ONLY_ANNOTATIONS, periodStatusOutput, } from "./shared.js";
27
+ import { withToolLogging, formatRecord, getContent, mutateContent, READ_ONLY_ANNOTATIONS, currentPeriodOutput, periodStatusOutput, } from "./shared.js";
19
28
  import { withBackendGuard } from "./backend-availability.js";
20
29
  export function registerAccountingCloseTools(server, client) {
21
30
  // -- period_close_status --
22
31
  server.registerTool("period_close_status", {
23
32
  title: "Period Close Status",
24
- description: "Get the current accounting period state (open / closing / closed / reopened). " +
25
- "Without periodId returns the current open or most recently closed period. " +
26
- "/ Devuelve el estado del periodo contable actual (abierto/cerrando/cerrado/reabierto).",
33
+ description: "Get the open or closed state of the current fiscal year, including its fiscal-year start, " +
34
+ "inclusive date range, and nullable closing details. Pass a four-digit fiscal-year label to read it explicitly. " +
35
+ "/ Devuelve el estado abierto o cerrado del ejercicio fiscal actual o indicado.",
27
36
  annotations: READ_ONLY_ANNOTATIONS,
28
37
  inputSchema: {
29
- periodId: z.string().optional().describe("Specific period ID (default: current) / ID periodo especifico"),
38
+ periodId: z
39
+ .string()
40
+ .regex(/^\d{4}$/)
41
+ .optional()
42
+ .describe("Compatibility field: fiscal-year label YYYY, not an arbitrary period ID (default: current) / Ejercicio fiscal YYYY"),
30
43
  },
31
- outputSchema: periodStatusOutput,
32
- }, async ({ periodId }) => withToolLogging("period_close_status", () => withBackendGuard("period_close_status", "/v1/periods/current", async () => {
33
- const result = await client.getCurrentPeriod({ periodId });
44
+ outputSchema: currentPeriodOutput,
45
+ }, async ({ periodId }) => withToolLogging("period_close_status", () => withBackendGuard("period_close_status", periodId ? `/v1/periods/${periodId}` : "/v1/periods/current", async () => {
46
+ const result = await client.getCurrentPeriod({ fiscalYear: periodId });
34
47
  return {
35
48
  content: [getContent(formatRecord("Period status", result))],
36
49
  structuredContent: result,
@@ -39,12 +52,15 @@ export function registerAccountingCloseTools(server, client) {
39
52
  // -- period_close --
40
53
  server.registerTool("period_close", {
41
54
  title: "Close Accounting Period",
42
- description: "TRUST AREA — FISCAL CLOSE. Close a monthly or quarterly accounting period. " +
43
- "Freezes invoices, expenses, journal entries and bank reconciliations for the period. " +
44
- "Requires confirm=true. Idempotent: re-closing an already closed period is a no-op. " +
45
- "Closed periods can be reopened with period_reopen + audit reason. " +
46
- "/ AREA DE CONFIANZA — CIERRE FISCAL. Cierra un periodo contable mensual o trimestral. " +
47
- "Congela facturas, gastos, asientos. Requiere confirm=true.",
55
+ description: "TRUST AREA — FISCAL CLOSE. NOT IMPLEMENTED SERVER-SIDE TODAY: the backend answers " +
56
+ "HTTP 501 NOT_IMPLEMENTED to POST /v1/periods/close, so calling this tool closes nothing. " +
57
+ "No invoices, expenses, journal entries or bank reconciliations are frozen, and there is no " +
58
+ "idempotent no-op to rely on. Close the period in the Frihet app instead. " +
59
+ "The call is still gated behind confirm=true and will surface the 501 as an error. " +
60
+ "Use period_close_status (implemented) to read the current period state. " +
61
+ "/ AREA DE CONFIANZA — CIERRE FISCAL. HOY NO IMPLEMENTADO EN EL SERVIDOR: el backend responde " +
62
+ "HTTP 501 a POST /v1/periods/close, asi que esta herramienta no cierra nada ni congela nada. " +
63
+ "El cierre debe hacerse en la app de Frihet.",
48
64
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
49
65
  inputSchema: {
50
66
  type: z
@@ -81,10 +97,12 @@ export function registerAccountingCloseTools(server, client) {
81
97
  // -- period_reopen --
82
98
  server.registerTool("period_reopen", {
83
99
  title: "Reopen Accounting Period",
84
- description: "TRUST AREA — FISCAL REOPEN. Reopen a closed accounting period. " +
85
- "Requires a compliance reason (audit log) and confirm=true. " +
86
- "Reopening allows backdated edits to invoices/expenses — use with extreme caution. " +
87
- "/ AREA DE CONFIANZA — REAPERTURA FISCAL. Reabre un periodo cerrado. Requiere motivo (auditoria) y confirm=true.",
100
+ description: "TRUST AREA — FISCAL REOPEN. NOT IMPLEMENTED SERVER-SIDE TODAY: the backend answers " +
101
+ "HTTP 501 NOT_IMPLEMENTED to POST /v1/periods/{id}/reopen, so calling this tool reopens nothing " +
102
+ "and writes no audit entry. Reopen the period in the Frihet app instead. " +
103
+ "The call is still gated behind confirm=true plus a reason, and will surface the 501 as an error. " +
104
+ "/ AREA DE CONFIANZA — REAPERTURA FISCAL. HOY NO IMPLEMENTADO EN EL SERVIDOR: el backend responde " +
105
+ "HTTP 501, asi que no se reabre nada ni se registra auditoria. Hazlo en la app de Frihet.",
88
106
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
89
107
  inputSchema: {
90
108
  periodId: z.string().describe("Closed period ID to reopen / ID del periodo cerrado"),
@@ -44,20 +44,20 @@ export function registerAuditGLTools(server, client) {
44
44
  // -- frihet_gl_entry_reject ------------------------------------------------
45
45
  server.registerTool("frihet_gl_entry_reject", {
46
46
  title: "Reject GL Entry",
47
- description: "Reject a General Ledger journal entry pending review with a required reason. " +
48
- "Sets entry status to 'rejected' and records the rejector + reason in the audit trail. " +
47
+ description: "Reject a General Ledger journal entry pending review with a required note. " +
48
+ "Sets entry status to 'rejected' and records the rejector + note in the audit trail. " +
49
49
  "Requires gestor/admin role. This is a TRUST AREA action. " +
50
- "Example: entryId='gl_2026_q1_042', reason='Importe incorrecto, revisar factura F-2026-042'. " +
51
- "/ Rechaza un asiento contable pendiente con una razon obligatoria. " +
50
+ "Example: entryId='gl_2026_q1_042', note='Importe incorrecto, revisar factura F-2026-042'. " +
51
+ "/ Rechaza un asiento contable pendiente con una nota obligatoria. " +
52
52
  "Requiere rol gestor/admin.",
53
53
  annotations: UPDATE_ANNOTATIONS,
54
54
  inputSchema: {
55
55
  entryId: z.string().describe("GL entry ID to reject / ID del asiento contable a rechazar"),
56
- reason: z.string().describe("Mandatory rejection reason (visible to submitter) / Razon del rechazo (obligatoria, visible al emisor)"),
56
+ note: z.string().min(1).describe("Mandatory rejection note (visible to submitter) / Nota del rechazo (obligatoria, visible al emisor)"),
57
57
  },
58
- outputSchema: openObjectOutput("Rejected GL entry record with reason and audit trail entry / Asiento contable rechazado con motivo y entrada de auditoría"),
59
- }, async ({ entryId, reason }) => withToolLogging("frihet_gl_entry_reject", () => withBackendGuard("frihet_gl_entry_reject", "/v1/gl/entries/reject", async () => {
60
- const result = await client.rejectGLEntry(entryId, reason);
58
+ outputSchema: openObjectOutput("Rejected GL entry record with note and audit trail entry / Asiento contable rechazado con nota y entrada de auditoría"),
59
+ }, async ({ entryId, note }) => withToolLogging("frihet_gl_entry_reject", () => withBackendGuard("frihet_gl_entry_reject", "/v1/gl/entries/reject", async () => {
60
+ const result = await client.rejectGLEntry(entryId, note);
61
61
  return {
62
62
  content: [
63
63
  mutateContent(formatRecord(`GL Entry ${entryId} rejected`, result)),
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Backend-availability guard for MCP tool handlers.
3
3
  *
4
- * PROBLEM (the bug this prevents): several tool families are wired here AHEAD of
5
- * their Frihet-ERP backend Cloud Functions shipping (fiscal `/v1/fiscal/*`, bank
6
- * rules `/v1/banking/rules`, gestoria `/v1/gestoria/*`, GL audit `/v1/gl/*`,
7
- * portal domain/onboard, IGIC, IS, VIES onboarding, payroll prep, permissions,
8
- * HR leaves/anomalies, accounting periods, webhook test). Until the CF deploys,
9
- * the API returns a genuine HTTP 404.
4
+ * HTTP 404 can mean an unavailable record, an unknown route, or an endpoint
5
+ * that is not enabled. The status alone cannot establish deployment state.
10
6
  *
11
7
  * If that raw 404 reaches `handleToolError` it is mapped to the generic message
12
8
  * "Resource not found. / Recurso no encontrado." — which an LLM reads as "the
@@ -14,9 +10,9 @@
14
10
  * ("you have no Modelo 303 to file", "this workspace has no permissions matrix").
15
11
  * That is a Trust-Area failure: the model invents fiscal/compliance facts.
16
12
  *
17
- * FIX: wrap the client call in `withBackendGuard`. A genuine 404 is converted into
18
- * an explicit STRUCTURED tool error that names the tool and states the backend is
19
- * not available yet — never a value the LLM can mistake for real business data.
13
+ * Preserve typed application NOT_FOUND errors for the normal denial handler.
14
+ * Ambiguous 404s remain explicit errors with no inferred data or deployment
15
+ * state. Only notDeployedError states that a route is not deployed.
20
16
  * Any other error (401/403/429/5xx/network) is RE-THROWN unchanged so the normal
21
17
  * `withToolLogging` → `handleToolError` path still surfaces it.
22
18
  *
@@ -46,27 +42,10 @@ export interface BackendGuardErrorResult {
46
42
  * Duck-typed so it works regardless of which client implementation threw.
47
43
  */
48
44
  export declare function isBackendNotFound(error: unknown): boolean;
49
- /**
50
- * Builds the explicit, LLM-safe "backend not available" tool error.
51
- */
45
+ /** Builds the explicit error for a route known not to be deployed. */
52
46
  export declare function backendUnavailableError(toolName: string, endpoint?: string): BackendGuardErrorResult;
53
- /**
54
- * Wraps a tool's client call. On a genuine 404 (backend not deployed) returns a
55
- * structured backend-unavailable error instead of letting the raw 404 surface as
56
- * a generic "resource not found" that the LLM would treat as real data. Any other
57
- * error is re-thrown so `withToolLogging`/`handleToolError` handle it normally.
58
- *
59
- * Usage:
60
- * ```ts
61
- * async ({ period }) => withToolLogging("get_modelo_303_summary", () =>
62
- * withBackendGuard("get_modelo_303_summary", "/v1/fiscal/303", async () => {
63
- * const result = await client.getFiscalModeloSummary("303", period);
64
- * return { content: [getContent(formatRecord("Modelo 303 Summary", result))],
65
- * structuredContent: result as unknown as Record<string, unknown> };
66
- * }),
67
- * )
68
- * ```
69
- */
47
+ export declare function notDeployedError(toolName: string, endpoint: string): BackendGuardErrorResult;
48
+ /** Keep application denials intact; ambiguous HTTP 404s are never empty data. */
70
49
  export declare function withBackendGuard<T extends {
71
50
  content: unknown[];
72
51
  }>(toolName: string, endpoint: string | undefined, fn: () => Promise<T>): Promise<T | BackendGuardErrorResult>;
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Backend-availability guard for MCP tool handlers.
3
3
  *
4
- * PROBLEM (the bug this prevents): several tool families are wired here AHEAD of
5
- * their Frihet-ERP backend Cloud Functions shipping (fiscal `/v1/fiscal/*`, bank
6
- * rules `/v1/banking/rules`, gestoria `/v1/gestoria/*`, GL audit `/v1/gl/*`,
7
- * portal domain/onboard, IGIC, IS, VIES onboarding, payroll prep, permissions,
8
- * HR leaves/anomalies, accounting periods, webhook test). Until the CF deploys,
9
- * the API returns a genuine HTTP 404.
4
+ * HTTP 404 can mean an unavailable record, an unknown route, or an endpoint
5
+ * that is not enabled. The status alone cannot establish deployment state.
10
6
  *
11
7
  * If that raw 404 reaches `handleToolError` it is mapped to the generic message
12
8
  * "Resource not found. / Recurso no encontrado." — which an LLM reads as "the
@@ -14,9 +10,9 @@
14
10
  * ("you have no Modelo 303 to file", "this workspace has no permissions matrix").
15
11
  * That is a Trust-Area failure: the model invents fiscal/compliance facts.
16
12
  *
17
- * FIX: wrap the client call in `withBackendGuard`. A genuine 404 is converted into
18
- * an explicit STRUCTURED tool error that names the tool and states the backend is
19
- * not available yet — never a value the LLM can mistake for real business data.
13
+ * Preserve typed application NOT_FOUND errors for the normal denial handler.
14
+ * Ambiguous 404s remain explicit errors with no inferred data or deployment
15
+ * state. Only notDeployedError states that a route is not deployed.
20
16
  * Any other error (401/403/429/5xx/network) is RE-THROWN unchanged so the normal
21
17
  * `withToolLogging` → `handleToolError` path still surfaces it.
22
18
  *
@@ -38,9 +34,7 @@ export function isBackendNotFound(error) {
38
34
  }
39
35
  return false;
40
36
  }
41
- /**
42
- * Builds the explicit, LLM-safe "backend not available" tool error.
43
- */
37
+ /** Builds the explicit error for a route known not to be deployed. */
44
38
  export function backendUnavailableError(toolName, endpoint) {
45
39
  const message = `The backend for "${toolName}" is not available yet, so no data could be retrieved. ` +
46
40
  `This is NOT an empty or zero result — the underlying Frihet ERP endpoint ` +
@@ -63,30 +57,37 @@ export function backendUnavailableError(toolName, endpoint) {
63
57
  isError: true,
64
58
  };
65
59
  }
66
- /**
67
- * Wraps a tool's client call. On a genuine 404 (backend not deployed) returns a
68
- * structured backend-unavailable error instead of letting the raw 404 surface as
69
- * a generic "resource not found" that the LLM would treat as real data. Any other
70
- * error is re-thrown so `withToolLogging`/`handleToolError` handle it normally.
71
- *
72
- * Usage:
73
- * ```ts
74
- * async ({ period }) => withToolLogging("get_modelo_303_summary", () =>
75
- * withBackendGuard("get_modelo_303_summary", "/v1/fiscal/303", async () => {
76
- * const result = await client.getFiscalModeloSummary("303", period);
77
- * return { content: [getContent(formatRecord("Modelo 303 Summary", result))],
78
- * structuredContent: result as unknown as Record<string, unknown> };
79
- * }),
80
- * )
81
- * ```
82
- */
60
+ /** An ambiguous 404 establishes neither missing business data nor deployment. */
61
+ function lookupUnavailableError(toolName, endpoint) {
62
+ const message = `Frihet could not resolve the requested resource or endpoint for "${toolName}" (HTTP 404). ` +
63
+ `No data could be retrieved. Do not infer an empty or zero result, missing business records, or deployment status. ` +
64
+ `Check the record ID and feature availability before deciding whether to retry. ` +
65
+ `/ Frihet no pudo resolver el recurso o endpoint solicitado (HTTP 404). ` +
66
+ `No se recuperaron datos. No deduzcas un resultado vacío o cero, la ausencia de registros ni el estado del despliegue.`;
67
+ return {
68
+ content: [{ type: "text", text: `Error: ${message}`, annotations: ERROR_CONTENT_ANNOTATIONS }],
69
+ structuredContent: {
70
+ error: "lookup_unavailable", code: "LOOKUP_UNAVAILABLE", tool: toolName, message,
71
+ ...(endpoint ? { endpoint } : {}),
72
+ },
73
+ isError: true,
74
+ };
75
+ }
76
+ export function notDeployedError(toolName, endpoint) {
77
+ const result = backendUnavailableError(toolName, endpoint);
78
+ result.structuredContent = { ...result.structuredContent, code: "NOT_DEPLOYED" };
79
+ return result;
80
+ }
81
+ /** Keep application denials intact; ambiguous HTTP 404s are never empty data. */
83
82
  export async function withBackendGuard(toolName, endpoint, fn) {
84
83
  try {
85
84
  return await fn();
86
85
  }
87
86
  catch (err) {
88
87
  if (isBackendNotFound(err)) {
89
- return backendUnavailableError(toolName, endpoint);
88
+ if (err.errorCode === "NOT_FOUND")
89
+ throw err;
90
+ return lookupUnavailableError(toolName, endpoint);
90
91
  }
91
92
  throw err;
92
93
  }
@@ -23,6 +23,13 @@ import { withBackendGuard } from "./backend-availability.js";
23
23
  const bankRuleItemOutput = z.object({
24
24
  id: z.string(),
25
25
  name: z.string(),
26
+ bankConditions: z.array(z.object({
27
+ field: z.string(),
28
+ operator: z.string(),
29
+ value: z.string(),
30
+ })).optional(),
31
+ action: z.string().optional(),
32
+ actionConfig: z.record(z.string(), z.unknown()).optional(),
26
33
  conditions: z.array(z.object({
27
34
  field: z.string(),
28
35
  operator: z.string(),
@@ -35,8 +42,39 @@ const bankRuleItemOutput = z.object({
35
42
  isActive: z.boolean().optional(),
36
43
  createdAt: z.string().optional(),
37
44
  }).passthrough();
38
- const CONDITION_FIELD = z.enum(["description", "amount", "counterparty", "iban", "reference"]);
39
- const CONDITION_OPERATOR = z.enum(["contains", "startsWith", "endsWith", "equals", "greaterThan", "lessThan"]);
45
+ // ERP authority: functions/src/banking/bankRuleCreate.ts
46
+ // BANK_RULE_FIELDS = ['description', 'reference', 'amount', 'counterparty']
47
+ // BANK_RULE_OPERATORS = ['contains', 'exact', 'starts_with', 'ends_with', 'regex',
48
+ // 'amount_above', 'amount_below', 'amount_between']
49
+ // BANK_RULE_ACTIONS = ['categorize_expense', 'match_invoice', 'match_client',
50
+ // 'ignore', 'create_expense', 'flag_review']
51
+ //
52
+ // Per-action required actionConfig shape (ERP):
53
+ // categorize_expense → { category: string }
54
+ // match_client → { clientId: string }
55
+ // create_expense → { expenseDefaults: { vendor?, category?, taxRate?, description? } }
56
+ // flag_review → { flag: string }
57
+ // match_invoice → (no required config)
58
+ // ignore → (no required config)
59
+ const CONDITION_FIELD = z.enum(["description", "reference", "amount", "counterparty"]);
60
+ const CONDITION_OPERATOR = z.enum([
61
+ "contains",
62
+ "exact",
63
+ "starts_with",
64
+ "ends_with",
65
+ "regex",
66
+ "amount_above",
67
+ "amount_below",
68
+ "amount_between",
69
+ ]);
70
+ const RULE_ACTION = z.enum([
71
+ "categorize_expense",
72
+ "match_invoice",
73
+ "match_client",
74
+ "ignore",
75
+ "create_expense",
76
+ "flag_review",
77
+ ]);
40
78
  export function registerBankRulesTools(server, client) {
41
79
  // -- frihet_bank_rules_list -----------------------------------------------
42
80
  server.registerTool("frihet_bank_rules_list", {
@@ -65,28 +103,41 @@ export function registerBankRulesTools(server, client) {
65
103
  title: "Create Bank Categorization Rule",
66
104
  description: "Create a new bank auto-categorization rule. " +
67
105
  "Rules apply automatically to matching incoming transactions. " +
68
- "A rule has conditions (AND logic) and actions (assign category, tag, or client). " +
69
- "Example: name='Mercadona groceries', condition description contains 'MERCADONA', " +
70
- "action category='groceries'. " +
106
+ "The ERP new-engine vocabulary: `bankConditions` (AND logic) + a single `action` " +
107
+ "(`categorize_expense` | `match_invoice` | `match_client` | `ignore` | " +
108
+ "`create_expense` | `flag_review`) and its `actionConfig` payload. " +
109
+ "Required actionConfig per action: categorize_expense→{category}, " +
110
+ "match_client→{clientId}, create_expense→{expenseDefaults:{vendor,category,…}}, " +
111
+ "flag_review→{flag}; match_invoice and ignore require no config. " +
112
+ "Example: name='Mercadona groceries', bankConditions=[{field:'description', " +
113
+ "operator:'contains', value:'MERCADONA'}], action='categorize_expense', " +
114
+ "actionConfig={category:'groceries'}. " +
71
115
  "/ Crea una nueva regla de categorizacion automatica de transacciones bancarias. " +
72
- "Las reglas aplican condiciones (logica AND) y acciones (categoria, etiqueta, cliente).",
116
+ "El nuevo motor ERP espera `bankConditions` (logica AND) y un unico `action` con su `actionConfig`.",
73
117
  annotations: CREATE_ANNOTATIONS,
74
118
  inputSchema: {
75
119
  name: z.string().describe("Rule name for identification / Nombre de la regla"),
76
- conditions: z.array(z.object({
120
+ bankConditions: z
121
+ .array(z.object({
77
122
  field: CONDITION_FIELD.describe("Transaction field to match / Campo de transaccion a comparar"),
78
123
  operator: CONDITION_OPERATOR.describe("Comparison operator / Operador de comparacion"),
79
124
  value: z.string().describe("Value to match against / Valor a comparar"),
80
- })).min(1).describe("Rule conditions (AND logic — all must match) / Condiciones (logica AND — todas deben cumplirse)"),
81
- actions: z.array(z.object({
82
- type: z.enum(["setCategory", "addTag", "assignClient"]).describe("Action type / Tipo de accion"),
83
- value: z.string().describe("Action value (category name, tag name, or client ID) / Valor de la accion"),
84
- })).min(1).describe("Actions to apply when rule matches / Acciones a aplicar cuando la regla se cumple"),
125
+ }))
126
+ .min(1)
127
+ .describe("Rule conditions (AND logic — all must match) / Condiciones (logica AND — todas deben cumplirse)"),
128
+ action: RULE_ACTION.describe("Action to apply when the rule matches / Accion a aplicar cuando la regla se cumple"),
129
+ actionConfig: z
130
+ .record(z.string(), z.unknown())
131
+ .describe("Action-specific payload. categorize_expense requires {category:string}. " +
132
+ "match_client requires {clientId:string}. create_expense requires " +
133
+ "{expenseDefaults:{vendor:string, category:string, taxRate?:number, description?:string}}. " +
134
+ "flag_review requires {flag:string}. match_invoice and ignore require no config. " +
135
+ "/ Payload segun la accion."),
85
136
  isActive: z.boolean().optional().describe("Whether the rule is active (default true) / Si la regla esta activa (por defecto true)"),
86
137
  },
87
- outputSchema: openObjectOutput("Created bank rule record with ID, conditions and actions / Regla bancaria creada con ID, condiciones y acciones"),
88
- }, async ({ name, conditions, actions, isActive }) => withToolLogging("frihet_bank_rule_create", () => withBackendGuard("frihet_bank_rule_create", "/v1/banking/rules", async () => {
89
- const result = await client.createBankRule({ name, conditions, actions, isActive });
138
+ outputSchema: openObjectOutput("Created bank rule record with ID, conditions and action / Regla bancaria creada con ID, condiciones y accion"),
139
+ }, async ({ name, bankConditions, action, actionConfig, isActive }) => withToolLogging("frihet_bank_rule_create", () => withBackendGuard("frihet_bank_rule_create", "/v1/banking/rules", async () => {
140
+ const result = await client.createBankRule({ name, bankConditions, action, actionConfig, isActive });
90
141
  return {
91
142
  content: [
92
143
  mutateContent(formatRecord(`Bank rule created: ${name}`, result)),
@@ -16,7 +16,7 @@
16
16
  * src/__tests__/banking-client-contract.test.ts.
17
17
  */
18
18
  import { z } from "zod/v4";
19
- import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, bankAccountItemOutput, bankTransactionItemOutput, } from "./shared.js";
19
+ import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, bankAccountItemOutput, bankTransactionItemOutput, } from "./shared.js";
20
20
  export function registerBankingTools(server, client) {
21
21
  // -- list_bank_accounts --
22
22
  server.registerTool("list_bank_accounts", {
@@ -134,18 +134,10 @@ export function registerBankingTools(server, client) {
134
134
  outputSchema: bankTransactionItemOutput,
135
135
  }, async ({ transactionId, documentId, documentType, confirm, notes }) => withToolLogging("match_transaction_to_invoice", async () => {
136
136
  if (!confirm) {
137
- return {
138
- content: [
139
- {
140
- type: "text",
141
- text: "Error: confirm=true is required to perform fiscal reconciliation. " +
142
- "This action links a bank transaction to a fiscal document and cannot be undone easily. " +
143
- "Set confirm=true when you are certain about the match. / " +
144
- "Se requiere confirm=true para realizar la conciliacion fiscal.",
145
- },
146
- ],
147
- isError: true,
148
- };
137
+ return confirmationRequiredError("Error: confirm=true is required to perform fiscal reconciliation. " +
138
+ "This action links a bank transaction to a fiscal document and cannot be undone easily. " +
139
+ "Set confirm=true when you are certain about the match. / " +
140
+ "Se requiere confirm=true para realizar la conciliacion fiscal.");
149
141
  }
150
142
  const result = await client.matchTransactionToDocument(transactionId, { documentId, documentType, notes });
151
143
  return {