@frihet/mcp-server 1.14.1 → 1.14.3

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/contract.test.d.ts +33 -0
  3. package/dist/__tests__/contract.test.d.ts.map +1 -0
  4. package/dist/__tests__/contract.test.js +208 -0
  5. package/dist/__tests__/contract.test.js.map +1 -0
  6. package/dist/__tests__/einvoice-day4-tools.test.js +33 -73
  7. package/dist/__tests__/einvoice-day4-tools.test.js.map +1 -1
  8. package/dist/__tests__/einvoice-tools.test.js +70 -107
  9. package/dist/__tests__/einvoice-tools.test.js.map +1 -1
  10. package/dist/__tests__/tool-exposure.test.js +4 -1
  11. package/dist/__tests__/tool-exposure.test.js.map +1 -1
  12. package/dist/client.d.ts +18 -0
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +42 -2
  15. package/dist/client.js.map +1 -1
  16. package/dist/index.js +19 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/observability.d.ts +21 -0
  19. package/dist/observability.d.ts.map +1 -1
  20. package/dist/observability.js +79 -4
  21. package/dist/observability.js.map +1 -1
  22. package/dist/tools/accountingClose.d.ts.map +1 -1
  23. package/dist/tools/accountingClose.js +17 -12
  24. package/dist/tools/accountingClose.js.map +1 -1
  25. package/dist/tools/audit_gl.d.ts.map +1 -1
  26. package/dist/tools/audit_gl.js +7 -6
  27. package/dist/tools/audit_gl.js.map +1 -1
  28. package/dist/tools/backend-availability.d.ts +73 -0
  29. package/dist/tools/backend-availability.d.ts.map +1 -0
  30. package/dist/tools/backend-availability.js +94 -0
  31. package/dist/tools/backend-availability.js.map +1 -0
  32. package/dist/tools/bank_rules.d.ts.map +1 -1
  33. package/dist/tools/bank_rules.js +5 -4
  34. package/dist/tools/bank_rules.js.map +1 -1
  35. package/dist/tools/einvoice.d.ts +25 -41
  36. package/dist/tools/einvoice.d.ts.map +1 -1
  37. package/dist/tools/einvoice.js +160 -259
  38. package/dist/tools/einvoice.js.map +1 -1
  39. package/dist/tools/fiscal.d.ts.map +1 -1
  40. package/dist/tools/fiscal.js +20 -17
  41. package/dist/tools/fiscal.js.map +1 -1
  42. package/dist/tools/gestoria.d.ts.map +1 -1
  43. package/dist/tools/gestoria.js +11 -10
  44. package/dist/tools/gestoria.js.map +1 -1
  45. package/dist/tools/hr.d.ts.map +1 -1
  46. package/dist/tools/hr.js +19 -18
  47. package/dist/tools/hr.js.map +1 -1
  48. package/dist/tools/igic.d.ts.map +1 -1
  49. package/dist/tools/igic.js +9 -8
  50. package/dist/tools/igic.js.map +1 -1
  51. package/dist/tools/impuesto_sociedades.d.ts.map +1 -1
  52. package/dist/tools/impuesto_sociedades.js +5 -4
  53. package/dist/tools/impuesto_sociedades.js.map +1 -1
  54. package/dist/tools/onboard_vies.d.ts.map +1 -1
  55. package/dist/tools/onboard_vies.js +5 -4
  56. package/dist/tools/onboard_vies.js.map +1 -1
  57. package/dist/tools/onboarding.d.ts.map +1 -1
  58. package/dist/tools/onboarding.js +5 -4
  59. package/dist/tools/onboarding.js.map +1 -1
  60. package/dist/tools/payroll.d.ts.map +1 -1
  61. package/dist/tools/payroll.js +5 -4
  62. package/dist/tools/payroll.js.map +1 -1
  63. package/dist/tools/permissions.d.ts.map +1 -1
  64. package/dist/tools/permissions.js +5 -4
  65. package/dist/tools/permissions.js.map +1 -1
  66. package/dist/tools/portal_domain.d.ts.map +1 -1
  67. package/dist/tools/portal_domain.js +7 -6
  68. package/dist/tools/portal_domain.js.map +1 -1
  69. package/dist/tools/shared.d.ts +53 -1
  70. package/dist/tools/shared.d.ts.map +1 -1
  71. package/dist/tools/shared.js +68 -2
  72. package/dist/tools/shared.js.map +1 -1
  73. package/package.json +2 -2
