@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.
- package/README.md +106 -348
- 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 +249 -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 +28 -21
- package/dist/demo-client.js +32 -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 +6 -2
- package/dist/server-composition.js +46 -5
- package/dist/tool-exposure.d.ts +7 -5
- package/dist/tool-exposure.js +21 -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 +254 -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 +18 -38
- 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 +242 -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/tools/clients.js
CHANGED
|
@@ -13,6 +13,171 @@ const addressSchema = z
|
|
|
13
13
|
})
|
|
14
14
|
.optional()
|
|
15
15
|
.describe("Client address / Direccion del cliente");
|
|
16
|
+
/**
|
|
17
|
+
* Splits a concatenated / camelCase / snake_case / kebab-case search query into
|
|
18
|
+
* space-separated tokens (e.g. "CanariasBlue" -> "Canarias Blue",
|
|
19
|
+
* "SLCanarias" -> "SL Canarias", "Canarias_Blue" -> "Canarias Blue").
|
|
20
|
+
*/
|
|
21
|
+
function splitCompoundQuery(q) {
|
|
22
|
+
return q
|
|
23
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
24
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
25
|
+
.replace(/[_-]+/g, " ")
|
|
26
|
+
.replace(/\s+/g, " ")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
const OPTIONAL_CLIENT_STRING_FIELDS = [
|
|
30
|
+
"email",
|
|
31
|
+
"phone",
|
|
32
|
+
"stage",
|
|
33
|
+
"taxId",
|
|
34
|
+
"createdAt",
|
|
35
|
+
"updatedAt",
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Normalizes a client record returned by the Frihet API so it always satisfies
|
|
39
|
+
* `clientItemOutput` without losing ERP-canonical data:
|
|
40
|
+
* - Strips `null` on optional string fields (`email`, `phone`, `stage`, `taxId`,
|
|
41
|
+
* `createdAt`, `updatedAt`) so `z.string().optional()` never rejects legacy docs.
|
|
42
|
+
* - Backfills `taxId` from legacy `nif` / `cif` / `vatNumber` / `tax_id` when
|
|
43
|
+
* `taxId` is absent or null, preserving all original fields.
|
|
44
|
+
* - Converts legacy string `address` into `{ street }`.
|
|
45
|
+
* - Omits `null` or empty `address` (which otherwise fails `addressOutputSchema`).
|
|
46
|
+
* - Maps ERP canonical address keys (`province` -> `state`, `zip` -> `postalCode`,
|
|
47
|
+
* `countryCode` -> `country`) because `publicApi.ts` (`normalizeAddress`) stores
|
|
48
|
+
* `zip` and `province`, whereas `addressOutputSchema` only declares `postalCode`
|
|
49
|
+
* and `state` (and strips unknown keys in `structuredContent`).
|
|
50
|
+
*/
|
|
51
|
+
function normalizeClientRecord(clientRecord) {
|
|
52
|
+
if (!clientRecord || typeof clientRecord !== "object" || Array.isArray(clientRecord)) {
|
|
53
|
+
return clientRecord;
|
|
54
|
+
}
|
|
55
|
+
const out = { ...clientRecord };
|
|
56
|
+
for (const field of OPTIONAL_CLIENT_STRING_FIELDS) {
|
|
57
|
+
if (out[field] === null) {
|
|
58
|
+
delete out[field];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (typeof out.taxId !== "string" || !out.taxId.trim()) {
|
|
62
|
+
for (const alias of ["nif", "cif", "vatNumber", "tax_id"]) {
|
|
63
|
+
const candidate = out[alias];
|
|
64
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
65
|
+
out.taxId = candidate.trim();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const rawAddress = out.address;
|
|
71
|
+
const rootStreet = typeof out.street === "string" && out.street.trim() ? out.street.trim() : undefined;
|
|
72
|
+
const rootCity = typeof out.city === "string" && out.city.trim() ? out.city.trim() : undefined;
|
|
73
|
+
const rootState = typeof out.state === "string" && out.state.trim()
|
|
74
|
+
? out.state.trim()
|
|
75
|
+
: typeof out.province === "string" && out.province.trim()
|
|
76
|
+
? out.province.trim()
|
|
77
|
+
: undefined;
|
|
78
|
+
const rootPostalCode = typeof out.postalCode === "string" && out.postalCode.trim()
|
|
79
|
+
? out.postalCode.trim()
|
|
80
|
+
: typeof out.zip === "string" && out.zip.trim()
|
|
81
|
+
? out.zip.trim()
|
|
82
|
+
: undefined;
|
|
83
|
+
const rootCountry = typeof out.country === "string" && out.country.trim()
|
|
84
|
+
? out.country.trim()
|
|
85
|
+
: typeof out.countryCode === "string" && out.countryCode.trim()
|
|
86
|
+
? out.countryCode.trim()
|
|
87
|
+
: undefined;
|
|
88
|
+
const buildFlatAddress = (streetOverride) => {
|
|
89
|
+
const built = {};
|
|
90
|
+
const st = streetOverride ?? rootStreet;
|
|
91
|
+
if (st !== undefined)
|
|
92
|
+
built.street = st;
|
|
93
|
+
if (rootCity !== undefined)
|
|
94
|
+
built.city = rootCity;
|
|
95
|
+
if (rootState !== undefined)
|
|
96
|
+
built.state = rootState;
|
|
97
|
+
if (rootPostalCode !== undefined)
|
|
98
|
+
built.postalCode = rootPostalCode;
|
|
99
|
+
if (rootCountry !== undefined)
|
|
100
|
+
built.country = rootCountry;
|
|
101
|
+
return Object.keys(built).length > 0 ? built : undefined;
|
|
102
|
+
};
|
|
103
|
+
if (rawAddress === undefined || rawAddress === null) {
|
|
104
|
+
if (rawAddress === null) {
|
|
105
|
+
delete out.address;
|
|
106
|
+
}
|
|
107
|
+
const flatAddr = buildFlatAddress();
|
|
108
|
+
if (flatAddr) {
|
|
109
|
+
out.address = flatAddr;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
if (typeof rawAddress === "string") {
|
|
114
|
+
const trimmed = rawAddress.trim();
|
|
115
|
+
const flatAddr = buildFlatAddress(trimmed || undefined);
|
|
116
|
+
if (!flatAddr) {
|
|
117
|
+
delete out.address;
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
out.address = flatAddr;
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
if (typeof rawAddress === "object" && !Array.isArray(rawAddress)) {
|
|
124
|
+
const addrObj = rawAddress;
|
|
125
|
+
const normalizedAddress = {};
|
|
126
|
+
for (const [k, v] of Object.entries(addrObj)) {
|
|
127
|
+
if (v !== null && v !== undefined) {
|
|
128
|
+
normalizedAddress[k] = v;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const street = typeof addrObj.street === "string" && addrObj.street.trim()
|
|
132
|
+
? addrObj.street.trim()
|
|
133
|
+
: rootStreet;
|
|
134
|
+
const city = typeof addrObj.city === "string" && addrObj.city.trim()
|
|
135
|
+
? addrObj.city.trim()
|
|
136
|
+
: rootCity;
|
|
137
|
+
const state = typeof addrObj.state === "string" && addrObj.state.trim()
|
|
138
|
+
? addrObj.state.trim()
|
|
139
|
+
: typeof addrObj.province === "string" && addrObj.province.trim()
|
|
140
|
+
? addrObj.province.trim()
|
|
141
|
+
: rootState;
|
|
142
|
+
const postalCode = typeof addrObj.postalCode === "string" && addrObj.postalCode.trim()
|
|
143
|
+
? addrObj.postalCode.trim()
|
|
144
|
+
: typeof addrObj.zip === "string" && addrObj.zip.trim()
|
|
145
|
+
? addrObj.zip.trim()
|
|
146
|
+
: rootPostalCode;
|
|
147
|
+
const country = typeof addrObj.country === "string" && addrObj.country.trim()
|
|
148
|
+
? addrObj.country.trim()
|
|
149
|
+
: typeof addrObj.countryCode === "string" && addrObj.countryCode.trim()
|
|
150
|
+
? addrObj.countryCode.trim()
|
|
151
|
+
: rootCountry;
|
|
152
|
+
if (street !== undefined)
|
|
153
|
+
normalizedAddress.street = street;
|
|
154
|
+
else
|
|
155
|
+
delete normalizedAddress.street;
|
|
156
|
+
if (city !== undefined)
|
|
157
|
+
normalizedAddress.city = city;
|
|
158
|
+
else
|
|
159
|
+
delete normalizedAddress.city;
|
|
160
|
+
if (state !== undefined)
|
|
161
|
+
normalizedAddress.state = state;
|
|
162
|
+
else
|
|
163
|
+
delete normalizedAddress.state;
|
|
164
|
+
if (postalCode !== undefined)
|
|
165
|
+
normalizedAddress.postalCode = postalCode;
|
|
166
|
+
else
|
|
167
|
+
delete normalizedAddress.postalCode;
|
|
168
|
+
if (country !== undefined)
|
|
169
|
+
normalizedAddress.country = country;
|
|
170
|
+
else
|
|
171
|
+
delete normalizedAddress.country;
|
|
172
|
+
if (Object.keys(normalizedAddress).length === 0) {
|
|
173
|
+
delete out.address;
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
out.address = normalizedAddress;
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
16
181
|
export function registerClientTools(server, client) {
|
|
17
182
|
// -- list_clients --
|
|
18
183
|
server.registerTool("list_clients", {
|
|
@@ -44,10 +209,93 @@ export function registerClientTools(server, client) {
|
|
|
44
209
|
},
|
|
45
210
|
outputSchema: paginatedOutput(clientItemOutput, { projectable: true }),
|
|
46
211
|
}, async ({ limit, offset, q, stage, fields, after }) => withToolLogging("list_clients", async () => {
|
|
47
|
-
const
|
|
212
|
+
const trimmedQ = q?.trim();
|
|
213
|
+
const trimmedAfter = after?.trim();
|
|
214
|
+
// Limitation in Frihet-ERP `listResources` (publicApi.ts L8158-8268):
|
|
215
|
+
// When `search` (`q`) is provided, the backend slices results by `offset`/`limit`
|
|
216
|
+
// and returns before evaluating `cursorParam` (`after`). Moreover, protected
|
|
217
|
+
// profiles (Claude `claudeToolResult` and OpenAI `applyOpenAIProfile`) rebuild
|
|
218
|
+
// `content` from `structuredContent` while `paginatedOutput` is frozen, so a
|
|
219
|
+
// text-only note would be silently discarded. Fail closed (`isError: true`
|
|
220
|
+
// without `structuredContent`) so no profile mistakes page 1 for a cursor page.
|
|
221
|
+
if (trimmedQ && trimmedAfter) {
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: "Error: text search ('q') cannot be combined with cursor pagination ('after') because the ERP backend slices search results by 'offset'/'limit' and ignores 'after'. Use 'offset' and 'limit' when searching with 'q', or omit 'q' when paginating with 'after'. / " +
|
|
226
|
+
"Error: la busqueda textual ('q') no se puede combinar con el cursor 'after' porque el backend pagina las busquedas mediante 'offset'/'limit' e ignora 'after'. Usa 'offset' y 'limit' con 'q', u omite 'q' al paginar con 'after'.",
|
|
227
|
+
}],
|
|
228
|
+
isError: true,
|
|
229
|
+
_meta: { "io.frihet/cursorWithQueryUnsupported": true },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
let result = await client.listClients({ limit, offset, after, fields, q, stage });
|
|
233
|
+
// When a concatenated / camelCase query (e.g. "CanariasBlue") yields zero total matches,
|
|
234
|
+
// retry with split tokens ("Canarias Blue") using the same offset/limit/fields/stage.
|
|
235
|
+
// Guardrails matching publicApi.ts `listResources`:
|
|
236
|
+
// 1. Never fallback when `after` cursor is supplied with `q`.
|
|
237
|
+
// 2. Never fallback when the first scan hit the 500-document cap (`truncated === true`),
|
|
238
|
+
// since total===0 only reflects the first 500 scanned records.
|
|
239
|
+
// 3. On page 2+ (`offset > 0`), only fallback when `result.total === 0` explicitly (not
|
|
240
|
+
// when `total === undefined`, which indicates an exhausted offset).
|
|
241
|
+
let usedFallback = false;
|
|
242
|
+
const splitQ = trimmedQ ? splitCompoundQuery(trimmedQ) : undefined;
|
|
243
|
+
const isSubsequentPage = (offset !== undefined && offset > 0) || Boolean(after);
|
|
244
|
+
const initialTruncated = result?.truncated === true;
|
|
245
|
+
const canFallback = splitQ !== undefined && splitQ !== trimmedQ && !after && !initialTruncated;
|
|
246
|
+
const hasZeroTotalMatches = Array.isArray(result?.data) &&
|
|
247
|
+
result.data.length === 0 &&
|
|
248
|
+
(isSubsequentPage ? result.total === 0 : result.total === 0 || result.total === undefined);
|
|
249
|
+
if (canFallback && hasZeroTotalMatches) {
|
|
250
|
+
try {
|
|
251
|
+
const fallbackResult = await client.listClients({ limit, offset, after, fields, q: splitQ, stage });
|
|
252
|
+
const fallbackTruncated = fallbackResult?.truncated === true;
|
|
253
|
+
if (fallbackTruncated ||
|
|
254
|
+
(fallbackResult?.data && (fallbackResult.data.length > 0 || (fallbackResult.total !== undefined && fallbackResult.total > 0)))) {
|
|
255
|
+
result = fallbackResult;
|
|
256
|
+
usedFallback = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Preserve original response if fallback fails
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const rawEnvelope = (result ?? {});
|
|
264
|
+
const isTruncated = rawEnvelope.truncated === true;
|
|
265
|
+
const normalizedData = Array.isArray(result?.data)
|
|
266
|
+
? result.data.map((c) => (c && typeof c === "object" ? normalizeClientRecord(c) : c))
|
|
267
|
+
: [];
|
|
268
|
+
// Conservative refusal when the ERP 500-document scan cap was saturated (`truncated: true`).
|
|
269
|
+
// Because `paginatedOutput` is a frozen schema contract and both Claude (`claudeToolResult`)
|
|
270
|
+
// and OpenAI (`applyOpenAIProfile`) overwrite `content` whenever `structuredContent` is
|
|
271
|
+
// present, returning `structuredContent` here would hide the truncation warning and present
|
|
272
|
+
// an incomplete 500-doc scan as exhaustive.
|
|
273
|
+
if (isTruncated) {
|
|
274
|
+
const partialCount = typeof result?.total === "number" ? result.total : normalizedData.length;
|
|
275
|
+
return {
|
|
276
|
+
content: [{
|
|
277
|
+
type: "text",
|
|
278
|
+
text: `Error: client search hit the ERP 500-document scan cap (truncated=true; ${partialCount} match(es) within the first 500 scanned records) and cannot be returned as an exhaustive result across protected profiles. Narrow 'q' (or filter by 'stage') or paginate without 'q' using 'after'. / ` +
|
|
279
|
+
`Error: la busqueda de clientes alcanzo el limite de escaneo de 500 documentos del ERP (truncated=true; ${partialCount} coincidencia(s) en los primeros 500 registros) y no puede devolverse como resultado exhaustivo. Acota 'q' (o filtra por 'stage') o pagina sin 'q' usando 'after'.`,
|
|
280
|
+
}],
|
|
281
|
+
isError: true,
|
|
282
|
+
_meta: { "io.frihet/searchScanTruncated": true },
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const cleanEnvelope = {
|
|
286
|
+
data: normalizedData,
|
|
287
|
+
total: typeof result?.total === "number" ? result.total : normalizedData.length,
|
|
288
|
+
limit: typeof result?.limit === "number" ? result.limit : (limit ?? 20),
|
|
289
|
+
offset: typeof result?.offset === "number" ? result.offset : (offset ?? 0),
|
|
290
|
+
...(typeof result?.nextCursor === "string" ? { nextCursor: result.nextCursor } : {}),
|
|
291
|
+
};
|
|
292
|
+
const notes = [];
|
|
293
|
+
if (usedFallback) {
|
|
294
|
+
notes.push(` (criterio aplicado: "${splitQ}")`);
|
|
295
|
+
}
|
|
48
296
|
return {
|
|
49
|
-
content: [listContent(formatPaginatedResponse("clients",
|
|
50
|
-
structuredContent:
|
|
297
|
+
content: [listContent(formatPaginatedResponse("clients", cleanEnvelope) + notes.join(""))],
|
|
298
|
+
structuredContent: cleanEnvelope,
|
|
51
299
|
};
|
|
52
300
|
}));
|
|
53
301
|
// -- get_client --
|
|
@@ -61,7 +309,7 @@ export function registerClientTools(server, client) {
|
|
|
61
309
|
},
|
|
62
310
|
outputSchema: clientItemOutput,
|
|
63
311
|
}, async ({ id }) => withToolLogging("get_client", async () => {
|
|
64
|
-
const result = await client.getClient(id);
|
|
312
|
+
const result = normalizeClientRecord(await client.getClient(id));
|
|
65
313
|
return {
|
|
66
314
|
content: [getContent(formatRecord("Client", result))],
|
|
67
315
|
structuredContent: result,
|
|
@@ -85,7 +333,7 @@ export function registerClientTools(server, client) {
|
|
|
85
333
|
},
|
|
86
334
|
outputSchema: clientItemOutput,
|
|
87
335
|
}, async (input) => withToolLogging("create_client", async () => {
|
|
88
|
-
const result = await client.createClient(input);
|
|
336
|
+
const result = normalizeClientRecord(await client.createClient(input));
|
|
89
337
|
const hints = enrichResponse("clients", "create", result);
|
|
90
338
|
return {
|
|
91
339
|
content: [mutateContent(formatRecord("Client created", result) + hints)],
|
|
@@ -109,7 +357,7 @@ export function registerClientTools(server, client) {
|
|
|
109
357
|
},
|
|
110
358
|
outputSchema: clientItemOutput,
|
|
111
359
|
}, async ({ id, ...data }) => withToolLogging("update_client", async () => {
|
|
112
|
-
const result = await client.updateClient(id, data);
|
|
360
|
+
const result = normalizeClientRecord(await client.updateClient(id, data));
|
|
113
361
|
return {
|
|
114
362
|
content: [mutateContent(formatRecord("Client updated", result))],
|
|
115
363
|
structuredContent: result,
|
package/dist/tools/deposits.js
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
* Deposit tools for the Frihet MCP server.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
|
-
import {
|
|
5
|
+
import { applyDepositFields, depositFields, DepositInputError } from "../deposit-contract.js";
|
|
6
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, enrichResponse, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, ERROR_CONTENT_ANNOTATIONS, paginatedOutput, deleteResultOutput, depositItemOutput, actionResultOutput } from "./shared.js";
|
|
7
|
+
function depositValidationResponse(error) {
|
|
8
|
+
return {
|
|
9
|
+
isError: true,
|
|
10
|
+
content: [{ type: "text", text: `Error: ${error.message}`, annotations: ERROR_CONTENT_ANNOTATIONS }],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
6
13
|
export function registerDepositTools(server, client) {
|
|
7
14
|
// -- list_deposits --
|
|
8
15
|
server.registerTool("list_deposits", {
|
|
@@ -69,69 +76,71 @@ export function registerDepositTools(server, client) {
|
|
|
69
76
|
// -- create_deposit --
|
|
70
77
|
server.registerTool("create_deposit", {
|
|
71
78
|
title: "Create Deposit",
|
|
72
|
-
description: "Record a
|
|
73
|
-
"
|
|
74
|
-
"Example: clientId='abc123', amount=500,
|
|
75
|
-
"/ Registra un
|
|
76
|
-
"
|
|
79
|
+
description: "Record a deposit in Frihet's bookkeeping; no money is transferred. Requires clientId, clientName, amount, date and description. " +
|
|
80
|
+
"The API initializes status='active'; status and notes are not writable. " +
|
|
81
|
+
"Example: clientId='abc123', clientName='Example Client', amount=500, date='2026-10-21', description='Project retainer' " +
|
|
82
|
+
"/ Registra un depósito contable sin transferir dinero. Requiere clientId, clientName, amount, date y description. " +
|
|
83
|
+
"La API inicia el estado 'active'; status y notes no son campos editables.",
|
|
77
84
|
annotations: CREATE_ANNOTATIONS,
|
|
78
|
-
inputSchema: {
|
|
79
|
-
clientId:
|
|
80
|
-
clientName:
|
|
81
|
-
amount:
|
|
82
|
-
currency:
|
|
83
|
-
date:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
status: z
|
|
89
|
-
.string()
|
|
90
|
-
.optional()
|
|
91
|
-
.describe("Status (e.g. 'pending', 'applied', 'refunded') / Estado"),
|
|
92
|
-
paymentMethod: z
|
|
93
|
-
.string()
|
|
94
|
-
.optional()
|
|
95
|
-
.describe("Payment method (e.g. 'bank_transfer', 'card') / Metodo de pago"),
|
|
96
|
-
reference: z.string().optional().describe("External reference or receipt number / Referencia externa"),
|
|
97
|
-
notes: z.string().optional().describe("Internal notes / Notas internas"),
|
|
98
|
-
},
|
|
85
|
+
inputSchema: z.object({
|
|
86
|
+
clientId: depositFields.clientId.describe("Client ID / ID del cliente"),
|
|
87
|
+
clientName: depositFields.clientName.describe("Client name / Nombre del cliente"),
|
|
88
|
+
amount: depositFields.amount.describe("Positive deposit amount / Importe positivo del depósito"),
|
|
89
|
+
currency: depositFields.currency.optional().describe("Currency code, defaults to EUR / Moneda, EUR por defecto"),
|
|
90
|
+
date: depositFields.receivedDate.describe("Received date (YYYY-MM-DD), sent as receivedDate / Fecha de recepción"),
|
|
91
|
+
description: depositFields.description.describe("Deposit description / Descripción del depósito"),
|
|
92
|
+
paymentMethod: depositFields.paymentMethod.describe("Payment method (e.g. bank_transfer) / Método de pago"),
|
|
93
|
+
reference: depositFields.paymentReference.describe("Payment reference, sent as paymentReference / Referencia del pago"),
|
|
94
|
+
}).strict(),
|
|
99
95
|
outputSchema: depositItemOutput,
|
|
100
96
|
}, async (input) => withToolLogging("create_deposit", async () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.createDeposit(input);
|
|
99
|
+
const hints = enrichResponse("deposits", "create", result);
|
|
100
|
+
return {
|
|
101
|
+
content: [mutateContent(formatRecord("Deposit created", result) + hints)],
|
|
102
|
+
structuredContent: { ...result },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof DepositInputError)
|
|
107
|
+
return depositValidationResponse(error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
107
110
|
}));
|
|
108
111
|
// -- update_deposit --
|
|
109
112
|
server.registerTool("update_deposit", {
|
|
110
113
|
title: "Update Deposit",
|
|
111
114
|
description: "Update an existing deposit using PATCH semantics. Only the provided fields will be changed. " +
|
|
112
|
-
"
|
|
113
|
-
"
|
|
115
|
+
"At least one writable field is required; status and notes are not writable. " +
|
|
116
|
+
"Example: id='abc123', reference='RECEIPT-42' " +
|
|
117
|
+
"/ Actualiza un depósito existente. Requiere al menos un campo editable; status y notes no se pueden guardar.",
|
|
114
118
|
annotations: UPDATE_ANNOTATIONS,
|
|
115
|
-
inputSchema: {
|
|
116
|
-
id: z.string().describe("Deposit ID / ID del
|
|
117
|
-
clientId:
|
|
118
|
-
clientName:
|
|
119
|
-
amount:
|
|
120
|
-
currency:
|
|
121
|
-
date:
|
|
122
|
-
description:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
notes: z.string().optional().describe("Notes / Notas"),
|
|
127
|
-
},
|
|
119
|
+
inputSchema: z.object({
|
|
120
|
+
id: z.string().min(1).describe("Deposit ID / ID del depósito"),
|
|
121
|
+
clientId: depositFields.clientId.optional().describe("Client ID / ID del cliente"),
|
|
122
|
+
clientName: depositFields.clientName.optional().describe("Client name / Nombre del cliente"),
|
|
123
|
+
amount: depositFields.amount.optional().describe("Positive amount / Importe positivo"),
|
|
124
|
+
currency: depositFields.currency.optional().describe("Currency code / Moneda"),
|
|
125
|
+
date: depositFields.receivedDate.optional().describe("Received date (YYYY-MM-DD) / Fecha de recepción"),
|
|
126
|
+
description: depositFields.description.optional().describe("Description / Descripción"),
|
|
127
|
+
paymentMethod: depositFields.paymentMethod.describe("Payment method / Método de pago"),
|
|
128
|
+
reference: depositFields.paymentReference.describe("Payment reference / Referencia del pago"),
|
|
129
|
+
}).strict().refine(({ id: _id, ...fields }) => Object.values(fields).some((value) => value !== undefined), { message: "Provide at least one writable deposit field / Indica al menos un campo del depósito" }),
|
|
128
130
|
outputSchema: depositItemOutput,
|
|
129
131
|
}, async ({ id, ...data }) => withToolLogging("update_deposit", async () => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
try {
|
|
133
|
+
const result = await client.updateDeposit(id, data);
|
|
134
|
+
return {
|
|
135
|
+
content: [mutateContent(formatRecord("Deposit updated", result))],
|
|
136
|
+
structuredContent: result,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof DepositInputError)
|
|
141
|
+
return depositValidationResponse(error);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
135
144
|
}));
|
|
136
145
|
// -- delete_deposit --
|
|
137
146
|
server.registerTool("delete_deposit", {
|
|
@@ -156,62 +165,63 @@ export function registerDepositTools(server, client) {
|
|
|
156
165
|
title: "Apply Deposit",
|
|
157
166
|
description: "Apply a deposit to an invoice or mark it as used. " +
|
|
158
167
|
"Transitions the deposit status to 'applied'. " +
|
|
159
|
-
"
|
|
160
|
-
"
|
|
168
|
+
"ERP requires invoiceId + invoiceNumber + amount on the body; id is the deposit ID; notes is not writable. " +
|
|
169
|
+
"Example: id='dep_abc123', invoiceId='inv_xyz', invoiceNumber='F-2026-007', amount=150 " +
|
|
170
|
+
"/ Aplica un deposito a una factura o lo marca como utilizado. Requiere invoiceId, invoiceNumber y amount; notes no se puede guardar.",
|
|
161
171
|
annotations: UPDATE_ANNOTATIONS,
|
|
162
|
-
inputSchema: {
|
|
163
|
-
id: z.string().describe("Deposit ID / ID del deposito"),
|
|
164
|
-
invoiceId:
|
|
165
|
-
.string()
|
|
166
|
-
.optional()
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
id: z.string().min(1).describe("Deposit ID / ID del deposito"),
|
|
174
|
+
invoiceId: applyDepositFields.invoiceId
|
|
167
175
|
.describe("Invoice ID to apply the deposit to / ID de la factura a la que aplicar el deposito"),
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
invoiceNumber: applyDepositFields.invoiceNumber
|
|
177
|
+
.describe("Invoice number (document number) the deposit applies to / Numero de la factura"),
|
|
178
|
+
amount: applyDepositFields.amount
|
|
179
|
+
.describe("Amount of the deposit to apply in EUR / Importe del deposito a aplicar en EUR"),
|
|
180
|
+
}).strict(),
|
|
170
181
|
outputSchema: actionResultOutput,
|
|
171
182
|
}, async ({ id, ...data }) => withToolLogging("apply_deposit", async () => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
try {
|
|
184
|
+
const result = await client.applyDeposit(id, data);
|
|
185
|
+
return {
|
|
186
|
+
content: [mutateContent(formatRecord("Deposit applied", result))],
|
|
187
|
+
structuredContent: result,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error instanceof DepositInputError)
|
|
192
|
+
return depositValidationResponse(error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
177
195
|
}));
|
|
178
196
|
// -- refund_deposit --
|
|
179
197
|
server.registerTool("refund_deposit", {
|
|
180
|
-
title: "
|
|
181
|
-
description: "
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"Example: id='dep_abc123',
|
|
185
|
-
"/
|
|
186
|
-
"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
title: "Record Deposit Refund",
|
|
199
|
+
description: "Record a full or partial deposit refund in Frihet's bookkeeping; no money is transferred. Requires confirm=true. " +
|
|
200
|
+
"Omit amount to record a refund of the remaining balance. Only a full refund sets status='refunded'. " +
|
|
201
|
+
"A new call can record another partial refund; after an uncertain result, check get_deposit before retrying. " +
|
|
202
|
+
"Example: id='dep_abc123', amount=50, confirm=true " +
|
|
203
|
+
"/ Registra una devolución total o parcial del depósito sin transferir dinero. Requiere confirm=true. " +
|
|
204
|
+
"Omite amount para registrar la devolución del saldo restante. Solo la devolución total marca el estado 'refunded'. " +
|
|
205
|
+
"Una nueva llamada puede registrar otra devolución parcial; ante un resultado incierto, consulta get_deposit antes de repetir.",
|
|
206
|
+
annotations: { ...UPDATE_ANNOTATIONS, destructiveHint: true, idempotentHint: false },
|
|
207
|
+
inputSchema: z.object({
|
|
208
|
+
id: z.string().min(1).describe("Deposit ID / ID del depósito"),
|
|
209
|
+
amount: z.number().positive().optional().describe("Positive refund amount in the deposit currency; omit for the remaining balance / Importe positivo en la moneda del depósito; omite para el saldo restante"),
|
|
192
210
|
confirm: z
|
|
193
211
|
.boolean()
|
|
194
|
-
.describe("Must be true to
|
|
195
|
-
},
|
|
212
|
+
.describe("Must be true to record the refund; no money is transferred / Debe ser true para registrar la devolución; no se transfiere dinero"),
|
|
213
|
+
}).strict(),
|
|
196
214
|
outputSchema: actionResultOutput,
|
|
197
|
-
}, async ({ id, confirm,
|
|
198
|
-
if (
|
|
199
|
-
return
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
text: "Error: confirm=true is required to refund a deposit. " +
|
|
204
|
-
"This returns the client's money and sets the deposit to status=refunded; " +
|
|
205
|
-
"there is no un-refund tool. Set confirm=true when you are certain. / " +
|
|
206
|
-
"Se requiere confirm=true para devolver un deposito. Es un movimiento de dinero irreversible.",
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
isError: true,
|
|
210
|
-
};
|
|
215
|
+
}, async ({ id, confirm, amount }) => withToolLogging("refund_deposit", async () => {
|
|
216
|
+
if (confirm !== true) {
|
|
217
|
+
return confirmationRequiredError("Error: confirm=true is required to record a deposit refund. " +
|
|
218
|
+
"This reduces the recorded remaining balance; no money is transferred and this tool cannot undo the entry. / " +
|
|
219
|
+
"Se requiere confirm=true para registrar la devolución y reducir el saldo contable. " +
|
|
220
|
+
"No se transfiere dinero y esta herramienta no permite deshacer el registro.");
|
|
211
221
|
}
|
|
212
|
-
const result = await client.refundDeposit(id,
|
|
222
|
+
const result = await client.refundDeposit(id, amount === undefined ? {} : { amount });
|
|
213
223
|
return {
|
|
214
|
-
content: [mutateContent(formatRecord("Deposit
|
|
224
|
+
content: [mutateContent(formatRecord("Deposit refund recorded (no money transferred)", result))],
|
|
215
225
|
structuredContent: result,
|
|
216
226
|
};
|
|
217
227
|
}));
|
package/dist/tools/einvoice.d.ts
CHANGED
|
@@ -48,19 +48,33 @@ import type { IFrihetClient } from "../client-interface.js";
|
|
|
48
48
|
* | facturae | Facturae | Spain (ES B2G mandatory) | Facturae 3.2.x — AEAT/FACe submission |
|
|
49
49
|
*/
|
|
50
50
|
export declare const eInvoiceFormatSchema: z.ZodEnum<{
|
|
51
|
+
facturae: "facturae";
|
|
51
52
|
"xrechnung-cii": "xrechnung-cii";
|
|
52
53
|
"xrechnung-ubl": "xrechnung-ubl";
|
|
53
54
|
"facturx-en16931": "facturx-en16931";
|
|
55
|
+
fatturapa: "fatturapa";
|
|
56
|
+
"peppol-bis-3": "peppol-bis-3";
|
|
57
|
+
ubl: "ubl";
|
|
58
|
+
cii: "cii";
|
|
54
59
|
"facturx-extended": "facturx-extended";
|
|
55
60
|
"facturx-basic": "facturx-basic";
|
|
56
61
|
"facturx-minimum": "facturx-minimum";
|
|
62
|
+
}>;
|
|
63
|
+
export type EInvoiceFormat = z.infer<typeof eInvoiceFormatSchema>;
|
|
64
|
+
/** Export is narrower than dispatch/validation. The ERP public export API
|
|
65
|
+
* exposes Factur-X EN16931 only; accepting Extended/Basic/Minimum here and
|
|
66
|
+
* silently mapping them to EN16931 would produce the wrong fiscal profile. */
|
|
67
|
+
export declare const eInvoiceExportFormatSchema: z.ZodEnum<{
|
|
68
|
+
facturae: "facturae";
|
|
69
|
+
"xrechnung-cii": "xrechnung-cii";
|
|
70
|
+
"xrechnung-ubl": "xrechnung-ubl";
|
|
71
|
+
"facturx-en16931": "facturx-en16931";
|
|
57
72
|
fatturapa: "fatturapa";
|
|
73
|
+
"peppol-bis-3": "peppol-bis-3";
|
|
74
|
+
"fa-2-ksef": "fa-2-ksef";
|
|
58
75
|
ubl: "ubl";
|
|
59
76
|
cii: "cii";
|
|
60
|
-
"peppol-bis-3": "peppol-bis-3";
|
|
61
|
-
facturae: "facturae";
|
|
62
77
|
}>;
|
|
63
|
-
export type EInvoiceFormat = z.infer<typeof eInvoiceFormatSchema>;
|
|
64
78
|
export declare const sendEInvoiceOutput: z.ZodObject<{
|
|
65
79
|
workflowRunId: z.ZodString;
|
|
66
80
|
status: z.ZodLiteral<"queued">;
|