@frihet/mcp-server 1.16.5 → 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.
Files changed (68) hide show
  1. package/README.md +92 -31
  2. package/dist/agent-onboarding.d.ts +98 -0
  3. package/dist/agent-onboarding.js +275 -0
  4. package/dist/api-origin.d.ts +9 -0
  5. package/dist/api-origin.js +41 -0
  6. package/dist/canary/mcp-harness.d.ts +77 -0
  7. package/dist/canary/mcp-harness.js +549 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +254 -0
  10. package/dist/client-interface.d.ts +22 -14
  11. package/dist/client.d.ts +114 -18
  12. package/dist/client.js +520 -58
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +209 -29
  15. package/dist/demo-fixtures.d.ts +3 -3
  16. package/dist/demo-fixtures.js +7 -7
  17. package/dist/fiscal-aliases.d.ts +44 -0
  18. package/dist/fiscal-aliases.js +62 -0
  19. package/dist/index.js +41 -56
  20. package/dist/logger.d.ts +2 -2
  21. package/dist/logger.js +140 -8
  22. package/dist/observability.d.ts +18 -29
  23. package/dist/observability.js +192 -105
  24. package/dist/openai-profile.d.ts +27 -15
  25. package/dist/openai-profile.js +1191 -145
  26. package/dist/openai-review-contract.d.ts +49 -0
  27. package/dist/openai-review-contract.js +919 -0
  28. package/dist/openai-review-oauth.d.ts +98 -0
  29. package/dist/openai-review-oauth.js +136 -0
  30. package/dist/prompts/register-all.d.ts +2 -0
  31. package/dist/prompts/register-all.js +2 -0
  32. package/dist/public-capability-contract.d.ts +39 -0
  33. package/dist/public-capability-contract.js +186 -0
  34. package/dist/redaction.d.ts +10 -0
  35. package/dist/redaction.js +74 -0
  36. package/dist/resources/register-all.js +6 -12
  37. package/dist/server-composition.d.ts +16 -0
  38. package/dist/server-composition.js +44 -0
  39. package/dist/tool-exposure.d.ts +25 -22
  40. package/dist/tool-exposure.js +199 -77
  41. package/dist/tools/accountingClose.d.ts +14 -5
  42. package/dist/tools/accountingClose.js +41 -23
  43. package/dist/tools/backend-availability.d.ts +3 -3
  44. package/dist/tools/backend-availability.js +3 -3
  45. package/dist/tools/clients.js +9 -9
  46. package/dist/tools/crm.js +22 -21
  47. package/dist/tools/deposits.js +30 -11
  48. package/dist/tools/einvoice.d.ts +1 -1
  49. package/dist/tools/expenses.d.ts +16 -0
  50. package/dist/tools/expenses.js +47 -27
  51. package/dist/tools/hr.d.ts +2 -3
  52. package/dist/tools/hr.js +8 -9
  53. package/dist/tools/intelligence.js +95 -4
  54. package/dist/tools/invoices.js +150 -47
  55. package/dist/tools/payroll.d.ts +5 -8
  56. package/dist/tools/payroll.js +19 -32
  57. package/dist/tools/permissions.d.ts +4 -7
  58. package/dist/tools/permissions.js +11 -14
  59. package/dist/tools/products.js +6 -6
  60. package/dist/tools/quotes.js +109 -24
  61. package/dist/tools/shared.d.ts +1431 -75
  62. package/dist/tools/shared.js +386 -76
  63. package/dist/tools/team.js +18 -14
  64. package/dist/tools/vendors.js +9 -9
  65. package/dist/tools/webhooks.js +35 -30
  66. package/dist/types.d.ts +28 -4
  67. package/docs/agent-onboarding.json +381 -0
  68. package/package.json +32 -7
@@ -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<Rec>;
79
- getInvoiceEInvoice(invoiceId: string): Promise<Rec>;
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(params?: {
92
- limit?: number;
93
- offset?: number;
94
- }): Promise<PaginatedResponse<Rec>>;
95
- getWebhook(id: string): Promise<Rec>;
96
- createWebhook(data: Rec): Promise<Rec>;
97
- updateWebhook(id: string, data: Rec): Promise<Rec>;
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(clientId: string, data: Rec): Promise<Rec>;
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" | "holded" | "siltra";
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
- periodId?: string;
460
+ fiscalYear?: string;
458
461
  }): Promise<Rec>;
