@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.
- package/README.md +1 -1
- package/dist/__tests__/contract.test.d.ts +33 -0
- package/dist/__tests__/contract.test.d.ts.map +1 -0
- package/dist/__tests__/contract.test.js +208 -0
- package/dist/__tests__/contract.test.js.map +1 -0
- package/dist/__tests__/einvoice-tools.test.js +70 -107
- package/dist/__tests__/einvoice-tools.test.js.map +1 -1
- package/dist/__tests__/tool-exposure.test.js +4 -1
- package/dist/__tests__/tool-exposure.test.js.map +1 -1
- package/dist/client.d.ts +18 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +42 -2
- package/dist/client.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/observability.d.ts +21 -0
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +79 -4
- package/dist/observability.js.map +1 -1
- package/dist/tools/accountingClose.d.ts.map +1 -1
- package/dist/tools/accountingClose.js +17 -12
- package/dist/tools/accountingClose.js.map +1 -1
- package/dist/tools/audit_gl.d.ts.map +1 -1
- package/dist/tools/audit_gl.js +7 -6
- package/dist/tools/audit_gl.js.map +1 -1
- package/dist/tools/backend-availability.d.ts +73 -0
- package/dist/tools/backend-availability.d.ts.map +1 -0
- package/dist/tools/backend-availability.js +94 -0
- package/dist/tools/backend-availability.js.map +1 -0
- package/dist/tools/bank_rules.d.ts.map +1 -1
- package/dist/tools/bank_rules.js +5 -4
- package/dist/tools/bank_rules.js.map +1 -1
- package/dist/tools/einvoice.d.ts +25 -41
- package/dist/tools/einvoice.d.ts.map +1 -1
- package/dist/tools/einvoice.js +142 -154
- package/dist/tools/einvoice.js.map +1 -1
- package/dist/tools/fiscal.d.ts.map +1 -1
- package/dist/tools/fiscal.js +20 -17
- package/dist/tools/fiscal.js.map +1 -1
- package/dist/tools/gestoria.d.ts.map +1 -1
- package/dist/tools/gestoria.js +11 -10
- package/dist/tools/gestoria.js.map +1 -1
- package/dist/tools/hr.d.ts.map +1 -1
- package/dist/tools/hr.js +19 -18
- package/dist/tools/hr.js.map +1 -1
- package/dist/tools/igic.d.ts.map +1 -1
- package/dist/tools/igic.js +9 -8
- package/dist/tools/igic.js.map +1 -1
- package/dist/tools/impuesto_sociedades.d.ts.map +1 -1
- package/dist/tools/impuesto_sociedades.js +5 -4
- package/dist/tools/impuesto_sociedades.js.map +1 -1
- package/dist/tools/onboard_vies.d.ts.map +1 -1
- package/dist/tools/onboard_vies.js +5 -4
- package/dist/tools/onboard_vies.js.map +1 -1
- package/dist/tools/onboarding.d.ts.map +1 -1
- package/dist/tools/onboarding.js +5 -4
- package/dist/tools/onboarding.js.map +1 -1
- package/dist/tools/payroll.d.ts.map +1 -1
- package/dist/tools/payroll.js +5 -4
- package/dist/tools/payroll.js.map +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +5 -4
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/portal_domain.d.ts.map +1 -1
- package/dist/tools/portal_domain.js +7 -6
- package/dist/tools/portal_domain.js.map +1 -1
- package/dist/tools/shared.d.ts +53 -1
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +68 -2
- package/dist/tools/shared.js.map +1 -1
- 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.
|
|
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":"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"}
|
|
@@ -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 —
|
|
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("
|
|
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
|
-
|
|
145
|
-
assert.
|
|
146
|
-
assert.
|
|
147
|
-
assert.
|
|
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("
|
|
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.
|
|
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(
|
|
172
|
+
assert.ok(/unavailable/i.test(result.content[0].text), "text should say unavailable");
|
|
169
173
|
});
|
|
170
174
|
});
|
|
171
|
-
describe("get_einvoice_status —
|
|
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("
|
|
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.
|
|
185
|
-
assert.
|
|
186
|
-
assert.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
assert.
|
|
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 —
|
|
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("
|
|
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(
|
|
212
|
-
assert.
|
|
213
|
-
|
|
214
|
-
assert.equal(sc["
|
|
215
|
-
assert.
|
|
216
|
-
|
|
217
|
-
|
|
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 —
|
|
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("
|
|
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.
|
|
260
|
-
assert.
|
|
261
|
-
assert.
|
|
262
|
-
assert.
|
|
263
|
-
assert.equal(sc["
|
|
264
|
-
assert.ok(sc["
|
|
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("
|
|
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 →
|
|
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(
|
|
363
|
-
assert.equal(sc["
|
|
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"],
|
|
366
|
-
assert.
|
|
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 →
|
|
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(
|
|
374
|
-
assert.equal(sc["
|
|
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.
|
|
329
|
+
assert.equal(sc["status"], undefined, "no fabricated status");
|
|
377
330
|
});
|
|
378
|
-
test("validate_einvoice_xml: 404 →
|
|
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(
|
|
384
|
-
assert.equal(sc["
|
|
385
|
-
assert.equal(sc["
|
|
386
|
-
assert.equal(sc["
|
|
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 →
|
|
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(
|
|
394
|
-
assert.equal(sc["
|
|
395
|
-
assert.
|
|
396
|
-
assert.equal(sc["
|
|
397
|
-
|
|
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) ─────────────────────────────────────
|