rechnungsapi-mcp 0.2.1 → 0.4.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 +1 -1
- package/README.md +67 -14
- package/dist/{chunk-MIISOIDM.js → chunk-SPJV6ABL.js} +56 -20
- package/dist/chunk-SPJV6ABL.js.map +1 -0
- package/dist/http.js +166 -22
- package/dist/http.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +28 -10
- package/dist/chunk-MIISOIDM.js.map +0 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
# rechnungsapi-mcp
|
|
2
2
|
|
|
3
|
-
MCP (Model Context Protocol) server
|
|
3
|
+
The official **[RechnungsAPI](https://rechnungsapi.de)** MCP (Model Context Protocol) server — the ZUGFeRD & XRechnung API from [rechnungsapi.de](https://rechnungsapi.de) for AI agents. It lets Claude and other MCP-compatible agents create, validate and analyze e-invoices directly from a conversation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You need a RechnungsAPI account and API token to use it.
|
|
6
|
+
|
|
7
|
+
- **Hosted by RechnungsAPI:** `https://mcp.rechnungsapi.de/mcp` — nothing to install, you only send your own token (see [What users configure, once it's hosted](#what-users-configure-once-its-hosted)).
|
|
8
|
+
- **Run it yourself:** `npx -y rechnungsapi-mcp` (see [Setup](#setup)).
|
|
9
|
+
|
|
10
|
+
[Website](https://rechnungsapi.de) · [API documentation](https://rechnungsapi.de/api-docs) · [SDK for developers](https://www.npmjs.com/package/rechnungsapi-sdk) · [Support](mailto:support@rechnungsapi.de)
|
|
11
|
+
|
|
12
|
+
Built on [`rechnungsapi-sdk`](https://www.npmjs.com/package/rechnungsapi-sdk).
|
|
13
|
+
|
|
14
|
+
## Documentation
|
|
15
|
+
|
|
16
|
+
- **[API documentation](https://rechnungsapi.de/api-docs)** — every endpoint these tools call, with request, response and error codes (English and German)
|
|
17
|
+
- **[MCP server & SDK guide](https://rechnungsapi.de/api-docs#mcp-sdk)** — how to connect AI clients to RechnungsAPI's hosted MCP server
|
|
18
|
+
- [Authentication](https://rechnungsapi.de/api-docs#authentication) — where your API token comes from and how it is sent
|
|
19
|
+
- [Invoice object reference](https://rechnungsapi.de/api-docs#invoice-object) — the fields the `create_*` tools expect in `invoice` (EN 16931 business terms)
|
|
20
|
+
- [Errors](https://rechnungsapi.de/api-docs#errors) — what the HTTP status codes and validation results mean
|
|
6
21
|
|
|
7
22
|
## Setup
|
|
8
23
|
|
|
9
|
-
Get your API token from
|
|
24
|
+
Get your API token from your RechnungsAPI profile — [sign in](https://rechnungsapi.de/login) (or [sign up](https://rechnungsapi.de/register)) and open **Profile**; see [Authentication](https://rechnungsapi.de/api-docs#authentication) — then add the server to your MCP client's config. The local (`npx`) setup below needs Node.js 20 or newer.
|
|
10
25
|
|
|
11
26
|
**Claude Code** (`.mcp.json` in your project, or via `claude mcp add`):
|
|
12
27
|
|
|
@@ -31,12 +46,12 @@ Get your API token from the [RechnungsAPI dashboard](https://rechnungsapi.de), t
|
|
|
31
46
|
| Variable | Required | Description |
|
|
32
47
|
|---|---|---|
|
|
33
48
|
| `RECHNUNGSAPI_TOKEN` | Yes | Your RechnungsAPI Bearer token |
|
|
34
|
-
| `RECHNUNGSAPI_BASE_URL` | No | Override the gateway base URL (e.g.
|
|
49
|
+
| `RECHNUNGSAPI_BASE_URL` | No | Override the gateway base URL (e.g. a staging or self-hosted gateway) |
|
|
35
50
|
| `RECHNUNGSAPI_V2_BASE_URL` | No | Override the v2 analyzer host |
|
|
36
51
|
|
|
37
52
|
## Self-hosting (Streamable HTTP)
|
|
38
53
|
|
|
39
|
-
By default this runs as a local `stdio` process, spawned per-user by their own AI client — that's what the setup above does, and it's the standard way MCP servers work. If instead you want to run **one shared, always-on server** that many users connect to remotely (no local install on their end at all — just a URL
|
|
54
|
+
By default this runs as a local `stdio` process, spawned per-user by their own AI client — that's what the setup above does, and it's the standard way MCP servers work. If instead you want to run **one shared, always-on server** that many users connect to remotely (no local install on their end at all — just a URL), use the HTTP mode instead.
|
|
40
55
|
|
|
41
56
|
The key architectural difference: `stdio` mode reads one fixed `RECHNUNGSAPI_TOKEN` from the environment at startup and reuses it for the whole process's life. HTTP mode instead reads **each request's own token** from its `Authorization: Bearer <token>` header, and builds a fresh, isolated client per request — so many different customers can safely share the same running server, each authenticated as themselves, never seeing each other's data.
|
|
42
57
|
|
|
@@ -51,13 +66,24 @@ Or via Docker:
|
|
|
51
66
|
|
|
52
67
|
```bash
|
|
53
68
|
docker build -t rechnungsapi-mcp .
|
|
54
|
-
docker run -p 3939:3939 rechnungsapi-mcp
|
|
69
|
+
docker run -p 127.0.0.1:3939:3939 rechnungsapi-mcp
|
|
55
70
|
```
|
|
56
71
|
|
|
57
|
-
Or
|
|
72
|
+
Or with Docker Compose: `docker compose up -d` builds the image from this repo's [`Dockerfile`](Dockerfile) (see [`docker-compose.yml`](docker-compose.yml)).
|
|
73
|
+
|
|
74
|
+
Or as a **Portainer stack**: paste [`deploy/portainer-stack.yml`](deploy/portainer-stack.yml) into Stacks → Add stack → Web editor and deploy. It needs no build — the container installs the published package from npm when it starts. To upgrade, change the version in its `command` and redeploy. (Use this file rather than `docker-compose.yml` in the web editor: there is no build context there for `build: .` to use.)
|
|
58
75
|
|
|
59
76
|
No `RECHNUNGSAPI_TOKEN` is configured on the server itself in this mode — each caller supplies their own.
|
|
60
77
|
|
|
78
|
+
### Server settings (HTTP mode)
|
|
79
|
+
|
|
80
|
+
| Variable | Default | Description |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `PORT` | `3939` | Port to listen on |
|
|
83
|
+
| `MAX_BODY_MB` | `64` | Largest accepted request body. Invoices arrive as base64 PDFs, so this is generous; anything larger is answered with `413`. |
|
|
84
|
+
| `RECHNUNGSAPI_BASE_URL` | production gateway | Override the gateway base URL |
|
|
85
|
+
| `RECHNUNGSAPI_V2_BASE_URL` | production v2 host | Override the v2 analyzer host |
|
|
86
|
+
|
|
61
87
|
### Endpoints
|
|
62
88
|
|
|
63
89
|
| Endpoint | Description |
|
|
@@ -67,7 +93,9 @@ No `RECHNUNGSAPI_TOKEN` is configured on the server itself in this mode — each
|
|
|
67
93
|
|
|
68
94
|
### Putting it behind a domain
|
|
69
95
|
|
|
70
|
-
See [`deploy/nginx-mcp.conf`](deploy/nginx-mcp.conf) for a ready-to-use reverse proxy config (e.g. for `mcp.rechnungsapi.de`), including the settings needed so nginx doesn't buffer/break the SSE streaming responses. Pair it with `certbot --nginx -d mcp.rechnungsapi.de` for HTTPS — required in practice, since real API tokens travel in every request.
|
|
96
|
+
See [`deploy/nginx-mcp.conf`](deploy/nginx-mcp.conf) for a ready-to-use reverse proxy config (e.g. for `mcp.rechnungsapi.de`), including the settings needed so nginx doesn't buffer/break the SSE streaming responses. Pair it with `certbot --nginx -d mcp.rechnungsapi.de` for HTTPS — required in practice, since real API tokens travel in every request. Also redirect plain HTTP to HTTPS (Nginx Proxy Manager: enable **Force SSL** on the proxy host), so a mistyped `http://` URL never sends a token unencrypted.
|
|
97
|
+
|
|
98
|
+
Two proxy settings matter for real invoices. First, the body limit: nginx refuses request bodies over 1 MB by default (`413`, before the request reaches this server), so the shipped config sets `client_max_body_size 64m` to match `MAX_BODY_MB` — in Nginx Proxy Manager, add the same line under the proxy host's **Advanced** tab if you ever see those `413`s. Second, the timeout: analysing a large scan can take a while, so the shipped config raises `proxy_read_timeout`; clients can also use the async tools for big files.
|
|
71
99
|
|
|
72
100
|
### What users configure, once it's hosted
|
|
73
101
|
|
|
@@ -85,25 +113,50 @@ See [`deploy/nginx-mcp.conf`](deploy/nginx-mcp.conf) for a ready-to-use reverse
|
|
|
85
113
|
|
|
86
114
|
No install, no `npx`, nothing running on their machine.
|
|
87
115
|
|
|
116
|
+
### Troubleshooting a client that "can't reach" the server
|
|
117
|
+
|
|
118
|
+
Every request is logged, so start with the container logs while the client tries to connect:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
docker logs -f rechnungsapi-mcp
|
|
122
|
+
# [http] POST /mcp -> 200 12ms auth=yes rpc=initialize proto=- ua="..."
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| What you see | What it means |
|
|
126
|
+
|---|---|
|
|
127
|
+
| Nothing at all | The request never arrived — check DNS, the reverse proxy, and firewalls. `curl https://your-host/health` from outside should return `{"ok":true,...}`. |
|
|
128
|
+
| `401 ... auth=no` | The client isn't sending an `Authorization` header — the connector's auth mode is probably set to OAuth/none instead of a Bearer/API-key header. |
|
|
129
|
+
| `401 ... auth=yes` | The header arrived but the token is empty or wrong — copy it again from your profile (see [Authentication](https://rechnungsapi.de/api-docs#authentication)). The `Bearer ` prefix is optional — a bare token is accepted. |
|
|
130
|
+
| `GET /mcp -> 405` | Expected. This server doesn't offer the optional server-push SSE stream; compliant clients continue over `POST`. |
|
|
131
|
+
| `OPTIONS /mcp -> 204` | Expected — a browser-based client's CORS preflight. |
|
|
132
|
+
| `POST /mcp -> 413` | The request body exceeded `MAX_BODY_MB`. |
|
|
133
|
+
| The client gets `413 Request Entity Too Large` (an HTML page) but the log shows nothing | The reverse proxy refused the body before it reached this server. nginx's default limit is 1 MB — set `client_max_body_size 64m;` (see [`deploy/nginx-mcp.conf`](deploy/nginx-mcp.conf)). |
|
|
134
|
+
| `POST /mcp -> 400` | The body wasn't valid JSON (JSON-RPC parse error `-32700`). |
|
|
135
|
+
|
|
136
|
+
Log lines record JSON-RPC method names only (e.g. `rpc=tools/call`), never arguments, so invoice data and tokens don't end up in your logs.
|
|
137
|
+
|
|
88
138
|
## Tools
|
|
89
139
|
|
|
90
140
|
| Tool | Description |
|
|
91
141
|
|---|---|
|
|
92
142
|
| `create_zugferd_invoice` | Create a ZUGFeRD PDF/A-3 from structured invoice JSON + a visual PDF |
|
|
93
|
-
| `
|
|
94
|
-
| `create_zugferd_pdf` | Embed an existing
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
143
|
+
| `create_xrechnung` | Create an XRechnung XML from structured invoice JSON |
|
|
144
|
+
| `create_zugferd_pdf` | Embed an existing XRechnung XML into a visual PDF |
|
|
145
|
+
| `extract_xrechnung_from_zugferd` | Extract the embedded XRechnung XML from a ZUGFeRD PDF as JSON |
|
|
146
|
+
| `validate_xrechnung_xml` | Validate an XRechnung XML against schema and business rules |
|
|
97
147
|
| `validate_zugferd_pdf` | Validate a ZUGFeRD PDF's embedded XML |
|
|
98
148
|
| `analyze_pdf_invoice` | Extract structured invoice JSON from a scanned/PDF invoice |
|
|
99
149
|
| `analyze_pdf_invoice_async_submit` / `analyze_pdf_invoice_async_status` | Async analysis for large files |
|
|
100
150
|
| `create_zugferd_from_pdf` | Convert a PDF/scan directly into a validated ZUGFeRD PDF |
|
|
101
151
|
|
|
152
|
+
Each tool maps to one RechnungsAPI endpoint — the [API documentation](https://rechnungsapi.de/api-docs#endpoints) shows its request and response.
|
|
153
|
+
|
|
102
154
|
## Development
|
|
103
155
|
|
|
104
156
|
```bash
|
|
105
157
|
yarn install
|
|
106
158
|
yarn typecheck
|
|
159
|
+
yarn test # builds first, then runs the stdio and HTTP suites against a stub API
|
|
107
160
|
yarn build
|
|
108
161
|
```
|
|
109
162
|
|
|
@@ -113,8 +166,8 @@ Test locally with the [MCP Inspector](https://github.com/modelcontextprotocol/in
|
|
|
113
166
|
RECHNUNGSAPI_TOKEN=your-token yarn inspect
|
|
114
167
|
```
|
|
115
168
|
|
|
116
|
-
> **Note:** `@modelcontextprotocol/sdk` is pinned to
|
|
169
|
+
> **Note:** `@modelcontextprotocol/sdk` is pinned to an exact version rather than `^1.x`. This server is reachable from the internet, so the library only changes after the test suite has run against the new version. Releases before 1.26.0 have published security advisories (they show up in `npm audit`), so don't go back to an older one.
|
|
117
170
|
|
|
118
171
|
## License
|
|
119
172
|
|
|
120
|
-
MIT
|
|
173
|
+
MIT © RechnungsAPI · [rechnungsapi.de](https://rechnungsapi.de) · support@rechnungsapi.de
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
// src/tools.ts
|
|
4
4
|
import { RechnungsApiError } from "rechnungsapi-sdk";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
var invoiceJsonSchema = z.
|
|
7
|
-
|
|
6
|
+
var invoiceJsonSchema = z.record(z.string(), z.unknown()).describe(
|
|
7
|
+
"Structured invoice data as a JSON object: header, seller, buyer, line items, VAT, totals and payment instructions (EN 16931 business terms). Field reference: https://rechnungsapi.de/api-docs#invoice-object"
|
|
8
|
+
);
|
|
9
|
+
var transportSchema = z.record(z.string(), z.unknown()).optional().describe(
|
|
10
|
+
"Optional email delivery: when set, the generated invoice is also sent by email in the same call. Options: https://rechnungsapi.de/api-docs#email-transport"
|
|
11
|
+
);
|
|
8
12
|
async function asToolResult(fn) {
|
|
9
13
|
try {
|
|
10
14
|
const result = await fn();
|
|
@@ -40,50 +44,50 @@ function registerTools(server, client) {
|
|
|
40
44
|
async ({ invoice, invoicePdf64, transport }) => asToolResult(() => client.createZugferdFromJson(invoice, invoicePdf64, { transport }))
|
|
41
45
|
);
|
|
42
46
|
server.registerTool(
|
|
43
|
-
"
|
|
47
|
+
"create_xrechnung",
|
|
44
48
|
{
|
|
45
|
-
title: "Create
|
|
46
|
-
description: "Create an
|
|
49
|
+
title: "Create XRechnung (XML)",
|
|
50
|
+
description: "Create an XRechnung XML document from structured invoice JSON.",
|
|
47
51
|
inputSchema: {
|
|
48
52
|
invoice: invoiceJsonSchema,
|
|
49
53
|
transport: transportSchema
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
|
-
async ({ invoice, transport }) => asToolResult(() => client.
|
|
56
|
+
async ({ invoice, transport }) => asToolResult(() => client.createXRechnungFromJson(invoice, { transport }))
|
|
53
57
|
);
|
|
54
58
|
server.registerTool(
|
|
55
59
|
"create_zugferd_pdf",
|
|
56
60
|
{
|
|
57
|
-
title: "Embed
|
|
58
|
-
description: "Embed an existing
|
|
61
|
+
title: "Embed XRechnung XML into a PDF",
|
|
62
|
+
description: "Embed an existing XRechnung XML document into a visual PDF to produce a ZUGFeRD PDF.",
|
|
59
63
|
inputSchema: {
|
|
60
64
|
invoicePdf64: z.string().describe("Base64-encoded visual PDF"),
|
|
61
|
-
|
|
65
|
+
xrechnungXml: z.string().describe("XRechnung XML to embed")
|
|
62
66
|
}
|
|
63
67
|
},
|
|
64
|
-
async ({ invoicePdf64,
|
|
68
|
+
async ({ invoicePdf64, xrechnungXml }) => asToolResult(() => client.createZugferdPdf(invoicePdf64, xrechnungXml))
|
|
65
69
|
);
|
|
66
70
|
server.registerTool(
|
|
67
|
-
"
|
|
71
|
+
"extract_xrechnung_from_zugferd",
|
|
68
72
|
{
|
|
69
|
-
title: "Extract
|
|
73
|
+
title: "Extract XRechnung from ZUGFeRD PDF",
|
|
70
74
|
description: "Extract the embedded XRechnung XML from a ZUGFeRD PDF and return it as structured JSON.",
|
|
71
75
|
inputSchema: {
|
|
72
76
|
zugferd64: z.string().describe("Base64-encoded ZUGFeRD PDF")
|
|
73
77
|
}
|
|
74
78
|
},
|
|
75
|
-
async ({ zugferd64 }) => asToolResult(() => client.
|
|
79
|
+
async ({ zugferd64 }) => asToolResult(() => client.extractXRechnungFromZugferd(zugferd64))
|
|
76
80
|
);
|
|
77
81
|
server.registerTool(
|
|
78
|
-
"
|
|
82
|
+
"validate_xrechnung_xml",
|
|
79
83
|
{
|
|
80
|
-
title: "Validate
|
|
81
|
-
description: "Validate an
|
|
84
|
+
title: "Validate XRechnung XML",
|
|
85
|
+
description: "Validate an XRechnung XML document against schema and business rules.",
|
|
82
86
|
inputSchema: {
|
|
83
|
-
xml: z.string().describe("The
|
|
87
|
+
xml: z.string().describe("The XRechnung XML content to validate")
|
|
84
88
|
}
|
|
85
89
|
},
|
|
86
|
-
async ({ xml }) => asToolResult(() => client.
|
|
90
|
+
async ({ xml }) => asToolResult(() => client.validateXRechnungXml(xml))
|
|
87
91
|
);
|
|
88
92
|
server.registerTool(
|
|
89
93
|
"validate_zugferd_pdf",
|
|
@@ -146,7 +150,39 @@ function registerTools(server, client) {
|
|
|
146
150
|
);
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
// src/version.ts
|
|
154
|
+
import { createRequire } from "module";
|
|
155
|
+
var require2 = createRequire(import.meta.url);
|
|
156
|
+
var SERVER_NAME = "rechnungsapi-mcp";
|
|
157
|
+
var SERVER_TITLE = "RechnungsAPI";
|
|
158
|
+
var SERVER_WEBSITE = "https://rechnungsapi.de";
|
|
159
|
+
var SERVER_DOCS = "https://rechnungsapi.de/api-docs#mcp-sdk";
|
|
160
|
+
var SERVER_DESCRIPTION = "RechnungsAPI (rechnungsapi.de), the ZUGFeRD & XRechnung API: create, validate and analyze e-invoices.";
|
|
161
|
+
var SERVER_INSTRUCTIONS = "RechnungsAPI (rechnungsapi.de) creates, validates and analyzes German e-invoices: ZUGFeRD (PDF/A-3) and XRechnung (XML). PDFs and images are passed as base64 strings. For large scanned invoices, submit them with analyze_pdf_invoice_async_submit and poll analyze_pdf_invoice_async_status. Invoice fields, endpoints and error codes are documented at https://rechnungsapi.de/api-docs";
|
|
162
|
+
var SERVER_VERSION = (() => {
|
|
163
|
+
try {
|
|
164
|
+
return require2("../package.json").version;
|
|
165
|
+
} catch {
|
|
166
|
+
return "0.0.0";
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
var SERVER_INFO = {
|
|
170
|
+
name: SERVER_NAME,
|
|
171
|
+
title: SERVER_TITLE,
|
|
172
|
+
version: SERVER_VERSION,
|
|
173
|
+
websiteUrl: SERVER_WEBSITE,
|
|
174
|
+
description: SERVER_DESCRIPTION
|
|
175
|
+
};
|
|
176
|
+
|
|
149
177
|
export {
|
|
150
|
-
registerTools
|
|
178
|
+
registerTools,
|
|
179
|
+
SERVER_NAME,
|
|
180
|
+
SERVER_TITLE,
|
|
181
|
+
SERVER_WEBSITE,
|
|
182
|
+
SERVER_DOCS,
|
|
183
|
+
SERVER_DESCRIPTION,
|
|
184
|
+
SERVER_INSTRUCTIONS,
|
|
185
|
+
SERVER_VERSION,
|
|
186
|
+
SERVER_INFO
|
|
151
187
|
};
|
|
152
|
-
//# sourceMappingURL=chunk-
|
|
188
|
+
//# sourceMappingURL=chunk-SPJV6ABL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools.ts","../src/version.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { RechnungsApiClient, RechnungsApiError } from \"rechnungsapi-sdk\"\nimport { z } from \"zod\"\n\nconst invoiceJsonSchema = z\n .record(z.string(), z.unknown())\n .describe(\n \"Structured invoice data as a JSON object: header, seller, buyer, line items, VAT, totals and payment instructions \" +\n \"(EN 16931 business terms). Field reference: https://rechnungsapi.de/api-docs#invoice-object\",\n )\n\nconst transportSchema = z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"Optional email delivery: when set, the generated invoice is also sent by email in the same call. \" +\n \"Options: https://rechnungsapi.de/api-docs#email-transport\",\n )\n\n/** Wraps a client call so thrown RechnungsApiError/generic errors become MCP tool errors instead of crashing the server. */\nasync function asToolResult(fn: () => Promise<unknown>) {\n try {\n const result = await fn()\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] }\n } catch (error) {\n if (error instanceof RechnungsApiError) {\n return {\n isError: true,\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ status: error.status, message: error.message, body: error.body }, null, 2),\n },\n ],\n }\n }\n const message = error instanceof Error ? error.message : String(error)\n return { isError: true, content: [{ type: \"text\" as const, text: message }] }\n }\n}\n\nexport function registerTools(server: McpServer, client: RechnungsApiClient): void {\n server.registerTool(\n \"create_zugferd_invoice\",\n {\n title: \"Create ZUGFeRD invoice\",\n description:\n \"Create a ZUGFeRD PDF/A-3 e-invoice (visual PDF with embedded XRechnung XML) from structured invoice JSON plus a visual PDF template.\",\n inputSchema: {\n invoice: invoiceJsonSchema,\n invoicePdf64: z.string().describe(\"Base64-encoded visual PDF the XML will be embedded into\"),\n transport: transportSchema,\n },\n },\n async ({ invoice, invoicePdf64, transport }) =>\n asToolResult(() => client.createZugferdFromJson(invoice, invoicePdf64, { transport })),\n )\n\n server.registerTool(\n \"create_xrechnung\",\n {\n title: \"Create XRechnung (XML)\",\n description: \"Create an XRechnung XML document from structured invoice JSON.\",\n inputSchema: {\n invoice: invoiceJsonSchema,\n transport: transportSchema,\n },\n },\n async ({ invoice, transport }) => asToolResult(() => client.createXRechnungFromJson(invoice, { transport })),\n )\n\n server.registerTool(\n \"create_zugferd_pdf\",\n {\n title: \"Embed XRechnung XML into a PDF\",\n description: \"Embed an existing XRechnung XML document into a visual PDF to produce a ZUGFeRD PDF.\",\n inputSchema: {\n invoicePdf64: z.string().describe(\"Base64-encoded visual PDF\"),\n xrechnungXml: z.string().describe(\"XRechnung XML to embed\"),\n },\n },\n async ({ invoicePdf64, xrechnungXml }) => asToolResult(() => client.createZugferdPdf(invoicePdf64, xrechnungXml)),\n )\n\n server.registerTool(\n \"extract_xrechnung_from_zugferd\",\n {\n title: \"Extract XRechnung from ZUGFeRD PDF\",\n description: \"Extract the embedded XRechnung XML from a ZUGFeRD PDF and return it as structured JSON.\",\n inputSchema: {\n zugferd64: z.string().describe(\"Base64-encoded ZUGFeRD PDF\"),\n },\n },\n async ({ zugferd64 }) => asToolResult(() => client.extractXRechnungFromZugferd(zugferd64)),\n )\n\n server.registerTool(\n \"validate_xrechnung_xml\",\n {\n title: \"Validate XRechnung XML\",\n description: \"Validate an XRechnung XML document against schema and business rules.\",\n inputSchema: {\n xml: z.string().describe(\"The XRechnung XML content to validate\"),\n },\n },\n async ({ xml }) => asToolResult(() => client.validateXRechnungXml(xml)),\n )\n\n server.registerTool(\n \"validate_zugferd_pdf\",\n {\n title: \"Validate ZUGFeRD PDF\",\n description: \"Validate a ZUGFeRD PDF's embedded XML content.\",\n inputSchema: {\n zugferdFile64: z.string().describe(\"Base64-encoded ZUGFeRD PDF\"),\n comparePDF2XML: z.boolean().optional().describe(\"Also cross-check the embedded XML against the visual PDF content\"),\n },\n },\n async ({ zugferdFile64, comparePDF2XML }) =>\n asToolResult(() => client.validateZugferdPdf(zugferdFile64, { comparePDF2XML })),\n )\n\n server.registerTool(\n \"analyze_pdf_invoice\",\n {\n title: \"Analyze a PDF/scanned invoice\",\n description:\n \"Extract structured invoice JSON from a scanned or digital PDF/PNG/JPEG/TIFF invoice using the high-accuracy analyzer.\",\n inputSchema: {\n pdfBase64: z.string().describe(\"Base64-encoded PDF, PNG, JPEG, or TIFF invoice\"),\n withLineItems: z.boolean().optional().describe(\"Also extract individual line items\"),\n },\n },\n async ({ pdfBase64, withLineItems }) =>\n asToolResult(() => client.analyzePdfInvoiceV2(pdfBase64, withLineItems ?? false)),\n )\n\n server.registerTool(\n \"analyze_pdf_invoice_async_submit\",\n {\n title: \"Submit a large PDF invoice for async analysis\",\n description:\n \"Submit a large scanned/PDF invoice for asynchronous analysis (for files too large for the synchronous analyzer). \" +\n \"Returns a job_id — poll it with analyze_pdf_invoice_async_status.\",\n inputSchema: {\n pdfBase64: z.string().describe(\"Base64-encoded PDF, PNG, JPEG, or TIFF invoice\"),\n withLineItems: z.boolean().optional().describe(\"Also extract individual line items\"),\n },\n },\n async ({ pdfBase64, withLineItems }) =>\n asToolResult(() => client.analyzePdfInvoiceAsync(pdfBase64, withLineItems)),\n )\n\n server.registerTool(\n \"analyze_pdf_invoice_async_status\",\n {\n title: \"Check async PDF analysis status\",\n description: \"Poll the status of an asynchronous PDF invoice analysis job submitted via analyze_pdf_invoice_async_submit.\",\n inputSchema: {\n jobId: z.string().describe(\"The job_id returned by analyze_pdf_invoice_async_submit\"),\n },\n },\n async ({ jobId }) => asToolResult(() => client.getAnalysisStatus(jobId)),\n )\n\n server.registerTool(\n \"create_zugferd_from_pdf\",\n {\n title: \"Convert a PDF/scan directly into a ZUGFeRD PDF\",\n description:\n \"Convert a PDF or scanned image invoice directly into a validated ZUGFeRD PDF/A-3 in one call \" +\n \"(analyzes, validates, and embeds in a single step).\",\n inputSchema: {\n pdfBase64: z.string().describe(\"Base64-encoded PDF, PNG, JPEG, or TIFF invoice\"),\n fileName: z.string().optional().describe(\"Original filename, used to name the resulting PDF\"),\n },\n },\n async ({ pdfBase64, fileName }) => asToolResult(() => client.createZugferdFromPdf(pdfBase64, fileName)),\n )\n}\n","import { createRequire } from \"node:module\"\n\nconst require = createRequire(import.meta.url)\n\nexport const SERVER_NAME = \"rechnungsapi-mcp\"\nexport const SERVER_TITLE = \"RechnungsAPI\"\nexport const SERVER_WEBSITE = \"https://rechnungsapi.de\"\nexport const SERVER_DOCS = \"https://rechnungsapi.de/api-docs#mcp-sdk\"\nexport const SERVER_DESCRIPTION =\n \"RechnungsAPI (rechnungsapi.de), the ZUGFeRD & XRechnung API: create, validate and analyze e-invoices.\"\n\n/**\n * Sent to every client in the initialize response. MCP clients may pass it on to the model, so it\n * says what the server is for and where the full API documentation lives.\n */\nexport const SERVER_INSTRUCTIONS =\n \"RechnungsAPI (rechnungsapi.de) creates, validates and analyzes German e-invoices: ZUGFeRD (PDF/A-3) and XRechnung (XML). \" +\n \"PDFs and images are passed as base64 strings. For large scanned invoices, submit them with analyze_pdf_invoice_async_submit \" +\n \"and poll analyze_pdf_invoice_async_status. Invoice fields, endpoints and error codes are documented at https://rechnungsapi.de/api-docs\"\n\nexport const SERVER_VERSION: string = (() => {\n try {\n return (require(\"../package.json\") as { version: string }).version\n } catch {\n return \"0.0.0\"\n }\n})()\n\n/** Who this server says it is in the MCP initialize handshake, so clients can show the brand. */\nexport const SERVER_INFO = {\n name: SERVER_NAME,\n title: SERVER_TITLE,\n version: SERVER_VERSION,\n websiteUrl: SERVER_WEBSITE,\n description: SERVER_DESCRIPTION,\n}\n"],"mappings":"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","names":["require"]}
|
package/dist/http.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
SERVER_DESCRIPTION,
|
|
4
|
+
SERVER_DOCS,
|
|
5
|
+
SERVER_INFO,
|
|
6
|
+
SERVER_INSTRUCTIONS,
|
|
7
|
+
SERVER_NAME,
|
|
8
|
+
SERVER_TITLE,
|
|
9
|
+
SERVER_VERSION,
|
|
10
|
+
SERVER_WEBSITE,
|
|
3
11
|
registerTools
|
|
4
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-SPJV6ABL.js";
|
|
5
13
|
|
|
6
14
|
// src/http.ts
|
|
7
15
|
import http from "http";
|
|
@@ -9,34 +17,138 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
9
17
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
18
|
import { RechnungsApiClient } from "rechnungsapi-sdk";
|
|
11
19
|
var PORT = Number(process.env.PORT ?? 3939);
|
|
20
|
+
var MAX_BODY_MB = Number(process.env.MAX_BODY_MB ?? 64);
|
|
21
|
+
var MAX_BODY_BYTES = (Number.isFinite(MAX_BODY_MB) && MAX_BODY_MB > 0 ? MAX_BODY_MB : 64) * 1024 * 1024;
|
|
22
|
+
var HttpError = class extends Error {
|
|
23
|
+
constructor(status, message, rpcCode) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.rpcCode = rpcCode;
|
|
27
|
+
}
|
|
28
|
+
status;
|
|
29
|
+
rpcCode;
|
|
30
|
+
};
|
|
31
|
+
var CORS_HEADERS = {
|
|
32
|
+
"access-control-allow-origin": "*",
|
|
33
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
34
|
+
"access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID",
|
|
35
|
+
"access-control-expose-headers": "Mcp-Session-Id, MCP-Protocol-Version",
|
|
36
|
+
"access-control-max-age": "86400"
|
|
37
|
+
};
|
|
12
38
|
function extractBearerToken(req) {
|
|
13
39
|
const header = req.headers["authorization"];
|
|
14
40
|
if (!header || Array.isArray(header)) return null;
|
|
15
|
-
const
|
|
16
|
-
|
|
41
|
+
const value = header.trim();
|
|
42
|
+
if (!value || /^bearer$/i.test(value)) return null;
|
|
43
|
+
const match = /^Bearer\s+(.+)$/i.exec(value);
|
|
44
|
+
const token = (match ? match[1] : value).trim();
|
|
45
|
+
return token || null;
|
|
46
|
+
}
|
|
47
|
+
function readJsonBody(req) {
|
|
48
|
+
const tooLarge = () => new HttpError(413, `Request body exceeds the ${MAX_BODY_BYTES / 1024 / 1024} MB limit`);
|
|
49
|
+
const declared = Number(req.headers["content-length"]);
|
|
50
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) return Promise.reject(tooLarge());
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let size = 0;
|
|
54
|
+
let rejected = false;
|
|
55
|
+
req.on("data", (chunk) => {
|
|
56
|
+
if (rejected) return;
|
|
57
|
+
size += chunk.length;
|
|
58
|
+
if (size > MAX_BODY_BYTES) {
|
|
59
|
+
rejected = true;
|
|
60
|
+
chunks.length = 0;
|
|
61
|
+
reject(tooLarge());
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
chunks.push(chunk);
|
|
65
|
+
});
|
|
66
|
+
req.on("end", () => {
|
|
67
|
+
if (rejected) return;
|
|
68
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
69
|
+
if (!raw) return resolve(void 0);
|
|
70
|
+
try {
|
|
71
|
+
resolve(JSON.parse(raw));
|
|
72
|
+
} catch {
|
|
73
|
+
reject(new HttpError(400, "Parse error: request body is not valid JSON", -32700));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
req.on("error", reject);
|
|
77
|
+
req.on("close", () => reject(new Error("connection closed before the request body was complete")));
|
|
78
|
+
});
|
|
17
79
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
22
|
-
return raw ? JSON.parse(raw) : void 0;
|
|
80
|
+
function routeOf(req) {
|
|
81
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
82
|
+
return path.replace(/\/+$/, "") || "/";
|
|
23
83
|
}
|
|
24
|
-
function sendJson(res, status, body) {
|
|
25
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
84
|
+
function sendJson(res, status, body, extraHeaders = {}) {
|
|
85
|
+
res.writeHead(status, { "content-type": "application/json", ...extraHeaders });
|
|
26
86
|
res.end(JSON.stringify(body));
|
|
27
87
|
}
|
|
28
|
-
|
|
88
|
+
var LANDING_HTML = `<!doctype html>
|
|
89
|
+
<html lang="en">
|
|
90
|
+
<head>
|
|
91
|
+
<meta charset="utf-8">
|
|
92
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
93
|
+
<meta name="robots" content="noindex">
|
|
94
|
+
<title>${SERVER_TITLE} MCP server</title>
|
|
95
|
+
<style>body{font:16px/1.55 system-ui,sans-serif;max-width:40rem;margin:4rem auto;padding:0 1rem;color:#1f2937}code{background:#f3f4f6;padding:.1rem .35rem;border-radius:.25rem}a{color:#2563eb}small{color:#6b7280}</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
<h1>${SERVER_TITLE} MCP server</h1>
|
|
99
|
+
<p>This is the MCP endpoint of <a href="${SERVER_WEBSITE}">${SERVER_TITLE}</a> (rechnungsapi.de), the ZUGFeRD & XRechnung API. It lets AI agents create, validate and analyze e-invoices.</p>
|
|
100
|
+
<p>MCP clients connect to <code>/mcp</code> on this host and send their own ${SERVER_TITLE} token as <code>Authorization: Bearer <token></code>.</p>
|
|
101
|
+
<ul>
|
|
102
|
+
<li><a href="${SERVER_DOCS}">How to connect: documentation</a></li>
|
|
103
|
+
<li><a href="${SERVER_WEBSITE}/api-docs">API documentation</a></li>
|
|
104
|
+
<li><a href="${SERVER_WEBSITE}">rechnungsapi.de</a></li>
|
|
105
|
+
</ul>
|
|
106
|
+
<p><small>rechnungsapi-mcp ${SERVER_VERSION} \xB7 rechnungsapi.de</small></p>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
`;
|
|
110
|
+
function sendLanding(req, res) {
|
|
111
|
+
const common = { "cache-control": "public, max-age=300", "x-content-type-options": "nosniff" };
|
|
112
|
+
if ((req.headers.accept ?? "").includes("text/html")) {
|
|
113
|
+
res.writeHead(200, {
|
|
114
|
+
...common,
|
|
115
|
+
"content-type": "text/html; charset=utf-8",
|
|
116
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
117
|
+
});
|
|
118
|
+
res.end(LANDING_HTML);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
sendJson(
|
|
122
|
+
res,
|
|
123
|
+
200,
|
|
124
|
+
{ name: SERVER_TITLE, description: SERVER_DESCRIPTION, version: SERVER_VERSION, mcp: "/mcp", health: "/health", docs: SERVER_DOCS, website: SERVER_WEBSITE },
|
|
125
|
+
common
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
function describeRpc(body) {
|
|
129
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
130
|
+
const methods = messages.map((m) => m && typeof m === "object" && "method" in m ? String(m.method) : null).filter((m) => m !== null);
|
|
131
|
+
return methods.length ? methods.join(",") : "-";
|
|
132
|
+
}
|
|
133
|
+
async function handleMcpRequest(req, res, log) {
|
|
29
134
|
const token = extractBearerToken(req);
|
|
30
135
|
if (!token) {
|
|
31
|
-
sendJson(
|
|
136
|
+
sendJson(
|
|
137
|
+
res,
|
|
138
|
+
401,
|
|
139
|
+
{ error: "Missing Authorization: Bearer <your-rechnungsapi-token> header" },
|
|
140
|
+
{ "www-authenticate": `Bearer realm="${SERVER_NAME}"` }
|
|
141
|
+
);
|
|
32
142
|
return;
|
|
33
143
|
}
|
|
144
|
+
const body = await readJsonBody(req);
|
|
145
|
+
log.rpc = describeRpc(body);
|
|
34
146
|
const client = new RechnungsApiClient({
|
|
35
147
|
apiToken: token,
|
|
36
148
|
baseUrl: process.env.RECHNUNGSAPI_BASE_URL,
|
|
37
149
|
v2BaseUrl: process.env.RECHNUNGSAPI_V2_BASE_URL
|
|
38
150
|
});
|
|
39
|
-
const mcpServer = new McpServer({
|
|
151
|
+
const mcpServer = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS });
|
|
40
152
|
registerTools(mcpServer, client);
|
|
41
153
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
42
154
|
res.on("close", () => {
|
|
@@ -44,32 +156,64 @@ async function handleMcpRequest(req, res) {
|
|
|
44
156
|
mcpServer.close();
|
|
45
157
|
});
|
|
46
158
|
await mcpServer.connect(transport);
|
|
47
|
-
const body = await readJsonBody(req);
|
|
48
159
|
await transport.handleRequest(req, res, body);
|
|
49
160
|
}
|
|
50
161
|
var server = http.createServer(async (req, res) => {
|
|
51
|
-
|
|
52
|
-
|
|
162
|
+
const startedAt = Date.now();
|
|
163
|
+
const log = {};
|
|
164
|
+
res.on("finish", () => {
|
|
165
|
+
const ua = req.headers["user-agent"] ?? "-";
|
|
166
|
+
const proto = req.headers["mcp-protocol-version"] ?? "-";
|
|
167
|
+
const auth = req.headers["authorization"] ? "yes" : "no";
|
|
168
|
+
console.log(
|
|
169
|
+
`[http] ${req.method} ${req.url} -> ${res.statusCode} ${Date.now() - startedAt}ms auth=${auth} rpc=${log.rpc ?? "-"} proto=${proto} ua="${ua}"`
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
const route = routeOf(req);
|
|
173
|
+
if (route === "/health" && (req.method === "GET" || req.method === "HEAD")) {
|
|
174
|
+
sendJson(res, 200, { ok: true, name: SERVER_NAME, version: SERVER_VERSION });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (route === "/" && (req.method === "GET" || req.method === "HEAD")) {
|
|
178
|
+
sendLanding(req, res);
|
|
53
179
|
return;
|
|
54
180
|
}
|
|
55
|
-
if (
|
|
56
|
-
sendJson(res, 404, { error: "Not found. POST to /mcp." });
|
|
181
|
+
if (route !== "/mcp") {
|
|
182
|
+
sendJson(res, 404, { error: "Not found. POST to /mcp.", docs: SERVER_DOCS });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
for (const [k, v] of Object.entries(CORS_HEADERS)) res.setHeader(k, v);
|
|
186
|
+
if (req.method === "OPTIONS") {
|
|
187
|
+
res.writeHead(204);
|
|
188
|
+
res.end();
|
|
57
189
|
return;
|
|
58
190
|
}
|
|
59
191
|
if (req.method !== "POST") {
|
|
60
|
-
sendJson(res, 405, { error: "Only POST is supported on /mcp (no server-initiated SSE stream)." });
|
|
192
|
+
sendJson(res, 405, { error: "Only POST is supported on /mcp (no server-initiated SSE stream)." }, { allow: "POST, OPTIONS" });
|
|
61
193
|
return;
|
|
62
194
|
}
|
|
63
195
|
try {
|
|
64
|
-
await handleMcpRequest(req, res);
|
|
196
|
+
await handleMcpRequest(req, res, log);
|
|
65
197
|
} catch (error) {
|
|
198
|
+
if (error instanceof HttpError) {
|
|
199
|
+
if (!res.headersSent) {
|
|
200
|
+
const body = error.rpcCode !== void 0 ? { jsonrpc: "2.0", error: { code: error.rpcCode, message: error.message }, id: null } : { error: error.message };
|
|
201
|
+
sendJson(res, error.status, body);
|
|
202
|
+
}
|
|
203
|
+
if (error.status === 413) {
|
|
204
|
+
const stop = setTimeout(() => req.destroy(), 1e4);
|
|
205
|
+
stop.unref();
|
|
206
|
+
req.once("close", () => clearTimeout(stop));
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
66
210
|
console.error("Request failed:", error);
|
|
67
211
|
if (!res.headersSent) {
|
|
68
|
-
sendJson(res, 500, { error:
|
|
212
|
+
sendJson(res, 500, { error: "Internal server error" });
|
|
69
213
|
}
|
|
70
214
|
}
|
|
71
215
|
});
|
|
72
216
|
server.listen(PORT, () => {
|
|
73
|
-
console.log(
|
|
217
|
+
console.log(`${SERVER_NAME} ${SERVER_VERSION} HTTP server listening on port ${PORT}`);
|
|
74
218
|
});
|
|
75
219
|
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["import http from \"node:http\"\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { RechnungsApiClient } from \"rechnungsapi-sdk\"\nimport { registerTools } from \"./tools.js\"\n\nconst PORT = Number(process.env.PORT ?? 3939)\n\nfunction extractBearerToken(req: http.IncomingMessage): string | null {\n const header = req.headers[\"authorization\"]\n if (!header || Array.isArray(header)) return null\n const match = /^Bearer\\s+(.+)$/i.exec(header)\n return match ? match[1] : null\n}\n\nasync function readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n const raw = Buffer.concat(chunks).toString(\"utf8\")\n return raw ? JSON.parse(raw) : undefined\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown) {\n res.writeHead(status, { \"content-type\": \"application/json\" })\n res.end(JSON.stringify(body))\n}\n\n/**\n * One request = one throwaway MCP server + SDK client, built from that\n * request's own Bearer token. Stateless mode (sessionIdGenerator: undefined)\n * is what makes this safe for many different customers hitting the same\n * running process concurrently — nothing is shared or cached between them.\n */\nasync function handleMcpRequest(req: http.IncomingMessage, res: http.ServerResponse) {\n const token = extractBearerToken(req)\n if (!token) {\n sendJson(res, 401, { error: \"Missing Authorization: Bearer <your-rechnungsapi-token> header\" })\n return\n }\n\n const client = new RechnungsApiClient({\n apiToken: token,\n baseUrl: process.env.RECHNUNGSAPI_BASE_URL,\n v2BaseUrl: process.env.RECHNUNGSAPI_V2_BASE_URL,\n })\n\n const mcpServer = new McpServer({ name: \"rechnungsapi-mcp\", version: \"0.1.0\" })\n registerTools(mcpServer, client)\n\n const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })\n res.on(\"close\", () => {\n transport.close()\n mcpServer.close()\n })\n\n await mcpServer.connect(transport)\n const body = await readJsonBody(req)\n await transport.handleRequest(req, res, body)\n}\n\nconst server = http.createServer(async (req, res) => {\n if (req.method === \"GET\" && req.url === \"/health\") {\n sendJson(res, 200, { ok: true })\n return\n }\n\n if (req.url !== \"/mcp\") {\n sendJson(res, 404, { error: \"Not found. POST to /mcp.\" })\n return\n }\n\n // This server is stateless (see handleMcpRequest) and never pushes\n // unsolicited server-to-client messages, so the optional GET/SSE stream\n // and DELETE/session-termination parts of the Streamable HTTP spec don't\n // apply here. Reject them fast and explicitly — StreamableHTTPServerTransport\n // has no defined behavior for them with sessionIdGenerator: undefined and\n // was observed to hang indefinitely rather than respond, which made MCP\n // clients' initial reachability probe (a bare GET) time out.\n if (req.method !== \"POST\") {\n sendJson(res, 405, { error: \"Only POST is supported on /mcp (no server-initiated SSE stream).\" })\n return\n }\n\n try {\n await handleMcpRequest(req, res)\n } catch (error) {\n console.error(\"Request failed:\", error)\n if (!res.headersSent) {\n sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) })\n }\n }\n})\n\nserver.listen(PORT, () => {\n console.log(`rechnungsapi-mcp HTTP server listening on port ${PORT}`)\n})\n"],"mappings":";;;;;;AAAA,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,qCAAqC;AAC9C,SAAS,0BAA0B;AAGnC,IAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,IAAI;AAE5C,SAAS,mBAAmB,KAA0C;AACpE,QAAM,SAAS,IAAI,QAAQ,eAAe;AAC1C,MAAI,CAAC,UAAU,MAAM,QAAQ,MAAM,EAAG,QAAO;AAC7C,QAAM,QAAQ,mBAAmB,KAAK,MAAM;AAC5C,SAAO,QAAQ,MAAM,CAAC,IAAI;AAC5B;AAEA,eAAe,aAAa,KAA6C;AACvE,QAAM,SAAmB,CAAC;AAC1B,mBAAiB,SAAS,IAAK,QAAO,KAAK,KAAe;AAC1D,QAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AACjD,SAAO,MAAM,KAAK,MAAM,GAAG,IAAI;AACjC;AAEA,SAAS,SAAS,KAA0B,QAAgB,MAAe;AACzE,MAAI,UAAU,QAAQ,EAAE,gBAAgB,mBAAmB,CAAC;AAC5D,MAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAQA,eAAe,iBAAiB,KAA2B,KAA0B;AACnF,QAAM,QAAQ,mBAAmB,GAAG;AACpC,MAAI,CAAC,OAAO;AACV,aAAS,KAAK,KAAK,EAAE,OAAO,iEAAiE,CAAC;AAC9F;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,mBAAmB;AAAA,IACpC,UAAU;AAAA,IACV,SAAS,QAAQ,IAAI;AAAA,IACrB,WAAW,QAAQ,IAAI;AAAA,EACzB,CAAC;AAED,QAAM,YAAY,IAAI,UAAU,EAAE,MAAM,oBAAoB,SAAS,QAAQ,CAAC;AAC9E,gBAAc,WAAW,MAAM;AAE/B,QAAM,YAAY,IAAI,8BAA8B,EAAE,oBAAoB,OAAU,CAAC;AACrF,MAAI,GAAG,SAAS,MAAM;AACpB,cAAU,MAAM;AAChB,cAAU,MAAM;AAAA,EAClB,CAAC;AAED,QAAM,UAAU,QAAQ,SAAS;AACjC,QAAM,OAAO,MAAM,aAAa,GAAG;AACnC,QAAM,UAAU,cAAc,KAAK,KAAK,IAAI;AAC9C;AAEA,IAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACnD,MAAI,IAAI,WAAW,SAAS,IAAI,QAAQ,WAAW;AACjD,aAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;AAC/B;AAAA,EACF;AAEA,MAAI,IAAI,QAAQ,QAAQ;AACtB,aAAS,KAAK,KAAK,EAAE,OAAO,2BAA2B,CAAC;AACxD;AAAA,EACF;AASA,MAAI,IAAI,WAAW,QAAQ;AACzB,aAAS,KAAK,KAAK,EAAE,OAAO,mEAAmE,CAAC;AAChG;AAAA,EACF;AAEA,MAAI;AACF,UAAM,iBAAiB,KAAK,GAAG;AAAA,EACjC,SAAS,OAAO;AACd,YAAQ,MAAM,mBAAmB,KAAK;AACtC,QAAI,CAAC,IAAI,aAAa;AACpB,eAAS,KAAK,KAAK,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IACtF;AAAA,EACF;AACF,CAAC;AAED,OAAO,OAAO,MAAM,MAAM;AACxB,UAAQ,IAAI,kDAAkD,IAAI,EAAE;AACtE,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["import http from \"node:http\"\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { RechnungsApiClient } from \"rechnungsapi-sdk\"\nimport { registerTools } from \"./tools.js\"\nimport {\n SERVER_DESCRIPTION,\n SERVER_DOCS,\n SERVER_INFO,\n SERVER_INSTRUCTIONS,\n SERVER_NAME,\n SERVER_TITLE,\n SERVER_VERSION,\n SERVER_WEBSITE,\n} from \"./version.js\"\n\nconst PORT = Number(process.env.PORT ?? 3939)\n\n// Invoices travel as base64 PDFs, so the cap is generous. Without one, anyone able\n// to send an Authorization header could make this process buffer an unbounded body\n// in memory before the API ever gets the chance to reject their token.\nconst MAX_BODY_MB = Number(process.env.MAX_BODY_MB ?? 64)\nconst MAX_BODY_BYTES = (Number.isFinite(MAX_BODY_MB) && MAX_BODY_MB > 0 ? MAX_BODY_MB : 64) * 1024 * 1024\n\nclass HttpError extends Error {\n constructor(\n readonly status: number,\n message: string,\n readonly rpcCode?: number,\n ) {\n super(message)\n }\n}\n\nconst CORS_HEADERS: Record<string, string> = {\n \"access-control-allow-origin\": \"*\",\n \"access-control-allow-methods\": \"POST, OPTIONS\",\n \"access-control-allow-headers\":\n \"Authorization, Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID\",\n \"access-control-expose-headers\": \"Mcp-Session-Id, MCP-Protocol-Version\",\n \"access-control-max-age\": \"86400\",\n}\n\nfunction extractBearerToken(req: http.IncomingMessage): string | null {\n const header = req.headers[\"authorization\"]\n if (!header || Array.isArray(header)) return null\n const value = header.trim()\n // \"Bearer\" with nothing after it is a missing credential, not a token named \"Bearer\".\n if (!value || /^bearer$/i.test(value)) return null\n // Some connector UIs (Claude.ai's included) make the user type the scheme\n // themselves, so a bare token with no \"Bearer \" prefix is a common slip.\n // Accept it rather than failing the whole connection over it.\n const match = /^Bearer\\s+(.+)$/i.exec(value)\n const token = (match ? match[1] : value).trim()\n return token || null\n}\n\nfunction readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n const tooLarge = () => new HttpError(413, `Request body exceeds the ${MAX_BODY_BYTES / 1024 / 1024} MB limit`)\n const declared = Number(req.headers[\"content-length\"])\n if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) return Promise.reject(tooLarge())\n\n // Event-based on purpose: breaking out of `for await (const c of req)` destroys the\n // request, and with it the socket, before the 413 can be delivered.\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let size = 0\n let rejected = false\n req.on(\"data\", (chunk: Buffer) => {\n if (rejected) return // keep consuming (and discarding) so the client can finish and read our answer\n size += chunk.length\n if (size > MAX_BODY_BYTES) {\n rejected = true\n chunks.length = 0\n reject(tooLarge())\n return\n }\n chunks.push(chunk)\n })\n req.on(\"end\", () => {\n if (rejected) return\n const raw = Buffer.concat(chunks).toString(\"utf8\")\n if (!raw) return resolve(undefined)\n try {\n resolve(JSON.parse(raw))\n } catch {\n reject(new HttpError(400, \"Parse error: request body is not valid JSON\", -32700))\n }\n })\n req.on(\"error\", reject)\n // A promise settles once, so this is a no-op after a normal end.\n req.on(\"close\", () => reject(new Error(\"connection closed before the request body was complete\")))\n })\n}\n\n/**\n * The request path without query string or trailing slashes, so a URL typed as \"/mcp/\" or\n * \"/mcp?x=1\" reaches the same handler as \"/mcp\" instead of failing with a confusing 404.\n */\nfunction routeOf(req: http.IncomingMessage): string {\n const path = (req.url ?? \"/\").split(\"?\")[0]\n return path.replace(/\\/+$/, \"\") || \"/\"\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown, extraHeaders: Record<string, string> = {}) {\n res.writeHead(status, { \"content-type\": \"application/json\", ...extraHeaders })\n res.end(JSON.stringify(body))\n}\n\n// What a person sees when they open the server's address in a browser. Static text only, so there is\n// nothing to escape, and noindex because the product pages on rechnungsapi.de are the ones to find.\nconst LANDING_HTML = `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex\">\n<title>${SERVER_TITLE} MCP server</title>\n<style>body{font:16px/1.55 system-ui,sans-serif;max-width:40rem;margin:4rem auto;padding:0 1rem;color:#1f2937}code{background:#f3f4f6;padding:.1rem .35rem;border-radius:.25rem}a{color:#2563eb}small{color:#6b7280}</style>\n</head>\n<body>\n<h1>${SERVER_TITLE} MCP server</h1>\n<p>This is the MCP endpoint of <a href=\"${SERVER_WEBSITE}\">${SERVER_TITLE}</a> (rechnungsapi.de), the ZUGFeRD & XRechnung API. It lets AI agents create, validate and analyze e-invoices.</p>\n<p>MCP clients connect to <code>/mcp</code> on this host and send their own ${SERVER_TITLE} token as <code>Authorization: Bearer <token></code>.</p>\n<ul>\n<li><a href=\"${SERVER_DOCS}\">How to connect: documentation</a></li>\n<li><a href=\"${SERVER_WEBSITE}/api-docs\">API documentation</a></li>\n<li><a href=\"${SERVER_WEBSITE}\">rechnungsapi.de</a></li>\n</ul>\n<p><small>rechnungsapi-mcp ${SERVER_VERSION} · rechnungsapi.de</small></p>\n</body>\n</html>\n`\n\nfunction sendLanding(req: http.IncomingMessage, res: http.ServerResponse) {\n const common = { \"cache-control\": \"public, max-age=300\", \"x-content-type-options\": \"nosniff\" }\n if ((req.headers.accept ?? \"\").includes(\"text/html\")) {\n res.writeHead(200, {\n ...common,\n \"content-type\": \"text/html; charset=utf-8\",\n \"content-security-policy\": \"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n })\n res.end(LANDING_HTML)\n return\n }\n sendJson(\n res,\n 200,\n { name: SERVER_TITLE, description: SERVER_DESCRIPTION, version: SERVER_VERSION, mcp: \"/mcp\", health: \"/health\", docs: SERVER_DOCS, website: SERVER_WEBSITE },\n common,\n )\n}\n\n/** JSON-RPC method names only — never params, which can carry invoice data. */\nfunction describeRpc(body: unknown): string {\n const messages = Array.isArray(body) ? body : [body]\n const methods = messages\n .map((m) => (m && typeof m === \"object\" && \"method\" in m ? String((m as { method: unknown }).method) : null))\n .filter((m): m is string => m !== null)\n return methods.length ? methods.join(\",\") : \"-\"\n}\n\n/**\n * One request = one throwaway MCP server + SDK client, built from that\n * request's own Bearer token. Stateless mode (sessionIdGenerator: undefined)\n * is what makes this safe for many different customers hitting the same\n * running process concurrently — nothing is shared or cached between them.\n */\nasync function handleMcpRequest(req: http.IncomingMessage, res: http.ServerResponse, log: { rpc?: string }) {\n const token = extractBearerToken(req)\n if (!token) {\n sendJson(\n res,\n 401,\n { error: \"Missing Authorization: Bearer <your-rechnungsapi-token> header\" },\n { \"www-authenticate\": `Bearer realm=\"${SERVER_NAME}\"` },\n )\n return\n }\n\n const body = await readJsonBody(req)\n log.rpc = describeRpc(body)\n\n const client = new RechnungsApiClient({\n apiToken: token,\n baseUrl: process.env.RECHNUNGSAPI_BASE_URL,\n v2BaseUrl: process.env.RECHNUNGSAPI_V2_BASE_URL,\n })\n\n const mcpServer = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS })\n registerTools(mcpServer, client)\n\n const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })\n res.on(\"close\", () => {\n transport.close()\n mcpServer.close()\n })\n\n await mcpServer.connect(transport)\n await transport.handleRequest(req, res, body)\n}\n\nconst server = http.createServer(async (req, res) => {\n const startedAt = Date.now()\n const log: { rpc?: string } = {}\n res.on(\"finish\", () => {\n const ua = req.headers[\"user-agent\"] ?? \"-\"\n const proto = req.headers[\"mcp-protocol-version\"] ?? \"-\"\n const auth = req.headers[\"authorization\"] ? \"yes\" : \"no\"\n console.log(\n `[http] ${req.method} ${req.url} -> ${res.statusCode} ${Date.now() - startedAt}ms auth=${auth} rpc=${log.rpc ?? \"-\"} proto=${proto} ua=\"${ua}\"`,\n )\n })\n\n const route = routeOf(req)\n\n if (route === \"/health\" && (req.method === \"GET\" || req.method === \"HEAD\")) {\n sendJson(res, 200, { ok: true, name: SERVER_NAME, version: SERVER_VERSION })\n return\n }\n\n if (route === \"/\" && (req.method === \"GET\" || req.method === \"HEAD\")) {\n sendLanding(req, res)\n return\n }\n\n if (route !== \"/mcp\") {\n sendJson(res, 404, { error: \"Not found. POST to /mcp.\", docs: SERVER_DOCS })\n return\n }\n\n for (const [k, v] of Object.entries(CORS_HEADERS)) res.setHeader(k, v)\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204)\n res.end()\n return\n }\n\n // This server is stateless (see handleMcpRequest) and never pushes\n // unsolicited server-to-client messages, so the optional GET/SSE stream\n // and DELETE/session-termination parts of the Streamable HTTP spec don't\n // apply here. Reject them fast and explicitly — StreamableHTTPServerTransport\n // has no defined behavior for them with sessionIdGenerator: undefined and\n // was observed to hang indefinitely rather than respond, which made MCP\n // clients' initial reachability probe (a bare GET) time out.\n if (req.method !== \"POST\") {\n sendJson(res, 405, { error: \"Only POST is supported on /mcp (no server-initiated SSE stream).\" }, { allow: \"POST, OPTIONS\" })\n return\n }\n\n try {\n await handleMcpRequest(req, res, log)\n } catch (error) {\n if (error instanceof HttpError) {\n if (!res.headersSent) {\n const body =\n error.rpcCode !== undefined\n ? { jsonrpc: \"2.0\", error: { code: error.rpcCode, message: error.message }, id: null }\n : { error: error.message }\n sendJson(res, error.status, body)\n }\n if (error.status === 413) {\n // The client may still be uploading. Keep discarding it so it can finish and read\n // this answer (closing early shows up client-side as EPIPE/ECONNRESET), but cap how\n // long that can go on so it can't be used to hold a connection open.\n const stop = setTimeout(() => req.destroy(), 10_000)\n stop.unref()\n req.once(\"close\", () => clearTimeout(stop))\n }\n return\n }\n console.error(\"Request failed:\", error)\n if (!res.headersSent) {\n sendJson(res, 500, { error: \"Internal server error\" })\n }\n }\n})\n\nserver.listen(PORT, () => {\n console.log(`${SERVER_NAME} ${SERVER_VERSION} HTTP server listening on port ${PORT}`)\n})\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
SERVER_INFO,
|
|
4
|
+
SERVER_INSTRUCTIONS,
|
|
3
5
|
registerTools
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SPJV6ABL.js";
|
|
5
7
|
|
|
6
8
|
// src/index.ts
|
|
7
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -13,7 +15,7 @@ function loadConfigFromEnv() {
|
|
|
13
15
|
const apiToken = process.env.RECHNUNGSAPI_TOKEN;
|
|
14
16
|
if (!apiToken) {
|
|
15
17
|
throw new Error(
|
|
16
|
-
`RECHNUNGSAPI_TOKEN environment variable is required. Get your API token from the RechnungsAPI dashboard and set it in your MCP client's server config (e.g. Claude Desktop/Code's mcp.json under "env")
|
|
18
|
+
`RECHNUNGSAPI_TOKEN environment variable is required. Get your API token from the RechnungsAPI dashboard and set it in your MCP client's server config (e.g. Claude Desktop/Code's mcp.json under "env"). Setup guide: https://rechnungsapi.de/api-docs#mcp-sdk`
|
|
17
19
|
);
|
|
18
20
|
}
|
|
19
21
|
return {
|
|
@@ -27,7 +29,7 @@ function loadConfigFromEnv() {
|
|
|
27
29
|
async function main() {
|
|
28
30
|
const config = loadConfigFromEnv();
|
|
29
31
|
const client = new RechnungsApiClient(config);
|
|
30
|
-
const server = new McpServer({
|
|
32
|
+
const server = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS });
|
|
31
33
|
registerTools(server, client);
|
|
32
34
|
const transport = new StdioServerTransport();
|
|
33
35
|
await server.connect(transport);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { RechnungsApiClient } from \"rechnungsapi-sdk\"\nimport { loadConfigFromEnv } from \"./config.js\"\nimport { registerTools } from \"./tools.js\"\n\nasync function main() {\n const config = loadConfigFromEnv()\n const client = new RechnungsApiClient(config)\n\n const server = new McpServer({
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { RechnungsApiClient } from \"rechnungsapi-sdk\"\nimport { loadConfigFromEnv } from \"./config.js\"\nimport { registerTools } from \"./tools.js\"\nimport { SERVER_INFO, SERVER_INSTRUCTIONS } from \"./version.js\"\n\nasync function main() {\n const config = loadConfigFromEnv()\n const client = new RechnungsApiClient(config)\n\n const server = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS })\n registerTools(server, client)\n\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\nmain().catch((error) => {\n console.error(\"rechnungsapi-mcp failed to start:\", error instanceof Error ? error.message : error)\n process.exit(1)\n})\n","export interface RechnungsApiMcpConfig {\n apiToken: string\n baseUrl?: string\n v2BaseUrl?: string\n}\n\nexport function loadConfigFromEnv(): RechnungsApiMcpConfig {\n const apiToken = process.env.RECHNUNGSAPI_TOKEN\n if (!apiToken) {\n throw new Error(\n \"RECHNUNGSAPI_TOKEN environment variable is required. Get your API token from the RechnungsAPI dashboard \" +\n \"and set it in your MCP client's server config (e.g. Claude Desktop/Code's mcp.json under \\\"env\\\"). \" +\n \"Setup guide: https://rechnungsapi.de/api-docs#mcp-sdk\",\n )\n }\n return {\n apiToken,\n baseUrl: process.env.RECHNUNGSAPI_BASE_URL,\n v2BaseUrl: process.env.RECHNUNGSAPI_V2_BASE_URL,\n }\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;;;ACI5B,SAAS,oBAA2C;AACzD,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,SAAS,QAAQ,IAAI;AAAA,IACrB,WAAW,QAAQ,IAAI;AAAA,EACzB;AACF;;;ADbA,eAAe,OAAO;AACpB,QAAM,SAAS,kBAAkB;AACjC,QAAM,SAAS,IAAI,mBAAmB,MAAM;AAE5C,QAAM,SAAS,IAAI,UAAU,aAAa,EAAE,cAAc,oBAAoB,CAAC;AAC/E,gBAAc,QAAQ,MAAM;AAE5B,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAChC;AAEA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,UAAQ,MAAM,qCAAqC,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AACjG,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rechnungsapi-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Official MCP server for RechnungsAPI (rechnungsapi.de), the ZUGFeRD & XRechnung API: let AI agents create, validate and analyze e-invoices.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author":
|
|
7
|
-
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "RechnungsAPI",
|
|
8
|
+
"email": "support@rechnungsapi.de",
|
|
9
|
+
"url": "https://rechnungsapi.de"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://rechnungsapi.de/api-docs#mcp-sdk",
|
|
8
12
|
"repository": {
|
|
9
13
|
"type": "git",
|
|
10
14
|
"url": "git+https://github.com/Rechnungsapi/rechnungsapi-mcp.git"
|
|
11
15
|
},
|
|
12
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"rechnungsapi",
|
|
18
|
+
"rechnungsapi.de",
|
|
19
|
+
"mcp",
|
|
20
|
+
"mcp-server",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"zugferd",
|
|
24
|
+
"xrechnung",
|
|
25
|
+
"e-rechnung",
|
|
26
|
+
"e-invoice",
|
|
27
|
+
"e-invoicing",
|
|
28
|
+
"en16931"
|
|
29
|
+
],
|
|
13
30
|
"type": "module",
|
|
14
31
|
"bin": {
|
|
15
32
|
"rechnungsapi-mcp": "./dist/index.js"
|
|
@@ -23,12 +40,13 @@
|
|
|
23
40
|
"start:http": "node dist/http.js",
|
|
24
41
|
"typecheck": "tsc --noEmit",
|
|
25
42
|
"test": "vitest run",
|
|
26
|
-
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
43
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
44
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
27
45
|
},
|
|
28
46
|
"dependencies": {
|
|
29
|
-
"@modelcontextprotocol/sdk": "1.
|
|
30
|
-
"rechnungsapi-sdk": "^0.1.
|
|
31
|
-
"zod": "^3.
|
|
47
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
48
|
+
"rechnungsapi-sdk": "^0.1.2",
|
|
49
|
+
"zod": "^3.25.76"
|
|
32
50
|
},
|
|
33
51
|
"devDependencies": {
|
|
34
52
|
"@types/node": "^22.10.2",
|
|
@@ -37,6 +55,6 @@
|
|
|
37
55
|
"vitest": "^2.1.8"
|
|
38
56
|
},
|
|
39
57
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
58
|
+
"node": ">=20"
|
|
41
59
|
}
|
|
42
60
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools.ts"],"sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { RechnungsApiClient, RechnungsApiError } from \"rechnungsapi-sdk\"\nimport { z } from \"zod\"\n\nconst invoiceJsonSchema = z\n .any()\n .describe(\"Structured invoice data (line items, parties, totals, etc.) as defined by the RechnungsAPI invoice schema\")\n\nconst transportSchema = z\n .any()\n .optional()\n .describe(\"Optional delivery/transport metadata (e.g. email delivery options)\")\n\n/** Wraps a client call so thrown RechnungsApiError/generic errors become MCP tool errors instead of crashing the server. */\nasync function asToolResult(fn: () => Promise<unknown>) {\n try {\n const result = await fn()\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] }\n } catch (error) {\n if (error instanceof RechnungsApiError) {\n return {\n isError: true,\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ status: error.status, message: error.message, body: error.body }, null, 2),\n },\n ],\n }\n }\n const message = error instanceof Error ? error.message : String(error)\n return { isError: true, content: [{ type: \"text\" as const, text: message }] }\n }\n}\n\nexport function registerTools(server: McpServer, client: RechnungsApiClient): void {\n server.registerTool(\n \"create_zugferd_invoice\",\n {\n title: \"Create ZUGFeRD invoice\",\n description:\n \"Create a ZUGFeRD PDF/A-3 e-invoice (visual PDF with embedded XRechnung XML) from structured invoice JSON plus a visual PDF template.\",\n inputSchema: {\n invoice: invoiceJsonSchema,\n invoicePdf64: z.string().describe(\"Base64-encoded visual PDF the XML will be embedded into\"),\n transport: transportSchema,\n },\n },\n async ({ invoice, invoicePdf64, transport }) =>\n asToolResult(() => client.createZugferdFromJson(invoice, invoicePdf64, { transport })),\n )\n\n server.registerTool(\n \"create_xinvoice\",\n {\n title: \"Create X-Invoice (XRechnung/UBL)\",\n description: \"Create an X-Invoice (XRechnung/UBL) XML document from structured invoice JSON.\",\n inputSchema: {\n invoice: invoiceJsonSchema,\n transport: transportSchema,\n },\n },\n async ({ invoice, transport }) => asToolResult(() => client.createXInvoiceFromJson(invoice, { transport })),\n )\n\n server.registerTool(\n \"create_zugferd_pdf\",\n {\n title: \"Embed X-Invoice XML into a PDF\",\n description: \"Embed an existing X-Invoice XML document into a visual PDF to produce a ZUGFeRD PDF.\",\n inputSchema: {\n invoicePdf64: z.string().describe(\"Base64-encoded visual PDF\"),\n xInvoiceXml: z.string().describe(\"X-Invoice XML to embed\"),\n },\n },\n async ({ invoicePdf64, xInvoiceXml }) => asToolResult(() => client.createZugferdPdf(invoicePdf64, xInvoiceXml)),\n )\n\n server.registerTool(\n \"extract_xinvoice_from_zugferd\",\n {\n title: \"Extract X-Invoice from ZUGFeRD PDF\",\n description: \"Extract the embedded XRechnung XML from a ZUGFeRD PDF and return it as structured JSON.\",\n inputSchema: {\n zugferd64: z.string().describe(\"Base64-encoded ZUGFeRD PDF\"),\n },\n },\n async ({ zugferd64 }) => asToolResult(() => client.extractXInvoiceFromZugferd(zugferd64)),\n )\n\n server.registerTool(\n \"validate_xinvoice_xml\",\n {\n title: \"Validate X-Invoice XML\",\n description: \"Validate an X-Invoice (XRechnung/UBL) XML document against schema and business rules.\",\n inputSchema: {\n xml: z.string().describe(\"The X-Invoice XML content to validate\"),\n },\n },\n async ({ xml }) => asToolResult(() => client.validateXInvoiceXml(xml)),\n )\n\n server.registerTool(\n \"validate_zugferd_pdf\",\n {\n title: \"Validate ZUGFeRD PDF\",\n description: \"Validate a ZUGFeRD PDF's embedded XML content.\",\n inputSchema: {\n zugferdFile64: z.string().describe(\"Base64-encoded ZUGFeRD PDF\"),\n comparePDF2XML: z.boolean().optional().describe(\"Also cross-check the embedded XML against the visual PDF content\"),\n },\n },\n async ({ zugferdFile64, comparePDF2XML }) =>\n asToolResult(() => client.validateZugferdPdf(zugferdFile64, { comparePDF2XML })),\n )\n\n server.registerTool(\n \"analyze_pdf_invoice\",\n {\n title: \"Analyze a PDF/scanned invoice\",\n description:\n \"Extract structured invoice JSON from a scanned or digital PDF/PNG/JPEG/TIFF invoice using the high-accuracy analyzer.\",\n inputSchema: {\n pdfBase64: z.string().describe(\"Base64-encoded PDF, PNG, JPEG, or TIFF invoice\"),\n withLineItems: z.boolean().optional().describe(\"Also extract individual line items\"),\n },\n },\n async ({ pdfBase64, withLineItems }) =>\n asToolResult(() => client.analyzePdfInvoiceV2(pdfBase64, withLineItems ?? false)),\n )\n\n server.registerTool(\n \"analyze_pdf_invoice_async_submit\",\n {\n title: \"Submit a large PDF invoice for async analysis\",\n description:\n \"Submit a large scanned/PDF invoice for asynchronous analysis (for files too large for the synchronous analyzer). \" +\n \"Returns a job_id — poll it with analyze_pdf_invoice_async_status.\",\n inputSchema: {\n pdfBase64: z.string().describe(\"Base64-encoded PDF, PNG, JPEG, or TIFF invoice\"),\n withLineItems: z.boolean().optional().describe(\"Also extract individual line items\"),\n },\n },\n async ({ pdfBase64, withLineItems }) =>\n asToolResult(() => client.analyzePdfInvoiceAsync(pdfBase64, withLineItems)),\n )\n\n server.registerTool(\n \"analyze_pdf_invoice_async_status\",\n {\n title: \"Check async PDF analysis status\",\n description: \"Poll the status of an asynchronous PDF invoice analysis job submitted via analyze_pdf_invoice_async_submit.\",\n inputSchema: {\n jobId: z.string().describe(\"The job_id returned by analyze_pdf_invoice_async_submit\"),\n },\n },\n async ({ jobId }) => asToolResult(() => client.getAnalysisStatus(jobId)),\n )\n\n server.registerTool(\n \"create_zugferd_from_pdf\",\n {\n title: \"Convert a PDF/scan directly into a ZUGFeRD PDF\",\n description:\n \"Convert a PDF or scanned image invoice directly into a validated ZUGFeRD PDF/A-3 in one call \" +\n \"(analyzes, validates, and embeds in a single step).\",\n inputSchema: {\n pdfBase64: z.string().describe(\"Base64-encoded PDF, PNG, JPEG, or TIFF invoice\"),\n fileName: z.string().optional().describe(\"Original filename, used to name the resulting PDF\"),\n },\n },\n async ({ pdfBase64, fileName }) => asToolResult(() => client.createZugferdFromPdf(pdfBase64, fileName)),\n )\n}\n"],"mappings":"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","names":[]}
|