@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
@@ -6,7 +6,7 @@
6
6
  * the outer public composition may still attach capability truth metadata.
7
7
  *
8
8
  * ── Why ──────────────────────────────────────────────────────────────────
9
- * Context rot is the 2026 problem: a flat list of 157 tool descriptions,
9
+ * Context rot is the 2026 problem: a flat list of 158 tool descriptions,
10
10
  * each a multi-paragraph bilingual blob, eats the agent's context window and
11
11
  * degrades tool selection before any work begins. Leaders cut flat lists.
12
12
  *
@@ -30,12 +30,12 @@
30
30
  * • search_tools(query) — fuzzy match → matching tool summaries
31
31
  * • describe_tool(name) — full original description + input fields
32
32
  *
33
- * The agent loads ~3 meta-tool descriptions + 157 terse one-liners instead of
34
- * 157 full bilingual blobs, then pulls full depth only for the handful of
33
+ * The agent loads ~3 meta-tool descriptions + 158 terse one-liners instead of
34
+ * 158 full bilingual blobs, then pulls full depth only for the handful of
35
35
  * tools it actually needs. Progressive disclosure, zero behavior change.
36
36
  *
37
37
  * IMPORTANT: this is purely an EXPOSURE layer. It does NOT live in
38
- * src/tools/*.ts, so the audited tool count stays 157 (+ meta). The meta-tools
38
+ * src/tools/*.ts, so the audited tool count stays 158 (+ meta). The meta-tools
39
39
  * are added only in grouped mode and are NOT counted as ERP tools.
40
40
  *
41
41
  * @see ./openai-profile.ts — the sibling interceptor this mirrors.
@@ -61,8 +61,9 @@ export const GROUPS = {
61
61
  },
62
62
  fiscal: {
63
63
  label: "Fiscal & compliance / Fiscal y cumplimiento",
64
- blurb: "Spanish/EU fiscal depth served on demand: Modelo 303/130/390/180/347/200/202/415/425/418, " +
65
- "VeriFactu, TicketBAI, Facturae/FACe/KSeF e-invoicing, IGIC/AIEM, GL audit, period close, VIES.",
64
+ blurb: "Spanish/EU fiscal depth served on demand: Modelo 303/130/390/347, VeriFactu, TicketBAI, " +
65
+ "Facturae/FACe e-invoicing, GL audit, period close, VIES " +
66
+ "(Modelo 180/200/202/415/418/425, AIEM and KSeF registered but not deployed).",
66
67
  },
67
68
  banking: {
68
69
  label: "Banking / Banca",
@@ -133,13 +134,14 @@ export const FILE_TO_GROUP = {
133
134
  kitchen: "pos",
134
135
  intelligence: "intelligence",
135
136
  gestoria: "intelligence",
137
+ search: "intelligence",
136
138
  products: "catalog",
137
139
  webhooks: "platform",
138
140
  portal_domain: "platform",
139
141
  };
140
142
  /**
141
143
  * Per-tool overrides where the source FILE places a tool in a different group
142
- * than a naive name match would (verified against the 157 registration sites).
144
+ * than a naive name match would (verified against the 158 registration sites).
143
145
  * These eight names live in a file whose domain differs from their name prefix
144
146
  * (e.g. e-invoicing tools say "invoice" but belong to fiscal/compliance).
145
147
  */
@@ -159,7 +161,7 @@ const NAME_OVERRIDES = {
159
161
  };
160
162
  /**
161
163
  * Assign a group by tool NAME. The name-based mapping reproduces the
162
- * source-file grouping exactly for all 157 current tools (verified), with the
164
+ * source-file grouping exactly for every current tool (verified), with the
163
165
  * eight cross-file cases pinned via NAME_OVERRIDES. Driven off the name (not a
164
166
  * hand-kept list) so a future tool lands somewhere sensible automatically.
165
167
  *
@@ -188,7 +190,7 @@ export function groupForTool(name) {
188
190
  return "pos";
189
191
  if (/(kitchen|menu_item)/.test(n))
190
192
  return "pos";
191
- if (/(business_context|monthly_summary|gestoria)/.test(n))
193
+ if (/(business_context|monthly_summary|gestoria|global_search)/.test(n))
192
194
  return "intelligence";
193
195
  if (/(product)/.test(n))
194
196
  return "catalog";
@@ -206,7 +208,7 @@ function firstSentence(desc) {
206
208
  const match = englishHalf.match(/^(.*?[.!?])(\s|$)/);
207
209
  const sentence = (match ? match[1] : englishHalf).trim();
208
210
  // Hard cap so a runaway description can't reintroduce context bloat.
209
- return sentence.length > 160 ? sentence.slice(0, 157).trimEnd() + "…" : sentence;
211
+ return sentence.length > 160 ? sentence.slice(0, 158).trimEnd() + "…" : sentence;
210
212
  }
211
213
  function inputSchemaFieldNames(schema) {
212
214
  if (schema instanceof z.ZodObject) {
@@ -388,7 +390,7 @@ server, options) {
388
390
  };
389
391
  // Register the meta-tools eagerly. Their handlers close over `catalog`/`groups`
390
392
  // which finish populating as the real tools register right after this call.
391
- registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes);
393
+ registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes, options?.describeToolExample);
392
394
  return handle;
393
395
  }
394
396
  /* ------------------------------------------------------------------ */
@@ -418,7 +420,7 @@ function textResult(payload) {
418
420
  */
419
421
  function registerMetaTools(
420
422
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
421
- originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes) {
423
+ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes, describeToolExample = "create_invoice") {
422
424
  const { catalog, groups } = handle;
423
425
  const oauthConfig = securitySchemes
424
426
  ? {
@@ -461,7 +463,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
461
463
  const describeToolInputShape = {
462
464
  name: z
463
465
  .string()
464
- .describe("Exact tool name as returned by search_tools (e.g. 'create_invoice') / " +
466
+ .describe(`Exact tool name as returned by search_tools (e.g. '${describeToolExample}') / ` +
465
467
  "Nombre exacto de la herramienta devuelto por search_tools."),
466
468
  };
467
469
  const listGroupsOutput = securitySchemes
@@ -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, and webhook test). Until a
8
- * staged CF deploys, the API returns a genuine HTTP 404. Live HR, payroll-read,
9
- * and period-read tools also retain this guard as workspace-rollout safety.
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, and webhook test). Until a
8
- * staged CF deploys, the API returns a genuine HTTP 404. Live HR, payroll-read,
9
- * and period-read tools also retain this guard as workspace-rollout safety.
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 {