@frihet/mcp-server 1.17.0 → 2.0.1

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 +106 -348
  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 +249 -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 +28 -21
  17. package/dist/demo-client.js +32 -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 +6 -2
  47. package/dist/server-composition.js +46 -5
  48. package/dist/tool-exposure.d.ts +7 -5
  49. package/dist/tool-exposure.js +21 -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 +254 -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 +18 -38
  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 +242 -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,17 +30,18 @@
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.
42
42
  */
43
43
  import { z } from "zod/v4";
44
+ import { READ_STAMP } from "./demo-fixtures.js";
44
45
  /**
45
46
  * Domain metadata. Keep these terse — they are the only group-level prose the
46
47
  * agent loads up front. Depth lives in the per-tool descriptions, fetched via
@@ -61,8 +62,9 @@ export const GROUPS = {
61
62
  },
62
63
  fiscal: {
63
64
  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.",
65
+ blurb: "Spanish/EU fiscal depth served on demand: Modelo 303/130/390/347, VeriFactu, TicketBAI, " +
66
+ "Facturae/FACe e-invoicing, GL audit, period close, VIES " +
67
+ "(Modelo 180/200/202/415/418/425, AIEM and KSeF registered but not deployed).",
66
68
  },
67
69
  banking: {
68
70
  label: "Banking / Banca",
@@ -133,13 +135,14 @@ export const FILE_TO_GROUP = {
133
135
  kitchen: "pos",
134
136
  intelligence: "intelligence",
135
137
  gestoria: "intelligence",
138
+ search: "intelligence",
136
139
  products: "catalog",
137
140
  webhooks: "platform",
138
141
  portal_domain: "platform",
139
142
  };
140
143
  /**
141
144
  * 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).
145
+ * than a naive name match would (verified against the 158 registration sites).
143
146
  * These eight names live in a file whose domain differs from their name prefix
144
147
  * (e.g. e-invoicing tools say "invoice" but belong to fiscal/compliance).
145
148
  */
@@ -159,7 +162,7 @@ const NAME_OVERRIDES = {
159
162
  };
160
163
  /**
161
164
  * 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
165
+ * source-file grouping exactly for every current tool (verified), with the
163
166
  * eight cross-file cases pinned via NAME_OVERRIDES. Driven off the name (not a
164
167
  * hand-kept list) so a future tool lands somewhere sensible automatically.
165
168
  *
@@ -188,7 +191,7 @@ export function groupForTool(name) {
188
191
  return "pos";
189
192
  if (/(kitchen|menu_item)/.test(n))
190
193
  return "pos";
191
- if (/(business_context|monthly_summary|gestoria)/.test(n))
194
+ if (/(business_context|monthly_summary|gestoria|global_search)/.test(n))
192
195
  return "intelligence";
193
196
  if (/(product)/.test(n))
194
197
  return "catalog";
@@ -206,7 +209,7 @@ function firstSentence(desc) {
206
209
  const match = englishHalf.match(/^(.*?[.!?])(\s|$)/);
207
210
  const sentence = (match ? match[1] : englishHalf).trim();
208
211
  // Hard cap so a runaway description can't reintroduce context bloat.
209
- return sentence.length > 160 ? sentence.slice(0, 157).trimEnd() + "…" : sentence;
212
+ return sentence.length > 160 ? sentence.slice(0, 158).trimEnd() + "…" : sentence;
210
213
  }
211
214
  function inputSchemaFieldNames(schema) {
212
215
  if (schema instanceof z.ZodObject) {
@@ -388,7 +391,7 @@ server, options) {
388
391
  };
389
392
  // Register the meta-tools eagerly. Their handlers close over `catalog`/`groups`
390
393
  // which finish populating as the real tools register right after this call.
391
- registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes);
394
+ registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes, options?.describeToolExample, options?.demoMode === true);
392
395
  return handle;
393
396
  }
394
397
  /* ------------------------------------------------------------------ */
@@ -418,8 +421,9 @@ function textResult(payload) {
418
421
  */
419
422
  function registerMetaTools(
420
423
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
421
- originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes) {
424
+ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes, describeToolExample = "create_invoice", demoMode = false) {
422
425
  const { catalog, groups } = handle;
426
+ const demoStamp = demoMode ? READ_STAMP : {};
423
427
  const oauthConfig = securitySchemes
424
428
  ? {
425
429
  securitySchemes: securitySchemes.map((scheme) => ({
@@ -461,7 +465,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
461
465
  const describeToolInputShape = {
462
466
  name: z
463
467
  .string()
464
- .describe("Exact tool name as returned by search_tools (e.g. 'create_invoice') / " +
468
+ .describe(`Exact tool name as returned by search_tools (e.g. '${describeToolExample}') / ` +
465
469
  "Nombre exacto de la herramienta devuelto por search_tools."),
466
470
  };
467
471
  const listGroupsOutput = securitySchemes
@@ -527,6 +531,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
527
531
  }))
528
532
  .filter((g) => g.toolCount > 0);
529
533
  return textResult({
534
+ ...demoStamp,
530
535
  groups: out,
531
536
  totalGroups: out.length,
532
537
  totalTools: catalog.size,
@@ -589,6 +594,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
589
594
  score,
590
595
  }));
591
596
  return textResult({
597
+ ...demoStamp,
592
598
  query: query || null,
593
599
  group: groupFilter ?? null,
594
600
  count: scored.length,
@@ -624,6 +630,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
624
630
  {
625
631
  type: "text",
626
632
  text: JSON.stringify({
633
+ ...demoStamp,
627
634
  error: name
628
635
  ? `No tool named '${name}'.`
629
636
  : "Provide { name } — the exact tool name from search_tools.",
@@ -636,6 +643,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
636
643
  };
637
644
  }
638
645
  return textResult({
646
+ ...demoStamp,
639
647
  name: entry.name,
640
648
  group: entry.group,
641
649
  title: entry.title,
@@ -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 {