beliq-mcp 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # beliq-mcp
2
2
 
3
- An [MCP](https://modelcontextprotocol.io) server for [beliq](https://beliq.eu), the EU e-invoicing compliance API. It lets MCP clients (Claude Desktop, Claude Code, Cursor, and others) **validate** electronic invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS, and other UBL/CII documents) against authority-pinned, drift-checked rules, and explain exactly what fails.
3
+ An [MCP](https://modelcontextprotocol.io) server for [beliq](https://beliq.eu), the EU e-invoicing compliance API. It lets MCP clients (Claude Desktop, Claude Code, Cursor, and others) **validate**, **parse**, **generate**, and **convert** electronic invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS, and other UBL/CII documents) against authority-pinned, drift-checked rules, and explain exactly what fails.
4
4
 
5
- beliq validates the compliant document. Transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.
5
+ beliq produces and checks the compliant document. Transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.
6
6
 
7
7
  ## Tools
8
8
 
9
9
  - **`beliq_validate_einvoice`** - validate a UBL/CII XML invoice (inline or by file path) or a Factur-X/ZUGFeRD PDF (by file path). Returns the verdict, the detected format and profile, the ruleset (Schematron) version it was checked against, and every error and warning with its rule id, severity, location, and message.
10
+ - **`beliq_parse_einvoice`** - parse a UBL/CII XML invoice or a Factur-X/ZUGFeRD PDF into a structured EN 16931 invoice (number, dates, currency, seller, buyer, lines, totals). Returns the detected format and profile and the extracted invoice.
11
+ - **`beliq_generate_einvoice`** - generate a compliant document (XRechnung, ZUGFeRD, Factur-X, or Peppol BIS) from an EN 16931 invoice object. XML comes back inline; a PDF is written to the `outputPath` you give. Validates the result before returning by default (`verify`), so a non-compliant document fails rather than coming back.
12
+ - **`beliq_convert_einvoice`** - convert a document from one EN 16931 format to another (`targetFormat` of cii, ubl, xrechnung, peppol-bis, facturx, or zugferd). An XML target comes back inline; a PDF target is written to `outputPath`. Reports any elements the conversion could not carry across.
10
13
  - **`beliq_check_account`** - verify the configured API key and report the plan and remaining quota. Calls `GET /v1/me`, which draws no quota; useful as a connection and credential smoke test.
11
14
 
12
15
  ## Installation
@@ -25,7 +28,7 @@ The server is configured entirely through environment variables (see below).
25
28
  |---|---|---|---|
26
29
  | `BELIQ_API_KEY` | yes | - | API key from the beliq dashboard (API Keys). |
27
30
  | `BELIQ_AUTH` | no | `header` | How the key is sent: `header` (X-API-Key) or `bearer` (Authorization: Bearer). |
28
- | `BELIQ_BASE_URL` | no | `https://api.beliq.eu` | Override for self-hosted or staging. |
31
+ | `BELIQ_BASE_URL` | no | `https://api.beliq.eu` | Override for a self-hosted deployment; defaults to the production API. |
29
32
 
30
33
  ## Client setup
31
34
 
@@ -57,7 +60,7 @@ Add to `claude_desktop_config.json` (Settings > Developer > Edit Config):
57
60
 
58
61
  Add to `~/.cursor/mcp.json` (or a project `.cursor/mcp.json`) using the same `mcpServers` block shown for Claude Desktop.
59
62
 
60
- ## Reading a validation result
63
+ ## Reading a result
61
64
 
62
65
  `beliq_validate_einvoice` returns a short text verdict plus a structured result:
63
66
 
@@ -66,7 +69,13 @@ Add to `~/.cursor/mcp.json` (or a project `.cursor/mcp.json`) using the same `mc
66
69
  - `schematronVersion` is the exact ruleset revision the check ran against.
67
70
  - `errors[]` and `warnings[]` each carry `ruleId`, `severity`, `location` (an XPath when available), and `message`.
68
71
 
69
- A PDF (Factur-X / ZUGFeRD) must be passed by `documentPath`, not inlined as text.
72
+ `beliq_parse_einvoice` returns the detected `format` and `profileDetected` plus the extracted `invoice` object (EN 16931 fields: number, dates, currency, seller, buyer, lines, totals, and any national extensions present).
73
+
74
+ `beliq_generate_einvoice` returns the `output` kind (`xml` or `pdf`), the `contentType`, and the `schematronVersion` the document was checked against. An XML document is also returned inline as `xml`; a PDF (and an XML when you set `outputPath`) is written to disk, and the call reports `outputPath` and `bytesWritten`. It does not overwrite an existing file: pick a path that does not exist.
75
+
76
+ `beliq_convert_einvoice` returns the `output` kind, the resolved `sourceFormat` and `targetFormat`, and `lostElementsCount`/`lostElements` for anything the conversion could not carry across. An XML target comes back inline as `xml`; a PDF target (facturx / zugferd) is written to `outputPath`. Like generate, it never overwrites an existing file.
77
+
78
+ A PDF (Factur-X / ZUGFeRD) must be passed by `documentPath` for validate, parse, and convert, not inlined as text.
70
79
 
71
80
  ## Agent skill
72
81
 
package/dist/schema.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { z } from 'zod';
2
- // Validate accepts the live, publicly offered source-syntax hints only. The
3
- // engine can technically detect more, but the public option set stays narrow
4
- // (LPD-1): callers pass the syntax family, not a country profile.
2
+ import { LIVE_CONVERT_SOURCE_FORMATS, LIVE_CONVERT_TARGET_FORMATS, LIVE_GENERATE_STANDARDS, LIVE_PARSE_FORMATS, LIVE_PROFILES, } from '@beliq/sdk';
3
+ // The tools expose the LIVE, authority-pinned public option sets (LPD-1),
4
+ // imported from the SDK so this stays one source of truth: callers pass a
5
+ // syntax family or a live standard, never a provisional/withheld profile. The
6
+ // engine can detect more; the public surface stays narrow.
5
7
  export const validateInputShape = {
6
8
  document: z
7
9
  .string()
@@ -44,4 +46,160 @@ export const checkAccountOutputShape = {
44
46
  plan: z.string().optional(),
45
47
  message: z.string(),
46
48
  };
49
+ // ── Parse ────────────────────────────────────────────────────────────────
50
+ export const parseInputShape = {
51
+ document: z
52
+ .string()
53
+ .min(1)
54
+ .optional()
55
+ .describe('The invoice as XML text. Provide this OR documentPath, not both.'),
56
+ documentPath: z
57
+ .string()
58
+ .min(1)
59
+ .optional()
60
+ .describe('Path to an invoice file on disk: a UBL/CII XML, or a Factur-X / ZUGFeRD PDF. Provide this OR document.'),
61
+ format: z
62
+ .enum(LIVE_PARSE_FORMATS)
63
+ .optional()
64
+ .describe("Source syntax hint. 'auto' (default) detects CII vs UBL from the document."),
65
+ };
66
+ // The extracted invoice is passed through verbatim from the API; the shape is
67
+ // documented on the endpoint, so the tool reports it without re-modelling it.
68
+ export const parseOutputShape = {
69
+ format: z.string(),
70
+ profileDetected: z.string().optional(),
71
+ invoice: z.record(z.string(), z.unknown()),
72
+ };
73
+ // ── Generate ──────────────────────────────────────────────────────────────
74
+ const partyAddress = z
75
+ .object({
76
+ city: z.string(),
77
+ postalCode: z.string(),
78
+ countryCode: z.string().describe('ISO 3166-1 alpha-2, e.g. DE.'),
79
+ street: z.string().optional(),
80
+ })
81
+ .passthrough();
82
+ const party = z
83
+ .object({
84
+ name: z.string(),
85
+ vatId: z.string().optional(),
86
+ address: partyAddress,
87
+ })
88
+ .passthrough();
89
+ const invoiceLine = z
90
+ .object({
91
+ description: z.string(),
92
+ quantity: z.number(),
93
+ unitCode: z.string().describe('UN/ECE Rec 20 unit code, e.g. HUR (hour), C62 (unit).'),
94
+ unitPrice: z.number(),
95
+ lineTotal: z.number(),
96
+ vatRate: z.number(),
97
+ vatCategoryCode: z.string().describe('UNCL5305 VAT category, e.g. S, Z, E.'),
98
+ })
99
+ .passthrough();
100
+ const invoice = z
101
+ .object({
102
+ number: z.string(),
103
+ issueDate: z.string().describe('ISO date, YYYY-MM-DD.'),
104
+ currencyCode: z.string().describe('ISO 4217, e.g. EUR.'),
105
+ seller: party,
106
+ buyer: party,
107
+ lines: z.array(invoiceLine).min(1),
108
+ totalNetAmount: z.number(),
109
+ totalTaxAmount: z.number(),
110
+ totalGrossAmount: z.number(),
111
+ })
112
+ .passthrough()
113
+ .describe('The EN 16931 invoice to render. Extra fields (dueDate, buyerReference, paymentMeans, taxSummary, per-country blocks) are passed through to the API.');
114
+ export const generateInputShape = {
115
+ standard: z.enum(LIVE_GENERATE_STANDARDS).describe('Target e-invoice standard to produce.'),
116
+ invoice,
117
+ output: z
118
+ .enum(['xml', 'pdf'])
119
+ .optional()
120
+ .describe("'xml' (default) returns the document inline. 'pdf' produces a Factur-X / ZUGFeRD hybrid PDF and needs outputPath to write it to."),
121
+ facturxProfile: z
122
+ .enum(LIVE_PROFILES)
123
+ .optional()
124
+ .describe('Profile for a Factur-X / ZUGFeRD PDF; applies only when standard is facturx or zugferd.'),
125
+ verify: z
126
+ .boolean()
127
+ .optional()
128
+ .describe('Validate the generated document before returning and fail if it is not compliant. Defaults to true.'),
129
+ outputPath: z
130
+ .string()
131
+ .min(1)
132
+ .optional()
133
+ .describe('Where to write the generated document on disk. Required for pdf output, optional for xml. The call fails if a file already exists at the path.'),
134
+ };
135
+ export const generateOutputShape = {
136
+ output: z.enum(['xml', 'pdf']),
137
+ contentType: z.string(),
138
+ schematronVersion: z.string().optional(),
139
+ pdfKind: z.string().optional(),
140
+ outputPath: z.string().optional(),
141
+ bytesWritten: z.number().optional(),
142
+ xml: z.string().optional(),
143
+ // The verify-it-yourself seal: sha256 of the returned document, the combined
144
+ // ruleset fingerprint it was checked against, and the validation verdict.
145
+ sha256: z.string().optional(),
146
+ rulesetSha256: z.string().optional(),
147
+ livemode: z.boolean().optional(),
148
+ validationResult: z
149
+ .object({
150
+ valid: z.boolean(),
151
+ schematronVersion: z.string().optional(),
152
+ errors: z.array(issueShape),
153
+ warnings: z.array(issueShape),
154
+ })
155
+ .optional(),
156
+ };
157
+ // ── Convert ────────────────────────────────────────────────────────────────
158
+ export const convertInputShape = {
159
+ document: z
160
+ .string()
161
+ .min(1)
162
+ .optional()
163
+ .describe('The source invoice as XML text. Provide this OR documentPath, not both.'),
164
+ documentPath: z
165
+ .string()
166
+ .min(1)
167
+ .optional()
168
+ .describe('Path to the source invoice on disk: a UBL/CII XML, or a Factur-X / ZUGFeRD PDF. Provide this OR document.'),
169
+ targetFormat: z
170
+ .enum(LIVE_CONVERT_TARGET_FORMATS)
171
+ .describe('Format to convert to. cii, ubl, xrechnung, and peppol-bis come back as XML inline; facturx and zugferd produce a PDF and need outputPath.'),
172
+ sourceFormat: z
173
+ .enum(LIVE_CONVERT_SOURCE_FORMATS)
174
+ .optional()
175
+ .describe("Source format hint. 'auto' (default) detects it from the document."),
176
+ targetProfile: z
177
+ .enum(LIVE_PROFILES)
178
+ .optional()
179
+ .describe('Profile for a facturx or zugferd target; ignored for the XML targets.'),
180
+ dropFranceCtcOverlay: z
181
+ .boolean()
182
+ .optional()
183
+ .describe('Drop the French CTC (Factur-X / Chorus Pro) overlay from the converted document.'),
184
+ outputPath: z
185
+ .string()
186
+ .min(1)
187
+ .optional()
188
+ .describe('Where to write the converted document on disk. Required for a facturx/zugferd (PDF) target, optional for an XML target. The call fails if a file already exists at the path.'),
189
+ };
190
+ export const convertOutputShape = {
191
+ output: z.enum(['xml', 'pdf']),
192
+ contentType: z.string(),
193
+ sourceFormat: z.string().optional(),
194
+ targetFormat: z.string().optional(),
195
+ profileDetected: z.string().optional(),
196
+ lostElementsCount: z.number().optional(),
197
+ lostElements: z.array(z.string()).optional(),
198
+ outputPath: z.string().optional(),
199
+ bytesWritten: z.number().optional(),
200
+ xml: z.string().optional(),
201
+ };
47
202
  const validateInputSchema = z.object(validateInputShape);
203
+ const parseInputSchema = z.object(parseInputShape);
204
+ const generateInputSchema = z.object(generateInputShape);
205
+ const convertInputSchema = z.object(convertInputShape);
package/dist/summary.js CHANGED
@@ -34,3 +34,60 @@ export function summarizeValidation(result) {
34
34
  `${plural(errors.length, 'error')}, ${plural(warnings.length, 'warning')}.`;
35
35
  return [header, ...listSection('Errors', errors), ...listSection('Warnings', warnings)].join('\n');
36
36
  }
37
+ /**
38
+ * A one-line human-readable summary of a parse: the detected syntax and
39
+ * profile, then the invoice number, currency, line count, and gross total. The
40
+ * full structured invoice rides alongside in structuredContent.
41
+ */
42
+ export function summarizeParse(result) {
43
+ const invoice = result.invoice;
44
+ const profile = result.profileDetected ? ` (profile ${result.profileDetected})` : '';
45
+ const number = invoice?.number ? `invoice ${invoice.number}` : 'invoice';
46
+ const lines = invoice?.lines?.length ?? 0;
47
+ const gross = invoice?.totalGrossAmount != null
48
+ ? `, gross ${invoice.totalGrossAmount} ${invoice.currencyCode ?? ''}`.trimEnd()
49
+ : '';
50
+ return `Parsed a ${result.format}${profile} document: ${number}, ${plural(lines, 'line')}${gross}.`;
51
+ }
52
+ /**
53
+ * A human-readable summary of a generate. For an XML result the document is
54
+ * appended in full so a client without structured output still receives it; a
55
+ * PDF is reported by the path it was written to. When the seal is present the
56
+ * document sha256 and validation verdict are stated so the model can cite them.
57
+ */
58
+ export function summarizeGenerate(g) {
59
+ const checked = g.schematronVersion ? `, checked against Schematron ${g.schematronVersion}` : '';
60
+ const seal = g.sha256
61
+ ? ` Document sha256 ${g.sha256}${g.valid === undefined ? '' : `; validation ${g.valid ? 'passed' : 'failed'}`}.`
62
+ : '';
63
+ const header = `Generated a ${g.standard} ${g.output} document${checked}.${seal}`;
64
+ if (g.output === 'pdf') {
65
+ const where = g.outputPath ? ` Written to ${g.outputPath} (${plural(g.bytesWritten ?? 0, 'byte')}).` : '';
66
+ return `${header}${where}`;
67
+ }
68
+ const wrote = g.outputPath ? ` Written to ${g.outputPath}.` : '';
69
+ const body = g.xml ? `\n\n${g.xml}` : '';
70
+ return `${header}${wrote}${body}`;
71
+ }
72
+ /**
73
+ * A human-readable summary of a convert: the source and target formats, any
74
+ * profile, and how many elements the conversion could not carry across (so a
75
+ * lossy conversion is visible). For an XML target the document is appended in
76
+ * full; a PDF target is reported by the path it was written to.
77
+ */
78
+ export function summarizeConvert(c) {
79
+ const fromClause = c.sourceFormat ? `${c.sourceFormat} ` : '';
80
+ const target = c.targetFormat ?? 'the target format';
81
+ const profile = c.profileDetected ? ` (profile ${c.profileDetected})` : '';
82
+ const lost = c.lostElementsCount && c.lostElementsCount > 0
83
+ ? ` ${plural(c.lostElementsCount, 'element')} could not be carried across.`
84
+ : '';
85
+ const header = `Converted ${fromClause}to ${target}${profile}.${lost}`;
86
+ if (c.output === 'pdf') {
87
+ const where = c.outputPath ? ` Written to ${c.outputPath} (${plural(c.bytesWritten ?? 0, 'byte')}).` : '';
88
+ return `${header}${where}`;
89
+ }
90
+ const wrote = c.outputPath ? ` Written to ${c.outputPath}.` : '';
91
+ const body = c.xml ? `\n\n${c.xml}` : '';
92
+ return `${header}${wrote}${body}`;
93
+ }
@@ -0,0 +1,24 @@
1
+ import { convertInputShape, convertOutputShape } from '../schema.js';
2
+ import { runConvert } from './shared.js';
3
+ const description = [
4
+ 'Convert an EU electronic invoice from one EN 16931 format to another.',
5
+ 'Pass the source document (inline via document, or a file via documentPath) and set targetFormat to cii, ubl, xrechnung, peppol-bis, facturx, or zugferd.',
6
+ 'An XML target (cii, ubl, xrechnung, peppol-bis) comes back inline; a PDF target (facturx, zugferd) is written to the outputPath you give.',
7
+ 'The result reports any elements the conversion could not carry across, so a lossy conversion is visible rather than silent.',
8
+ 'beliq produces the converted document; transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.',
9
+ ].join(' ');
10
+ export function registerConvert(server, deps) {
11
+ server.registerTool('beliq_convert_einvoice', {
12
+ title: 'beliq: Convert E-Invoice',
13
+ description,
14
+ inputSchema: convertInputShape,
15
+ outputSchema: convertOutputShape,
16
+ annotations: {
17
+ title: 'beliq: Convert E-Invoice',
18
+ readOnlyHint: false,
19
+ destructiveHint: false,
20
+ idempotentHint: false,
21
+ openWorldHint: true,
22
+ },
23
+ }, async (args) => runConvert(args, deps));
24
+ }
@@ -0,0 +1,24 @@
1
+ import { generateInputShape, generateOutputShape } from '../schema.js';
2
+ import { runGenerate } from './shared.js';
3
+ const description = [
4
+ 'Generate a compliant EU electronic invoice from an EN 16931 invoice object.',
5
+ 'Set standard to the target (xrechnung, zugferd, facturx, or peppol-bis) and pass the invoice fields.',
6
+ "output 'xml' (default) returns the document inline; 'pdf' produces a Factur-X / ZUGFeRD hybrid PDF, which needs outputPath to write it to.",
7
+ 'By default the result is validated before it is returned (verify), so a non-compliant document fails rather than coming back.',
8
+ 'beliq produces the compliant document; transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.',
9
+ ].join(' ');
10
+ export function registerGenerate(server, deps) {
11
+ server.registerTool('beliq_generate_einvoice', {
12
+ title: 'beliq: Generate E-Invoice',
13
+ description,
14
+ inputSchema: generateInputShape,
15
+ outputSchema: generateOutputShape,
16
+ annotations: {
17
+ title: 'beliq: Generate E-Invoice',
18
+ readOnlyHint: false,
19
+ destructiveHint: false,
20
+ idempotentHint: false,
21
+ openWorldHint: true,
22
+ },
23
+ }, async (args) => runGenerate(args, deps));
24
+ }
@@ -0,0 +1,23 @@
1
+ import { parseInputShape, parseOutputShape } from '../schema.js';
2
+ import { runParse } from './shared.js';
3
+ const description = [
4
+ 'Parse an EU electronic invoice into a structured EN 16931 invoice object.',
5
+ 'Accepts a UBL or CII XML document (inline via document, or a file via documentPath), or a Factur-X / ZUGFeRD PDF via documentPath.',
6
+ 'Returns the detected format and profile and the extracted invoice (number, dates, currency, seller, buyer, lines, totals, and more).',
7
+ 'Use it to read the contents of an invoice; use validate to check compliance.',
8
+ ].join(' ');
9
+ export function registerParse(server, deps) {
10
+ server.registerTool('beliq_parse_einvoice', {
11
+ title: 'beliq: Parse E-Invoice',
12
+ description,
13
+ inputSchema: parseInputShape,
14
+ outputSchema: parseOutputShape,
15
+ annotations: {
16
+ title: 'beliq: Parse E-Invoice',
17
+ readOnlyHint: true,
18
+ destructiveHint: false,
19
+ idempotentHint: true,
20
+ openWorldHint: true,
21
+ },
22
+ }, async (args) => runParse(args, deps));
23
+ }
@@ -1,7 +1,13 @@
1
1
  import { registerValidate } from './validate.js';
2
2
  import { registerCheckAccount } from './checkAccount.js';
3
+ import { registerParse } from './parse.js';
4
+ import { registerGenerate } from './generate.js';
5
+ import { registerConvert } from './convert.js';
3
6
  /** Register every beliq tool on the server with the given dependencies. */
4
7
  export function registerAllTools(server, deps) {
5
8
  registerValidate(server, deps);
9
+ registerParse(server, deps);
10
+ registerGenerate(server, deps);
11
+ registerConvert(server, deps);
6
12
  registerCheckAccount(server, deps);
7
13
  }
@@ -1,6 +1,6 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { BeliqApiError } from '@beliq/sdk';
3
- import { summarizeValidation } from '../summary.js';
3
+ import { summarizeConvert, summarizeGenerate, summarizeParse, summarizeValidation } from '../summary.js';
4
4
  function errorResult(message) {
5
5
  return { content: [{ type: 'text', text: message }], isError: true };
6
6
  }
@@ -10,10 +10,12 @@ function nonEmpty(value) {
10
10
  /** A bad tool input that should come back as a normal (non-fatal) tool error. */
11
11
  class BeliqInputError extends Error {
12
12
  }
13
+ /** Convert targets that produce a PDF rather than an inline XML document. */
14
+ const PDF_CONVERT_TARGETS = new Set(['facturx', 'zugferd']);
13
15
  /**
14
- * Resolve the tool input to the bytes/text the SDK validates. Exactly one of
15
- * document (inline XML) or documentPath (a file on disk) must be set; a file is
16
- * read as raw bytes so the SDK can sniff XML vs an embedded-XML PDF.
16
+ * Resolve a document-carrying input to the bytes/text the SDK reads. Exactly
17
+ * one of document (inline XML) or documentPath (a file on disk) must be set; a
18
+ * file is read as raw bytes so the SDK can sniff XML vs an embedded-XML PDF.
17
19
  */
18
20
  async function resolveDocument(input) {
19
21
  const hasDoc = nonEmpty(input.document);
@@ -103,3 +105,198 @@ export async function runCheckAccount(deps) {
103
105
  throw err;
104
106
  }
105
107
  }
108
+ /**
109
+ * Parse one document into a structured EN 16931 invoice and shape the MCP
110
+ * result: a one-line summary in the text content plus the full invoice in
111
+ * structuredContent. Input and API errors come back as isError so the model can
112
+ * self-correct; only unexpected faults throw.
113
+ */
114
+ export async function runParse(input, deps) {
115
+ let document;
116
+ try {
117
+ document = await resolveDocument(input);
118
+ }
119
+ catch (err) {
120
+ if (err instanceof BeliqInputError)
121
+ return errorResult(err.message);
122
+ return errorResult(`Could not read the document: ${err.message}`);
123
+ }
124
+ try {
125
+ const result = await deps.client.parse(document, { format: input.format });
126
+ return {
127
+ content: [{ type: 'text', text: summarizeParse(result) }],
128
+ structuredContent: {
129
+ format: result.format,
130
+ profileDetected: result.profileDetected,
131
+ invoice: result.invoice,
132
+ },
133
+ };
134
+ }
135
+ catch (err) {
136
+ if (err instanceof BeliqApiError) {
137
+ const code = err.code ? ` (${err.code})` : '';
138
+ return errorResult(`beliq API error ${err.status}${code}: ${err.message}`);
139
+ }
140
+ throw err;
141
+ }
142
+ }
143
+ /**
144
+ * Generate a compliant document from an EN 16931 invoice. XML comes back inline
145
+ * in the text content; a PDF (or an XML with outputPath set) is written to disk
146
+ * and reported by path. verify defaults to true so the tool fails closed rather
147
+ * than hand back a non-compliant document. Input and API errors come back as
148
+ * isError so the model can self-correct.
149
+ */
150
+ export async function runGenerate(input, deps) {
151
+ const output = input.output ?? 'xml';
152
+ const outputPath = nonEmpty(input.outputPath) ? input.outputPath.trim() : undefined;
153
+ if (output === 'pdf' && !outputPath) {
154
+ return errorResult('A pdf output needs outputPath: the file path to write the generated PDF to.');
155
+ }
156
+ let result;
157
+ try {
158
+ result = await deps.client.generate({
159
+ standard: input.standard,
160
+ invoice: input.invoice,
161
+ output,
162
+ facturxProfile: input.facturxProfile,
163
+ verify: input.verify ?? true,
164
+ // Always seal so the tool can hand back the document sha256 and the
165
+ // validation verdict for the model to cite.
166
+ seal: true,
167
+ });
168
+ }
169
+ catch (err) {
170
+ if (err instanceof BeliqApiError) {
171
+ const code = err.code ? ` (${err.code})` : '';
172
+ return errorResult(`beliq API error ${err.status}${code}: ${err.message}`);
173
+ }
174
+ throw err;
175
+ }
176
+ let bytesWritten;
177
+ if (outputPath) {
178
+ try {
179
+ await writeFile(outputPath, result.bytes, { flag: 'wx' });
180
+ bytesWritten = result.bytes.byteLength;
181
+ }
182
+ catch (err) {
183
+ if (err.code === 'EEXIST') {
184
+ return errorResult(`A file already exists at ${outputPath}. Choose a path that does not exist.`);
185
+ }
186
+ return errorResult(`Could not write the document: ${err.message}`);
187
+ }
188
+ }
189
+ const vr = result.validationResult;
190
+ const text = summarizeGenerate({
191
+ standard: input.standard,
192
+ output,
193
+ schematronVersion: result.meta.schematronVersion,
194
+ outputPath,
195
+ bytesWritten,
196
+ xml: output === 'xml' ? result.xml : undefined,
197
+ sha256: result.sha256,
198
+ valid: vr?.valid,
199
+ });
200
+ return {
201
+ content: [{ type: 'text', text }],
202
+ structuredContent: {
203
+ output,
204
+ contentType: result.contentType,
205
+ schematronVersion: result.meta.schematronVersion,
206
+ pdfKind: result.meta.pdfKind,
207
+ outputPath,
208
+ bytesWritten,
209
+ xml: output === 'xml' ? result.xml : undefined,
210
+ sha256: result.sha256,
211
+ rulesetSha256: result.meta.rulesetSha256,
212
+ livemode: result.meta.livemode,
213
+ validationResult: vr
214
+ ? {
215
+ valid: vr.valid,
216
+ schematronVersion: vr.schematronVersion,
217
+ errors: vr.errors ?? [],
218
+ warnings: vr.warnings ?? [],
219
+ }
220
+ : undefined,
221
+ },
222
+ };
223
+ }
224
+ /**
225
+ * Convert a document from one EN 16931 format to another. An XML target comes
226
+ * back inline; a PDF target (facturx / zugferd) is written to disk and reported
227
+ * by path. Any elements the conversion could not carry across are surfaced, so
228
+ * a lossy conversion is visible. Input and API errors come back as isError so
229
+ * the model can self-correct.
230
+ */
231
+ export async function runConvert(input, deps) {
232
+ let document;
233
+ try {
234
+ document = await resolveDocument(input);
235
+ }
236
+ catch (err) {
237
+ if (err instanceof BeliqInputError)
238
+ return errorResult(err.message);
239
+ return errorResult(`Could not read the document: ${err.message}`);
240
+ }
241
+ const output = PDF_CONVERT_TARGETS.has(input.targetFormat) ? 'pdf' : 'xml';
242
+ const outputPath = nonEmpty(input.outputPath) ? input.outputPath.trim() : undefined;
243
+ if (output === 'pdf' && !outputPath) {
244
+ return errorResult(`Converting to ${input.targetFormat} produces a PDF and needs outputPath: the file path to write it to.`);
245
+ }
246
+ let result;
247
+ try {
248
+ result = await deps.client.convert(document, {
249
+ targetFormat: input.targetFormat,
250
+ sourceFormat: input.sourceFormat,
251
+ targetProfile: input.targetProfile,
252
+ dropFranceCtcOverlay: input.dropFranceCtcOverlay,
253
+ });
254
+ }
255
+ catch (err) {
256
+ if (err instanceof BeliqApiError) {
257
+ const code = err.code ? ` (${err.code})` : '';
258
+ return errorResult(`beliq API error ${err.status}${code}: ${err.message}`);
259
+ }
260
+ throw err;
261
+ }
262
+ const xml = output === 'xml' ? new TextDecoder().decode(result.bytes) : undefined;
263
+ const targetFormat = result.meta.targetFormat ?? input.targetFormat;
264
+ let bytesWritten;
265
+ if (outputPath) {
266
+ try {
267
+ await writeFile(outputPath, result.bytes, { flag: 'wx' });
268
+ bytesWritten = result.bytes.byteLength;
269
+ }
270
+ catch (err) {
271
+ if (err.code === 'EEXIST') {
272
+ return errorResult(`A file already exists at ${outputPath}. Choose a path that does not exist.`);
273
+ }
274
+ return errorResult(`Could not write the document: ${err.message}`);
275
+ }
276
+ }
277
+ const text = summarizeConvert({
278
+ output,
279
+ sourceFormat: result.meta.sourceFormat,
280
+ targetFormat,
281
+ profileDetected: result.meta.profileDetected,
282
+ lostElementsCount: result.meta.lostElementsCount,
283
+ outputPath,
284
+ bytesWritten,
285
+ xml,
286
+ });
287
+ return {
288
+ content: [{ type: 'text', text }],
289
+ structuredContent: {
290
+ output,
291
+ contentType: result.contentType,
292
+ sourceFormat: result.meta.sourceFormat,
293
+ targetFormat,
294
+ profileDetected: result.meta.profileDetected,
295
+ lostElementsCount: result.meta.lostElementsCount,
296
+ lostElements: result.meta.lostElements,
297
+ outputPath,
298
+ bytesWritten,
299
+ xml,
300
+ },
301
+ };
302
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beliq-mcp",
3
- "version": "0.1.1",
4
- "description": "beliq MCP server: validate EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules, for MCP clients.",
3
+ "version": "0.3.0",
4
+ "description": "beliq MCP server: validate, parse, generate, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules, for MCP clients.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "author": {
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@beliq/sdk": "^0.1.0",
59
+ "@beliq/sdk": "^0.2.0",
60
60
  "@modelcontextprotocol/sdk": "^1.29.0",
61
61
  "zod": "^4.4.3"
62
62
  },