beliq-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 beliq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # beliq-mcp
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.
4
+
5
+ beliq validates the compliant document. Transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.
6
+
7
+ ## Tools
8
+
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_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
+
12
+ ## Installation
13
+
14
+ Requires Node.js >= 20.15. Published to npm, so clients can run it with `npx`:
15
+
16
+ ```
17
+ npx -y beliq-mcp
18
+ ```
19
+
20
+ The server is configured entirely through environment variables (see below).
21
+
22
+ ## Configuration
23
+
24
+ | Variable | Required | Default | Description |
25
+ |---|---|---|---|
26
+ | `BELIQ_API_KEY` | yes | - | API key from the beliq dashboard (API Keys). |
27
+ | `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. |
29
+
30
+ ## Client setup
31
+
32
+ ### Claude Code
33
+
34
+ ```
35
+ claude mcp add beliq -e BELIQ_API_KEY=your-key -- npx -y beliq-mcp
36
+ ```
37
+
38
+ ### Claude Desktop
39
+
40
+ Add to `claude_desktop_config.json` (Settings > Developer > Edit Config):
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "beliq": {
46
+ "command": "npx",
47
+ "args": ["-y", "beliq-mcp"],
48
+ "env": {
49
+ "BELIQ_API_KEY": "your-key"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Cursor
57
+
58
+ Add to `~/.cursor/mcp.json` (or a project `.cursor/mcp.json`) using the same `mcpServers` block shown for Claude Desktop.
59
+
60
+ ## Reading a validation result
61
+
62
+ `beliq_validate_einvoice` returns a short text verdict plus a structured result:
63
+
64
+ - `valid` is true only when there are no errors; warnings do not make a document invalid.
65
+ - `format` and `profileDetected` report the detected syntax and business profile.
66
+ - `schematronVersion` is the exact ruleset revision the check ran against.
67
+ - `errors[]` and `warnings[]` each carry `ruleId`, `severity`, `location` (an XPath when available), and `message`.
68
+
69
+ A PDF (Factur-X / ZUGFeRD) must be passed by `documentPath`, not inlined as text.
70
+
71
+ ## Agent skill
72
+
73
+ [`skill/SKILL.md`](./skill/SKILL.md) is a portable agent skill that teaches a model when to validate, how to read `errors`/`warnings`, and how to report a verdict, using the tools above. Drop it into a skills directory for an agent that should validate invoices on request.
74
+
75
+ ## Development
76
+
77
+ This server depends on the published [`@beliq/sdk`](https://www.npmjs.com/package/@beliq/sdk), which carries the request, transport, and result-shaping logic. No lockfile is committed; `@beliq/sdk` is resolved fresh at install time.
78
+
79
+ - `npm install`
80
+ - `npm run build` - compile to `dist/`
81
+ - `npm run typecheck`
82
+ - `npm run lint`
83
+ - `npm test` - unit tests (result summary) and an in-memory MCP round-trip with a fake SDK client
84
+ - `BELIQ_API_KEY=your-key npm run test:integration` - live smoke tests against the real API
85
+ - `npm run scrub:check` - check for em-dashes in source and docs
86
+
87
+ Run the built server directly for a quick check:
88
+
89
+ ```
90
+ BELIQ_API_KEY=your-key node dist/index.js
91
+ ```
92
+
93
+ ## License
94
+
95
+ [MIT](./LICENSE)
package/dist/config.js ADDED
@@ -0,0 +1,14 @@
1
+ import { DEFAULT_BASE_URL } from '@beliq/sdk';
2
+ /**
3
+ * Build the typed config from the environment. Throws if BELIQ_API_KEY is
4
+ * missing so the server fails fast before connecting the transport.
5
+ */
6
+ export function loadConfig(env = process.env) {
7
+ const apiKey = env.BELIQ_API_KEY?.trim();
8
+ if (!apiKey) {
9
+ throw new Error('BELIQ_API_KEY is required. Create a key in the beliq dashboard under API Keys and set it in the MCP server env.');
10
+ }
11
+ const baseUrl = (env.BELIQ_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, '');
12
+ const auth = env.BELIQ_AUTH?.trim().toLowerCase() === 'bearer' ? 'bearer' : 'header';
13
+ return { apiKey, baseUrl, auth };
14
+ }
package/dist/deps.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // On stdio, stdout carries the JSON-RPC protocol. All diagnostics MUST go to
3
+ // stderr; a stray console.log anywhere reachable from the running server
4
+ // corrupts the protocol stream.
5
+ import { createRequire } from 'node:module';
6
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { Beliq } from '@beliq/sdk';
9
+ import { loadConfig } from './config.js';
10
+ import { registerAllTools } from './tools/register.js';
11
+ const require = createRequire(import.meta.url);
12
+ const { version } = require('../package.json');
13
+ async function main() {
14
+ let config;
15
+ try {
16
+ config = loadConfig();
17
+ }
18
+ catch (err) {
19
+ console.error(`[beliq-mcp] ${err.message}`);
20
+ process.exit(1);
21
+ }
22
+ const client = new Beliq({ apiKey: config.apiKey, baseUrl: config.baseUrl, auth: config.auth });
23
+ const server = new McpServer({ name: 'beliq-mcp', version });
24
+ registerAllTools(server, { client });
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ console.error(`[beliq-mcp] v${version} ready on stdio (base ${config.baseUrl})`);
28
+ }
29
+ main().catch((err) => {
30
+ console.error('[beliq-mcp] fatal:', err);
31
+ process.exit(1);
32
+ });
package/dist/schema.js ADDED
@@ -0,0 +1,47 @@
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.
5
+ export const validateInputShape = {
6
+ document: z
7
+ .string()
8
+ .min(1)
9
+ .optional()
10
+ .describe('The invoice as XML text. Provide this OR documentPath, not both.'),
11
+ documentPath: z
12
+ .string()
13
+ .min(1)
14
+ .optional()
15
+ .describe('Path to an invoice file on disk: a UBL/CII XML, or a PDF carrying embedded XML (Factur-X / ZUGFeRD). Provide this OR document.'),
16
+ format: z
17
+ .enum(['auto', 'cii', 'ubl'])
18
+ .optional()
19
+ .describe("Source syntax hint. 'auto' (default) detects CII vs UBL from the document."),
20
+ franceCtc: z
21
+ .boolean()
22
+ .optional()
23
+ .describe('Apply the French CTC (Factur-X / Chorus Pro) rule overlay during validation.'),
24
+ };
25
+ const issueShape = z.object({
26
+ ruleId: z.string(),
27
+ severity: z.string(),
28
+ location: z.string().optional(),
29
+ message: z.string(),
30
+ });
31
+ export const validateOutputShape = {
32
+ valid: z.boolean(),
33
+ format: z.string(),
34
+ profileDetected: z.string().optional(),
35
+ schematronVersion: z.string().optional(),
36
+ errorCount: z.number(),
37
+ warningCount: z.number(),
38
+ errors: z.array(issueShape),
39
+ warnings: z.array(issueShape),
40
+ };
41
+ export const checkAccountOutputShape = {
42
+ ok: z.boolean(),
43
+ status: z.number(),
44
+ plan: z.string().optional(),
45
+ message: z.string(),
46
+ };
47
+ const validateInputSchema = z.object(validateInputShape);
@@ -0,0 +1,36 @@
1
+ /** How many issues to spell out before collapsing the rest into a count. */
2
+ const MAX_LISTED_ISSUES = 20;
3
+ function issueLine(issue) {
4
+ const where = issue.location ? ` at ${issue.location}` : '';
5
+ return `- ${issue.ruleId} (${issue.severity})${where}: ${issue.message}`;
6
+ }
7
+ function plural(n, word) {
8
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
9
+ }
10
+ function listSection(title, issues) {
11
+ if (!issues.length)
12
+ return [];
13
+ const shown = issues.slice(0, MAX_LISTED_ISSUES);
14
+ const lines = [`${title}:`, ...shown.map(issueLine)];
15
+ if (issues.length > shown.length) {
16
+ lines.push(`... and ${issues.length - shown.length} more.`);
17
+ }
18
+ return lines;
19
+ }
20
+ /**
21
+ * A compact human-readable verdict for the MCP text content: the pass/fail
22
+ * line with format, profile, and ruleset version, then the errors and warnings
23
+ * (capped). The full structured result rides alongside in structuredContent.
24
+ */
25
+ export function summarizeValidation(result) {
26
+ const errors = (result.errors ?? []);
27
+ const warnings = (result.warnings ?? []);
28
+ const verdict = result.valid ? 'VALID' : 'INVALID';
29
+ const profile = result.profileDetected ? ` (profile ${result.profileDetected})` : '';
30
+ const schematron = result.schematronVersion
31
+ ? `, checked against Schematron ${result.schematronVersion}`
32
+ : '';
33
+ const header = `Validation: ${verdict}. Format ${result.format}${profile}${schematron}. ` +
34
+ `${plural(errors.length, 'error')}, ${plural(warnings.length, 'warning')}.`;
35
+ return [header, ...listSection('Errors', errors), ...listSection('Warnings', warnings)].join('\n');
36
+ }
@@ -0,0 +1,17 @@
1
+ import { checkAccountOutputShape } from '../schema.js';
2
+ import { runCheckAccount } from './shared.js';
3
+ export function registerCheckAccount(server, deps) {
4
+ server.registerTool('beliq_check_account', {
5
+ title: 'beliq: Check Account',
6
+ description: 'Verify the configured beliq API key and report the plan and remaining quota. Calls GET /v1/me, which draws no quota. Use it as a connection and credential smoke test.',
7
+ inputSchema: {},
8
+ outputSchema: checkAccountOutputShape,
9
+ annotations: {
10
+ title: 'beliq: Check Account',
11
+ readOnlyHint: true,
12
+ destructiveHint: false,
13
+ idempotentHint: true,
14
+ openWorldHint: true,
15
+ },
16
+ }, async () => runCheckAccount(deps));
17
+ }
@@ -0,0 +1,7 @@
1
+ import { registerValidate } from './validate.js';
2
+ import { registerCheckAccount } from './checkAccount.js';
3
+ /** Register every beliq tool on the server with the given dependencies. */
4
+ export function registerAllTools(server, deps) {
5
+ registerValidate(server, deps);
6
+ registerCheckAccount(server, deps);
7
+ }
@@ -0,0 +1,105 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { BeliqApiError } from '@beliq/sdk';
3
+ import { summarizeValidation } from '../summary.js';
4
+ function errorResult(message) {
5
+ return { content: [{ type: 'text', text: message }], isError: true };
6
+ }
7
+ function nonEmpty(value) {
8
+ return typeof value === 'string' && value.trim().length > 0;
9
+ }
10
+ /** A bad tool input that should come back as a normal (non-fatal) tool error. */
11
+ class BeliqInputError extends Error {
12
+ }
13
+ /**
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.
17
+ */
18
+ async function resolveDocument(input) {
19
+ const hasDoc = nonEmpty(input.document);
20
+ const hasPath = nonEmpty(input.documentPath);
21
+ if (hasDoc === hasPath) {
22
+ throw new BeliqInputError('Provide exactly one of document (XML text) or documentPath (a file path).');
23
+ }
24
+ if (hasDoc)
25
+ return input.document;
26
+ return readFile(input.documentPath.trim());
27
+ }
28
+ /**
29
+ * Validate one document and shape the MCP result: a human-readable verdict in
30
+ * the text content plus the full structured result. Input and API errors come
31
+ * back as isError so the model can self-correct; only unexpected faults throw.
32
+ */
33
+ export async function runValidate(input, deps) {
34
+ let document;
35
+ try {
36
+ document = await resolveDocument(input);
37
+ }
38
+ catch (err) {
39
+ if (err instanceof BeliqInputError)
40
+ return errorResult(err.message);
41
+ return errorResult(`Could not read the document: ${err.message}`);
42
+ }
43
+ try {
44
+ const result = await deps.client.validate(document, {
45
+ format: input.format,
46
+ franceCtc: input.franceCtc,
47
+ });
48
+ const errors = result.errors ?? [];
49
+ const warnings = result.warnings ?? [];
50
+ return {
51
+ content: [{ type: 'text', text: summarizeValidation(result) }],
52
+ structuredContent: {
53
+ valid: result.valid,
54
+ format: result.format,
55
+ profileDetected: result.profileDetected,
56
+ schematronVersion: result.schematronVersion,
57
+ errorCount: errors.length,
58
+ warningCount: warnings.length,
59
+ errors,
60
+ warnings,
61
+ },
62
+ };
63
+ }
64
+ catch (err) {
65
+ if (err instanceof BeliqApiError) {
66
+ const code = err.code ? ` (${err.code})` : '';
67
+ return errorResult(`beliq API error ${err.status}${code}: ${err.message}`);
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+ /**
73
+ * Verify the configured key against GET /v1/me, which draws no quota. Reports
74
+ * validity rather than throwing, so a rejected key is a normal (non-error)
75
+ * result the model can act on.
76
+ */
77
+ export async function runCheckAccount(deps) {
78
+ const reply = (r) => ({
79
+ content: [{ type: 'text', text: r.message }],
80
+ structuredContent: { ...r },
81
+ });
82
+ try {
83
+ const account = await deps.client.me();
84
+ const plan = account.plan?.name ?? undefined;
85
+ const quota = account.quota;
86
+ const quotaText = quota
87
+ ? ` Quota: ${quota.used}/${quota.limit} used, ${quota.remaining} remaining.`
88
+ : '';
89
+ return reply({
90
+ ok: true,
91
+ status: 200,
92
+ plan,
93
+ message: `beliq credentials valid; the API key was accepted.${plan ? ` Plan: ${plan}.` : ''}${quotaText}`,
94
+ });
95
+ }
96
+ catch (err) {
97
+ if (err instanceof BeliqApiError) {
98
+ const message = err.status === 401
99
+ ? 'beliq API key rejected (401). Check the key set in BELIQ_API_KEY.'
100
+ : `Credential check failed (${err.status}): ${err.message}`;
101
+ return reply({ ok: false, status: err.status, message });
102
+ }
103
+ throw err;
104
+ }
105
+ }
@@ -0,0 +1,23 @@
1
+ import { validateInputShape, validateOutputShape } from '../schema.js';
2
+ import { runValidate } from './shared.js';
3
+ const description = [
4
+ 'Validate an EU electronic invoice against authority-pinned, drift-checked rules and report whether it is compliant.',
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 verdict (valid or not), 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.',
7
+ 'beliq validates the compliant document; transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.',
8
+ ].join(' ');
9
+ export function registerValidate(server, deps) {
10
+ server.registerTool('beliq_validate_einvoice', {
11
+ title: 'beliq: Validate E-Invoice',
12
+ description,
13
+ inputSchema: validateInputShape,
14
+ outputSchema: validateOutputShape,
15
+ annotations: {
16
+ title: 'beliq: Validate E-Invoice',
17
+ readOnlyHint: true,
18
+ destructiveHint: false,
19
+ idempotentHint: true,
20
+ openWorldHint: true,
21
+ },
22
+ }, async (args) => runValidate(args, deps));
23
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "beliq-mcp",
3
+ "version": "0.1.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.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "author": {
8
+ "name": "beliq",
9
+ "email": "hello@beliq.eu"
10
+ },
11
+ "homepage": "https://beliq.eu",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/beliq-eu/beliq-mcp.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/beliq-eu/beliq-mcp/issues"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "beliq",
23
+ "e-invoice",
24
+ "einvoice",
25
+ "xrechnung",
26
+ "zugferd",
27
+ "factur-x",
28
+ "peppol",
29
+ "en16931",
30
+ "validation",
31
+ "compliance"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20.15"
35
+ },
36
+ "bin": {
37
+ "beliq-mcp": "dist/index.js"
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "publishConfig": {
43
+ "provenance": true
44
+ },
45
+ "scripts": {
46
+ "clean": "rm -rf dist",
47
+ "build": "npm run clean && tsc",
48
+ "dev": "tsc --watch",
49
+ "start": "node dist/index.js",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "eslint src",
52
+ "lint:fix": "eslint src --fix",
53
+ "test": "vitest run --exclude test/integration.test.ts",
54
+ "test:integration": "vitest run test/integration.test.ts",
55
+ "scrub:check": "bash scripts/scrub-check.sh",
56
+ "prepublishOnly": "npm run build"
57
+ },
58
+ "dependencies": {
59
+ "@beliq/sdk": "^0.1.0",
60
+ "@modelcontextprotocol/sdk": "^1.29.0",
61
+ "zod": "^4.4.3"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^20.14.0",
65
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
66
+ "@typescript-eslint/parser": "^7.18.0",
67
+ "eslint": "^8.57.0",
68
+ "typescript": "^5.5.4",
69
+ "vitest": "^2.1.8"
70
+ }
71
+ }