@@ -30,6 +30,43 @@
30
30
  import { z } from "zod/v4";
31
31
  import { withToolLogging, mutateContent, getContent, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, } from "./shared.js";
32
32
  /* ------------------------------------------------------------------ */
33
+ /* Honest "backend endpoint not yet available" result */
34
+ /* ------------------------------------------------------------------ */
35
+ /**
36
+ * Build an HONEST unavailable result for a tool whose backend endpoint is not
37
+ * yet deployed.
38
+ *
39
+ * NORTH #2: a tool that returns a fake-success stub on a backend 404 is a HOLE —
40
+ * an AI agent receives fabricated data (a queued workflow, a valid XML, a DATEV
41
+ * file URL) and reports success. SOUL §16: removing that drama is the whole
42
+ * point. So instead of fabricating a plausible-looking payload we return an
43
+ * explicit error result:
44
+ * - `isError: true` → the MCP client/agent treats it as a failure
45
+ * - `_unavailable: true` → machine-readable marker (also drives the
46
+ * observability downgrade in observability.ts)
47
+ * - `_plannedEndpoint` → which REST endpoint will back this when live
48
+ *
49
+ * The agent gets the truth ("not available yet"), never fabricated business data.
50
+ */
51
+ function unavailableResult(opts) {
52
+ const note = `${opts.what} is not yet available: the backend endpoint ` +
53
+ `${opts.plannedEndpoint} is not deployed. No data was produced — this is an ` +
54
+ `honest "unavailable" response, NOT a stub success. ` +
55
+ (opts.workaround ? opts.workaround + " " : "") +
56
+ `/ Esta operación aún no está disponible: el endpoint ${opts.plannedEndpoint} ` +
57
+ `no está desplegado. No se ha generado ningún dato.`;
58
+ return {
59
+ content: [{ type: "text", text: `Unavailable — ${opts.what}.\n${note}` }],
60
+ structuredContent: {
61
+ _unavailable: true,
62
+ _note: note,
63
+ _plannedEndpoint: opts.plannedEndpoint,
64
+ tool: opts.tool,
65
+ },
66
+ isError: true,
67
+ };
68
+ }
69
+ /* ------------------------------------------------------------------ */
33
70
  /* Shared format union */
34
71
  /* ------------------------------------------------------------------ */
35
72
  /**
@@ -96,38 +133,44 @@ export const exportDatevOutput = z.object({
96
133
  fiscalPeriod: z.string().describe("Fiscal period covered (e.g. '2026-01' or '2026-Q1')"),
97
134
  encoding: z.literal("cp1252").describe("File encoding — always CP1252 per DATEV EXTF spec"),
98
135
  }).passthrough();
99
- /* Day 4 Wave output schemas */
136
+ /* Day 4 Wave output schemas — fields match the LIVE CF response
137
+ * (Frihet-ERP functions/src/publicApi.ts) exactly. No fabricated fields. */
100
138
  export const einvoiceExportOutput = z.object({
101
- xmlUrl: z.string().describe("Signed URL to download the e-invoice XML (valid 24h)"),
102
- filename: z.string().describe("Suggested filename (e.g. INV-2026-001-facturae.xml)"),
103
- format: z.string().describe("E-invoice format used"),
104
- signed: z.boolean().describe("Whether XAdES signature was applied (Facturae only)"),
139
+ xml: z.string().describe("Raw e-invoice XML content (the document itself, returned inline by the CF)"),
140
+ contentType: z.string().describe("MIME type of the XML payload (application/xml)"),
141
+ filename: z.string().describe("Suggested filename (e.g. INV-2026-001_Facturae.xml)"),
142
+ signed: z.boolean().describe("Whether XAdES signature was applied (Facturae + signed=true only)"),
143
+ format: z.string().describe("E-invoice format used (echoes the requested format)"),
105
144
  }).passthrough();