459
462
  closePeriod(data: {
460
463
  type: "monthly" | "quarterly";
@@ -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
- return { id, url: "https://app.frihet.io/demo/invoice.pdf", contentType: "application/pdf", ...READ_STAMP };
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
- xml: `<?xml version="1.0" encoding="UTF-8"?>\n<Invoice><!-- DEMO example e-invoice for ${invoiceId} --></Invoice>`,
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(params) {
194
- return demoEmptyPage(params);
262
+ async listWebhooks() {
263
+ const { data, total } = demoEmptyPage();
264
+ return { data: data, total };
195
265
  }
196
266
  async getWebhook(id) {
197
- return { id, ...READ_STAMP };
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", data, { active: data.active ?? true });
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
- return simulateAction(id, { ...data });
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(clientId, data) {
223
- return simulateWrite("demo_act", data, { clientId });
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) {
@@ -257,23 +356,49 @@ export class DemoFrihetClient {
257
356
  // ---------------------------------------------------------------- Intelligence
258
357
  async getBusinessContext() {
259
358
  return {
260
- businessName: "Demo Studio SL",
261
- fiscalRegime: "autonomo",
262
- currency: "EUR",
263
- country: "ES",
264
- metrics: { openInvoices: 4, overdueInvoices: 1, totalClients: demoClients.length, totalProducts: demoProducts.length },
359
+ business: {
360
+ name: "Demo Studio SL",
361
+ fiscalZone: "IVA",
362
+ currency: "EUR",
363
+ language: "es",
364
+ country: "ES",
365
+ },
366
+ defaults: { taxRate: 21, irpfRate: 15, dueDays: 30, currency: "EUR" },
367
+ plan: {
368
+ name: "free",
369
+ invoices: { used: 5, limit: 999 },
370
+ expenses: { used: demoExpenses.length, limit: 5 },
371
+ aiMessages: { used: 2, limit: 30 },
372
+ },
373
+ series: [{ id: "default", prefix: "F", current: 5, year: 2026 }],
374
+ recentActivity: {
375
+ lastInvoice: { number: "F-2026-005", date: "2026-07-18", client: "Acme Studio" },
376
+ lastExpense: { date: "2026-07-17", vendor: "Demo Supplies", amount: 405.6 },
377
+ overdueCount: 1,
378
+ overdueAmount: 640,
379
+ unpaidCount: 2,
380
+ },
381
+ topClients: [{ name: "Acme Studio", totalRevenue: 2200, invoiceCount: 2 }],
382
+ currentMonth: {
383
+ revenue: 3960.4,
384
+ expenses: 405.6,
385
+ profit: 3554.8,
386
+ invoiceCount: 5,
387
+ expenseCount: demoExpenses.length,
388
+ },
265
389
  ...READ_STAMP,
266
390
  };
267
391
  }
268
392
  async getMonthlySummary(month) {
269
393
  return {
270
- month: month ?? "2026-07",
271
- revenue: 3960.4,
272
- expenses: 405.6,
273
- net: 3554.8,
274
- invoiceCount: 5,
275
- expenseCount: demoExpenses.length,
276
- currency: "EUR",
394
+ period: month ?? "2026-07",
395
+ revenue: { total: 3960.4, taxBase: 3273.06, tax: 687.34, irpf: 0 },
396
+ expenses: { total: 405.6, deductible: 405.6, tax: 70.4 },
397
+ profit: { gross: 3554.8, net: 2937.86 },
398
+ invoices: { created: 5, sent: 1, paid: 3, overdue: 1 },
399
+ topClients: [{ name: "Acme Studio", totalRevenue: 2200, invoiceCount: 2 }],
400
+ byCategory: { supplies: 405.6 },
401
+ taxLiability: { vatPayable: 616.94, irpfRetained: 0, estimatedModel303: 616.94 },
277
402
  ...READ_STAMP,
278
403
  };
279
404
  }
@@ -530,7 +655,17 @@ export class DemoFrihetClient {
530
655
  return simulateAction(entryId, { status: "closed", clockOutAt: DEMO_NOW });
531
656
  }
532
657
  async getOvertimeReport(params) {
533
- return { period: params.period, totalRegularHours: 0, totalOvertimeHours: 0, byEmployee: [], generatedAt: DEMO_NOW, ...READ_STAMP };
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
+ };
534
669
  }
535
670
  async listAnomalies(params) {
536
671
  return demoEmptyPage(params);
@@ -539,12 +674,23 @@ export class DemoFrihetClient {
539
674
  async testWebhook(id, data) {
540
675
  return { webhookId: id, delivered: true, statusCode: 200, responseTimeMs: 42, eventType: data?.eventType ?? "ping", attemptedAt: DEMO_NOW, ...FISCAL_STAMP };
541
676
  }
542
- // ---------------------------------------------------------------- Payroll (simulated fiscal)
677
+ // ---------------------------------------------------------------- Payroll (read-only demo)
543
678
  async exportPayroll(params) {
544
- return { format: params.format, month: params.month, fileUrl: "https://app.frihet.io/demo/payroll.txt", filename: "demo-payroll.txt", rowCount: 0, generatedAt: DEMO_NOW, ...FISCAL_STAMP };
679
+ return {
680
+ month: params.month,
681
+ format: params.format,
682
+ employees: [],
683
+ summary: { exportedCount: 0, skippedNotReady: 0, totalGrossAnnual: 0 },
684
+ ...READ_STAMP,
685
+ };
545
686
  }
546
687
  async getPayrollChecklist(params) {
547
- return { month: params.month, totalEmployees: 0, readyEmployees: 0, missingEmployees: 0, employees: [], generatedAt: DEMO_NOW, ...FISCAL_STAMP };
688
+ return {
689
+ month: params.month,
690
+ employees: [],
691
+ summary: { total: 0, ready: 0, notReady: 0, reviewedThisMonth: 0 },
692
+ ...READ_STAMP,
693
+ };
548
694
  }
549
695
  // ---------------------------------------------------------------- Onboarding
550
696
  async getOnboardingStatus() {
@@ -555,14 +701,48 @@ export class DemoFrihetClient {
555
701
  }
556
702
  // ---------------------------------------------------------------- Permissions
557
703
  async getPermissionsMatrix() {
558
- return { roles: [{ role: "owner", permissions: ["*"] }], resources: ["invoices", "expenses", "clients"], generatedAt: DEMO_NOW, ...READ_STAMP };
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
+ };
559
713
  }
560
714
  async getMyPermissions() {
561
- return { userId: "demo_user_001", role: "owner", permissions: ["*"], workspaceId: "demo_ws_001", ...READ_STAMP };
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
+ };
562
734
  }
563
735
  // ---------------------------------------------------------------- Period close
564
736
  async getCurrentPeriod(params) {
565
- return { id: params?.periodId ?? "demo_period_2026_07", type: "monthly", status: "open", startDate: "2026-07-01", endDate: "2026-07-31", ...READ_STAMP };
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
+ };
566
746
  }
567
747
  async closePeriod(data) {
568
748
  return simulateWrite("demo_period", { type: data.type }, { status: "closed", closedAt: DEMO_NOW });
@@ -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>> & DemoStamp;
34
- /** Wrap a fixture array in a stamped PaginatedResponse. */
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 stamped PaginatedResponse for surfaces without dedicated fixtures. */
39
+ /** Empty PaginatedResponse for surfaces without dedicated fixtures. */
40
40
  export declare function demoEmptyPage(params?: {
41
41
  limit?: number;
42
42
  offset?: number;
@@ -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 stamped PaginatedResponse. */
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: params?.limit ?? 50,
45
- offset: params?.offset ?? 0,
46
- ...READ_STAMP,
46
+ limit,
47
+ offset,
47
48
  };
48
49
  }
49
- /** Empty stamped PaginatedResponse for surfaces without dedicated fixtures. */
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