@frihet/mcp-server 1.14.1 → 1.14.2

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 (70) 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-tools.test.js +70 -107
  7. package/dist/__tests__/einvoice-tools.test.js.map +1 -1
  8. package/dist/__tests__/tool-exposure.test.js +4 -1
  9. package/dist/__tests__/tool-exposure.test.js.map +1 -1
  10. package/dist/client.d.ts +18 -0
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +42 -2
  13. package/dist/client.js.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/observability.d.ts +21 -0
  16. package/dist/observability.d.ts.map +1 -1
  17. package/dist/observability.js +79 -4
  18. package/dist/observability.js.map +1 -1
  19. package/dist/tools/accountingClose.d.ts.map +1 -1
  20. package/dist/tools/accountingClose.js +17 -12
  21. package/dist/tools/accountingClose.js.map +1 -1
  22. package/dist/tools/audit_gl.d.ts.map +1 -1
  23. package/dist/tools/audit_gl.js +7 -6
  24. package/dist/tools/audit_gl.js.map +1 -1
  25. package/dist/tools/backend-availability.d.ts +73 -0
  26. package/dist/tools/backend-availability.d.ts.map +1 -0
  27. package/dist/tools/backend-availability.js +94 -0
  28. package/dist/tools/backend-availability.js.map +1 -0
  29. package/dist/tools/bank_rules.d.ts.map +1 -1
  30. package/dist/tools/bank_rules.js +5 -4
  31. package/dist/tools/bank_rules.js.map +1 -1
  32. package/dist/tools/einvoice.d.ts +25 -41
  33. package/dist/tools/einvoice.d.ts.map +1 -1
  34. package/dist/tools/einvoice.js +142 -154
  35. package/dist/tools/einvoice.js.map +1 -1
  36. package/dist/tools/fiscal.d.ts.map +1 -1
  37. package/dist/tools/fiscal.js +20 -17
  38. package/dist/tools/fiscal.js.map +1 -1
  39. package/dist/tools/gestoria.d.ts.map +1 -1
  40. package/dist/tools/gestoria.js +11 -10
  41. package/dist/tools/gestoria.js.map +1 -1
  42. package/dist/tools/hr.d.ts.map +1 -1
  43. package/dist/tools/hr.js +19 -18
  44. package/dist/tools/hr.js.map +1 -1
  45. package/dist/tools/igic.d.ts.map +1 -1
  46. package/dist/tools/igic.js +9 -8
  47. package/dist/tools/igic.js.map +1 -1
  48. package/dist/tools/impuesto_sociedades.d.ts.map +1 -1
  49. package/dist/tools/impuesto_sociedades.js +5 -4
  50. package/dist/tools/impuesto_sociedades.js.map +1 -1
  51. package/dist/tools/onboard_vies.d.ts.map +1 -1
  52. package/dist/tools/onboard_vies.js +5 -4
  53. package/dist/tools/onboard_vies.js.map +1 -1
  54. package/dist/tools/onboarding.d.ts.map +1 -1
  55. package/dist/tools/onboarding.js +5 -4
  56. package/dist/tools/onboarding.js.map +1 -1
  57. package/dist/tools/payroll.d.ts.map +1 -1
  58. package/dist/tools/payroll.js +5 -4
  59. package/dist/tools/payroll.js.map +1 -1
  60. package/dist/tools/permissions.d.ts.map +1 -1
  61. package/dist/tools/permissions.js +5 -4
  62. package/dist/tools/permissions.js.map +1 -1
  63. package/dist/tools/portal_domain.d.ts.map +1 -1
  64. package/dist/tools/portal_domain.js +7 -6
  65. package/dist/tools/portal_domain.js.map +1 -1
  66. package/dist/tools/shared.d.ts +53 -1
  67. package/dist/tools/shared.d.ts.map +1 -1
  68. package/dist/tools/shared.js +68 -2
  69. package/dist/tools/shared.js.map +1 -1
  70. package/package.json +2 -2
package/README.md CHANGED
@@ -39,7 +39,7 @@
39
39
  | **ChatGPT Apps** | Coming soon | [chatgpt.com](https://chatgpt.com) |
40
40
  | **Anthropic Claude Directory** | Coming soon | [claude.ai/settings/connectors](https://claude.ai/settings/connectors) |
41
41
 
42
- > **Tool count:** npm `latest` (1.14.1) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).
42
+ > **Tool count:** npm `latest` (1.14.2) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).
43
43
 
44
44
  ---