106
145
  export const faceSubmitOutput = z.object({
107
- registroFACe: z.string().describe("FACe registration number (número de registro) returned by the portal"),
108
- status: z.enum(["submitted", "error"]).describe("Submission outcome"),
109
- submittedAt: z.string().describe("ISO 8601 timestamp of the submission"),
110
- mode: z.string().describe("Mode used: mock | sandbox | production"),
146
+ status: z.literal("submitted").describe("Submission accepted by FACe (the CF returns an error otherwise)"),
147
+ numeroRegistro: z.string().describe("FACe registration number (número de registro) returned by the portal"),
148
+ codigo: z.string().describe("FACe result code returned by the portal (resultado.codigo)"),
149
+ descripcion: z.string().describe("Human-readable FACe result description (resultado.descripcion)"),
150
+ idempotent: z.boolean().optional().describe("True if this echoes a prior already-registered submission (no resubmission occurred)"),
111
151
  }).passthrough();
112
152
  export const faceStatusOutput = z.object({
113
- registroFACe: z.string().describe("FACe registration number"),
114
- statusCode: z.string().describe("FACe status code (e.g. '1200'=Registrada, '1400'=Contabilizada, '3100'=Rechazada)"),
115
- statusDescription: z.string().describe("Human-readable FACe status description"),
116
- rejectionReason: z.string().optional().describe("Rejection reason if statusCode=3100"),
153
+ numeroRegistro: z.string().describe("FACe registration number for this submission"),
154
+ estadoTramitacion: z.string().nullable().describe("FACe processing-state code (estado de tramitación)"),
155
+ codigo: z.string().nullable().describe("FACe status code (e.g. '1200'=Registrada, '1400'=Contabilizada, '3100'=Rechazada)"),
156
+ descripcion: z.string().nullable().describe("Human-readable FACe status description"),
157
+ motivo: z.string().nullable().describe("Reason/motive associated with the current status (e.g. rejection motive)"),
117
158
  }).passthrough();
118
159
  export const ticketbaiSubmitOutput = z.object({
119
- tbaiId: z.string().describe("TicketBAI identifier (TBAI-XXXXX) returned by hacienda foral"),
120
- territory: z.enum(["bizkaia", "gipuzkoa", "araba"]).describe("Basque territory auto-detected from workspace address"),
121
- status: z.enum(["submitted", "accepted", "rejected", "error"]).describe("Submission status"),
122
- sandbox: z.boolean().describe("Whether this was a sandbox (test) submission"),
123
- qrUrl: z.string().optional().describe("QR code URL to print on the invoice (TBAI compliance)"),
160
+ accepted: z.boolean().describe("Whether the hacienda foral accepted the submission"),
161
+ csv: z.string().describe("CSV (Código Seguro de Verificación) returned by the hacienda foral"),
162
+ tbaiIdentifier: z.string().describe("TicketBAI identifier (TBAI-XXXXX) returned by the hacienda foral"),
163
+ qrUrl: z.string().describe("QR code URL to print on the invoice (TBAI compliance)"),
164
+ idempotent: z.boolean().optional().describe("True if this echoes a prior already-accepted submission (no resubmission occurred)"),
124
165
  }).passthrough();
125
166
  export const ticketbaiStatusOutput = z.object({
126
- tbaiId: z.string().describe("TicketBAI identifier"),
127
- territory: z.enum(["bizkaia", "gipuzkoa", "araba"]).describe("Basque territory"),
128
- status: z.enum(["submitted", "accepted", "rejected", "error"]).describe("Current hacienda acknowledgement status"),
129
- rejectionReason: z.string().optional().describe("Rejection reason from hacienda (if rejected)"),
130
- error: z.string().optional().describe("Internal error message (if error)"),
167
+ accepted: z.boolean().describe("Whether the submission has been accepted by the hacienda foral"),
168
+ csv: z.string().describe("CSV (Código Seguro de Verificación) for the submission"),
169
+ tbaiIdentifier: z.string().describe("TicketBAI identifier"),
170
+ estado: z.string().describe("Current submission status stored server-side (e.g. 'accepted', 'rejected')"),
171
+ qrUrl: z.string().nullable().describe("QR code URL to print on the invoice (null if not yet available)"),
172
+ submittedAt: z.string().nullable().describe("ISO 8601 timestamp the submission was recorded (null if unknown)"),
173
+ errors: z.array(z.string()).nullable().describe("Hacienda error messages if rejected (null if none)"),
131
174
  }).passthrough();
