@frihet/mcp-server 1.17.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -59
- package/dist/agent-onboarding.d.ts +1 -1
- package/dist/agent-onboarding.js +3 -3
- package/dist/canary/mcp-harness.d.ts +1 -1
- package/dist/canary/mcp-harness.js +4 -1
- package/dist/capability-truth.js +20 -12
- package/dist/claude-candidate-contract.d.ts +482 -0
- package/dist/claude-candidate-contract.js +87 -0
- package/dist/claude-output-privacy.d.ts +17 -0
- package/dist/claude-output-privacy.js +240 -0
- package/dist/claude-profile.d.ts +18 -0
- package/dist/claude-profile.js +260 -0
- package/dist/client-interface.d.ts +26 -20
- package/dist/client.d.ts +65 -29
- package/dist/client.js +278 -167
- package/dist/demo-client.d.ts +27 -21
- package/dist/demo-client.js +31 -14
- package/dist/deposit-contract.d.ts +29 -0
- package/dist/deposit-contract.js +76 -0
- package/dist/fetch-no-redirect.d.ts +3 -0
- package/dist/fetch-no-redirect.js +16 -0
- package/dist/fiscal-aliases.d.ts +2 -2
- package/dist/fiscal-aliases.js +2 -2
- package/dist/fiscal-period.d.ts +26 -0
- package/dist/fiscal-period.js +31 -0
- package/dist/google-profile.d.ts +19 -0
- package/dist/google-profile.js +122 -0
- package/dist/index.js +31 -7
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +2 -2
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.js +3 -2
- package/dist/openai-profile.d.ts +2 -2
- package/dist/openai-profile.js +37 -11
- package/dist/openai-review-contract.d.ts +1 -1
- package/dist/openai-review-contract.js +4 -5
- package/dist/openai-review-oauth.d.ts +107 -7
- package/dist/openai-review-oauth.js +161 -17
- package/dist/prompts/register-all.js +32 -78
- package/dist/redaction.d.ts +5 -0
- package/dist/redaction.js +9 -0
- package/dist/resources/register-all.js +150 -54
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +5 -2
- package/dist/server-composition.js +35 -5
- package/dist/tool-exposure.d.ts +6 -5
- package/dist/tool-exposure.js +15 -13
- package/dist/tools/audit_gl.js +8 -8
- package/dist/tools/backend-availability.d.ts +8 -29
- package/dist/tools/backend-availability.js +31 -30
- package/dist/tools/bank_rules.js +66 -15
- package/dist/tools/banking.js +5 -13
- package/dist/tools/clients.js +252 -6
- package/dist/tools/deposits.js +107 -97
- package/dist/tools/einvoice.d.ts +17 -3
- package/dist/tools/einvoice.js +59 -12
- package/dist/tools/fiscal.d.ts +3 -4
- package/dist/tools/fiscal.js +93 -60
- package/dist/tools/gestoria.js +15 -7
- package/dist/tools/hr.js +41 -11
- package/dist/tools/igic.d.ts +9 -8
- package/dist/tools/igic.js +34 -64
- package/dist/tools/impuesto_sociedades.d.ts +4 -5
- package/dist/tools/impuesto_sociedades.js +16 -37
- package/dist/tools/intelligence.js +164 -10
- package/dist/tools/invoices.js +109 -42
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/quotes.js +86 -25
- package/dist/tools/recurring.js +26 -25
- package/dist/tools/register-all.d.ts +2 -2
- package/dist/tools/register-all.js +9 -6
- package/dist/tools/search.d.ts +19 -0
- package/dist/tools/search.js +84 -0
- package/dist/tools/shared.d.ts +17 -37
- package/dist/tools/shared.js +96 -30
- package/dist/tools/team.js +8 -16
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +240 -6
- package/dist/types.d.ts +3 -1
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +15 -5
- package/package.json +26 -12
- package/dist/tools/onboarding.d.ts +0 -19
- package/dist/tools/onboarding.js +0 -64
package/dist/tool-exposure.js
CHANGED
|
@@ -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
|
|
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 +
|
|
34
|
-
*
|
|
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
|
|
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/
|
|
65
|
-
"
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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
|
package/dist/tools/audit_gl.js
CHANGED
|
@@ -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
|
|
48
|
-
"Sets entry status to 'rejected' and records the rejector +
|
|
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',
|
|
51
|
-
"/ Rechaza un asiento contable pendiente con una
|
|
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
|
-
|
|
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
|
|
59
|
-
}, async ({ entryId,
|
|
60
|
-
const result = await client.rejectGLEntry(entryId,
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
88
|
+
if (err.errorCode === "NOT_FOUND")
|
|
89
|
+
throw err;
|
|
90
|
+
return lookupUnavailableError(toolName, endpoint);
|
|
90
91
|
}
|
|
91
92
|
throw err;
|
|
92
93
|
}
|
package/dist/tools/bank_rules.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
}))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
88
|
-
}, async ({ name,
|
|
89
|
-
const result = await client.createBankRule({ name,
|
|
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)),
|
package/dist/tools/banking.js
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 {
|