@frihet/mcp-server 1.16.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -82
- package/dist/agent-onboarding.d.ts +98 -0
- package/dist/agent-onboarding.js +275 -0
- package/dist/api-origin.d.ts +9 -0
- package/dist/api-origin.js +41 -0
- package/dist/canary/mcp-harness.d.ts +77 -0
- package/dist/canary/mcp-harness.js +552 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +262 -0
- 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 +47 -33
- package/dist/client.d.ts +172 -40
- package/dist/client.js +708 -165
- package/dist/demo-client.d.ts +41 -32
- package/dist/demo-client.js +202 -31
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- 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 +44 -0
- package/dist/fiscal-aliases.js +62 -0
- 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 +68 -59
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +194 -106
- package/dist/openai-profile.d.ts +26 -22
- package/dist/openai-profile.js +1193 -157
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +714 -41
- package/dist/openai-review-oauth.d.ts +149 -2
- package/dist/openai-review-oauth.js +220 -6
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +34 -78
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +15 -0
- package/dist/redaction.js +83 -0
- package/dist/resources/register-all.js +154 -64
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +19 -0
- package/dist/server-composition.js +74 -0
- package/dist/tool-exposure.d.ts +31 -27
- package/dist/tool-exposure.js +211 -87
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- 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 +261 -15
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +114 -85
- package/dist/tools/einvoice.d.ts +16 -2
- package/dist/tools/einvoice.js +59 -12
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- 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.d.ts +2 -3
- package/dist/tools/hr.js +49 -20
- 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 +166 -12
- package/dist/tools/invoices.js +224 -54
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/payroll.d.ts +5 -8
- package/dist/tools/payroll.js +19 -32
- package/dist/tools/permissions.d.ts +4 -7
- package/dist/tools/permissions.js +11 -14
- package/dist/tools/products.js +6 -6
- package/dist/tools/quotes.js +172 -26
- 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 +1445 -109
- package/dist/tools/shared.js +478 -102
- package/dist/tools/team.js +24 -28
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +249 -15
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +31 -5
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +391 -0
- package/package.json +49 -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,91 @@ 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
|
+
};
|
|
230
|
+
}
|
|
231
|
+
let result = await client.listClients({ limit, offset, after, fields, q, stage });
|
|
232
|
+
// When a concatenated / camelCase query (e.g. "CanariasBlue") yields zero total matches,
|
|
233
|
+
// retry with split tokens ("Canarias Blue") using the same offset/limit/fields/stage.
|
|
234
|
+
// Guardrails matching publicApi.ts `listResources`:
|
|
235
|
+
// 1. Never fallback when `after` cursor is supplied with `q`.
|
|
236
|
+
// 2. Never fallback when the first scan hit the 500-document cap (`truncated === true`),
|
|
237
|
+
// since total===0 only reflects the first 500 scanned records.
|
|
238
|
+
// 3. On page 2+ (`offset > 0`), only fallback when `result.total === 0` explicitly (not
|
|
239
|
+
// when `total === undefined`, which indicates an exhausted offset).
|
|
240
|
+
let usedFallback = false;
|
|
241
|
+
const splitQ = trimmedQ ? splitCompoundQuery(trimmedQ) : undefined;
|
|
242
|
+
const isSubsequentPage = (offset !== undefined && offset > 0) || Boolean(after);
|
|
243
|
+
const initialTruncated = result?.truncated === true;
|
|
244
|
+
const canFallback = splitQ !== undefined && splitQ !== trimmedQ && !after && !initialTruncated;
|
|
245
|
+
const hasZeroTotalMatches = Array.isArray(result?.data) &&
|
|
246
|
+
result.data.length === 0 &&
|
|
247
|
+
(isSubsequentPage ? result.total === 0 : result.total === 0 || result.total === undefined);
|
|
248
|
+
if (canFallback && hasZeroTotalMatches) {
|
|
249
|
+
try {
|
|
250
|
+
const fallbackResult = await client.listClients({ limit, offset, after, fields, q: splitQ, stage });
|
|
251
|
+
const fallbackTruncated = fallbackResult?.truncated === true;
|
|
252
|
+
if (fallbackTruncated ||
|
|
253
|
+
(fallbackResult?.data && (fallbackResult.data.length > 0 || (fallbackResult.total !== undefined && fallbackResult.total > 0)))) {
|
|
254
|
+
result = fallbackResult;
|
|
255
|
+
usedFallback = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Preserve original response if fallback fails
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const rawEnvelope = (result ?? {});
|
|
263
|
+
const isTruncated = rawEnvelope.truncated === true;
|
|
264
|
+
const normalizedData = Array.isArray(result?.data)
|
|
265
|
+
? result.data.map((c) => (c && typeof c === "object" ? normalizeClientRecord(c) : c))
|
|
266
|
+
: [];
|
|
267
|
+
// Conservative refusal when the ERP 500-document scan cap was saturated (`truncated: true`).
|
|
268
|
+
// Because `paginatedOutput` is a frozen schema contract and both Claude (`claudeToolResult`)
|
|
269
|
+
// and OpenAI (`applyOpenAIProfile`) overwrite `content` whenever `structuredContent` is
|
|
270
|
+
// present, returning `structuredContent` here would hide the truncation warning and present
|
|
271
|
+
// an incomplete 500-doc scan as exhaustive.
|
|
272
|
+
if (isTruncated) {
|
|
273
|
+
const partialCount = typeof result?.total === "number" ? result.total : normalizedData.length;
|
|
274
|
+
return {
|
|
275
|
+
content: [{
|
|
276
|
+
type: "text",
|
|
277
|
+
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'. / ` +
|
|
278
|
+
`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'.`,
|
|
279
|
+
}],
|
|
280
|
+
isError: true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const cleanEnvelope = {
|
|
284
|
+
data: normalizedData,
|
|
285
|
+
total: typeof result?.total === "number" ? result.total : normalizedData.length,
|
|
286
|
+
limit: typeof result?.limit === "number" ? result.limit : (limit ?? 20),
|
|
287
|
+
offset: typeof result?.offset === "number" ? result.offset : (offset ?? 0),
|
|
288
|
+
...(typeof result?.nextCursor === "string" ? { nextCursor: result.nextCursor } : {}),
|
|
289
|
+
};
|
|
290
|
+
const notes = [];
|
|
291
|
+
if (usedFallback) {
|
|
292
|
+
notes.push(` (criterio aplicado: "${splitQ}")`);
|
|
293
|
+
}
|
|
48
294
|
return {
|
|
49
|
-
content: [listContent(formatPaginatedResponse("clients",
|
|
50
|
-
structuredContent:
|
|
295
|
+
content: [listContent(formatPaginatedResponse("clients", cleanEnvelope) + notes.join(""))],
|
|
296
|
+
structuredContent: cleanEnvelope,
|
|
51
297
|
};
|
|
52
298
|
}));
|
|
53
299
|
// -- get_client --
|
|
@@ -61,7 +307,7 @@ export function registerClientTools(server, client) {
|
|
|
61
307
|
},
|
|
62
308
|
outputSchema: clientItemOutput,
|
|
63
309
|
}, async ({ id }) => withToolLogging("get_client", async () => {
|
|
64
|
-
const result = await client.getClient(id);
|
|
310
|
+
const result = normalizeClientRecord(await client.getClient(id));
|
|
65
311
|
return {
|
|
66
312
|
content: [getContent(formatRecord("Client", result))],
|
|
67
313
|
structuredContent: result,
|
|
@@ -77,19 +323,19 @@ export function registerClientTools(server, client) {
|
|
|
77
323
|
"Los clientes se usan al crear facturas y presupuestos.",
|
|
78
324
|
annotations: CREATE_ANNOTATIONS,
|
|
79
325
|
inputSchema: {
|
|
80
|
-
name: z.string().describe("Client/company name / Nombre del cliente o empresa"),
|
|
81
|
-
email: z.string().optional().describe("Email address / Correo electronico"),
|
|
82
|
-
phone: z.string().optional().describe("Phone number / Telefono"),
|
|
326
|
+
name: z.string().trim().min(1).max(200).describe("Client/company name / Nombre del cliente o empresa"),
|
|
327
|
+
email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
|
|
328
|
+
phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
|
|
83
329
|
taxId: z.string().optional().describe("Tax ID (NIF/CIF/VAT) / NIF o CIF"),
|
|
84
330
|
address: addressSchema,
|
|
85
331
|
},
|
|
86
332
|
outputSchema: clientItemOutput,
|
|
87
333
|
}, async (input) => withToolLogging("create_client", async () => {
|
|
88
|
-
const result = await client.createClient(input);
|
|
334
|
+
const result = normalizeClientRecord(await client.createClient(input));
|
|
89
335
|
const hints = enrichResponse("clients", "create", result);
|
|
90
336
|
return {
|
|
91
|
-
content: [mutateContent(formatRecord("Client created", result))],
|
|
92
|
-
structuredContent: { ...result
|
|
337
|
+
content: [mutateContent(formatRecord("Client created", result) + hints)],
|
|
338
|
+
structuredContent: { ...result },
|
|
93
339
|
};
|
|
94
340
|
}));
|
|
95
341
|
// -- update_client --
|
|
@@ -100,16 +346,16 @@ export function registerClientTools(server, client) {
|
|
|
100
346
|
"/ Actualiza un cliente existente. Solo se modifican los campos proporcionados.",
|
|
101
347
|
annotations: UPDATE_ANNOTATIONS,
|
|
102
348
|
inputSchema: {
|
|
103
|
-
id: z.string().describe("Client ID / ID del cliente"),
|
|
104
|
-
name: z.string().optional().describe("Name / Nombre"),
|
|
105
|
-
email: z.string().optional().describe("Email / Correo"),
|
|
106
|
-
phone: z.string().optional().describe("Phone / Telefono"),
|
|
349
|
+
id: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
|
|
350
|
+
name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
|
|
351
|
+
email: z.string().trim().email().max(255).optional().describe("Email / Correo"),
|
|
352
|
+
phone: z.string().trim().min(1).max(50).optional().describe("Phone / Telefono"),
|
|
107
353
|
taxId: z.string().optional().describe("Tax ID / NIF/CIF"),
|
|
108
354
|
address: addressSchema,
|
|
109
355
|
},
|
|
110
356
|
outputSchema: clientItemOutput,
|
|
111
357
|
}, async ({ id, ...data }) => withToolLogging("update_client", async () => {
|
|
112
|
-
const result = await client.updateClient(id, data);
|
|
358
|
+
const result = normalizeClientRecord(await client.updateClient(id, data));
|
|
113
359
|
return {
|
|
114
360
|
content: [mutateContent(formatRecord("Client updated", result))],
|
|
115
361
|
structuredContent: result,
|
package/dist/tools/crm.js
CHANGED
|
@@ -37,11 +37,11 @@ export function registerCrmTools(server, client) {
|
|
|
37
37
|
"/ Anade un nuevo contacto a un cliente. Requiere al menos un nombre.",
|
|
38
38
|
annotations: CREATE_ANNOTATIONS,
|
|
39
39
|
inputSchema: {
|
|
40
|
-
clientId: z.string().describe("Client ID / ID del cliente"),
|
|
41
|
-
name: z.string().describe("Contact name / Nombre del contacto"),
|
|
42
|
-
email: z.string().optional().describe("Email address / Correo electronico"),
|
|
43
|
-
phone: z.string().optional().describe("Phone number / Telefono"),
|
|
44
|
-
role: z.string().optional().describe("Role or title (e.g. CEO, CTO, Billing) / Cargo o rol"),
|
|
40
|
+
clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
|
|
41
|
+
name: z.string().trim().min(1).max(200).describe("Contact name / Nombre del contacto"),
|
|
42
|
+
email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
|
|
43
|
+
phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
|
|
44
|
+
role: z.string().trim().min(1).max(200).optional().describe("Role or title (e.g. CEO, CTO, Billing) / Cargo o rol"),
|
|
45
45
|
isPrimary: z.boolean().optional().describe("Whether this is the primary contact / Si es el contacto principal"),
|
|
46
46
|
},
|
|
47
47
|
outputSchema: contactItemOutput,
|
|
@@ -49,8 +49,8 @@ export function registerCrmTools(server, client) {
|
|
|
49
49
|
const result = await client.createClientContact(clientId, data);
|
|
50
50
|
const hints = enrichResponse("contacts", "create", result);
|
|
51
51
|
return {
|
|
52
|
-
content: [mutateContent(formatRecord("Contact created", result))],
|
|
53
|
-
structuredContent: { ...result
|
|
52
|
+
content: [mutateContent(formatRecord("Contact created", result) + hints)],
|
|
53
|
+
structuredContent: { ...result },
|
|
54
54
|
};
|
|
55
55
|
}));
|
|
56
56
|
// -- delete_client_contact --
|
|
@@ -60,8 +60,8 @@ export function registerCrmTools(server, client) {
|
|
|
60
60
|
"/ Elimina permanentemente un contacto de un cliente. Esta accion no se puede deshacer.",
|
|
61
61
|
annotations: DELETE_ANNOTATIONS,
|
|
62
62
|
inputSchema: {
|
|
63
|
-
clientId: z.string().describe("Client ID / ID del cliente"),
|
|
64
|
-
contactId: z.string().describe("Contact ID / ID del contacto"),
|
|
63
|
+
clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
|
|
64
|
+
contactId: z.string().trim().min(1).max(128).describe("Contact ID / ID del contacto"),
|
|
65
65
|
},
|
|
66
66
|
outputSchema: deleteResultOutput,
|
|
67
67
|
}, async ({ clientId, contactId }) => withToolLogging("delete_client_contact", async () => {
|
|
@@ -82,7 +82,7 @@ export function registerCrmTools(server, client) {
|
|
|
82
82
|
"/ Lista todas las actividades CRM de un cliente con paginacion opcional.",
|
|
83
83
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
84
84
|
inputSchema: {
|
|
85
|
-
clientId: z.string().describe("Client ID / ID del cliente"),
|
|
85
|
+
clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
|
|
86
86
|
limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
|
|
87
87
|
offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
|
|
88
88
|
},
|
|
@@ -98,18 +98,18 @@ export function registerCrmTools(server, client) {
|
|
|
98
98
|
server.registerTool("log_client_activity", {
|
|
99
99
|
title: "Log Client Activity",
|
|
100
100
|
description: "Log a CRM activity against a client. Use to track calls, emails, meetings, or tasks. " +
|
|
101
|
+
"Do not include passwords, credentials, payment-card data, health data, or official/government identifiers in free-text fields. " +
|
|
101
102
|
"Example: clientId='abc123', type='call', title='Discussed Q2 proposal', description='Client interested in upgrade' " +
|
|
102
|
-
"/ Registra una actividad CRM para un cliente.
|
|
103
|
+
"/ Registra una actividad CRM para un cliente. No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales en los campos de texto libre.",
|
|
103
104
|
annotations: CREATE_ANNOTATIONS,
|
|
104
|
-
inputSchema: {
|
|
105
|
+
inputSchema: z.object({
|
|
105
106
|
clientId: z.string().describe("Client ID / ID del cliente"),
|
|
106
107
|
type: z
|
|
107
108
|
.enum(["call", "email", "meeting", "task"])
|
|
108
109
|
.describe("Activity type / Tipo de actividad"),
|
|
109
|
-
title: z.string().describe("Activity title / Titulo de la actividad"),
|
|
110
|
-
description: z.string().optional().describe("Detailed description / Descripcion detallada"),
|
|
111
|
-
|
|
112
|
-
},
|
|
110
|
+
title: z.string().trim().min(1).max(500).describe("Activity title / Titulo de la actividad"),
|
|
111
|
+
description: z.string().trim().min(1).max(5000).optional().describe("Detailed description; exclude secrets, credentials, card or health data, and official identifiers / Descripcion detallada; excluye secretos, credenciales, datos de tarjetas o salud e identificadores oficiales"),
|
|
112
|
+
}).strict(),
|
|
113
113
|
outputSchema: activityItemOutput,
|
|
114
114
|
}, async ({ clientId, ...data }) => withToolLogging("log_client_activity", async () => {
|
|
115
115
|
const result = await client.logClientActivity(clientId, data);
|
|
@@ -144,12 +144,13 @@ export function registerCrmTools(server, client) {
|
|
|
144
144
|
server.registerTool("create_client_note", {
|
|
145
145
|
title: "Create Client Note",
|
|
146
146
|
description: "Add a note to a client. Notes are free-form text entries useful for keeping context. " +
|
|
147
|
+
"Do not include passwords, credentials, payment-card data, health data, or official/government identifiers. " +
|
|
147
148
|
"Example: clientId='abc123', content='Prefers invoices in English. Payment NET 30.' " +
|
|
148
|
-
"/ Anade una nota a un cliente.
|
|
149
|
+
"/ Anade una nota a un cliente. No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales.",
|
|
149
150
|
annotations: CREATE_ANNOTATIONS,
|
|
150
151
|
inputSchema: {
|
|
151
|
-
clientId: z.string().describe("Client ID / ID del cliente"),
|
|
152
|
-
content: z.string().describe("Note content / Contenido de
|
|
152
|
+
clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
|
|
153
|
+
content: z.string().trim().min(1).max(10000).describe("Note content; exclude secrets, credentials, card or health data, and official identifiers / Contenido; excluye secretos, credenciales, datos de tarjetas o salud e identificadores oficiales"),
|
|
153
154
|
},
|
|
154
155
|
outputSchema: noteItemOutput,
|
|
155
156
|
}, async ({ clientId, content }) => withToolLogging("create_client_note", async () => {
|
|
@@ -166,8 +167,8 @@ export function registerCrmTools(server, client) {
|
|
|
166
167
|
"/ Elimina permanentemente una nota de un cliente. Esta accion no se puede deshacer.",
|
|
167
168
|
annotations: DELETE_ANNOTATIONS,
|
|
168
169
|
inputSchema: {
|
|
169
|
-
clientId: z.string().describe("Client ID / ID del cliente"),
|
|
170
|
-
noteId: z.string().describe("Note ID / ID de la nota"),
|
|
170
|
+
clientId: z.string().trim().min(1).max(128).describe("Client ID / ID del cliente"),
|
|
171
|
+
noteId: z.string().trim().min(1).max(128).describe("Note ID / ID de la nota"),
|
|
171
172
|
},
|
|
172
173
|
outputSchema: deleteResultOutput,
|
|
173
174
|
}, async ({ clientId, noteId }) => withToolLogging("delete_client_note", async () => {
|