132
175
  export const kSeFSubmitOutput = z.object({
133
176
  _notImplemented: z.boolean().optional().describe("Always true — endpoint pending PR #417"),
@@ -166,7 +209,7 @@ export function registerEInvoiceTools(server, _client) {
166
209
  " • peppol — transmit via PEPPOL access point (peppol-bis-3 only)\n" +
167
210
  " • download — generate and return a signed download URL only\n" +
168
211
  "\n" +
169
- "NOTE: Stub response real CF endpoint https://api.frihet.io/v1/einvoice/send wired in transport Wave. " +
212
+ "If the dispatch backend is not deployed for this workspace, returns an honest 'unavailable' response — never fabricated workflow data. " +
170
213
  "/ Envia una factura electronica al destinatario mediante el canal de transporte seleccionado. " +
171
214
  "Devuelve de forma asincrona — consultar get_einvoice_status para seguimiento.",
172
215
  annotations: CREATE_ANNOTATIONS,
@@ -199,27 +242,15 @@ export function registerEInvoiceTools(server, _client) {
199
242
  }
200
243
  catch (err) {
201
244
  if (isNotFoundError(err)) {
202
- const stubResult = {
203
- workflowRunId: `wfr_stub_${Date.now()}`,
204
- status: "queued",
205
- estimatedCompletionSec: 15,
206
- _stub: true,
207
- _note: "CF endpoint pending deploy",
208
- _plannedEndpoint: plannedEndpoint,
209
- };
210
- return {
211
- content: [
212
- mutateContent(`E-invoice queued (stub — CF pending deploy):\n` +
213
- ` Invoice: ${invoiceId}\n` +
214
- ` Format: ${format}\n` +
215
- ` Dispatch: ${dispatchMode}\n` +
216
- ` WorkflowRunId: ${stubResult.workflowRunId}\n` +
217
- ` Status: queued\n` +
218
- ` Estimated: ~${stubResult.estimatedCompletionSec}s\n\n` +
219
- `Use get_einvoice_status with the workflowRunId to poll for completion.`),
220
- ],
221
- structuredContent: stubResult,
222
- };
245
+ // HONEST unavailable — the /v1/einvoice/send transport endpoint is
246
+ // genuinely absent (not deployed). Do NOT fabricate a queued workflow.
247
+ return unavailableResult({
248
+ tool: "send_einvoice",
249
+ plannedEndpoint,
250
+ what: "E-invoice dispatch",
251
+ workaround: "Use einvoice_export to produce the XML, then face_submit (Spain B2G) " +
252
+ "or ticketbai_submit (Basque Country) — those endpoints are live.",
253
+ });
223
254
  }
224
255
  throw err;
225
256
  }
@@ -231,7 +262,7 @@ export function registerEInvoiceTools(server, _client) {
231
262
  "Returns current step, ack ID (network confirmation), and download URLs once complete. " +
232
263
  "Poll every 5–10 seconds until status is 'succeeded', 'failed', or 'cancelled'. " +
233
264
  "\n\n" +
234
- "NOTE: Stub response real CF endpoint https://api.frihet.io/v1/einvoice/status wired in transport Wave. " +
265
+ "If the status backend is not deployed for this workspace, returns an honest 'unavailable' response — never a fabricated status. " +
235
266
  "/ Consulta el estado de un flujo de envio de factura electronica.",
236
267
  annotations: READ_ONLY_ANNOTATIONS,
237
268
  inputSchema: {
@@ -257,27 +288,13 @@ export function registerEInvoiceTools(server, _client) {
257
288
  }
258
289
  catch (err) {
259
290
  if (isNotFoundError(err)) {
260
- const stubResult = {
261
- status: "succeeded",
262
- step: "dispatch_complete",
263
- ackId: `ack_stub_${workflowRunId.slice(-8)}`,
264
- pdfA3Url: undefined,
265
- xmlUrl: `https://storage.frihet.io/stub/${workflowRunId}.xml`,
266
- _stub: true,
267
- _note: "CF endpoint pending deploy",
268
- _plannedEndpoint: plannedEndpoint,
269
- };
270
- return {
271
- content: [
272
- getContent(`E-invoice status (stub — CF pending deploy):\n` +
273
- ` WorkflowRunId: ${workflowRunId}\n` +
274
- ` Status: ${stubResult.status}\n` +
275
- ` Step: ${stubResult.step}\n` +
276
- ` AckId: ${stubResult.ackId}\n` +
277
- ` XML URL: ${stubResult.xmlUrl}`),
278
- ],
279
- structuredContent: stubResult,
280
- };
291
+ // HONEST unavailable — the /v1/einvoice/status endpoint is genuinely
292
+ // absent. Do NOT fabricate a "succeeded" status + fake XML URL.
293
+ return unavailableResult({
294
+ tool: "get_einvoice_status",
295
+ plannedEndpoint,
296
+ what: "E-invoice dispatch status polling",
297
+ });
281
298
  }
282
299
  throw err;
283
300
  }
@@ -292,7 +309,7 @@ export function registerEInvoiceTools(server, _client) {
292
309
  "Use before dispatch to catch errors early without incurring network transmission costs. " +
293
310
  "A valid=true response means the document passes all schema + business rule checks. " +
294
311
  "\n\n" +
295
- "NOTE: Stub response real CF endpoint https://api.frihet.io/v1/einvoice/validate wired in transport Wave. " +
312
+ "If the validation backend is not deployed for this workspace, returns an honest 'unavailable' response — never a fabricated valid=true. " +
296
313
  "/ Valida un documento XML de factura electronica contra el esquema y reglas schematron del formato especificado.",
297
314
  annotations: READ_ONLY_ANNOTATIONS,
298
315
  inputSchema: {
@@ -303,19 +320,6 @@ export function registerEInvoiceTools(server, _client) {
303
320
  outputSchema: validateEInvoiceOutput,
304
321
  }, async ({ xml, format }) => withToolLogging("validate_einvoice_xml", async () => {
305
322
  const plannedEndpoint = "/v1/einvoice/validate";
306
- const validatorMap = {
307
- "xrechnung-cii": "kosit",
308
- "xrechnung-ubl": "kosit",
309
- "facturx-en16931": "mustang",
310
- "facturx-extended": "mustang",
311
- "facturx-basic": "mustang",
312
- "facturx-minimum": "mustang",
313
- "fatturapa": "xsd",
314
- "ubl": "schematron",
315
- "cii": "schematron",
316
- "peppol-bis-3": "schematron",
317
- "facturae": "xsd",
318
- };
319
323
  try {
320
324
  const result = await _client.validateEInvoiceXml({ xml, format });
321
325
  return {
@@ -332,26 +336,15 @@ export function registerEInvoiceTools(server, _client) {
332
336
  }
333
337
  catch (err) {
334
338
  if (isNotFoundError(err)) {
335
- const stubResult = {
336
- valid: true,
337
- errors: [],
338
- validator: validatorMap[format],
339
- durationMs: 42,
340
- _stub: true,
341
- _note: "CF endpoint pending deploy",
342
- _plannedEndpoint: plannedEndpoint,
343
- };
344
- return {
345
- content: [
346
- getContent(`E-invoice validation result (stub — CF pending deploy):\n` +
347
- ` Format: ${format}\n` +
348
- ` Valid: ${stubResult.valid}\n` +
349
- ` Errors: ${stubResult.errors.length}\n` +
350
- ` Validator: ${stubResult.validator}\n` +
351
- ` Duration: ${stubResult.durationMs}ms`),
352
- ],
353
- structuredContent: stubResult,
354
- };
339
+ // HONEST unavailable — the /v1/einvoice/validate endpoint is
340
+ // genuinely absent. Do NOT fabricate valid=true (a fabricated
341
+ // "passes validation" is the most dangerous stub of all: an agent
342
+ // would dispatch an unvalidated invoice believing it is compliant).
343
+ return unavailableResult({
344
+ tool: "validate_einvoice_xml",
345
+ plannedEndpoint,
346
+ what: "E-invoice XML validation",
347
+ });
355
348
  }
356
349
  throw err;
357
350
  }
@@ -370,7 +363,7 @@ export function registerEInvoiceTools(server, _client) {
370
363
  "Output encoding is always CP1252 per DATEV EXTF specification. " +
371
364
  "Date range: both periodStart and periodEnd must be ISO 8601 dates (YYYY-MM-DD). " +
372
365
  "\n\n" +
373
- "NOTE: Stub response real CF endpoint https://api.frihet.io/v1/datev/export wired in transport Wave. " +
366
+ "If the DATEV export backend is not deployed for this workspace, returns an honest 'unavailable' response — never a fabricated file URL. " +
374
367
  "/ Exporta datos contables en formato DATEV EXTF para importacion en DATEV o sistemas compatibles.",
375
368
  annotations: READ_ONLY_ANNOTATIONS,
376
369
  inputSchema: {
@@ -384,12 +377,6 @@ export function registerEInvoiceTools(server, _client) {
384
377
  outputSchema: exportDatevOutput,
385
378
  }, async ({ periodStart, periodEnd, format }) => withToolLogging("export_datev", async () => {
386
379
  const plannedEndpoint = "/v1/einvoice/export-datev";
387
- const formatFileMap = {
388
- "extf-buchungsstapel": "EXTF_Buchungsstapel",
389
- "extf-debitoren": "EXTF_Debitoren",
390
- "extf-kreditoren": "EXTF_Kreditoren",
391
- };
392
- const periodLabel = periodStart.slice(0, 7); // YYYY-MM
393
380
  try {
394
381
  const result = await _client.exportDatev({ periodStart, periodEnd, format });
395
382
  return {
@@ -407,29 +394,15 @@ export function registerEInvoiceTools(server, _client) {
407
394
  }
408
395
  catch (err) {
409
396
  if (isNotFoundError(err)) {
410
- const filename = `${formatFileMap[format]}_${periodLabel}.csv`;
411
- const stubResult = {
412
- fileUrl: `https://storage.frihet.io/stub/datev/${filename}`,
413
- filename,
414
- rowCount: 0,
415
- fiscalPeriod: periodLabel,
416
- encoding: "cp1252",
417
- _stub: true,
418
- _note: "CF endpoint pending deploy",
419
- _plannedEndpoint: plannedEndpoint,
420
- };
421
- return {
422
- content: [
423
- getContent(`DATEV export ready (stub — CF pending deploy):\n` +
424
- ` Format: ${format}\n` +
425
- ` Period: ${periodStart} → ${periodEnd}\n` +
426
- ` Filename: ${stubResult.filename}\n` +
427
- ` Rows: ${stubResult.rowCount}\n` +
428
- ` Encoding: ${stubResult.encoding}\n` +
429
- ` Download URL: ${stubResult.fileUrl}`),
430
- ],
431
- structuredContent: stubResult,
432
- };
397
+ // HONEST unavailable — the /v1/einvoice/export-datev endpoint is
398
+ // genuinely absent. Do NOT fabricate a fileUrl pointing at a stub
399
+ // path with rowCount=0 (an agent would hand a broken/empty link to
400
+ // an accountant).
401
+ return unavailableResult({
402
+ tool: "export_datev",
403
+ plannedEndpoint,
404
+ what: "DATEV EXTF export",
405
+ });
433
406
  }
434
407
  throw err;
435
408
  }
@@ -473,26 +446,13 @@ export function registerEInvoiceTools(server, _client) {
473
446
  }
474
447
  catch (err) {
475
448
  if (isNotFoundError(err)) {
476
- const stubResult = {
477
- xmlUrl: `https://storage.frihet.io/stub/einvoice/${invoiceId}-${format}.xml`,
478
- filename: `${invoiceId}-${format}.xml`,
479
- format,
480
- signed: signed ?? false,
481
- _stub: true,
482
- _note: "CF endpoint pending deploy",
483
- _plannedEndpoint: plannedEndpoint,
484
- };
485
- return {
486
- content: [
487
- getContent(`E-invoice export ready (stub — CF pending deploy):\n` +
488
- ` Invoice: ${invoiceId}\n` +
489
- ` Format: ${format}\n` +
490
- ` Signed: ${stubResult.signed}\n` +
491
- ` Filename: ${stubResult.filename}\n` +
492
- ` Download URL: ${stubResult.xmlUrl}`),
493
- ],
494
- structuredContent: stubResult,
495
- };
449
+ // HONEST unavailable — the export CF is genuinely absent. Do NOT
450
+ // fabricate a download URL pointing at a non-existent stub file.
451
+ return unavailableResult({
452
+ tool: "einvoice_export",
453
+ plannedEndpoint,
454
+ what: "E-invoice XML export",
455
+ });
496
456
  }
497
457
  throw err;
498
458
  }
@@ -538,26 +498,15 @@ export function registerEInvoiceTools(server, _client) {
538
498
  }
539
499
  catch (err) {
540
500
  if (isNotFoundError(err)) {
541
- const stubResult = {
542
- registroFACe: `RCF_stub_${Date.now()}`,
543
- status: "submitted",
544
- submittedAt: new Date().toISOString(),
545
- mode: resolvedMode,
546
- _stub: true,
547
- _note: "CF endpoint pending deploy",
548
- _plannedEndpoint: plannedEndpoint,
549
- };
550
- return {
551
- content: [
552
- mutateContent(`FACe submission queued (stub — CF pending deploy):\n` +
553
- ` Invoice: ${invoiceId}\n` +
554
- ` Mode: ${resolvedMode}\n` +
555
- ` Registro: ${stubResult.registroFACe}\n` +
556
- ` Status: ${stubResult.status}\n\n` +
557
- `Use face_status with invoiceId to poll the portal for confirmation.`),
558
- ],
559
- structuredContent: stubResult,
560
- };
501
+ // HONEST unavailable — the FACe submit CF is genuinely absent. Do
502
+ // NOT fabricate a registro: a fake "submitted" B2G reference would
503
+ // make an agent report a filing that never happened.
504
+ return unavailableResult({
505
+ tool: "face_submit",
506
+ plannedEndpoint,
507
+ what: "FACe (Spain B2G) submission",
508
+ workaround: "No registro was created — do not treat this as an accepted submission.",
509
+ });
561
510
  }
562
511
  throw err;
563
512
  }
@@ -599,25 +548,13 @@ export function registerEInvoiceTools(server, _client) {
599
548
  }
600
549
  catch (err) {
601
550
  if (isNotFoundError(err)) {
602
- const stubResult = {
603
- registroFACe: `RCF_stub_${invoiceId.slice(-8)}`,
604
- statusCode: "1200",
605
- statusDescription: "Registrada",
606
- rejectionReason: undefined,
607
- _stub: true,
608
- _note: "CF endpoint pending deploy",
609
- _plannedEndpoint: plannedEndpoint,
610
- };
611
- return {
612
- content: [
613
- getContent(`FACe status (stub — CF pending deploy):\n` +
614
- ` Invoice: ${invoiceId}\n` +
615
- ` Registro: ${stubResult.registroFACe}\n` +
616
- ` Status code: ${stubResult.statusCode}\n` +
617
- ` Description: ${stubResult.statusDescription}`),
618
- ],
619
- structuredContent: stubResult,
620
- };
551
+ // HONEST unavailable — do NOT fabricate a "Registrada" (1200) state
552
+ // for a submission that was never made.
553
+ return unavailableResult({
554
+ tool: "face_status",
555
+ plannedEndpoint,
556
+ what: "FACe submission status",
557
+ });
621
558
  }
622
559
  throw err;
623
560
  }
@@ -662,28 +599,14 @@ export function registerEInvoiceTools(server, _client) {
662
599
  }
663
600
  catch (err) {
664
601
  if (isNotFoundError(err)) {
665
- const stubResult = {
666
- tbaiId: `TBAI-stub-${Date.now()}`,
667
- territory: "bizkaia",
668
- status: "submitted",
669
- sandbox: isSandbox,
670
- qrUrl: `https://batuz.eus/QRTBAI/?id=TBAI-stub`,
671
- _stub: true,
672
- _note: "CF endpoint pending deploy",
673
- _plannedEndpoint: plannedEndpoint,
674
- };
675
- return {
676
- content: [
677
- mutateContent(`TicketBAI submitted (stub — CF pending deploy):\n` +
678
- ` Invoice: ${invoiceId}\n` +
679
- ` Territory: ${stubResult.territory}\n` +
680
- ` TBAI ID: ${stubResult.tbaiId}\n` +
681
- ` Sandbox: ${isSandbox}\n` +
682
- ` QR URL: ${stubResult.qrUrl}\n\n` +
683
- `Use ticketbai_status with invoiceId to poll for hacienda acknowledgement.`),
684
- ],
685
- structuredContent: stubResult,
686
- };
602
+ // HONEST unavailable — do NOT fabricate a TBAI identifier + QR URL.
603
+ // A fake TicketBAI id printed on an invoice is a fiscal forgery.
604
+ return unavailableResult({
605
+ tool: "ticketbai_submit",
606
+ plannedEndpoint,
607
+ what: "TicketBAI (Basque Country) submission",
608
+ workaround: "No TBAI identifier was issued — do not print a QR or treat this as filed.",
609
+ });
687
610
  }
688
611
  throw err;
689
612
  }
@@ -726,26 +649,13 @@ export function registerEInvoiceTools(server, _client) {
726
649
  }
727
650
  catch (err) {
728
651
  if (isNotFoundError(err)) {
729
- const stubResult = {
730
- tbaiId: `TBAI-stub-${invoiceId.slice(-8)}`,
731
- territory: "bizkaia",
732
- status: "accepted",
733
- rejectionReason: undefined,
734
- error: undefined,
735
- _stub: true,
736
- _note: "CF endpoint pending deploy",
737
- _plannedEndpoint: plannedEndpoint,
738
- };
739
- return {
740
- content: [
741
- getContent(`TicketBAI status (stub — CF pending deploy):\n` +
742
- ` Invoice: ${invoiceId}\n` +
743
- ` TBAI ID: ${stubResult.tbaiId}\n` +
744
- ` Territory: ${stubResult.territory}\n` +
745
- ` Status: ${stubResult.status}`),
746
- ],
747
- structuredContent: stubResult,
748
- };
652
+ // HONEST unavailable — do NOT fabricate an "accepted" hacienda
653
+ // acknowledgement for a TicketBAI that was never submitted.
654
+ return unavailableResult({
655
+ tool: "ticketbai_status",
656
+ plannedEndpoint,
657
+ what: "TicketBAI submission status",
658
+ });
749
659
  }
750
660
  throw err;
751
661
  }
@@ -776,34 +686,25 @@ export function registerEInvoiceTools(server, _client) {
776
686
  },
777
687
  outputSchema: kSeFSubmitOutput,
778
688
  }, async ({ invoiceId, mode }) => withToolLogging("ksef_submit", async () => {
779
- // STUB: KSeF CF endpoint not yet deployed — PR #417 required.
780
- // When PR #417 merges (api.frihet.io/v1/invoices/:id/ksef/submit live),
781
- // replace this block with: await (_client as IFrihetClientWithDay4EInvoice).kSeFSubmit({ invoiceId, mode })
689
+ // HONEST unavailable: KSeF CF endpoint not yet deployed — PR #417 required.
690
+ // Returns isError:true + _unavailable so an AI agent treats this as a
691
+ // failure, NOT as a (successful) tool result it can act on. When PR #417
692
+ // merges (api.frihet.io/v1/invoices/:id/ksef/submit live), replace this
693
+ // block with: await (_client as IFrihetClientWithDay4EInvoice).kSeFSubmit({ invoiceId, mode })
782
694
  const resolvedMode = mode ?? "production";
783
- const stubResult = {
784
- _notImplemented: true,
785
- _note: "KSeF endpoint not yet deployed. " +
786
- "Depends on api.frihet.io/v1/invoices/:id/ksef/submit which lands in Frihet-ERP PR #417. " +
787
- "Call ksef_submit again after PR #417 merges to activate live KSeF submissions.",
788
- _plannedEndpoint: `/v1/invoices/${invoiceId}/ksef/submit`,
789
- invoiceId,
790
- mode: resolvedMode,
791
- };
792
- return {
793
- content: [
794
- {
795
- type: "text",
796
- text: `KSeF submission not yet available.\n` +
797
- ` Invoice: ${invoiceId}\n` +
798
- ` Mode: ${resolvedMode}\n\n` +
799
- `The KSeF Cloud Function is pending deployment (Frihet-ERP PR #417). ` +
800
- `This tool will activate automatically once PR #417 merges to main. ` +
801
- `In the meantime, use einvoice_export with format='ubl' + manual KSeF portal upload ` +
802
- `at ksef.mf.gov.pl as a workaround.`,
803
- },
804
- ],
805
- structuredContent: stubResult,
806
- };
695
+ const result = unavailableResult({
696
+ tool: "ksef_submit",
697
+ plannedEndpoint: `/v1/invoices/${invoiceId}/ksef/submit`,
698
+ what: "KSeF (Poland) submission",
699
+ workaround: "It will activate automatically once Frihet-ERP PR #417 merges. " +
700
+ "In the meantime, use einvoice_export with format='ubl' + manual KSeF portal " +
701
+ "upload at ksef.mf.gov.pl.",
702
+ });
703
+ // Preserve the forward-compat marker + echo the request for callers/tests.
704
+ result.structuredContent._notImplemented = true;
705
+ result.structuredContent.invoiceId = invoiceId;
706
+ result.structuredContent.mode = resolvedMode;
707
+ return result;
807
708
  }));
808
709
  }
809
710
  /* ------------------------------------------------------------------ */