45
45
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Output-schema CONTRACT tests.
3
+ *
4
+ * WHY THIS FILE EXISTS (the bug class it locks down forever):
5
+ *
6
+ * The MCP tools declare Zod `outputSchema`s and put the raw Cloud Function (CF)
7
+ * response into `structuredContent`. The CF response has TWO shapes that have
8
+ * historically drifted from the schemas and broken tools silently:
9
+ *
10
+ * 1. The `{ data, meta }` ENVELOPE. Single-object reads (e.g. the shipped
11
+ * `/v1/fiscal/modelo/:code` CF) return `res.json({ data: <item>, meta })`;
12
+ * list reads return `{ data: [<item>...], total, limit, offset }`. The
13
+ * client unwraps single objects to `body.data` (`requestUnwrapped`) and
14
+ * keeps the paginated envelope (`requestPaginated`). If the schema and the
15
+ * unwrap disagree, the tool emits a structuredContent the model can't parse.
16
+ *
17
+ * 2. Firestore-TIMESTAMP-shaped fields. Firestore-direct reads can leak
18
+ * `{ _seconds, _nanoseconds }` objects on date-ish fields instead of ISO
19
+ * strings. The enumerated date fields are typed `z.string()`, so a raw
20
+ * timestamp on `createdAt` would FAIL — but `.passthrough()` carries extra
21
+ * Firestore-internal timestamp fields (`_syncedAt`, `lastTouchedAt`, …)
22
+ * through untouched. This test pins BOTH facts: enumerated dates stay ISO
23
+ * strings, and extra `{_seconds}` fields pass via `.passthrough()`.
24
+ *
25
+ * If a future refactor tightens a schema (drops `.passthrough()`, or retypes a
26
+ * passthrough timestamp field as `z.string()`), or the CF starts returning a
27
+ * shape the schema rejects, THIS test goes red in CI — instead of the bug
28
+ * reaching an LLM as an unparseable tool result.
29
+ *
30
+ * Run: npm test (after build) — node:test + node:assert.
31
+ */
32
+ export {};
33
+ //# sourceMappingURL=contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/contract.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG"}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Output-schema CONTRACT tests.
3
+ *
4
+ * WHY THIS FILE EXISTS (the bug class it locks down forever):
5
+ *
6
+ * The MCP tools declare Zod `outputSchema`s and put the raw Cloud Function (CF)
7
+ * response into `structuredContent`. The CF response has TWO shapes that have
8
+ * historically drifted from the schemas and broken tools silently:
9
+ *
10
+ * 1. The `{ data, meta }` ENVELOPE. Single-object reads (e.g. the shipped
11
+ * `/v1/fiscal/modelo/:code` CF) return `res.json({ data: <item>, meta })`;
12
+ * list reads return `{ data: [<item>...], total, limit, offset }`. The
13
+ * client unwraps single objects to `body.data` (`requestUnwrapped`) and
14
+ * keeps the paginated envelope (`requestPaginated`). If the schema and the
15
+ * unwrap disagree, the tool emits a structuredContent the model can't parse.
16
+ *
17
+ * 2. Firestore-TIMESTAMP-shaped fields. Firestore-direct reads can leak
18
+ * `{ _seconds, _nanoseconds }` objects on date-ish fields instead of ISO
19
+ * strings. The enumerated date fields are typed `z.string()`, so a raw
20
+ * timestamp on `createdAt` would FAIL — but `.passthrough()` carries extra
21
+ * Firestore-internal timestamp fields (`_syncedAt`, `lastTouchedAt`, …)
22
+ * through untouched. This test pins BOTH facts: enumerated dates stay ISO
23
+ * strings, and extra `{_seconds}` fields pass via `.passthrough()`.
24
+ *
25
+ * If a future refactor tightens a schema (drops `.passthrough()`, or retypes a
26
+ * passthrough timestamp field as `z.string()`), or the CF starts returning a
27
+ * shape the schema rejects, THIS test goes red in CI — instead of the bug
28
+ * reaching an LLM as an unparseable tool result.
29
+ *
30
+ * Run: npm test (after build) — node:test + node:assert.
31
+ */
32
+ import { test, describe } from "node:test";
33
+ import assert from "node:assert/strict";
34
+ import { fiscalModeloSummaryOutput, verifactuStatusOutput, paginatedOutput, invoiceItemOutput, expenseItemOutput, clientItemOutput, } from "../tools/shared.js";
35
+ // ── Fixtures: representative CF responses ────────────────────────────────────
36
+ /** A Firestore Timestamp as it appears in a JSON-serialized CF response. */
37
+ const FIRESTORE_TS = { _seconds: 1_771_200_000, _nanoseconds: 0 };
38
+ /**
39
+ * The fiscal-summary CF body AFTER `requestUnwrapped` strips the `{ data, meta }`
40
+ * envelope — i.e. exactly what the tool receives. Includes the `model` (not
41
+ * `modeloCode`) shape the live CF emits, the `modelo303` totals block, a
42
+ * `summary` counts block, and a passthrough Firestore-timestamp field
43
+ * (`computedAt`) that is NOT in the schema's enumerated fields.
44
+ */
45
+ const FISCAL_303_UNWRAPPED = {
46
+ model: "303",
47
+ period: "2026-Q1",
48
+ months: ["2026-01", "2026-02", "2026-03"],
49
+ modelo303: {
50
+ baseImponible: 10_000,
51
+ cuotaRepercutida: 2_100,
52
+ baseDeducible: 4_000,
53
+ cuotaDeducible: 840,
54
+ resultado: 1_260,
55
+ },
56
+ summary: { invoiceCount: 12, expenseCount: 7 },
57
+ readonly: true,
58
+ note: "Informational summary — never filed to AEAT.",
59
+ // Firestore-internal timestamp leaking through .passthrough() — must validate.
60
+ computedAt: FIRESTORE_TS,
61
+ };
62
+ /** The raw `{ data, meta }` envelope as the CF emits it pre-unwrap. */
63
+ const FISCAL_303_ENVELOPE = {
64
+ data: FISCAL_303_UNWRAPPED,
65
+ meta: { generatedAt: FIRESTORE_TS, source: "cf:getFiscalModeloSummary" },
66
+ };
67
+ /** Mirror of `requestUnwrapped`: pull `.data` out of a single-object envelope. */
68
+ function unwrap(body) {
69
+ if (body !== null && typeof body === "object" && !Array.isArray(body) && "data" in body) {
70
+ const inner = body.data;
71
+ if (inner !== null && typeof inner === "object" && !Array.isArray(inner)) {
72
+ return inner;
73
+ }
74
+ }
75
+ return body;
76
+ }
77
+ const VERIFACTU_STATUS = {
78
+ invoiceId: "inv_2026_001",
79
+ lastSubmissionAt: "2026-04-15T10:30:00.000Z", // enumerated date field stays ISO
80
+ hash: "a1b2c3",
81
+ status: "success",
82
+ aeatResponse: "Aceptado",
83
+ qrUrl: "https://www2.agenciatributaria.gob.es/qr/...",
84
+ // Firestore-internal timestamp via passthrough — must validate.
85
+ _chainTouchedAt: FIRESTORE_TS,
86
+ };
87
+ /** A paginated list envelope as `requestPaginated` keeps it: data[] + counts. */
88
+ const INVOICE_LIST_ENVELOPE = {
89
+ data: [
90
+ {
91
+ id: "inv_1",
92
+ clientName: "ACME SL",
93
+ items: [{ description: "Consulting", quantity: 1, unitPrice: 1000 }],
94
+ issueDate: "2026-03-01",
95
+ dueDate: "2026-03-31",
96
+ status: "sent",
97
+ total: 1210,
98
+ createdAt: "2026-03-01T09:00:00.000Z", // enumerated date → ISO string
99
+ // Firestore-internal timestamp via passthrough.
100
+ _firestoreUpdatedAt: FIRESTORE_TS,
101
+ },
102
+ ],
103
+ total: 1,
104
+ limit: 20,
105
+ offset: 0,
106
+ nextCursor: "inv_1",
107
+ };
108
+ const EXPENSE_LIST_ENVELOPE = {
109
+ data: [
110
+ {
111
+ id: "exp_1",
112
+ description: "AWS",
113
+ amount: 42.5,
114
+ category: "software",
115
+ date: "2026-03-02",
116
+ taxDeductible: true,
117
+ createdAt: "2026-03-02T00:00:00.000Z",
118
+ _syncedAt: FIRESTORE_TS,
119
+ },
120
+ ],
121
+ total: 1,
122
+ limit: 20,
123
+ offset: 0,
124
+ };
125
+ const CLIENT_LIST_ENVELOPE = {
126
+ data: [
127
+ {
128
+ id: "cli_1",
129
+ name: "ACME SL",
130
+ email: "facturas@acme.es",
131
+ taxId: "B12345678",
132
+ address: { city: "Madrid", country: "ES" },
133
+ createdAt: "2026-01-10T00:00:00.000Z",
134
+ _importedAt: FIRESTORE_TS,
135
+ },
136
+ ],
137
+ total: 1,
138
+ limit: 20,
139
+ offset: 0,
140
+ };
141
+ // ── Tests ────────────────────────────────────────────────────────────────────
142
+ describe("output-schema contract — fiscal single-object reads", () => {
143
+ test("fiscalModeloSummaryOutput accepts the unwrapped 303 CF body", () => {
144
+ const unwrapped = unwrap(FISCAL_303_ENVELOPE);
145
+ const parsed = fiscalModeloSummaryOutput.safeParse(unwrapped);
146
+ assert.equal(parsed.success, true, parsed.success ? "" : JSON.stringify(parsed.error.issues, null, 2));
147
+ });
148
+ test("unwrap() strips the { data, meta } envelope (matches client.requestUnwrapped)", () => {
149
+ const unwrapped = unwrap(FISCAL_303_ENVELOPE);
150
+ assert.equal(unwrapped.model, "303");
151
+ assert.equal("meta" in unwrapped, false, "meta must NOT leak into the unwrapped item");
152
+ });
153
+ test("a Firestore-Timestamp {_seconds} field survives via .passthrough()", () => {
154
+ const parsed = fiscalModeloSummaryOutput.safeParse(unwrap(FISCAL_303_ENVELOPE));
155
+ assert.equal(parsed.success, true);
156
+ if (parsed.success) {
157
+ const ts = parsed.data.computedAt;
158
+ assert.equal(ts?._seconds, FIRESTORE_TS._seconds, "timestamp field must pass through intact");
159
+ }
160
+ });
161
+ test("verifactuStatusOutput accepts the status CF body incl. passthrough timestamp", () => {
162
+ const parsed = verifactuStatusOutput.safeParse(VERIFACTU_STATUS);
163
+ assert.equal(parsed.success, true, parsed.success ? "" : JSON.stringify(parsed.error.issues, null, 2));
164
+ });
165
+ test("enumerated date fields MUST stay strings — a raw {_seconds} there is rejected", () => {
166
+ // This pins the contract: the CF must emit ISO strings for enumerated dates.
167
+ // If it ever leaks a raw timestamp onto `lastSubmissionAt`, the tool would
168
+ // emit garbage — so we assert the schema (correctly) refuses it.
169
+ const bad = { ...VERIFACTU_STATUS, lastSubmissionAt: FIRESTORE_TS };
170
+ const parsed = verifactuStatusOutput.safeParse(bad);
171
+ assert.equal(parsed.success, false, "raw timestamp on an enumerated z.string() date must fail");
172
+ });
173
+ });
174
+ describe("output-schema contract — core list reads ({ data, ... } envelope)", () => {
175
+ test("invoices list envelope validates against paginatedOutput(invoiceItemOutput)", () => {
176
+ const schema = paginatedOutput(invoiceItemOutput);
177
+ const parsed = schema.safeParse(INVOICE_LIST_ENVELOPE);
178
+ assert.equal(parsed.success, true, parsed.success ? "" : JSON.stringify(parsed.error.issues, null, 2));
179
+ });
180
+ test("a list item carrying a Firestore Timestamp passthrough field validates", () => {
181
+ const schema = paginatedOutput(invoiceItemOutput);
182
+ const parsed = schema.safeParse(INVOICE_LIST_ENVELOPE);
183
+ assert.equal(parsed.success, true);
184
+ if (parsed.success) {
185
+ const item = parsed.data.data[0];
186
+ const ts = item._firestoreUpdatedAt;
187
+ assert.equal(ts?._seconds, FIRESTORE_TS._seconds);
188
+ }
189
+ });
190
+ test("expenses list envelope validates against paginatedOutput(expenseItemOutput)", () => {
191
+ const schema = paginatedOutput(expenseItemOutput);
192
+ const parsed = schema.safeParse(EXPENSE_LIST_ENVELOPE);
193
+ assert.equal(parsed.success, true, parsed.success ? "" : JSON.stringify(parsed.error.issues, null, 2));
194
+ });
195
+ test("clients list envelope validates against paginatedOutput(clientItemOutput)", () => {
196
+ const schema = paginatedOutput(clientItemOutput);
197
+ const parsed = schema.safeParse(CLIENT_LIST_ENVELOPE);
198
+ assert.equal(parsed.success, true, parsed.success ? "" : JSON.stringify(parsed.error.issues, null, 2));
199
+ });
200
+ test("paginatedOutput requires the envelope counts (data/total/limit/offset)", () => {
201
+ const schema = paginatedOutput(invoiceItemOutput);
202
+ // Missing `total`/`limit`/`offset` (a bare array-ish body) must fail — this
203
+ // is the drift that breaks pagination if the CF stops emitting counts.
204
+ const parsed = schema.safeParse({ data: INVOICE_LIST_ENVELOPE.data });
205
+ assert.equal(parsed.success, false, "envelope without counts must be rejected");
206
+ });
207
+ });
208
+ //# sourceMappingURL=contract.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.test.js","sourceRoot":"","sources":["../../src/__tests__/contract.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAExC,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,gFAAgF;AAEhF,4EAA4E;AAC5E,MAAM,YAAY,GAAG,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,EAAW,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,oBAAoB,GAAG;IAC3B,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC;IACzC,SAAS,EAAE;QACT,aAAa,EAAE,MAAM;QACrB,gBAAgB,EAAE,KAAK;QACvB,aAAa,EAAE,KAAK;QACpB,cAAc,EAAE,GAAG;QACnB,SAAS,EAAE,KAAK;KACjB;IACD,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE;IAC9C,QAAQ,EAAE,IAAa;IACvB,IAAI,EAAE,8CAA8C;IACpD,+EAA+E;IAC/E,UAAU,EAAE,YAAY;CACzB,CAAC;AAEF,uEAAuE;AACvE,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,oBAAoB;IAC1B,IAAI,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,2BAA2B,EAAE;CACzE,CAAC;AAEF,kFAAkF;AAClF,SAAS,MAAM,CAAC,IAAa;IAC3B,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACxF,MAAM,KAAK,GAAI,IAA0B,CAAC,IAAI,CAAC;QAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,gBAAgB,GAAG;IACvB,SAAS,EAAE,cAAc;IACzB,gBAAgB,EAAE,0BAA0B,EAAE,kCAAkC;IAChF,IAAI,EAAE,QAAQ;IACd,MAAM,EAAE,SAAkB;IAC1B,YAAY,EAAE,UAAU;IACxB,KAAK,EAAE,8CAA8C;IACrD,gEAAgE;IAChE,eAAe,EAAE,YAAY;CAC9B,CAAC;AAEF,iFAAiF;AACjF,MAAM,qBAAqB,GAAG;IAC5B,IAAI,EAAE;QACJ;YACE,EAAE,EAAE,OAAO;YACX,UAAU,EAAE,SAAS;YACrB,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;YACpE,SAAS,EAAE,YAAY;YACvB,OAAO,EAAE,YAAY;YACrB,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,IAAI;YACX,SAAS,EAAE,0BAA0B,EAAE,+BAA+B;YACtE,gDAAgD;YAChD,mBAAmB,EAAE,YAAY;SAClC;KACF;IACD,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,CAAC;IACT,UAAU,EAAE,OAAO;CACpB,CAAC;AAEF,MAAM,qBAAqB,GAAG;IAC5B,IAAI,EAAE;QACJ;YACE,EAAE,EAAE,OAAO;YACX,WAAW,EAAE,KAAK;YAClB,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,YAAY;YAClB,aAAa,EAAE,IAAI;YACnB,SAAS,EAAE,0BAA0B;YACrC,SAAS,EAAE,YAAY;SACxB;KACF;IACD,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,CAAC;CACV,CAAC;AAEF,MAAM,oBAAoB,GAAG;IAC3B,IAAI,EAAE;QACJ;YACE,EAAE,EAAE,OAAO;YACX,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,kBAAkB;YACzB,KAAK,EAAE,WAAW;YAClB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE;YAC1C,SAAS,EAAE,0BAA0B;YACrC,WAAW,EAAE,YAAY;SAC1B;KACF;IACD,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,CAAC;CACV,CAAC;AAEF,gFAAgF;AAEhF,QAAQ,CAAC,qDAAqD,EAAE,GAAG,EAAE;IACnE,IAAI,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACvE,MAAM,SAAS,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAC9D,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACzF,MAAM,SAAS,GAAG,MAAM,CAAC,mBAAmB,CAA4B,CAAC;QACzE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,SAAS,EAAE,KAAK,EAAE,4CAA4C,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC9E,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,EAAE,GAAI,MAAM,CAAC,IAAgC,CAAC,UAAmC,CAAC;YACxF,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE,0CAA0C,CAAC,CAAC;QAChG,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8EAA8E,EAAE,GAAG,EAAE;QACxF,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QACjE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACzF,6EAA6E;QAC7E,2EAA2E;QAC3E,iEAAiE;QACjE,MAAM,GAAG,GAAG,EAAE,GAAG,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC;QACpE,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACpD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,0DAA0D,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mEAAmE,EAAE,GAAG,EAAE;IACjF,IAAI,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACvF,MAAM,MAAM,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wEAAwE,EAAE,GAAG,EAAE;QAClF,MAAM,MAAM,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAA4B,CAAC;YAC5D,MAAM,EAAE,GAAG,IAAI,CAAC,mBAA4C,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACvF,MAAM,MAAM,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACrF,MAAM,MAAM,GAAG,eAAe,CAAC,gBAAgB,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wEAAwE,EAAE,GAAG,EAAE;QAClF,MAAM,MAAM,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;QAClD,4EAA4E;QAC5E,uEAAuE;QACvE,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,qBAAqB,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,0CAA0C,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -119,7 +119,7 @@ describe("E-Invoice Tools — registerAllTools includes new tools (127→133)",
119
119
  assert.equal(server.tools.size, 10);
