@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/team.js
CHANGED
|
@@ -12,27 +12,27 @@
|
|
|
12
12
|
* Backend: /v1/team/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod/v4";
|
|
15
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, teamMemberItemOutput, } from "./shared.js";
|
|
15
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, paginatedOutput, actionResultOutput, teamMemberItemOutput, } from "./shared.js";
|
|
16
16
|
export function registerTeamTools(server, client) {
|
|
17
17
|
// -- list_team_members --
|
|
18
18
|
server.registerTool("list_team_members", {
|
|
19
19
|
title: "List Team Members",
|
|
20
|
-
description: "List
|
|
21
|
-
"
|
|
22
|
-
"Useful for
|
|
23
|
-
"/ Lista
|
|
24
|
-
"
|
|
25
|
-
"Util para
|
|
20
|
+
description: "List active workspace team members and pending invitations. The workspace owner is NOT a member row and is excluded. " +
|
|
21
|
+
"Each row carries member ID, name, email, role, and status (active/pending/expired/invalid; role is null on invalid invitations). " +
|
|
22
|
+
"Useful for reviewing team access and outstanding invitations. " +
|
|
23
|
+
"/ Lista miembros activos del equipo e invitaciones pendientes. El propietario del espacio NO aparece como miembro y queda excluido. " +
|
|
24
|
+
"Cada fila lleva ID, nombre, email, rol y estado (active/pending/expired/invalid; rol null en invitaciones invalidas). " +
|
|
25
|
+
"Util para revisar el acceso del equipo y las invitaciones pendientes.",
|
|
26
26
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
27
27
|
inputSchema: {
|
|
28
28
|
role: z
|
|
29
|
-
.enum(["owner", "admin", "
|
|
29
|
+
.enum(["owner", "admin", "editor", "accountant", "viewer"])
|
|
30
30
|
.optional()
|
|
31
31
|
.describe("Filter by role / Filtrar por rol"),
|
|
32
32
|
status: z
|
|
33
|
-
.enum(["active", "pending"])
|
|
33
|
+
.enum(["active", "pending", "expired", "invalid"])
|
|
34
34
|
.optional()
|
|
35
|
-
.describe("Filter by
|
|
35
|
+
.describe("Filter by status / Filtrar por estado"),
|
|
36
36
|
limit: z.number().int().min(1).max(100).optional().describe("Max results (1-100) / Resultados maximos"),
|
|
37
37
|
offset: z.number().int().min(0).optional().describe("Offset / Desplazamiento"),
|
|
38
38
|
},
|
|
@@ -49,16 +49,18 @@ export function registerTeamTools(server, client) {
|
|
|
49
49
|
title: "Invite Team Member",
|
|
50
50
|
description: "Invite a new member to the workspace by email address. " +
|
|
51
51
|
"An invitation email is sent — the member must accept before gaining access. " +
|
|
52
|
-
"
|
|
53
|
-
"
|
|
52
|
+
"Assignable roles: admin, editor, accountant, viewer. Exact access follows the workspace's current role policy. " +
|
|
53
|
+
"Owner is excluded from invitations and must be transferred via a dedicated flow. " +
|
|
54
|
+
"Example: email='ana@example.com', role='accountant' " +
|
|
54
55
|
"/ Invita a un nuevo miembro al espacio de trabajo por correo electronico. " +
|
|
55
56
|
"Se envia un email de invitacion — el miembro debe aceptar antes de acceder. " +
|
|
56
|
-
"Roles:
|
|
57
|
+
"Roles asignables: admin, editor, accountant, viewer. El acceso exacto depende de la politica de roles vigente del espacio. " +
|
|
58
|
+
"Owner queda excluido de las invitaciones y debe transferirse por un flujo dedicado.",
|
|
57
59
|
annotations: CREATE_ANNOTATIONS,
|
|
58
60
|
inputSchema: {
|
|
59
61
|
email: z.string().email().describe("Email address of the invitee / Email del invitado"),
|
|
60
62
|
role: z
|
|
61
|
-
.enum(["admin", "
|
|
63
|
+
.enum(["admin", "editor", "accountant", "viewer"])
|
|
62
64
|
.describe("Role to assign (owner cannot be invited, must be transferred) / Rol a asignar (owner no se puede invitar, debe transferirse)"),
|
|
63
65
|
name: z
|
|
64
66
|
.string()
|
|
@@ -79,15 +81,17 @@ export function registerTeamTools(server, client) {
|
|
|
79
81
|
description: "Change the role of an existing team member. " +
|
|
80
82
|
"Only workspace admins or owners can change roles. " +
|
|
81
83
|
"Cannot change the owner's role — use a dedicated ownership transfer flow. " +
|
|
82
|
-
"
|
|
84
|
+
"Allowed roles: admin, editor, accountant, viewer. " +
|
|
85
|
+
"Example: memberId='mbr_abc123', role='accountant' " +
|
|
83
86
|
"/ Cambia el rol de un miembro existente del espacio de trabajo. " +
|
|
84
87
|
"Solo administradores o propietarios pueden cambiar roles. " +
|
|
85
|
-
"No se puede cambiar el rol del propietario — usa el flujo de transferencia de propiedad."
|
|
88
|
+
"No se puede cambiar el rol del propietario — usa el flujo de transferencia de propiedad. " +
|
|
89
|
+
"Roles permitidos: admin, editor, accountant, viewer.",
|
|
86
90
|
annotations: UPDATE_ANNOTATIONS,
|
|
87
91
|
inputSchema: {
|
|
88
92
|
memberId: z.string().describe("Team member ID / ID del miembro del equipo"),
|
|
89
93
|
role: z
|
|
90
|
-
.enum(["admin", "
|
|
94
|
+
.enum(["admin", "editor", "accountant", "viewer"])
|
|
91
95
|
.describe("New role to assign / Nuevo rol a asignar"),
|
|
92
96
|
},
|
|
93
97
|
outputSchema: actionResultOutput,
|
|
@@ -119,17 +123,9 @@ export function registerTeamTools(server, client) {
|
|
|
119
123
|
outputSchema: actionResultOutput,
|
|
120
124
|
}, async ({ memberId, confirm }) => withToolLogging("remove_team_member", async () => {
|
|
121
125
|
if (!confirm) {
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
type: "text",
|
|
126
|
-
text: "Error: confirm=true is required to remove a team member. " +
|
|
127
|
-
"The member will immediately lose access to the workspace. Set confirm=true to proceed. / " +
|
|
128
|
-
"Se requiere confirm=true para eliminar un miembro del equipo.",
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
isError: true,
|
|
132
|
-
};
|
|
126
|
+
return confirmationRequiredError("Error: confirm=true is required to remove a team member. " +
|
|
127
|
+
"The member will immediately lose access to the workspace. Set confirm=true to proceed. / " +
|
|
128
|
+
"Se requiere confirm=true para eliminar un miembro del equipo.");
|
|
133
129
|
}
|
|
134
130
|
await client.removeTeamMember(memberId);
|
|
135
131
|
return {
|
package/dist/tools/time.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Backend: /v1/time/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
|
|
15
15
|
*/
|
|
16
16
|
import { z } from "zod/v4";
|
|
17
|
-
import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, deleteResultOutput, timeEntryItemOutput, timeSummaryOutput, } from "./shared.js";
|
|
17
|
+
import { withToolLogging, confirmationRequiredError, formatPaginatedResponse, formatRecord, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, paginatedOutput, deleteResultOutput, timeEntryItemOutput, timeSummaryOutput, } from "./shared.js";
|
|
18
18
|
export function registerTimeTools(server, client) {
|
|
19
19
|
// -- list_time_entries --
|
|
20
20
|
server.registerTool("list_time_entries", {
|
|
@@ -132,17 +132,9 @@ export function registerTimeTools(server, client) {
|
|
|
132
132
|
outputSchema: deleteResultOutput,
|
|
133
133
|
}, async ({ id, confirm }) => withToolLogging("delete_time_entry", async () => {
|
|
134
134
|
if (!confirm) {
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
type: "text",
|
|
139
|
-
text: "Error: confirm=true is required to delete a time entry. " +
|
|
140
|
-
"Deleted entries affect billing reports. Set confirm=true to proceed. / " +
|
|
141
|
-
"Se requiere confirm=true para eliminar una entrada de tiempo.",
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
isError: true,
|
|
145
|
-
};
|
|
135
|
+
return confirmationRequiredError("Error: confirm=true is required to delete a time entry. " +
|
|
136
|
+
"Deleted entries affect billing reports. Set confirm=true to proceed. / " +
|
|
137
|
+
"Se requiere confirm=true para eliminar una entrada de tiempo.");
|
|
146
138
|
}
|
|
147
139
|
await client.deleteTimeEntry(id);
|
|
148
140
|
return {
|
package/dist/tools/vendors.js
CHANGED
|
@@ -13,6 +13,168 @@ const addressSchema = z
|
|
|
13
13
|
})
|
|
14
14
|
.optional()
|
|
15
15
|
.describe("Vendor address / Direccion del proveedor");
|
|
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_VENDOR_STRING_FIELDS = [
|
|
30
|
+
"email",
|
|
31
|
+
"phone",
|
|
32
|
+
"taxId",
|
|
33
|
+
"createdAt",
|
|
34
|
+
"updatedAt",
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes a vendor record returned by the Frihet API so it always satisfies
|
|
38
|
+
* `vendorItemOutput` without losing ERP-canonical data:
|
|
39
|
+
* - Strips `null` on optional string fields (`email`, `phone`, `taxId`,
|
|
40
|
+
* `createdAt`, `updatedAt`) so `z.string().optional()` never rejects legacy docs.
|
|
41
|
+
* - Backfills `taxId` from legacy `nif` / `cif` / `vatNumber` / `tax_id` when
|
|
42
|
+
* `taxId` is absent or null, preserving all original fields.
|
|
43
|
+
* - Converts legacy string `address` into `{ street }`.
|
|
44
|
+
* - Omits `null` or empty `address`.
|
|
45
|
+
* - Maps ERP canonical address keys (`province` -> `state`, `zip` -> `postalCode`,
|
|
46
|
+
* `countryCode` -> `country`).
|
|
47
|
+
*/
|
|
48
|
+
function normalizeVendorRecord(vendorRecord) {
|
|
49
|
+
if (!vendorRecord || typeof vendorRecord !== "object" || Array.isArray(vendorRecord)) {
|
|
50
|
+
return vendorRecord;
|
|
51
|
+
}
|
|
52
|
+
const out = { ...vendorRecord };
|
|
53
|
+
for (const field of OPTIONAL_VENDOR_STRING_FIELDS) {
|
|
54
|
+
if (out[field] === null) {
|
|
55
|
+
delete out[field];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (typeof out.taxId !== "string" || !out.taxId.trim()) {
|
|
59
|
+
for (const alias of ["nif", "cif", "vatNumber", "tax_id"]) {
|
|
60
|
+
const candidate = out[alias];
|
|
61
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
62
|
+
out.taxId = candidate.trim();
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const rawAddress = out.address;
|
|
68
|
+
const rootStreet = typeof out.street === "string" && out.street.trim() ? out.street.trim() : undefined;
|
|
69
|
+
const rootCity = typeof out.city === "string" && out.city.trim() ? out.city.trim() : undefined;
|
|
70
|
+
const rootState = typeof out.state === "string" && out.state.trim()
|
|
71
|
+
? out.state.trim()
|
|
72
|
+
: typeof out.province === "string" && out.province.trim()
|
|
73
|
+
? out.province.trim()
|
|
74
|
+
: undefined;
|
|
75
|
+
const rootPostalCode = typeof out.postalCode === "string" && out.postalCode.trim()
|
|
76
|
+
? out.postalCode.trim()
|
|
77
|
+
: typeof out.zip === "string" && out.zip.trim()
|
|
78
|
+
? out.zip.trim()
|
|
79
|
+
: undefined;
|
|
80
|
+
const rootCountry = typeof out.country === "string" && out.country.trim()
|
|
81
|
+
? out.country.trim()
|
|
82
|
+
: typeof out.countryCode === "string" && out.countryCode.trim()
|
|
83
|
+
? out.countryCode.trim()
|
|
84
|
+
: undefined;
|
|
85
|
+
const buildFlatAddress = (streetOverride) => {
|
|
86
|
+
const built = {};
|
|
87
|
+
const st = streetOverride ?? rootStreet;
|
|
88
|
+
if (st !== undefined)
|
|
89
|
+
built.street = st;
|
|
90
|
+
if (rootCity !== undefined)
|
|
91
|
+
built.city = rootCity;
|
|
92
|
+
if (rootState !== undefined)
|
|
93
|
+
built.state = rootState;
|
|
94
|
+
if (rootPostalCode !== undefined)
|
|
95
|
+
built.postalCode = rootPostalCode;
|
|
96
|
+
if (rootCountry !== undefined)
|
|
97
|
+
built.country = rootCountry;
|
|
98
|
+
return Object.keys(built).length > 0 ? built : undefined;
|
|
99
|
+
};
|
|
100
|
+
if (rawAddress === undefined || rawAddress === null) {
|
|
101
|
+
if (rawAddress === null) {
|
|
102
|
+
delete out.address;
|
|
103
|
+
}
|
|
104
|
+
const flatAddr = buildFlatAddress();
|
|
105
|
+
if (flatAddr) {
|
|
106
|
+
out.address = flatAddr;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
if (typeof rawAddress === "string") {
|
|
111
|
+
const trimmed = rawAddress.trim();
|
|
112
|
+
const flatAddr = buildFlatAddress(trimmed || undefined);
|
|
113
|
+
if (!flatAddr) {
|
|
114
|
+
delete out.address;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
out.address = flatAddr;
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
if (typeof rawAddress === "object" && !Array.isArray(rawAddress)) {
|
|
121
|
+
const addrObj = rawAddress;
|
|
122
|
+
const normalizedAddress = {};
|
|
123
|
+
for (const [k, v] of Object.entries(addrObj)) {
|
|
124
|
+
if (v !== null && v !== undefined) {
|
|
125
|
+
normalizedAddress[k] = v;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const street = typeof addrObj.street === "string" && addrObj.street.trim()
|
|
129
|
+
? addrObj.street.trim()
|
|
130
|
+
: rootStreet;
|
|
131
|
+
const city = typeof addrObj.city === "string" && addrObj.city.trim()
|
|
132
|
+
? addrObj.city.trim()
|
|
133
|
+
: rootCity;
|
|
134
|
+
const state = typeof addrObj.state === "string" && addrObj.state.trim()
|
|
135
|
+
? addrObj.state.trim()
|
|
136
|
+
: typeof addrObj.province === "string" && addrObj.province.trim()
|
|
137
|
+
? addrObj.province.trim()
|
|
138
|
+
: rootState;
|
|
139
|
+
const postalCode = typeof addrObj.postalCode === "string" && addrObj.postalCode.trim()
|
|
140
|
+
? addrObj.postalCode.trim()
|
|
141
|
+
: typeof addrObj.zip === "string" && addrObj.zip.trim()
|
|
142
|
+
? addrObj.zip.trim()
|
|
143
|
+
: rootPostalCode;
|
|
144
|
+
const country = typeof addrObj.country === "string" && addrObj.country.trim()
|
|
145
|
+
? addrObj.country.trim()
|
|
146
|
+
: typeof addrObj.countryCode === "string" && addrObj.countryCode.trim()
|
|
147
|
+
? addrObj.countryCode.trim()
|
|
148
|
+
: rootCountry;
|
|
149
|
+
if (street !== undefined)
|
|
150
|
+
normalizedAddress.street = street;
|
|
151
|
+
else
|
|
152
|
+
delete normalizedAddress.street;
|
|
153
|
+
if (city !== undefined)
|
|
154
|
+
normalizedAddress.city = city;
|
|
155
|
+
else
|
|
156
|
+
delete normalizedAddress.city;
|
|
157
|
+
if (state !== undefined)
|
|
158
|
+
normalizedAddress.state = state;
|
|
159
|
+
else
|
|
160
|
+
delete normalizedAddress.state;
|
|
161
|
+
if (postalCode !== undefined)
|
|
162
|
+
normalizedAddress.postalCode = postalCode;
|
|
163
|
+
else
|
|
164
|
+
delete normalizedAddress.postalCode;
|
|
165
|
+
if (country !== undefined)
|
|
166
|
+
normalizedAddress.country = country;
|
|
167
|
+
else
|
|
168
|
+
delete normalizedAddress.country;
|
|
169
|
+
if (Object.keys(normalizedAddress).length === 0) {
|
|
170
|
+
delete out.address;
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
out.address = normalizedAddress;
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
16
178
|
export function registerVendorTools(server, client) {
|
|
17
179
|
// -- list_vendors --
|
|
18
180
|
server.registerTool("list_vendors", {
|
|
@@ -31,10 +193,82 @@ export function registerVendorTools(server, client) {
|
|
|
31
193
|
},
|
|
32
194
|
outputSchema: paginatedOutput(vendorItemOutput, { projectable: true }),
|
|
33
195
|
}, async ({ q, limit, offset, after, fields }) => withToolLogging("list_vendors", async () => {
|
|
34
|
-
const
|
|
196
|
+
const trimmedQ = q?.trim();
|
|
197
|
+
const trimmedAfter = after?.trim();
|
|
198
|
+
// Limitation in Frihet-ERP `listResources` (publicApi.ts L8158-8268):
|
|
199
|
+
// When `search` (`q`) is provided, the backend slices results by `offset`/`limit`
|
|
200
|
+
// and returns before evaluating `cursorParam` (`after`). Protected profiles
|
|
201
|
+
// (Claude `claudeToolResult` and OpenAI `applyOpenAIProfile`) rebuild `content`
|
|
202
|
+
// from `structuredContent` while `paginatedOutput` is frozen, so fail closed
|
|
203
|
+
// (`isError: true` without `structuredContent`).
|
|
204
|
+
if (trimmedQ && trimmedAfter) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: "text",
|
|
208
|
+
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'. / " +
|
|
209
|
+
"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'.",
|
|
210
|
+
}],
|
|
211
|
+
isError: true,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
let result = await client.listVendors({ q, limit, offset, after, fields });
|
|
215
|
+
let usedFallback = false;
|
|
216
|
+
const splitQ = trimmedQ ? splitCompoundQuery(trimmedQ) : undefined;
|
|
217
|
+
const isSubsequentPage = (offset !== undefined && offset > 0) || Boolean(after);
|
|
218
|
+
const initialTruncated = result?.truncated === true;
|
|
219
|
+
const canFallback = splitQ !== undefined && splitQ !== trimmedQ && !after && !initialTruncated;
|
|
220
|
+
const hasZeroTotalMatches = Array.isArray(result?.data) &&
|
|
221
|
+
result.data.length === 0 &&
|
|
222
|
+
(isSubsequentPage ? result.total === 0 : result.total === 0 || result.total === undefined);
|
|
223
|
+
if (canFallback && hasZeroTotalMatches) {
|
|
224
|
+
try {
|
|
225
|
+
const fallbackResult = await client.listVendors({ q: splitQ, limit, offset, after, fields });
|
|
226
|
+
const fallbackTruncated = fallbackResult?.truncated === true;
|
|
227
|
+
if (fallbackTruncated ||
|
|
228
|
+
(fallbackResult?.data && (fallbackResult.data.length > 0 || (fallbackResult.total !== undefined && fallbackResult.total > 0)))) {
|
|
229
|
+
result = fallbackResult;
|
|
230
|
+
usedFallback = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Preserve original response if fallback fails
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const rawEnvelope = (result ?? {});
|
|
238
|
+
const isTruncated = rawEnvelope.truncated === true;
|
|
239
|
+
const normalizedData = Array.isArray(result?.data)
|
|
240
|
+
? result.data.map((v) => (v && typeof v === "object" ? normalizeVendorRecord(v) : v))
|
|
241
|
+
: [];
|
|
242
|
+
// Conservative refusal when the ERP 500-document scan cap was saturated (`truncated: true`).
|
|
243
|
+
// Because `paginatedOutput` is a frozen schema contract and both Claude (`claudeToolResult`)
|
|
244
|
+
// and OpenAI (`applyOpenAIProfile`) overwrite `content` whenever `structuredContent` is
|
|
245
|
+
// present, returning `structuredContent` here would hide the truncation warning and present
|
|
246
|
+
// an incomplete 500-doc scan as exhaustive.
|
|
247
|
+
if (isTruncated) {
|
|
248
|
+
const partialCount = typeof result?.total === "number" ? result.total : normalizedData.length;
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: `Error: vendor 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 paginate without 'q' using 'after'. / ` +
|
|
253
|
+
`Error: la busqueda de proveedores 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 pagina sin 'q' usando 'after'.`,
|
|
254
|
+
}],
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const cleanEnvelope = {
|
|
259
|
+
data: normalizedData,
|
|
260
|
+
total: typeof result?.total === "number" ? result.total : normalizedData.length,
|
|
261
|
+
limit: typeof result?.limit === "number" ? result.limit : (limit ?? 20),
|
|
262
|
+
offset: typeof result?.offset === "number" ? result.offset : (offset ?? 0),
|
|
263
|
+
...(typeof result?.nextCursor === "string" ? { nextCursor: result.nextCursor } : {}),
|
|
264
|
+
};
|
|
265
|
+
const notes = [];
|
|
266
|
+
if (usedFallback) {
|
|
267
|
+
notes.push(` (criterio aplicado: "${splitQ}")`);
|
|
268
|
+
}
|
|
35
269
|
return {
|
|
36
|
-
content: [listContent(formatPaginatedResponse("vendors",
|
|
37
|
-
structuredContent:
|
|
270
|
+
content: [listContent(formatPaginatedResponse("vendors", cleanEnvelope) + notes.join(""))],
|
|
271
|
+
structuredContent: cleanEnvelope,
|
|
38
272
|
};
|
|
39
273
|
}));
|
|
40
274
|
// -- get_vendor --
|
|
@@ -48,7 +282,7 @@ export function registerVendorTools(server, client) {
|
|
|
48
282
|
},
|
|
49
283
|
outputSchema: vendorItemOutput,
|
|
50
284
|
}, async ({ id }) => withToolLogging("get_vendor", async () => {
|
|
51
|
-
const result = await client.getVendor(id);
|
|
285
|
+
const result = normalizeVendorRecord(await client.getVendor(id));
|
|
52
286
|
return {
|
|
53
287
|
content: [getContent(formatRecord("Vendor", result))],
|
|
54
288
|
structuredContent: result,
|
|
@@ -64,19 +298,19 @@ export function registerVendorTools(server, client) {
|
|
|
64
298
|
"Los proveedores se usan al registrar gastos y pedidos de compra.",
|
|
65
299
|
annotations: CREATE_ANNOTATIONS,
|
|
66
300
|
inputSchema: {
|
|
67
|
-
name: z.string().describe("Vendor/company name / Nombre del proveedor o empresa"),
|
|
68
|
-
email: z.string().optional().describe("Email address / Correo electronico"),
|
|
69
|
-
phone: z.string().optional().describe("Phone number / Telefono"),
|
|
301
|
+
name: z.string().trim().min(1).max(200).describe("Vendor/company name / Nombre del proveedor o empresa"),
|
|
302
|
+
email: z.string().trim().email().max(255).optional().describe("Email address / Correo electronico"),
|
|
303
|
+
phone: z.string().trim().min(1).max(50).optional().describe("Phone number / Telefono"),
|
|
70
304
|
taxId: z.string().optional().describe("Tax ID (NIF/CIF/VAT) / NIF o CIF"),
|
|
71
305
|
address: addressSchema,
|
|
72
306
|
},
|
|
73
307
|
outputSchema: vendorItemOutput,
|
|
74
308
|
}, async (input) => withToolLogging("create_vendor", async () => {
|
|
75
|
-
const result = await client.createVendor(input);
|
|
309
|
+
const result = normalizeVendorRecord(await client.createVendor(input));
|
|
76
310
|
const hints = enrichResponse("vendors", "create", result);
|
|
77
311
|
return {
|
|
78
|
-
content: [mutateContent(formatRecord("Vendor created", result))],
|
|
79
|
-
structuredContent: { ...result
|
|
312
|
+
content: [mutateContent(formatRecord("Vendor created", result) + hints)],
|
|
313
|
+
structuredContent: { ...result },
|
|
80
314
|
};
|
|
81
315
|
}));
|
|
82
316
|
// -- update_vendor --
|
|
@@ -87,16 +321,16 @@ export function registerVendorTools(server, client) {
|
|
|
87
321
|
"/ Actualiza un proveedor existente. Solo se modifican los campos proporcionados.",
|
|
88
322
|
annotations: UPDATE_ANNOTATIONS,
|
|
89
323
|
inputSchema: {
|
|
90
|
-
id: z.string().describe("Vendor ID / ID del proveedor"),
|
|
91
|
-
name: z.string().optional().describe("Name / Nombre"),
|
|
92
|
-
email: z.string().optional().describe("Email / Correo"),
|
|
93
|
-
phone: z.string().optional().describe("Phone / Telefono"),
|
|
324
|
+
id: z.string().trim().min(1).max(128).describe("Vendor ID / ID del proveedor"),
|
|
325
|
+
name: z.string().trim().min(1).max(200).optional().describe("Name / Nombre"),
|
|
326
|
+
email: z.string().trim().email().max(255).optional().describe("Email / Correo"),
|
|
327
|
+
phone: z.string().trim().min(1).max(50).optional().describe("Phone / Telefono"),
|
|
94
328
|
taxId: z.string().optional().describe("Tax ID / NIF/CIF"),
|
|
95
329
|
address: addressSchema,
|
|
96
330
|
},
|
|
97
331
|
outputSchema: vendorItemOutput,
|
|
98
332
|
}, async ({ id, ...data }) => withToolLogging("update_vendor", async () => {
|
|
99
|
-
const result = await client.updateVendor(id, data);
|
|
333
|
+
const result = normalizeVendorRecord(await client.updateVendor(id, data));
|
|
100
334
|
return {
|
|
101
335
|
content: [mutateContent(formatRecord("Vendor updated", result))],
|
|
102
336
|
structuredContent: result,
|
package/dist/tools/webhooks.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Webhook management tools for the Frihet MCP server.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
|
-
import { withToolLogging,
|
|
5
|
+
import { withToolLogging, formatRecord, formatUnpaginatedListResponse, listContent, getContent, mutateContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, UPDATE_ANNOTATIONS, DELETE_ANNOTATIONS, deleteResultOutput, webhookCreateOutput, webhookItemOutput, webhookListOutput, webhookTestResultOutput } from "./shared.js";
|
|
6
6
|
export function registerWebhookTools(server, client) {
|
|
7
7
|
// -- list_webhooks --
|
|
8
8
|
server.registerTool("list_webhooks", {
|
|
@@ -10,15 +10,12 @@ export function registerWebhookTools(server, client) {
|
|
|
10
10
|
description: "List all configured webhooks. Webhooks send HTTP POST notifications when events occur in Frihet. " +
|
|
11
11
|
"/ Lista todos los webhooks configurados. Los webhooks envian notificaciones HTTP POST cuando ocurren eventos en Frihet.",
|
|
12
12
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
13
|
-
inputSchema: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
outputSchema: paginatedOutput(webhookItemOutput),
|
|
18
|
-
}, async ({ limit, offset }) => withToolLogging("list_webhooks", async () => {
|
|
19
|
-
const result = await client.listWebhooks({ limit, offset });
|
|
13
|
+
inputSchema: z.object({}).strict(),
|
|
14
|
+
outputSchema: webhookListOutput,
|
|
15
|
+
}, async () => withToolLogging("list_webhooks", async () => {
|
|
16
|
+
const result = await client.listWebhooks();
|
|
20
17
|
return {
|
|
21
|
-
content: [listContent(
|
|
18
|
+
content: [listContent(formatUnpaginatedListResponse("webhooks", result))],
|
|
22
19
|
structuredContent: result,
|
|
23
20
|
};
|
|
24
21
|
}));
|
|
@@ -28,9 +25,9 @@ export function registerWebhookTools(server, client) {
|
|
|
28
25
|
description: "Get a single webhook configuration by its ID. " +
|
|
29
26
|
"/ Obtiene la configuracion de un webhook por su ID.",
|
|
30
27
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
31
|
-
inputSchema: {
|
|
28
|
+
inputSchema: z.object({
|
|
32
29
|
id: z.string().describe("Webhook ID / ID del webhook"),
|
|
33
|
-
},
|
|
30
|
+
}).strict(),
|
|
34
31
|
outputSchema: webhookItemOutput,
|
|
35
32
|
}, async ({ id }) => withToolLogging("get_webhook", async () => {
|
|
36
33
|
const result = await client.getWebhook(id);
|
|
@@ -42,31 +39,37 @@ export function registerWebhookTools(server, client) {
|
|
|
42
39
|
// -- create_webhook --
|
|
43
40
|
server.registerTool("create_webhook", {
|
|
44
41
|
title: "Create Webhook",
|
|
45
|
-
description: "Register a
|
|
42
|
+
description: "Register a named webhook endpoint. You must specify the name, URL, and events to receive notifications " +
|
|
46
43
|
"and which events to subscribe to. " +
|
|
47
|
-
"Available events: invoice.created, invoice.updated, invoice.paid,
|
|
48
|
-
"expense.created, expense.updated,
|
|
44
|
+
"Available events: invoice.created, invoice.updated, invoice.paid, " +
|
|
45
|
+
"expense.created, expense.updated, client.created, client.updated, " +
|
|
49
46
|
"quote.created, quote.updated, quote.accepted. " +
|
|
50
|
-
"Example: url='https://example.com/webhook', events=['invoice.created','invoice.paid'], secret='my-secret' " +
|
|
47
|
+
"Example: name='Invoice events', url='https://example.com/webhook', events=['invoice.created','invoice.paid'], secret='my-secret' " +
|
|
51
48
|
"/ Registra un nuevo endpoint de webhook. Especifica la URL y los eventos a suscribir.",
|
|
52
49
|
annotations: CREATE_ANNOTATIONS,
|
|
53
|
-
inputSchema: {
|
|
54
|
-
|
|
50
|
+
inputSchema: z.object({
|
|
51
|
+
name: z.string().max(200).describe("Webhook name / Nombre del webhook"),
|
|
52
|
+
url: z.string().url().max(2000).describe("Webhook endpoint URL / URL del endpoint del webhook"),
|
|
55
53
|
events: z
|
|
56
|
-
.array(z.string())
|
|
54
|
+
.array(z.string().max(100))
|
|
57
55
|
.min(1)
|
|
58
56
|
.describe("Events to subscribe to (e.g. ['invoice.created', 'invoice.paid']) " +
|
|
59
57
|
"/ Eventos a suscribir"),
|
|
60
|
-
|
|
61
|
-
.
|
|
58
|
+
status: z
|
|
59
|
+
.enum(["active", "inactive"])
|
|
62
60
|
.optional()
|
|
63
|
-
.describe("
|
|
61
|
+
.describe("Webhook status (default: active) / Estado del webhook"),
|
|
62
|
+
metadata: z
|
|
63
|
+
.record(z.string(), z.unknown())
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Arbitrary webhook metadata / Metadatos del webhook"),
|
|
64
66
|
secret: z
|
|
65
67
|
.string()
|
|
68
|
+
.max(500)
|
|
66
69
|
.optional()
|
|
67
70
|
.describe("Signing secret for payload verification / Secreto para verificar las notificaciones"),
|
|
68
|
-
},
|
|
69
|
-
outputSchema:
|
|
71
|
+
}).strict(),
|
|
72
|
+
outputSchema: webhookCreateOutput,
|
|
70
73
|
}, async (input) => withToolLogging("create_webhook", async () => {
|
|
71
74
|
const result = await client.createWebhook(input);
|
|
72
75
|
return {
|
|
@@ -78,16 +81,18 @@ export function registerWebhookTools(server, client) {
|
|
|
78
81
|
server.registerTool("update_webhook", {
|
|
79
82
|
title: "Update Webhook",
|
|
80
83
|
description: "Update an existing webhook configuration using PATCH semantics. Only provided fields change. " +
|
|
81
|
-
"Example: id='abc123',
|
|
84
|
+
"Example: id='abc123', status='paused' to pause a webhook. " +
|
|
82
85
|
"/ Actualiza la configuracion de un webhook. Solo se modifican los campos proporcionados.",
|
|
83
86
|
annotations: UPDATE_ANNOTATIONS,
|
|
84
|
-
inputSchema: {
|
|
87
|
+
inputSchema: z.object({
|
|
85
88
|
id: z.string().describe("Webhook ID / ID del webhook"),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
name: z.string().max(200).optional().describe("Webhook name / Nombre del webhook"),
|
|
90
|
+
url: z.string().url().max(2000).optional().describe("Endpoint URL / URL"),
|
|
91
|
+
events: z.array(z.string().max(100)).min(1).optional().describe("Events / Eventos"),
|
|
92
|
+
status: z.enum(["active", "inactive", "paused"]).optional().describe("Status / Estado"),
|
|
93
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Metadata / Metadatos"),
|
|
94
|
+
secret: z.string().max(500).optional().describe("Signing secret / Secreto"),
|
|
95
|
+
}).strict(),
|
|
91
96
|
outputSchema: webhookItemOutput,
|
|
92
97
|
}, async ({ id, ...data }) => withToolLogging("update_webhook", async () => {
|
|
93
98
|
const result = await client.updateWebhook(id, data);
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface PaginatedResponse<T> {
|
|
|
11
11
|
export interface ApiError {
|
|
12
12
|
error: string;
|
|
13
13
|
message?: string;
|
|
14
|
+
detail?: string;
|
|
14
15
|
}
|
|
15
16
|
export interface PaginationParams {
|
|
16
17
|
limit?: number;
|
|
@@ -37,11 +38,13 @@ export interface Invoice {
|
|
|
37
38
|
status?: string;
|
|
38
39
|
notes?: string;
|
|
39
40
|
taxRate?: number;
|
|
41
|
+
operationType?: "service" | "goods";
|
|
42
|
+
fiscalTreatment?: "not_subject_location" | "export_exempt";
|
|
40
43
|
total?: number;
|
|
41
44
|
createdAt?: string;
|
|
42
45
|
updatedAt?: string;
|
|
43
46
|
}
|
|
44
|
-
export type CreateInvoiceInput = Pick<Invoice, "clientName" | "items"> & Partial<Pick<Invoice, "issueDate" | "dueDate" | "status" | "notes" | "taxRate">>;
|
|
47
|
+
export type CreateInvoiceInput = Pick<Invoice, "clientName" | "items"> & Partial<Pick<Invoice, "issueDate" | "dueDate" | "status" | "notes" | "taxRate" | "operationType" | "fiscalTreatment">>;
|
|
45
48
|
export type UpdateInvoiceInput = Partial<CreateInvoiceInput>;
|
|
46
49
|
export interface Expense {
|
|
47
50
|
id: string;
|
|
@@ -99,13 +102,36 @@ export type CreateQuoteInput = Pick<Quote, "clientName" | "items"> & Partial<Pic
|
|
|
99
102
|
export type UpdateQuoteInput = Partial<CreateQuoteInput>;
|
|
100
103
|
export interface Webhook {
|
|
101
104
|
id: string;
|
|
105
|
+
userId?: string;
|
|
106
|
+
name: string;
|
|
102
107
|
url: string;
|
|
103
108
|
events: string[];
|
|
104
|
-
active
|
|
105
|
-
|
|
109
|
+
status: "active" | "inactive" | "paused";
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
hasSecret: boolean;
|
|
112
|
+
pausedReason?: string;
|
|
113
|
+
lastTriggeredAt?: string;
|
|
106
114
|
createdAt?: string;
|
|
107
115
|
updatedAt?: string;
|
|
108
116
|
}
|
|
109
|
-
export
|
|
110
|
-
|
|
117
|
+
export interface CreateWebhookInput {
|
|
118
|
+
name: string;
|
|
119
|
+
url: string;
|
|
120
|
+
events: string[];
|
|
121
|
+
status?: "active" | "inactive";
|
|
122
|
+
metadata?: Record<string, unknown>;
|
|
123
|
+
secret?: string;
|
|
124
|
+
}
|
|
125
|
+
export interface UpdateWebhookInput {
|
|
126
|
+
name?: string;
|
|
127
|
+
url?: string;
|
|
128
|
+
events?: string[];
|
|
129
|
+
status?: "active" | "inactive" | "paused";
|
|
130
|
+
metadata?: Record<string, unknown>;
|
|
131
|
+
secret?: string;
|
|
132
|
+
}
|
|
133
|
+
/** Create may echo only the caller-supplied signing secret, once. */
|
|
134
|
+
export type CreateWebhookResult = Webhook & {
|
|
135
|
+
secret?: string;
|
|
136
|
+
};
|
|
111
137
|
//# sourceMappingURL=types.d.ts.map
|