beliq-mcp 0.1.0 → 0.2.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 +14 -5
- package/dist/schema.js +148 -3
- package/dist/summary.js +53 -0
- package/dist/tools/convert.js +24 -0
- package/dist/tools/generate.js +24 -0
- package/dist/tools/parse.js +23 -0
- package/dist/tools/register.js +6 -0
- package/dist/tools/shared.js +185 -5
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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,147 @@ 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
|
+
};
|
|
144
|
+
// ── Convert ────────────────────────────────────────────────────────────────
|
|
145
|
+
export const convertInputShape = {
|
|
146
|
+
document: z
|
|
147
|
+
.string()
|
|
148
|
+
.min(1)
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('The source invoice as XML text. Provide this OR documentPath, not both.'),
|
|
151
|
+
documentPath: z
|
|
152
|
+
.string()
|
|
153
|
+
.min(1)
|
|
154
|
+
.optional()
|
|
155
|
+
.describe('Path to the source invoice on disk: a UBL/CII XML, or a Factur-X / ZUGFeRD PDF. Provide this OR document.'),
|
|
156
|
+
targetFormat: z
|
|
157
|
+
.enum(LIVE_CONVERT_TARGET_FORMATS)
|
|
158
|
+
.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.'),
|
|
159
|
+
sourceFormat: z
|
|
160
|
+
.enum(LIVE_CONVERT_SOURCE_FORMATS)
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Source format hint. 'auto' (default) detects it from the document."),
|
|
163
|
+
targetProfile: z
|
|
164
|
+
.enum(LIVE_PROFILES)
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('Profile for a facturx or zugferd target; ignored for the XML targets.'),
|
|
167
|
+
dropFranceCtcOverlay: z
|
|
168
|
+
.boolean()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('Drop the French CTC (Factur-X / Chorus Pro) overlay from the converted document.'),
|
|
171
|
+
outputPath: z
|
|
172
|
+
.string()
|
|
173
|
+
.min(1)
|
|
174
|
+
.optional()
|
|
175
|
+
.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.'),
|
|
176
|
+
};
|
|
177
|
+
export const convertOutputShape = {
|
|
178
|
+
output: z.enum(['xml', 'pdf']),
|
|
179
|
+
contentType: z.string(),
|
|
180
|
+
sourceFormat: z.string().optional(),
|
|
181
|
+
targetFormat: z.string().optional(),
|
|
182
|
+
profileDetected: z.string().optional(),
|
|
183
|
+
lostElementsCount: z.number().optional(),
|
|
184
|
+
lostElements: z.array(z.string()).optional(),
|
|
185
|
+
outputPath: z.string().optional(),
|
|
186
|
+
bytesWritten: z.number().optional(),
|
|
187
|
+
xml: z.string().optional(),
|
|
188
|
+
};
|
|
47
189
|
const validateInputSchema = z.object(validateInputShape);
|
|
190
|
+
const parseInputSchema = z.object(parseInputShape);
|
|
191
|
+
const generateInputSchema = z.object(generateInputShape);
|
|
192
|
+
const convertInputSchema = z.object(convertInputShape);
|
package/dist/summary.js
CHANGED
|
@@ -34,3 +34,56 @@ 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.
|
|
56
|
+
*/
|
|
57
|
+
export function summarizeGenerate(g) {
|
|
58
|
+
const checked = g.schematronVersion ? `, checked against Schematron ${g.schematronVersion}` : '';
|
|
59
|
+
const header = `Generated a ${g.standard} ${g.output} document${checked}.`;
|
|
60
|
+
if (g.output === 'pdf') {
|
|
61
|
+
const where = g.outputPath ? ` Written to ${g.outputPath} (${plural(g.bytesWritten ?? 0, 'byte')}).` : '';
|
|
62
|
+
return `${header}${where}`;
|
|
63
|
+
}
|
|
64
|
+
const wrote = g.outputPath ? ` Written to ${g.outputPath}.` : '';
|
|
65
|
+
const body = g.xml ? `\n\n${g.xml}` : '';
|
|
66
|
+
return `${header}${wrote}${body}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A human-readable summary of a convert: the source and target formats, any
|
|
70
|
+
* profile, and how many elements the conversion could not carry across (so a
|
|
71
|
+
* lossy conversion is visible). For an XML target the document is appended in
|
|
72
|
+
* full; a PDF target is reported by the path it was written to.
|
|
73
|
+
*/
|
|
74
|
+
export function summarizeConvert(c) {
|
|
75
|
+
const fromClause = c.sourceFormat ? `${c.sourceFormat} ` : '';
|
|
76
|
+
const target = c.targetFormat ?? 'the target format';
|
|
77
|
+
const profile = c.profileDetected ? ` (profile ${c.profileDetected})` : '';
|
|
78
|
+
const lost = c.lostElementsCount && c.lostElementsCount > 0
|
|
79
|
+
? ` ${plural(c.lostElementsCount, 'element')} could not be carried across.`
|
|
80
|
+
: '';
|
|
81
|
+
const header = `Converted ${fromClause}to ${target}${profile}.${lost}`;
|
|
82
|
+
if (c.output === 'pdf') {
|
|
83
|
+
const where = c.outputPath ? ` Written to ${c.outputPath} (${plural(c.bytesWritten ?? 0, 'byte')}).` : '';
|
|
84
|
+
return `${header}${where}`;
|
|
85
|
+
}
|
|
86
|
+
const wrote = c.outputPath ? ` Written to ${c.outputPath}.` : '';
|
|
87
|
+
const body = c.xml ? `\n\n${c.xml}` : '';
|
|
88
|
+
return `${header}${wrote}${body}`;
|
|
89
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/tools/register.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/shared.js
CHANGED
|
@@ -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
|
|
15
|
-
* document (inline XML) or documentPath (a file on disk) must be set; a
|
|
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,181 @@ 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
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (err instanceof BeliqApiError) {
|
|
168
|
+
const code = err.code ? ` (${err.code})` : '';
|
|
169
|
+
return errorResult(`beliq API error ${err.status}${code}: ${err.message}`);
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
let bytesWritten;
|
|
174
|
+
if (outputPath) {
|
|
175
|
+
try {
|
|
176
|
+
await writeFile(outputPath, result.bytes, { flag: 'wx' });
|
|
177
|
+
bytesWritten = result.bytes.byteLength;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err.code === 'EEXIST') {
|
|
181
|
+
return errorResult(`A file already exists at ${outputPath}. Choose a path that does not exist.`);
|
|
182
|
+
}
|
|
183
|
+
return errorResult(`Could not write the document: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const text = summarizeGenerate({
|
|
187
|
+
standard: input.standard,
|
|
188
|
+
output,
|
|
189
|
+
schematronVersion: result.meta.schematronVersion,
|
|
190
|
+
outputPath,
|
|
191
|
+
bytesWritten,
|
|
192
|
+
xml: output === 'xml' ? result.xml : undefined,
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text }],
|
|
196
|
+
structuredContent: {
|
|
197
|
+
output,
|
|
198
|
+
contentType: result.contentType,
|
|
199
|
+
schematronVersion: result.meta.schematronVersion,
|
|
200
|
+
pdfKind: result.meta.pdfKind,
|
|
201
|
+
outputPath,
|
|
202
|
+
bytesWritten,
|
|
203
|
+
xml: output === 'xml' ? result.xml : undefined,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Convert a document from one EN 16931 format to another. An XML target comes
|
|
209
|
+
* back inline; a PDF target (facturx / zugferd) is written to disk and reported
|
|
210
|
+
* by path. Any elements the conversion could not carry across are surfaced, so
|
|
211
|
+
* a lossy conversion is visible. Input and API errors come back as isError so
|
|
212
|
+
* the model can self-correct.
|
|
213
|
+
*/
|
|
214
|
+
export async function runConvert(input, deps) {
|
|
215
|
+
let document;
|
|
216
|
+
try {
|
|
217
|
+
document = await resolveDocument(input);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
if (err instanceof BeliqInputError)
|
|
221
|
+
return errorResult(err.message);
|
|
222
|
+
return errorResult(`Could not read the document: ${err.message}`);
|
|
223
|
+
}
|
|
224
|
+
const output = PDF_CONVERT_TARGETS.has(input.targetFormat) ? 'pdf' : 'xml';
|
|
225
|
+
const outputPath = nonEmpty(input.outputPath) ? input.outputPath.trim() : undefined;
|
|
226
|
+
if (output === 'pdf' && !outputPath) {
|
|
227
|
+
return errorResult(`Converting to ${input.targetFormat} produces a PDF and needs outputPath: the file path to write it to.`);
|
|
228
|
+
}
|
|
229
|
+
let result;
|
|
230
|
+
try {
|
|
231
|
+
result = await deps.client.convert(document, {
|
|
232
|
+
targetFormat: input.targetFormat,
|
|
233
|
+
sourceFormat: input.sourceFormat,
|
|
234
|
+
targetProfile: input.targetProfile,
|
|
235
|
+
dropFranceCtcOverlay: input.dropFranceCtcOverlay,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (err instanceof BeliqApiError) {
|
|
240
|
+
const code = err.code ? ` (${err.code})` : '';
|
|
241
|
+
return errorResult(`beliq API error ${err.status}${code}: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
const xml = output === 'xml' ? new TextDecoder().decode(result.bytes) : undefined;
|
|
246
|
+
const targetFormat = result.meta.targetFormat ?? input.targetFormat;
|
|
247
|
+
let bytesWritten;
|
|
248
|
+
if (outputPath) {
|
|
249
|
+
try {
|
|
250
|
+
await writeFile(outputPath, result.bytes, { flag: 'wx' });
|
|
251
|
+
bytesWritten = result.bytes.byteLength;
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
if (err.code === 'EEXIST') {
|
|
255
|
+
return errorResult(`A file already exists at ${outputPath}. Choose a path that does not exist.`);
|
|
256
|
+
}
|
|
257
|
+
return errorResult(`Could not write the document: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const text = summarizeConvert({
|
|
261
|
+
output,
|
|
262
|
+
sourceFormat: result.meta.sourceFormat,
|
|
263
|
+
targetFormat,
|
|
264
|
+
profileDetected: result.meta.profileDetected,
|
|
265
|
+
lostElementsCount: result.meta.lostElementsCount,
|
|
266
|
+
outputPath,
|
|
267
|
+
bytesWritten,
|
|
268
|
+
xml,
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: 'text', text }],
|
|
272
|
+
structuredContent: {
|
|
273
|
+
output,
|
|
274
|
+
contentType: result.contentType,
|
|
275
|
+
sourceFormat: result.meta.sourceFormat,
|
|
276
|
+
targetFormat,
|
|
277
|
+
profileDetected: result.meta.profileDetected,
|
|
278
|
+
lostElementsCount: result.meta.lostElementsCount,
|
|
279
|
+
lostElements: result.meta.lostElements,
|
|
280
|
+
outputPath,
|
|
281
|
+
bytesWritten,
|
|
282
|
+
xml,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beliq-mcp",
|
|
3
|
-
"version": "0.
|
|
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.2.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": {
|