@frihet/mcp-server 1.16.6 → 1.17.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 +92 -31
- 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 +549 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +254 -0
- package/dist/client-interface.d.ts +22 -14
- package/dist/client.d.ts +114 -18
- package/dist/client.js +488 -56
- package/dist/demo-client.d.ts +16 -13
- package/dist/demo-client.js +171 -17
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/index.js +41 -56
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +192 -105
- package/dist/openai-profile.d.ts +25 -21
- package/dist/openai-profile.js +1166 -156
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +715 -41
- package/dist/openai-review-oauth.d.ts +47 -0
- package/dist/openai-review-oauth.js +73 -3
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +2 -0
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +10 -0
- package/dist/redaction.js +74 -0
- package/dist/resources/register-all.js +6 -12
- package/dist/server-composition.d.ts +16 -0
- package/dist/server-composition.js +44 -0
- package/dist/tool-exposure.d.ts +25 -22
- package/dist/tool-exposure.js +199 -77
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/backend-availability.d.ts +3 -3
- package/dist/tools/backend-availability.js +3 -3
- package/dist/tools/clients.js +9 -9
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +30 -11
- package/dist/tools/einvoice.d.ts +1 -1
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +8 -9
- package/dist/tools/intelligence.js +2 -2
- package/dist/tools/invoices.js +150 -47
- 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 +109 -24
- package/dist/tools/shared.d.ts +1431 -75
- package/dist/tools/shared.js +386 -76
- package/dist/tools/team.js +18 -14
- package/dist/tools/vendors.js +9 -9
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +28 -4
- package/docs/agent-onboarding.json +381 -0
- package/package.json +30 -7
package/dist/demo-client.d.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* stamp; this is the one accepted, documented gap).
|
|
19
19
|
*/
|
|
20
20
|
import type { IFrihetClient } from "./client-interface.js";
|
|
21
|
-
import type { PaginatedResponse } from "./types.js";
|
|
21
|
+
import type { CreateWebhookInput, CreateWebhookResult, PaginatedResponse, UpdateWebhookInput, Webhook } from "./types.js";
|
|
22
|
+
import type { BinaryDocument, EInvoiceDocument } from "./client.js";
|
|
22
23
|
type Rec = Record<string, unknown>;
|
|
23
24
|
export declare class DemoFrihetClient implements IFrihetClient {
|
|
24
25
|
listInvoices(params?: {
|
|
@@ -75,8 +76,8 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
75
76
|
deleteVendor(_id: string): Promise<void>;
|
|
76
77
|
sendInvoice(id: string, to?: string): Promise<Rec>;
|
|
77
78
|
markInvoicePaid(id: string, paidDate?: string): Promise<Rec>;
|
|
78
|
-
getInvoicePdf(id: string): Promise<
|
|
79
|
-
getInvoiceEInvoice(invoiceId: string): Promise<
|
|
79
|
+
getInvoicePdf(id: string): Promise<BinaryDocument>;
|
|
80
|
+
getInvoiceEInvoice(invoiceId: string): Promise<EInvoiceDocument>;
|
|
80
81
|
createCreditNote(invoiceId: string, data: {
|
|
81
82
|
reason: string;
|
|
82
83
|
reasonDescription?: string;
|
|
@@ -88,13 +89,15 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
88
89
|
daysOverdue?: number;
|
|
89
90
|
}): Promise<Rec>;
|
|
90
91
|
sendQuote(id: string, to?: string): Promise<Rec>;
|
|
91
|
-
listWebhooks(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
listWebhooks(): Promise<{
|
|
93
|
+
data: Webhook[];
|
|
94
|
+
total: number;
|
|
95
|
+
_demo?: true;
|
|
96
|
+
_demoNotice?: string;
|
|
97
|
+
}>;
|
|
98
|
+
getWebhook(id: string): Promise<Webhook>;
|
|
99
|
+
createWebhook(data: CreateWebhookInput): Promise<CreateWebhookResult>;
|
|
100
|
+
updateWebhook(id: string, data: UpdateWebhookInput): Promise<Webhook>;
|
|
98
101
|
deleteWebhook(_id: string): Promise<void>;
|
|
99
102
|
listClientContacts(_clientId: string, params?: {
|
|
100
103
|
limit?: number;
|
|
@@ -106,7 +109,7 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
106
109
|
limit?: number;
|
|
107
110
|
offset?: number;
|
|
108
111
|
}): Promise<PaginatedResponse<Rec>>;
|
|
109
|
-
logClientActivity(
|
|
112
|
+
logClientActivity(_clientId: string, data: Rec): Promise<Rec>;
|
|
110
113
|
listClientNotes(_clientId: string, params?: {
|
|
111
114
|
limit?: number;
|
|
112
115
|
offset?: number;
|
|
@@ -441,7 +444,7 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
441
444
|
eventType?: string;
|
|
442
445
|
}): Promise<Rec>;
|
|
443
446
|
exportPayroll(params: {
|
|
444
|
-
format: "a3" | "contasol" | "sage" | "
|
|
447
|
+
format: "a3" | "contasol" | "sage" | "siltra";
|
|
445
448
|
month: string;
|
|
446
449
|
}): Promise<Rec>;
|
|
447
450
|
getPayrollChecklist(params: {
|
|
@@ -454,7 +457,7 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
454
457
|
getPermissionsMatrix(): Promise<Rec>;
|
|
455
458
|
getMyPermissions(): Promise<Rec>;
|
|
456
459
|
getCurrentPeriod(params?: {
|
|
457
|
-
|
|
460
|
+
fiscalYear?: string;
|
|
458
461
|
}): Promise<Rec>;
|
|
459
462
|
closePeriod(data: {
|
|
460
463
|
type: "monthly" | "quarterly";
|
package/dist/demo-client.js
CHANGED
|
@@ -17,7 +17,62 @@
|
|
|
17
17
|
* - delete / remove methods → resolve void (interface return type forbids a
|
|
18
18
|
* stamp; this is the one accepted, documented gap).
|
|
19
19
|
*/
|
|
20
|
+
import { Buffer } from "node:buffer";
|
|
20
21
|
import { READ_STAMP, FISCAL_STAMP, DEMO_NOW, demoPage, demoEmptyPage, findOrStub, demoId, simulateWrite, simulateAction, demoClients, demoProducts, demoInvoices, demoExpenses, demoQuotes, demoVendors, demoBankAccounts, demoTransactions, } from "./demo-fixtures.js";
|
|
22
|
+
const DEMO_RBAC_ROLES = ["owner", "admin", "manager", "sales", "accountant", "employee", "viewer"];
|
|
23
|
+
const DEMO_RBAC_RESOURCES = [
|
|
24
|
+
"workspace", "invoices", "quotes", "expenses", "clients", "products", "accounting",
|
|
25
|
+
"people", "payroll", "integrations", "banking", "settings", "audit_log", "billing",
|
|
26
|
+
];
|
|
27
|
+
const DEMO_RBAC_ACTIONS = ["read", "create", "update", "delete", "bulk_delete", "export"];
|
|
28
|
+
const DEMO_FULL_ACTIONS = [...DEMO_RBAC_ACTIONS];
|
|
29
|
+
const DEMO_PERMISSION_MATRIX = {
|
|
30
|
+
owner: {
|
|
31
|
+
workspace: ["read", "update", "export"],
|
|
32
|
+
invoices: DEMO_FULL_ACTIONS, quotes: DEMO_FULL_ACTIONS, expenses: DEMO_FULL_ACTIONS,
|
|
33
|
+
clients: DEMO_FULL_ACTIONS, products: DEMO_FULL_ACTIONS, accounting: DEMO_FULL_ACTIONS,
|
|
34
|
+
people: DEMO_FULL_ACTIONS, payroll: DEMO_FULL_ACTIONS, integrations: DEMO_FULL_ACTIONS,
|
|
35
|
+
banking: ["read", "create", "update", "delete", "export"],
|
|
36
|
+
settings: ["read", "update"], audit_log: ["read", "export"],
|
|
37
|
+
billing: ["read", "create", "update", "delete", "export"],
|
|
38
|
+
},
|
|
39
|
+
admin: {
|
|
40
|
+
workspace: ["read", "update", "export"],
|
|
41
|
+
invoices: DEMO_FULL_ACTIONS, quotes: DEMO_FULL_ACTIONS, expenses: DEMO_FULL_ACTIONS,
|
|
42
|
+
clients: DEMO_FULL_ACTIONS, products: DEMO_FULL_ACTIONS, accounting: DEMO_FULL_ACTIONS,
|
|
43
|
+
people: DEMO_FULL_ACTIONS, payroll: DEMO_FULL_ACTIONS, integrations: DEMO_FULL_ACTIONS,
|
|
44
|
+
banking: ["read", "create", "update", "delete", "export"],
|
|
45
|
+
settings: ["read", "update"], audit_log: ["read", "export"],
|
|
46
|
+
},
|
|
47
|
+
manager: {
|
|
48
|
+
workspace: ["read"], invoices: ["read", "export"], quotes: ["read", "export"],
|
|
49
|
+
clients: ["read", "export"], products: ["read", "export"], expenses: ["read", "export"],
|
|
50
|
+
banking: ["read"], people: DEMO_FULL_ACTIONS, payroll: ["read", "export"],
|
|
51
|
+
settings: ["read"], integrations: ["read"], audit_log: ["read"],
|
|
52
|
+
},
|
|
53
|
+
sales: {
|
|
54
|
+
workspace: ["read"], invoices: ["read", "create", "update", "export"],
|
|
55
|
+
quotes: ["read", "create", "update", "export"], clients: ["read", "create", "update", "export"],
|
|
56
|
+
products: ["read", "export"], expenses: ["read", "export"],
|
|
57
|
+
},
|
|
58
|
+
accountant: {
|
|
59
|
+
workspace: ["read"], invoices: ["read", "create", "update", "export"],
|
|
60
|
+
quotes: ["read", "export"], expenses: ["read", "create", "update", "export"],
|
|
61
|
+
clients: ["read", "export"], products: ["read", "export"],
|
|
62
|
+
banking: ["read", "create", "update", "export"], accounting: ["read", "create", "update", "export"],
|
|
63
|
+
people: ["read"], audit_log: ["read", "export"],
|
|
64
|
+
},
|
|
65
|
+
employee: { workspace: ["read"] },
|
|
66
|
+
viewer: {
|
|
67
|
+
workspace: ["read", "export"], invoices: ["read", "export"], quotes: ["read", "export"],
|
|
68
|
+
expenses: ["read", "export"], clients: ["read", "export"], products: ["read", "export"],
|
|
69
|
+
banking: ["read"], accounting: ["read", "export"], people: ["read"], payroll: ["read"],
|
|
70
|
+
settings: ["read"], integrations: ["read"], audit_log: ["read"],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const DEMO_OWNER_CAPABILITIES = Object.entries(DEMO_PERMISSION_MATRIX.owner)
|
|
74
|
+
.flatMap(([resource, actions]) => actions.map((action) => `${resource}:${action}`))
|
|
75
|
+
.sort();
|
|
21
76
|
export class DemoFrihetClient {
|
|
22
77
|
// ---------------------------------------------------------------- Invoices
|
|
23
78
|
async listInvoices(params) {
|
|
@@ -139,13 +194,27 @@ export class DemoFrihetClient {
|
|
|
139
194
|
return simulateAction(id, { status: "paid", paidAt: paidDate ?? DEMO_NOW });
|
|
140
195
|
}
|
|
141
196
|
async getInvoicePdf(id) {
|
|
142
|
-
|
|
197
|
+
// #1393: shape mirrors the live BinaryDocument contract (id,
|
|
198
|
+
// contentType, sizeBytes, base64) so demo-mode outputSchema validation
|
|
199
|
+
// runs against the same fields as the live path.
|
|
200
|
+
const demoPdfBytes = Buffer.from("%PDF-1.4\n%%DEMO e-invoice for " + id + "\n%%EOF\n", "utf8");
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
contentType: "application/pdf",
|
|
204
|
+
sizeBytes: demoPdfBytes.byteLength,
|
|
205
|
+
base64: demoPdfBytes.toString("base64"),
|
|
206
|
+
...READ_STAMP,
|
|
207
|
+
};
|
|
143
208
|
}
|
|
144
209
|
async getInvoiceEInvoice(invoiceId) {
|
|
210
|
+
// #1393: mirrors the live XML arm of the MIME-discriminated contract.
|
|
211
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<Invoice><!-- DEMO example e-invoice for ${invoiceId} --></Invoice>`;
|
|
145
212
|
return {
|
|
146
|
-
|
|
213
|
+
id: invoiceId,
|
|
214
|
+
xml,
|
|
215
|
+
contentType: "application/xml; charset=utf-8",
|
|
216
|
+
sizeBytes: Buffer.byteLength(xml, "utf8"),
|
|
147
217
|
filename: `${invoiceId}.xml`,
|
|
148
|
-
format: "facturae",
|
|
149
218
|
...FISCAL_STAMP,
|
|
150
219
|
};
|
|
151
220
|
}
|
|
@@ -190,17 +259,42 @@ export class DemoFrihetClient {
|
|
|
190
259
|
return { id, status: "sent", sentAt: DEMO_NOW, ...(to ? { to } : {}), ...FISCAL_STAMP };
|
|
191
260
|
}
|
|
192
261
|
// ---------------------------------------------------------------- Webhooks
|
|
193
|
-
async listWebhooks(
|
|
194
|
-
|
|
262
|
+
async listWebhooks() {
|
|
263
|
+
const { data, total } = demoEmptyPage();
|
|
264
|
+
return { data: data, total };
|
|
195
265
|
}
|
|
196
266
|
async getWebhook(id) {
|
|
197
|
-
return {
|
|
267
|
+
return {
|
|
268
|
+
id,
|
|
269
|
+
name: "Demo webhook",
|
|
270
|
+
url: "https://example.com/webhooks/frihet",
|
|
271
|
+
events: ["invoice.paid"],
|
|
272
|
+
status: "active",
|
|
273
|
+
metadata: { demo: true },
|
|
274
|
+
hasSecret: false,
|
|
275
|
+
createdAt: DEMO_NOW,
|
|
276
|
+
updatedAt: DEMO_NOW,
|
|
277
|
+
...READ_STAMP,
|
|
278
|
+
};
|
|
198
279
|
}
|
|
199
280
|
async createWebhook(data) {
|
|
200
|
-
return simulateWrite("demo_wh",
|
|
281
|
+
return simulateWrite("demo_wh", { ...data }, {
|
|
282
|
+
status: data.status ?? "active",
|
|
283
|
+
hasSecret: typeof data.secret === "string" && data.secret.length > 0,
|
|
284
|
+
});
|
|
201
285
|
}
|
|
202
286
|
async updateWebhook(id, data) {
|
|
203
|
-
|
|
287
|
+
const { secret, ...fields } = data;
|
|
288
|
+
const { success: _success, ...result } = simulateAction(id, {
|
|
289
|
+
name: "Demo webhook",
|
|
290
|
+
url: "https://example.com/webhooks/frihet",
|
|
291
|
+
events: ["invoice.paid"],
|
|
292
|
+
status: "active",
|
|
293
|
+
metadata: { demo: true },
|
|
294
|
+
hasSecret: typeof secret === "string" && secret.length > 0,
|
|
295
|
+
...fields,
|
|
296
|
+
});
|
|
297
|
+
return result;
|
|
204
298
|
}
|
|
205
299
|
async deleteWebhook(_id) {
|
|
206
300
|
return;
|
|
@@ -219,8 +313,13 @@ export class DemoFrihetClient {
|
|
|
219
313
|
async listClientActivities(_clientId, params) {
|
|
220
314
|
return demoEmptyPage(params);
|
|
221
315
|
}
|
|
222
|
-
async logClientActivity(
|
|
223
|
-
return simulateWrite("demo_act", data, {
|
|
316
|
+
async logClientActivity(_clientId, data) {
|
|
317
|
+
return simulateWrite("demo_act", data, {
|
|
318
|
+
type: data.type === "email" ? "email_sent" : data.type,
|
|
319
|
+
timestamp: DEMO_NOW,
|
|
320
|
+
createdBy: "user",
|
|
321
|
+
metadata: {},
|
|
322
|
+
});
|
|
224
323
|
}
|
|
225
324
|
// ---------------------------------------------------------------- CRM: Notes
|
|
226
325
|
async listClientNotes(_clientId, params) {
|
|
@@ -556,7 +655,17 @@ export class DemoFrihetClient {
|
|
|
556
655
|
return simulateAction(entryId, { status: "closed", clockOutAt: DEMO_NOW });
|
|
557
656
|
}
|
|
558
657
|
async getOvertimeReport(params) {
|
|
559
|
-
return {
|
|
658
|
+
return {
|
|
659
|
+
period: params.period,
|
|
660
|
+
employeeId: params.employeeId ?? null,
|
|
661
|
+
recordCount: 0,
|
|
662
|
+
dailyOvertime: [],
|
|
663
|
+
weeklyOvertime: [],
|
|
664
|
+
monthlyTotal: { workedMinutes: 0, overtimeMinutes: 0, regularMinutes: 0 },
|
|
665
|
+
annualOvertimeHours: 0,
|
|
666
|
+
alerts: [],
|
|
667
|
+
...READ_STAMP,
|
|
668
|
+
};
|
|
560
669
|
}
|
|
561
670
|
async listAnomalies(params) {
|
|
562
671
|
return demoEmptyPage(params);
|
|
@@ -565,12 +674,23 @@ export class DemoFrihetClient {
|
|
|
565
674
|
async testWebhook(id, data) {
|
|
566
675
|
return { webhookId: id, delivered: true, statusCode: 200, responseTimeMs: 42, eventType: data?.eventType ?? "ping", attemptedAt: DEMO_NOW, ...FISCAL_STAMP };
|
|
567
676
|
}
|
|
568
|
-
// ---------------------------------------------------------------- Payroll (
|
|
677
|
+
// ---------------------------------------------------------------- Payroll (read-only demo)
|
|
569
678
|
async exportPayroll(params) {
|
|
570
|
-
return {
|
|
679
|
+
return {
|
|
680
|
+
month: params.month,
|
|
681
|
+
format: params.format,
|
|
682
|
+
employees: [],
|
|
683
|
+
summary: { exportedCount: 0, skippedNotReady: 0, totalGrossAnnual: 0 },
|
|
684
|
+
...READ_STAMP,
|
|
685
|
+
};
|
|
571
686
|
}
|
|
572
687
|
async getPayrollChecklist(params) {
|
|
573
|
-
return {
|
|
688
|
+
return {
|
|
689
|
+
month: params.month,
|
|
690
|
+
employees: [],
|
|
691
|
+
summary: { total: 0, ready: 0, notReady: 0, reviewedThisMonth: 0 },
|
|
692
|
+
...READ_STAMP,
|
|
693
|
+
};
|
|
574
694
|
}
|
|
575
695
|
// ---------------------------------------------------------------- Onboarding
|
|
576
696
|
async getOnboardingStatus() {
|
|
@@ -581,14 +701,48 @@ export class DemoFrihetClient {
|
|
|
581
701
|
}
|
|
582
702
|
// ---------------------------------------------------------------- Permissions
|
|
583
703
|
async getPermissionsMatrix() {
|
|
584
|
-
return {
|
|
704
|
+
return {
|
|
705
|
+
roles: DEMO_RBAC_ROLES,
|
|
706
|
+
resources: DEMO_RBAC_RESOURCES,
|
|
707
|
+
actions: DEMO_RBAC_ACTIONS,
|
|
708
|
+
legacyAliases: { editor: "sales" },
|
|
709
|
+
matrix: DEMO_PERMISSION_MATRIX,
|
|
710
|
+
source: "hand-maintained demo snapshot of the Frihet RBAC model; not derived from firestore.rules and not verified against it",
|
|
711
|
+
...READ_STAMP,
|
|
712
|
+
};
|
|
585
713
|
}
|
|
586
714
|
async getMyPermissions() {
|
|
587
|
-
return {
|
|
715
|
+
return {
|
|
716
|
+
role: "owner",
|
|
717
|
+
isOwner: true,
|
|
718
|
+
resources: DEMO_PERMISSION_MATRIX.owner,
|
|
719
|
+
scopes: DEMO_OWNER_CAPABILITIES,
|
|
720
|
+
legacyFieldSemantics: { resources: "rbacResources", scopes: "rbacCapabilities" },
|
|
721
|
+
rbac: {
|
|
722
|
+
role: "owner",
|
|
723
|
+
isOwner: true,
|
|
724
|
+
resources: DEMO_PERMISSION_MATRIX.owner,
|
|
725
|
+
capabilities: DEMO_OWNER_CAPABILITIES,
|
|
726
|
+
},
|
|
727
|
+
apiKeyScopes: [],
|
|
728
|
+
apiKeyUnrestricted: true,
|
|
729
|
+
denied: { einvoice: false },
|
|
730
|
+
deniedSemantics: "Known API-key scope denials only; not an exhaustive effective-authorization report.",
|
|
731
|
+
notIncluded: ["featureFlags", "deployedEndpoints"],
|
|
732
|
+
...READ_STAMP,
|
|
733
|
+
};
|
|
588
734
|
}
|
|
589
735
|
// ---------------------------------------------------------------- Period close
|
|
590
736
|
async getCurrentPeriod(params) {
|
|
591
|
-
|
|
737
|
+
const fiscalYear = params?.fiscalYear ?? "2026";
|
|
738
|
+
return {
|
|
739
|
+
fiscalYear,
|
|
740
|
+
fiscalYearStart: "01-01",
|
|
741
|
+
status: "open",
|
|
742
|
+
dateRange: { from: `${fiscalYear}-01-01`, to: `${fiscalYear}-12-31` },
|
|
743
|
+
closing: null,
|
|
744
|
+
...READ_STAMP,
|
|
745
|
+
};
|
|
592
746
|
}
|
|
593
747
|
async closePeriod(data) {
|
|
594
748
|
return simulateWrite("demo_period", { type: data.type }, { status: "closed", closedAt: DEMO_NOW });
|
package/dist/demo-fixtures.d.ts
CHANGED
|
@@ -30,13 +30,13 @@ export declare const EMPTY_STAMP: DemoStamp;
|
|
|
30
30
|
export declare const DEMO_NOW = "2026-07-15T10:00:00.000Z";
|
|
31
31
|
/** Test IBAN (ECBS/AEAT example — never a real account). */
|
|
32
32
|
export declare const DEMO_TEST_IBAN = "ES9121000418450200051332";
|
|
33
|
-
export type DemoPaginated = PaginatedResponse<Record<string, unknown
|
|
34
|
-
/** Wrap a fixture array in a
|
|
33
|
+
export type DemoPaginated = PaginatedResponse<Record<string, unknown>>;
|
|
34
|
+
/** Wrap a fixture array in a PaginatedResponse. */
|
|
35
35
|
export declare function demoPage(data: Record<string, unknown>[], params?: {
|
|
36
36
|
limit?: number;
|
|
37
37
|
offset?: number;
|
|
38
38
|
}): DemoPaginated;
|
|
39
|
-
/** Empty
|
|
39
|
+
/** Empty PaginatedResponse for surfaces without dedicated fixtures. */
|
|
40
40
|
export declare function demoEmptyPage(params?: {
|
|
41
41
|
limit?: number;
|
|
42
42
|
offset?: number;
|
package/dist/demo-fixtures.js
CHANGED
|
@@ -36,24 +36,24 @@ export const EMPTY_STAMP = {
|
|
|
36
36
|
export const DEMO_NOW = "2026-07-15T10:00:00.000Z";
|
|
37
37
|
/** Test IBAN (ECBS/AEAT example — never a real account). */
|
|
38
38
|
export const DEMO_TEST_IBAN = "ES9121000418450200051332";
|
|
39
|
-
/** Wrap a fixture array in a
|
|
39
|
+
/** Wrap a fixture array in a PaginatedResponse. */
|
|
40
40
|
export function demoPage(data, params) {
|
|
41
|
+
const limit = params?.limit ?? 50;
|
|
42
|
+
const offset = params?.offset ?? 0;
|
|
41
43
|
return {
|
|
42
|
-
data,
|
|
44
|
+
data: data.slice(offset, offset + limit),
|
|
43
45
|
total: data.length,
|
|
44
|
-
limit
|
|
45
|
-
offset
|
|
46
|
-
...READ_STAMP,
|
|
46
|
+
limit,
|
|
47
|
+
offset,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
|
-
/** Empty
|
|
50
|
+
/** Empty PaginatedResponse for surfaces without dedicated fixtures. */
|
|
50
51
|
export function demoEmptyPage(params) {
|
|
51
52
|
return {
|
|
52
53
|
data: [],
|
|
53
54
|
total: 0,
|
|
54
55
|
limit: params?.limit ?? 50,
|
|
55
56
|
offset: params?.offset ?? 0,
|
|
56
|
-
...EMPTY_STAMP,
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
/* ------------------------------------------------------------------ */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fiscal `modelo` tool naming aliases — prefix-drift fix (issue #50).
|
|
3
|
+
*
|
|
4
|
+
* ── Problem ──────────────────────────────────────────────────────────────
|
|
5
|
+
* The `modelo` fiscal tools split 5/5 on naming prefix: the mainland AEAT
|
|
6
|
+
* models (`fiscal.ts`) use `get_modelo_*`, while the Canary IGIC + Impuesto
|
|
7
|
+
* Sociedades models (`igic.ts`, `impuesto_sociedades.ts`) use the house
|
|
8
|
+
* `frihet_*` convention. An agent that learns the `frihet_modelo_NNN_summary`
|
|
9
|
+
* pattern from IGIC/IS will guess `frihet_modelo_303_summary` for VAT and get
|
|
10
|
+
* tool-not-found — 303/130/347 are the most-used models.
|
|
11
|
+
*
|
|
12
|
+
* ── Fix ──────────────────────────────────────────────────────────────────
|
|
13
|
+
* Register thin NAME ALIASES that resolve to the exact same registered tool
|
|
14
|
+
* (same handler, same schema, same annotations — zero semantic divergence),
|
|
15
|
+
* WITHOUT calling `registerTool()` a second time. The MCP SDK's `McpServer`
|
|
16
|
+
* resolves `tools/call` by looking up `this._registeredTools[name]`
|
|
17
|
+
* (mcp.js: `setToolRequestHandlers` -> `CallToolRequestSchema` handler), so
|
|
18
|
+
* pointing an alias key at the SAME registered-tool object makes it callable
|
|
19
|
+
* with zero duplication and zero drift risk.
|
|
20
|
+
*
|
|
21
|
+
* This intentionally does NOT touch `src/tools/*.ts` (where the 157-tool
|
|
22
|
+
* audit / pin tests count literal `registerTool(` call sites) and is applied
|
|
23
|
+
* to the REAL `McpServer` instance only, AFTER `registerAllTools()` — the
|
|
24
|
+
* `tool-exposure.test.ts` / `openai-*.test.ts` suites exercise
|
|
25
|
+
* `registerAllTools()` against a bare `StubMcpServer` and never call
|
|
26
|
+
* `applyFiscalAliases`, so the 157-tool pins are untouched by design.
|
|
27
|
+
*
|
|
28
|
+
* KNOWN LIMITATION: this relies on the SDK's internal (unprefixed-by-type,
|
|
29
|
+
* but underscore-private) `_registeredTools` field. It is FAIL-OPEN — if a
|
|
30
|
+
* future SDK release renames/removes that field, `applyFiscalAliases` is a
|
|
31
|
+
* silent no-op (the canonical `get_modelo_*` names keep working; only the
|
|
32
|
+
* alias convenience is lost). Pinned by `fiscal-aliases.test.ts`.
|
|
33
|
+
*/
|
|
34
|
+
/** alias name -> canonical registered tool name. */
|
|
35
|
+
export declare const FISCAL_MODELO_ALIASES: Readonly<Record<string, string>>;
|
|
36
|
+
/**
|
|
37
|
+
* Apply the fiscal `modelo` aliases to a live `McpServer` instance. Call this
|
|
38
|
+
* AFTER `registerAllTools()` so every canonical tool already exists.
|
|
39
|
+
*
|
|
40
|
+
* Fail-open: if the SDK's internal registry is missing or reshaped, this is a
|
|
41
|
+
* silent no-op — it never throws, and never blocks server startup.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyFiscalAliases(server: unknown): void;
|
|
44
|
+
//# sourceMappingURL=fiscal-aliases.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fiscal `modelo` tool naming aliases — prefix-drift fix (issue #50).
|
|
3
|
+
*
|
|
4
|
+
* ── Problem ──────────────────────────────────────────────────────────────
|
|
5
|
+
* The `modelo` fiscal tools split 5/5 on naming prefix: the mainland AEAT
|
|
6
|
+
* models (`fiscal.ts`) use `get_modelo_*`, while the Canary IGIC + Impuesto
|
|
7
|
+
* Sociedades models (`igic.ts`, `impuesto_sociedades.ts`) use the house
|
|
8
|
+
* `frihet_*` convention. An agent that learns the `frihet_modelo_NNN_summary`
|
|
9
|
+
* pattern from IGIC/IS will guess `frihet_modelo_303_summary` for VAT and get
|
|
10
|
+
* tool-not-found — 303/130/347 are the most-used models.
|
|
11
|
+
*
|
|
12
|
+
* ── Fix ──────────────────────────────────────────────────────────────────
|
|
13
|
+
* Register thin NAME ALIASES that resolve to the exact same registered tool
|
|
14
|
+
* (same handler, same schema, same annotations — zero semantic divergence),
|
|
15
|
+
* WITHOUT calling `registerTool()` a second time. The MCP SDK's `McpServer`
|
|
16
|
+
* resolves `tools/call` by looking up `this._registeredTools[name]`
|
|
17
|
+
* (mcp.js: `setToolRequestHandlers` -> `CallToolRequestSchema` handler), so
|
|
18
|
+
* pointing an alias key at the SAME registered-tool object makes it callable
|
|
19
|
+
* with zero duplication and zero drift risk.
|
|
20
|
+
*
|
|
21
|
+
* This intentionally does NOT touch `src/tools/*.ts` (where the 157-tool
|
|
22
|
+
* audit / pin tests count literal `registerTool(` call sites) and is applied
|
|
23
|
+
* to the REAL `McpServer` instance only, AFTER `registerAllTools()` — the
|
|
24
|
+
* `tool-exposure.test.ts` / `openai-*.test.ts` suites exercise
|
|
25
|
+
* `registerAllTools()` against a bare `StubMcpServer` and never call
|
|
26
|
+
* `applyFiscalAliases`, so the 157-tool pins are untouched by design.
|
|
27
|
+
*
|
|
28
|
+
* KNOWN LIMITATION: this relies on the SDK's internal (unprefixed-by-type,
|
|
29
|
+
* but underscore-private) `_registeredTools` field. It is FAIL-OPEN — if a
|
|
30
|
+
* future SDK release renames/removes that field, `applyFiscalAliases` is a
|
|
31
|
+
* silent no-op (the canonical `get_modelo_*` names keep working; only the
|
|
32
|
+
* alias convenience is lost). Pinned by `fiscal-aliases.test.ts`.
|
|
33
|
+
*/
|
|
34
|
+
/** alias name -> canonical registered tool name. */
|
|
35
|
+
export const FISCAL_MODELO_ALIASES = {
|
|
36
|
+
frihet_modelo_303_summary: "get_modelo_303_summary",
|
|
37
|
+
frihet_modelo_130_summary: "get_modelo_130_summary",
|
|
38
|
+
frihet_modelo_390_summary: "get_modelo_390_summary",
|
|
39
|
+
frihet_modelo_180_summary: "get_modelo_180_summary",
|
|
40
|
+
frihet_modelo_347_summary: "get_modelo_347_summary",
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Apply the fiscal `modelo` aliases to a live `McpServer` instance. Call this
|
|
44
|
+
* AFTER `registerAllTools()` so every canonical tool already exists.
|
|
45
|
+
*
|
|
46
|
+
* Fail-open: if the SDK's internal registry is missing or reshaped, this is a
|
|
47
|
+
* silent no-op — it never throws, and never blocks server startup.
|
|
48
|
+
*/
|
|
49
|
+
export function applyFiscalAliases(server) {
|
|
50
|
+
const registry = server._registeredTools;
|
|
51
|
+
if (!registry || typeof registry !== "object")
|
|
52
|
+
return;
|
|
53
|
+
for (const [alias, canonical] of Object.entries(FISCAL_MODELO_ALIASES)) {
|
|
54
|
+
const target = registry[canonical];
|
|
55
|
+
if (!target)
|
|
56
|
+
continue; // Canonical tool not registered in this profile (e.g. an allow-listed mode) — skip, no divergence.
|
|
57
|
+
if (registry[alias])
|
|
58
|
+
continue; // Never overwrite an existing registration.
|
|
59
|
+
registry[alias] = target; // Same object reference as the canonical tool — zero semantic divergence.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=fiscal-aliases.js.map
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
15
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
16
|
import { FrihetClient } from "./client.js";
|
|
17
17
|
import { DemoFrihetClient } from "./demo-client.js";
|
|
18
|
+
import { normalizePublicApiBaseUrl } from "./api-origin.js";
|
|
18
19
|
// Single source of truth for the server version: package.json (read at runtime
|
|
19
20
|
// from dist/../package.json). Hardcoding it here caused repeated version drift
|
|
20
21
|
// (server.json/index.ts/console out of sync → audit:mcp-refs gate failure).
|
|
@@ -28,11 +29,10 @@ const PKG_VERSION = (() => {
|
|
|
28
29
|
return "0.0.0";
|
|
29
30
|
}
|
|
30
31
|
})();
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import { resolveToolMode, applyToolExposureProfile, GROUPED_META_TOOL_COUNT } from "./tool-exposure.js";
|
|
32
|
+
import { OPENAI_ALLOWED_TOOL_COUNT, OPENAI_EXCLUDED_COUNT, OPENAI_EXCLUDED_RESOURCE_COUNT } from "./openai-profile.js";
|
|
33
|
+
import { resolveToolMode, GROUPED_META_TOOL_COUNT } from "./tool-exposure.js";
|
|
34
|
+
import { localMcpSurfaceComposition, registerMcpSurface, } from "./server-composition.js";
|
|
35
|
+
import { AGENT_SERVER_INSTRUCTIONS, API_KEY_LOCATION, } from "./agent-onboarding.js";
|
|
36
36
|
import { log } from "./logger.js";
|
|
37
37
|
import { registerShutdownHook } from "./metrics.js";
|
|
38
38
|
import { setTraceContext } from "./observability.js";
|
|
@@ -44,33 +44,40 @@ function main() {
|
|
|
44
44
|
const demoFlag = process.env.FRIHET_DEMO;
|
|
45
45
|
const demoMode = demoFlag === "1" || demoFlag === "true";
|
|
46
46
|
if (!demoMode && !apiKey) {
|
|
47
|
+
// Human-readable first, then one machine-readable line. An agent that only
|
|
48
|
+
// gets prose here has to guess; the JSON line gives it a code to branch on
|
|
49
|
+
// and the two recoveries that actually exist. /settings/api is the real,
|
|
50
|
+
// routable screen — there is no "Developers" section in the app.
|
|
47
51
|
console.error("Error: FRIHET_API_KEY environment variable is required.\n\n" +
|
|
48
52
|
"Get your API key:\n" +
|
|
49
53
|
" 1. Create a free account at https://app.frihet.io\n" +
|
|
50
|
-
" 2.
|
|
51
|
-
" 3. Create a key and add it to your MCP configuration\n\n" +
|
|
54
|
+
" 2. Open https://app.frihet.io/settings/api\n" +
|
|
55
|
+
" 3. Create a key (it starts with fri_) and add it to your MCP configuration\n\n" +
|
|
52
56
|
"Or try it instantly with no key: set FRIHET_DEMO=1 for example data.\n\n" +
|
|
53
57
|
"Documentation: https://docs.frihet.io/desarrolladores/mcp-server\n");
|
|
58
|
+
console.error(JSON.stringify({
|
|
59
|
+
error: {
|
|
60
|
+
code: "FRIHET_API_KEY_MISSING",
|
|
61
|
+
message: "FRIHET_API_KEY environment variable is required",
|
|
62
|
+
obtainAt: API_KEY_LOCATION,
|
|
63
|
+
recovery: [
|
|
64
|
+
{ action: "set_env", env: { FRIHET_API_KEY: "fri_<key>" } },
|
|
65
|
+
{ action: "run_without_credentials", env: { FRIHET_DEMO: "1" } },
|
|
66
|
+
],
|
|
67
|
+
docs: "https://docs.frihet.io/desarrolladores/mcp-server",
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
54
70
|
process.exit(1);
|
|
55
71
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
const baseUrlRaw = process.env.FRIHET_API_URL;
|
|
73
|
+
let baseUrl;
|
|
74
|
+
if (!demoMode && baseUrlRaw !== undefined) {
|
|
59
75
|
try {
|
|
60
|
-
|
|
76
|
+
baseUrl = normalizePublicApiBaseUrl(baseUrlRaw);
|
|
61
77
|
}
|
|
62
|
-
catch {
|
|
63
|
-
console.error(`Error:
|
|
64
|
-
"It must be a
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
if (parsed.protocol !== "https:") {
|
|
68
|
-
console.error(`Error: FRIHET_API_URL must use https:// (got "${parsed.protocol}").\n`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
if (!parsed.hostname.endsWith("frihet.io")) {
|
|
72
|
-
console.error(`Error: FRIHET_API_URL hostname must be under frihet.io (got "${parsed.hostname}").\n` +
|
|
73
|
-
"This prevents redirection to untrusted servers.\n");
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(`Error: ${error instanceof Error ? error.message : "FRIHET_API_URL is invalid"}.\n` +
|
|
80
|
+
"It must be a canonical https:// URL with a trusted frihet.io hostname.\n");
|
|
74
81
|
process.exit(1);
|
|
75
82
|
}
|
|
76
83
|
}
|
|
@@ -80,69 +87,47 @@ function main() {
|
|
|
80
87
|
if (demoMode) {
|
|
81
88
|
console.error("[frihet-mcp] DEMO MODE — serving example fixtures, no real data, nothing persisted");
|
|
82
89
|
}
|
|
83
|
-
// Set
|
|
84
|
-
//
|
|
90
|
+
// Set the fixed protocol fact for Langfuse (reads LANGFUSE_* from process.env automatically).
|
|
91
|
+
// Client identity is deliberately omitted from telemetry.
|
|
85
92
|
setTraceContext({
|
|
86
93
|
mcpVersion: "mcp/1.0",
|
|
87
|
-
clientName: process.env.FRIHET_CLIENT_NAME,
|
|
88
94
|
});
|
|
89
95
|
const server = new McpServer({
|
|
90
96
|
name: "frihet-erp",
|
|
91
97
|
version: PKG_VERSION,
|
|
92
98
|
description: "AI-native MCP server for Frihet ERP — invoices, expenses, clients, products, quotes, webhooks, and deposits. " +
|
|
93
|
-
"Provides
|
|
94
|
-
"11 resources (7 static + 4
|
|
99
|
+
"Provides a catalogue of 157 canonical operations; fiscal aliases and optional grouped discovery names are reported separately. " +
|
|
100
|
+
"The local package serves 11 resources (7 static + 4 API-backed) and 10 workflow prompts " +
|
|
95
101
|
"with full Spanish tax compliance (IVA, IGIC, IPSI).",
|
|
102
|
+
}, {
|
|
103
|
+
// Delivered by `initialize` to every MCP client, which hands it to the model
|
|
104
|
+
// before the first tool call. This is the only onboarding channel that costs
|
|
105
|
+
// the operator zero configuration. Source: src/agent-onboarding.ts.
|
|
106
|
+
instructions: AGENT_SERVER_INSTRUCTIONS,
|
|
96
107
|
});
|
|
97
108
|
const openaiMode = process.env.FRIHET_OPENAI_MODE === "true";
|
|
98
109
|
const toolMode = resolveToolMode();
|
|
99
|
-
// PROFILE COMPOSITION ORDER (both interceptors wrap registerTool):
|
|
100
|
-
// 1. applyToolExposureProfile FIRST (innermost) — so the 3 discovery
|
|
101
|
-
// meta-tools register against the REAL server.registerTool and bypass the
|
|
102
|
-
// OpenAI allow-list gate. In allow-list mode it catalogs ONLY the reviewed
|
|
103
|
-
// tools, keeping the progressive-disclosure surface == the reviewed 53.
|
|
104
|
-
// 2. applyOpenAIProfile SECOND (outermost) — a business-tool registration is
|
|
105
|
-
// first gated/redacted/annotated/openWorldHint-justified by OpenAI, THEN
|
|
106
|
-
// collapsed by the grouped interceptor, so the terse collapsed line is the
|
|
107
|
-
// final description and OpenAI's handler redaction survives.
|
|
108
|
-
// Order only matters when BOTH are active (openai-mcp grouped); openai-only and
|
|
109
|
-
// grouped-only are unaffected by the swap.
|
|
110
|
-
// Apply grouped tool-exposure profile if enabled (progressive disclosure).
|
|
111
|
-
// FRIHET_TOOL_MODE=grouped collapses the full tool descriptions into terse
|
|
112
|
-
// one-liners + adds list_tool_groups / search_tools / describe_tool meta-tools,
|
|
113
|
-
// so agents load depth on demand instead of a flat 157-tool wall of context.
|
|
114
|
-
// Default (unset / "full") is byte-identical to current behavior. When OpenAI
|
|
115
|
-
// mode is also on, pass the reviewed allow-list so the catalog/meta-tools are
|
|
116
|
-
// pinned to exactly the 53 reviewed tools.
|
|
117
110
|
if (toolMode === "grouped") {
|
|
118
|
-
applyToolExposureProfile(server, openaiMode ? { allowlist: OPENAI_REVIEWED_TOOL_ALLOWLIST } : undefined);
|
|
119
111
|
log({
|
|
120
112
|
level: "info",
|
|
121
113
|
message: `Grouped tool-exposure active — tools collapsed to terse summaries, ${GROUPED_META_TOOL_COUNT} discovery meta-tools added (list_tool_groups, search_tools, describe_tool); full depth served on demand`,
|
|
122
114
|
operation: "startup",
|
|
123
115
|
});
|
|
124
116
|
}
|
|
125
|
-
// Apply OpenAI-safe profile if enabled (strips sensitive fields, fixes annotations)
|
|
126
117
|
if (openaiMode) {
|
|
127
|
-
applyOpenAIProfile(server);
|
|
128
118
|
log({
|
|
129
119
|
level: "info",
|
|
130
120
|
message: `OpenAI safety profile active — ${OPENAI_ALLOWED_TOOL_COUNT} tools allowed, prompts hidden, ${OPENAI_EXCLUDED_COUNT} defense-in-depth exclusions, ${OPENAI_EXCLUDED_RESOURCE_COUNT} resources excluded, gov IDs + credentials redacted`,
|
|
131
121
|
operation: "startup",
|
|
132
122
|
});
|
|
133
123
|
}
|
|
134
|
-
|
|
135
|
-
registerAllTools(server, client);
|
|
136
|
-
// Register 11 resources (7 static + 4 dynamic via API)
|
|
137
|
-
registerAllResources(server, client);
|
|
138
|
-
// Register 10 workflow prompts
|
|
139
|
-
registerAllPrompts(server);
|
|
124
|
+
registerMcpSurface(server, client, localMcpSurfaceComposition(openaiMode, toolMode === "grouped"));
|
|
140
125
|
// Register shutdown hook to log final metrics summary
|
|
141
126
|
registerShutdownHook();
|
|
142
127
|
// Connect via stdio transport
|
|
143
128
|
const transport = new StdioServerTransport();
|
|
144
129
|
server.connect(transport).then(() => {
|
|
145
|
-
console.error(`[frihet-mcp] v${PKG_VERSION} |
|
|
130
|
+
console.error(`[frihet-mcp] v${PKG_VERSION} | capability truth enabled | https://github.com/Frihet-io/frihet-mcp`);
|
|
146
131
|
log({
|
|
147
132
|
level: "info",
|
|
148
133
|
message: "Frihet MCP server running on stdio",
|