120
120
  });
121
121
  });
122
- describe("send_einvoice — stub response shape", () => {
122
+ describe("send_einvoice — honest unavailable response (absent endpoint)", () => {
123
123
  let sendTool;
124
124
  beforeEach(async () => {
125
125
  const server = new StubMcpServer();
@@ -132,7 +132,7 @@ describe("send_einvoice — stub response shape", () => {
132
132
  assert.ok(sendTool, "send_einvoice not registered");
133
133
  assert.equal(sendTool.config.title, "Send E-Invoice");
134
134
  });
135
- test("happy path valid input returns queued status", async () => {
135
+ test("absent endpoint isError + _unavailable, NEVER a fabricated queued status", async () => {
136
136
  assert.ok(sendTool, "send_einvoice not registered");
137
137
  const result = await sendTool.handler({
138
138
  invoiceId: "inv_test_123",
@@ -141,19 +141,23 @@ describe("send_einvoice — stub response shape", () => {
141
141
  });
142
142
  assert.ok(result.structuredContent, "structuredContent missing");
143
143
  const sc = result.structuredContent;
144
- assert.equal(sc["status"], "queued", "status should be 'queued'");
145
- assert.ok(typeof sc["workflowRunId"] === "string", "workflowRunId should be string");
146
- assert.ok(typeof sc["estimatedCompletionSec"] === "number", "estimatedCompletionSec should be number");
147
- assert.ok(sc["_stub"] === true, "_stub flag should be true");
144
+ // HONEST: no fabricated workflow/status, agent sees a failure.
145
+ assert.equal(result.isError, true, "must be isError:true");
146
+ assert.equal(sc["_unavailable"], true, "_unavailable flag should be true");
147
+ assert.equal(sc["_stub"], undefined, "must NOT carry a fake-success _stub flag");
148
+ assert.equal(sc["workflowRunId"], undefined, "must NOT fabricate a workflowRunId");
149
+ assert.equal(sc["status"], undefined, "must NOT fabricate a queued status");
150
+ assert.ok(typeof sc["_plannedEndpoint"] === "string", "_plannedEndpoint should be set");
148
151
  });
149
- test("happy path — peppol-bis-3 format accepted", async () => {
152
+ test("peppol-bis-3 also returns honest unavailable", async () => {
150
153
  assert.ok(sendTool, "send_einvoice not registered");
151
154
  const result = await sendTool.handler({
152
155
  invoiceId: "inv_peppol_456",
153
156
  format: "peppol-bis-3",
154
157
  dispatchMode: "peppol",
155
158
  });
156
- assert.equal(result.structuredContent["status"], "queued");
159
+ assert.equal(result.isError, true);
160
+ assert.equal(result.structuredContent["_unavailable"], true);
157
161
  });
158
162
  test("content block has type text", async () => {
159
163
  assert.ok(sendTool, "send_einvoice not registered");
@@ -165,10 +169,10 @@ describe("send_einvoice — stub response shape", () => {
165
169
  assert.ok(Array.isArray(result.content), "content should be array");
166
170
  assert.ok(result.content.length > 0, "content should have at least 1 block");
167
171
  assert.equal(result.content[0].type, "text");
168
- assert.ok(typeof result.content[0].text === "string");
172
+ assert.ok(/unavailable/i.test(result.content[0].text), "text should say unavailable");
169
173
  });
170
174
  });
171
- describe("get_einvoice_status — stub response shape", () => {
175
+ describe("get_einvoice_status — honest unavailable response (absent endpoint)", () => {
172
176
  let statusTool;
173
177
  beforeEach(async () => {
174
178
  const server = new StubMcpServer();
@@ -177,22 +181,19 @@ describe("get_einvoice_status — stub response shape", () => {
177
181
  registerEInvoiceTools(server, clientStub);
178
182
  statusTool = server.tools.get("get_einvoice_status");
179
183
  });
180
- test("happy path returns status shape", async () => {
184
+ test("absent endpoint isError + _unavailable, NEVER a fabricated 'succeeded'", async () => {
181
185
  assert.ok(statusTool, "get_einvoice_status not registered");
182
186
  const result = await statusTool.handler({ workflowRunId: "wfr_stub_abc123" });
183
187
  const sc = result.structuredContent;
184
- assert.ok(["queued", "running", "succeeded", "failed", "cancelled"].includes(sc["status"]), `status '${sc["status"]}' not in allowed values`);
185
- assert.ok(typeof sc["step"] === "string", "step should be string");
186
- assert.ok(sc["_stub"] === true, "_stub flag should be true");
187
- });
188
- test("ackId present in stub response", async () => {
189
- assert.ok(statusTool, "get_einvoice_status not registered");
190
- const result = await statusTool.handler({ workflowRunId: "wfr_stub_deadbeef" });
191
- const sc = result.structuredContent;
192
- assert.ok(typeof sc["ackId"] === "string", "ackId should be present as string");
188
+ assert.equal(result.isError, true, "must be isError:true");
189
+ assert.equal(sc["_unavailable"], true, "_unavailable flag should be true");
190
+ assert.equal(sc["status"], undefined, "must NOT fabricate a 'succeeded' status");
191
+ assert.equal(sc["ackId"], undefined, "must NOT fabricate an ackId");
192
+ assert.equal(sc["xmlUrl"], undefined, "must NOT fabricate an XML URL");
193
+ assert.equal(sc["_stub"], undefined, "must NOT carry a fake-success _stub flag");
193
194
  });
194
195
  });
195
- describe("validate_einvoice_xml — stub response shape", () => {
196
+ describe("validate_einvoice_xml — honest unavailable response (absent endpoint)", () => {
196
197
  let validateTool;
197
198
  beforeEach(async () => {
198
199
  const server = new StubMcpServer();
@@ -201,45 +202,23 @@ describe("validate_einvoice_xml — stub response shape", () => {
201
202
  registerEInvoiceTools(server, clientStub);
202
203
  validateTool = server.tools.get("validate_einvoice_xml");
203
204
  });
204
- test("happy path returns valid=true with empty errors", async () => {
205
+ test("absent endpoint isError + _unavailable, NEVER a fabricated valid=true", async () => {
205
206
  assert.ok(validateTool, "validate_einvoice_xml not registered");
206
207
  const result = await validateTool.handler({
207
208
  xml: "<Invoice><ID>TEST-001</ID></Invoice>",
208
209
  format: "xrechnung-cii",
209
210
  });
210
211
  const sc = result.structuredContent;
211
- assert.equal(sc["valid"], true, "valid should be true (stub)");
212
- assert.ok(Array.isArray(sc["errors"]), "errors should be array");
213
- assert.equal(sc["errors"].length, 0, "errors should be empty (stub)");
214
- assert.equal(sc["validator"], "kosit", "xrechnung-cii should use kosit validator");
215
- assert.ok(typeof sc["durationMs"] === "number", "durationMs should be number");
216
- });
217
- test("facturx-en16931 uses mustang validator", async () => {
218
- assert.ok(validateTool, "validate_einvoice_xml not registered");
219
- const result = await validateTool.handler({
220
- xml: "<rsm:CrossIndustryInvoice/>",
221
- format: "facturx-en16931",
222
- });
223
- assert.equal(result.structuredContent["validator"], "mustang");
224
- });
225
- test("fatturapa uses xsd validator", async () => {
226
- assert.ok(validateTool, "validate_einvoice_xml not registered");
227
- const result = await validateTool.handler({
228
- xml: "<FatturaElettronica/>",
229
- format: "fatturapa",
230
- });
231
- assert.equal(result.structuredContent["validator"], "xsd");
232
- });
233
- test("peppol-bis-3 uses schematron validator", async () => {
234
- assert.ok(validateTool, "validate_einvoice_xml not registered");
235
- const result = await validateTool.handler({
236
- xml: "<Invoice/>",
237
- format: "peppol-bis-3",
238
- });
239
- assert.equal(result.structuredContent["validator"], "schematron");
212
+ assert.equal(result.isError, true, "must be isError:true");
213
+ assert.equal(sc["_unavailable"], true, "_unavailable flag should be true");
214
+ // A fabricated valid=true is the most dangerous stub — must never appear.
215
+ assert.equal(sc["valid"], undefined, "must NOT fabricate valid=true");
216
+ assert.equal(sc["validator"], undefined, "must NOT fabricate a validator verdict");
217
+ assert.equal(sc["_stub"], undefined, "must NOT carry a fake-success _stub flag");
218
+ assert.ok(typeof sc["_plannedEndpoint"] === "string", "_plannedEndpoint should be set");
240
219
  });
241
220
  });
242
- describe("export_datev — stub response shape", () => {
221
+ describe("export_datev — honest unavailable response (absent endpoint)", () => {
243
222
  let datevTool;
244
223
  beforeEach(async () => {
245
224
  const server = new StubMcpServer();
@@ -248,7 +227,7 @@ describe("export_datev — stub response shape", () => {
248
227
  registerEInvoiceTools(server, clientStub);
249
228
  datevTool = server.tools.get("export_datev");
250
229
  });
251
- test("happy path extf-buchungsstapel returns correct shape", async () => {
230
+ test("absent endpoint isError + _unavailable, NEVER a fabricated fileUrl", async () => {
252
231
  assert.ok(datevTool, "export_datev not registered");
253
232
  const result = await datevTool.handler({
254
233
  periodStart: "2026-01-01",
@@ -256,39 +235,12 @@ describe("export_datev — stub response shape", () => {
256
235
  format: "extf-buchungsstapel",
257
236
  });
258
237
  const sc = result.structuredContent;
259
- assert.ok(typeof sc["fileUrl"] === "string", "fileUrl should be string");
260
- assert.ok(typeof sc["filename"] === "string", "filename should be string");
261
- assert.ok(typeof sc["rowCount"] === "number", "rowCount should be number");
262
- assert.ok(typeof sc["fiscalPeriod"] === "string", "fiscalPeriod should be string");
263
- assert.equal(sc["encoding"], "cp1252", "encoding must always be cp1252");
264
- assert.ok(sc["_stub"] === true, "_stub flag should be true");
265
- });
266
- test("filename contains EXTF_Buchungsstapel for buchungsstapel format", async () => {
267
- assert.ok(datevTool, "export_datev not registered");
268
- const result = await datevTool.handler({
269
- periodStart: "2026-01-01",
270
- periodEnd: "2026-01-31",
271
- format: "extf-buchungsstapel",
272
- });
273
- assert.ok(result.structuredContent["filename"].includes("EXTF_Buchungsstapel"), "filename should include EXTF_Buchungsstapel");
274
- });
275
- test("filename contains EXTF_Debitoren for extf-debitoren", async () => {
276
- assert.ok(datevTool, "export_datev not registered");
277
- const result = await datevTool.handler({
278
- periodStart: "2026-03-01",
279
- periodEnd: "2026-03-31",
280
- format: "extf-debitoren",
281
- });
282
- assert.ok(result.structuredContent["filename"].includes("EXTF_Debitoren"), "filename should include EXTF_Debitoren");
283
- });
284
- test("fiscalPeriod derives from periodStart YYYY-MM", async () => {
285
- assert.ok(datevTool, "export_datev not registered");
286
- const result = await datevTool.handler({
287
- periodStart: "2026-07-01",
288
- periodEnd: "2026-07-31",
289
- format: "extf-kreditoren",
290
- });
291
- assert.equal(result.structuredContent["fiscalPeriod"], "2026-07");
238
+ assert.equal(result.isError, true, "must be isError:true");
239
+ assert.equal(sc["_unavailable"], true, "_unavailable flag should be true");
240
+ assert.equal(sc["fileUrl"], undefined, "must NOT fabricate a download fileUrl");
241
+ assert.equal(sc["filename"], undefined, "must NOT fabricate a filename");
242
+ assert.equal(sc["_stub"], undefined, "must NOT carry a fake-success _stub flag");
243
+ assert.ok(typeof sc["_plannedEndpoint"] === "string", "_plannedEndpoint should be set");
292
244
  });
293
245
  });
294
246
  describe("Langfuse wrapper — patchServerWithTracing wraps all tool callbacks", () => {
@@ -347,54 +299,65 @@ function getValidArgs(toolName) {
347
299
  }
348
300
  }
349
301
  // ── 404-fallback tests ────────────────────────────────────────────────────────
350
- describe("404-fallback path — CF endpoint not yet deployed", () => {
302
+ describe("absent-endpoint path — honest unavailable, NO fabricated success", () => {
351
303
  async function makeServer(client) {
352
304
  const server = new StubMcpServer();
353
305
  const { registerEInvoiceTools } = await import("../tools/einvoice.js");
354
306
  registerEInvoiceTools(server, client);
355
307
  return server;
356
308
  }
357
- test("send_einvoice: 404 → stub with _stub=true, _plannedEndpoint set", async () => {
309
+ test("send_einvoice: 404 → isError + _unavailable + _plannedEndpoint, no fabricated data", async () => {
358
310
  const server = await makeServer(make404Client());
359
311
  const tool = server.tools.get("send_einvoice");
360
312
  const result = await tool.handler({ invoiceId: "inv_test", format: "xrechnung-cii", dispatchMode: "email" });
361
313
  const sc = result.structuredContent;
362
- assert.equal(sc["_stub"], true, "_stub should be true on 404-fallback");
363
- assert.equal(sc["_note"], "CF endpoint pending deploy");
314
+ assert.equal(result.isError, true, "must be isError:true");
315
+ assert.equal(sc["_unavailable"], true, "_unavailable should be true");
316
+ assert.equal(sc["_stub"], undefined, "must NOT be a fake-success stub");
364
317
  assert.ok(typeof sc["_plannedEndpoint"] === "string", "_plannedEndpoint should be set");
365
- assert.equal(sc["status"], "queued", "fallback status should be queued");
366
- assert.ok(typeof sc["workflowRunId"] === "string", "workflowRunId should be string");
318
+ assert.equal(sc["status"], undefined, "no fabricated queued status");
319
+ assert.equal(sc["workflowRunId"], undefined, "no fabricated workflowRunId");
367
320
  });
368
- test("get_einvoice_status: 404 → stub with _stub=true, _plannedEndpoint set", async () => {
321
+ test("get_einvoice_status: 404 → isError + _unavailable + _plannedEndpoint", async () => {
369
322
  const server = await makeServer(make404Client());
370
323
  const tool = server.tools.get("get_einvoice_status");
371
324
  const result = await tool.handler({ workflowRunId: "wfr_pending_123" });
372
325
  const sc = result.structuredContent;
373
- assert.equal(sc["_stub"], true, "_stub should be true on 404-fallback");
374
- assert.equal(sc["_note"], "CF endpoint pending deploy");
326
+ assert.equal(result.isError, true, "must be isError:true");
327
+ assert.equal(sc["_unavailable"], true, "_unavailable should be true");
375
328
  assert.ok(typeof sc["_plannedEndpoint"] === "string", "_plannedEndpoint should be set");
376
- assert.ok(sc["_plannedEndpoint"], "/v1/einvoice/status/wfr_pending_123");
329
+ assert.equal(sc["status"], undefined, "no fabricated status");
377
330
  });
378
- test("validate_einvoice_xml: 404 → stub with _stub=true, validator derived from format", async () => {
331
+ test("validate_einvoice_xml: 404 → isError + _unavailable, NEVER a fabricated valid verdict", async () => {
379
332
  const server = await makeServer(make404Client());
380
333
  const tool = server.tools.get("validate_einvoice_xml");
381
334
  const result = await tool.handler({ xml: "<Invoice/>", format: "xrechnung-cii" });
382
335
  const sc = result.structuredContent;
383
- assert.equal(sc["_stub"], true, "_stub should be true on 404-fallback");
384
- assert.equal(sc["_note"], "CF endpoint pending deploy");
385
- assert.equal(sc["validator"], "kosit", "validator should derive from format even in fallback");
386
- assert.equal(sc["valid"], true);
336
+ assert.equal(result.isError, true, "must be isError:true");
337
+ assert.equal(sc["_unavailable"], true, "_unavailable should be true");
338
+ assert.equal(sc["valid"], undefined, "must NOT fabricate a valid verdict");
339
+ assert.equal(sc["validator"], undefined, "must NOT fabricate a validator");
387
340
  });
388
- test("export_datev: 404 → stub with _stub=true, filename derived from params", async () => {
341
+ test("export_datev: 404 → isError + _unavailable, no fabricated file", async () => {
389
342
  const server = await makeServer(make404Client());
390
343
  const tool = server.tools.get("export_datev");
391
344
  const result = await tool.handler({ periodStart: "2026-03-01", periodEnd: "2026-03-31", format: "extf-buchungsstapel" });
392
345
  const sc = result.structuredContent;
393
- assert.equal(sc["_stub"], true, "_stub should be true on 404-fallback");
394
- assert.equal(sc["_note"], "CF endpoint pending deploy");
395
- assert.ok(sc["filename"].includes("EXTF_Buchungsstapel"), "filename should include format prefix");
396
- assert.equal(sc["fiscalPeriod"], "2026-03", "fiscalPeriod should derive from periodStart");
397
- assert.equal(sc["encoding"], "cp1252");
346
+ assert.equal(result.isError, true, "must be isError:true");
347
+ assert.equal(sc["_unavailable"], true, "_unavailable should be true");
348
+ assert.equal(sc["fileUrl"], undefined, "must NOT fabricate a fileUrl");
349
+ assert.equal(sc["filename"], undefined, "must NOT fabricate a filename");
350
+ });
351
+ test("ksef_submit: forward-compat stub → isError + _unavailable + _notImplemented preserved", async () => {
352
+ const server = await makeServer(make404Client());
353
+ const tool = server.tools.get("ksef_submit");
354
+ const result = await tool.handler({ invoiceId: "inv_test", mode: "production" });
355
+ const sc = result.structuredContent;
356
+ assert.equal(result.isError, true, "must be isError:true");
357
+ assert.equal(sc["_unavailable"], true, "_unavailable should be true");
358
+ assert.equal(sc["_notImplemented"], true, "_notImplemented marker preserved for forward-compat");
359
+ assert.ok(typeof sc["_plannedEndpoint"] === "string", "_plannedEndpoint should be set");
360
+ assert.equal(sc["mode"], "production", "echoes the requested mode");
398
361
  });
399
362
  });
400
363
  // ── Success-path tests (live client mock) ─────────────────────────────────────