rechnungsapi-mcp 0.3.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Finbc GmbH
3
+ Copyright (c) 2026 RechnungsAPI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,12 +1,27 @@
1
1
  # rechnungsapi-mcp
2
2
 
3
- MCP (Model Context Protocol) server for [RechnungsAPI](https://rechnungsapi.de) — lets Claude and other MCP-compatible AI agents create, validate, and analyze ZUGFeRD and X-Invoice e-invoices directly from a conversation.
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
- Built on [`rechnungsapi-sdk`](https://github.com/Rechnungsapi/rechnungsapi-sdk).
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 the [RechnungsAPI dashboard](https://rechnungsapi.de), then add the server to your MCP client's config.
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. for a sandbox environment) |
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, like Apollo.io's hosted MCP server), use the HTTP mode instead.
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 as a Portainer stack: paste [`docker-compose.yml`](docker-compose.yml) into Portainer's Stacks → Add stack → Web editor, then deploy.
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
 
@@ -98,9 +126,12 @@ docker logs -f rechnungsapi-mcp
98
126
  |---|---|
99
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,...}`. |
100
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. |
101
- | `401 ... auth=yes` | The header arrived but the token is empty or wrong. The `Bearer ` prefix is optional — a bare token is accepted. |
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. |
102
130
  | `GET /mcp -> 405` | Expected. This server doesn't offer the optional server-push SSE stream; compliant clients continue over `POST`. |
103
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`). |
104
135
 
105
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.
106
137
 
@@ -109,20 +140,23 @@ Log lines record JSON-RPC method names only (e.g. `rpc=tools/call`), never argum
109
140
  | Tool | Description |
110
141
  |---|---|
111
142
  | `create_zugferd_invoice` | Create a ZUGFeRD PDF/A-3 from structured invoice JSON + a visual PDF |
112
- | `create_xinvoice` | Create an X-Invoice (XRechnung/UBL) XML from structured invoice JSON |
113
- | `create_zugferd_pdf` | Embed an existing X-Invoice XML into a visual PDF |
114
- | `extract_xinvoice_from_zugferd` | Extract the embedded XRechnung XML from a ZUGFeRD PDF as JSON |
115
- | `validate_xinvoice_xml` | Validate an X-Invoice XML against schema and business rules |
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 |
116
147
  | `validate_zugferd_pdf` | Validate a ZUGFeRD PDF's embedded XML |
117
148
  | `analyze_pdf_invoice` | Extract structured invoice JSON from a scanned/PDF invoice |
118
149
  | `analyze_pdf_invoice_async_submit` / `analyze_pdf_invoice_async_status` | Async analysis for large files |
119
150
  | `create_zugferd_from_pdf` | Convert a PDF/scan directly into a validated ZUGFeRD PDF |
120
151
 
152
+ Each tool maps to one RechnungsAPI endpoint — the [API documentation](https://rechnungsapi.de/api-docs#endpoints) shows its request and response.
153
+
121
154
  ## Development
122
155
 
123
156
  ```bash
124
157
  yarn install
125
158
  yarn typecheck
159
+ yarn test # builds first, then runs the stdio and HTTP suites against a stub API
126
160
  yarn build
127
161
  ```
128
162
 
@@ -132,8 +166,8 @@ Test locally with the [MCP Inspector](https://github.com/modelcontextprotocol/in
132
166
  RECHNUNGSAPI_TOKEN=your-token yarn inspect
133
167
  ```
134
168
 
135
- > **Note:** `@modelcontextprotocol/sdk` is pinned to `1.22.0` (not `^1.x`). Versions ≥1.23.0 introduced a Zod v3/v4 compatibility layer that currently triggers a `TS2589: Type instantiation is excessively deep` compiler error with `registerTool` (see [modelcontextprotocol/typescript-sdk#1180](https://github.com/modelcontextprotocol/typescript-sdk/issues/1180) and [#1423](https://github.com/modelcontextprotocol/typescript-sdk/issues/1423)). Re-evaluate this pin once that's fixed upstream.
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.
136
170
 
137
171
  ## License
138
172
 
139
- 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.any().describe("Structured invoice data (line items, parties, totals, etc.) as defined by the RechnungsAPI invoice schema");
7
- var transportSchema = z.any().optional().describe("Optional delivery/transport metadata (e.g. email delivery options)");
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
- "create_xinvoice",
47
+ "create_xrechnung",
44
48
  {
45
- title: "Create X-Invoice (XRechnung/UBL)",
46
- description: "Create an X-Invoice (XRechnung/UBL) XML document from structured invoice JSON.",
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.createXInvoiceFromJson(invoice, { transport }))
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 X-Invoice XML into a PDF",
58
- description: "Embed an existing X-Invoice XML document into a visual PDF to produce a ZUGFeRD PDF.",
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
- xInvoiceXml: z.string().describe("X-Invoice XML to embed")
65
+ xrechnungXml: z.string().describe("XRechnung XML to embed")
62
66
  }
63
67
  },
64
- async ({ invoicePdf64, xInvoiceXml }) => asToolResult(() => client.createZugferdPdf(invoicePdf64, xInvoiceXml))
68
+ async ({ invoicePdf64, xrechnungXml }) => asToolResult(() => client.createZugferdPdf(invoicePdf64, xrechnungXml))
65
69
  );
66
70
  server.registerTool(
67
- "extract_xinvoice_from_zugferd",
71
+ "extract_xrechnung_from_zugferd",
68
72
  {
69
- title: "Extract X-Invoice from ZUGFeRD PDF",
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.extractXInvoiceFromZugferd(zugferd64))
79
+ async ({ zugferd64 }) => asToolResult(() => client.extractXRechnungFromZugferd(zugferd64))
76
80
  );
77
81
  server.registerTool(
78
- "validate_xinvoice_xml",
82
+ "validate_xrechnung_xml",
79
83
  {
80
- title: "Validate X-Invoice XML",
81
- description: "Validate an X-Invoice (XRechnung/UBL) XML document against schema and business rules.",
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 X-Invoice XML content to validate")
87
+ xml: z.string().describe("The XRechnung XML content to validate")
84
88
  }
85
89
  },
86
- async ({ xml }) => asToolResult(() => client.validateXInvoiceXml(xml))
90
+ async ({ xml }) => asToolResult(() => client.validateXRechnungXml(xml))
87
91
  );
88
92
  server.registerTool(
89
93
  "validate_zugferd_pdf",
@@ -150,6 +154,11 @@ function registerTools(server, client) {
150
154
  import { createRequire } from "module";
151
155
  var require2 = createRequire(import.meta.url);
152
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";
153
162
  var SERVER_VERSION = (() => {
154
163
  try {
155
164
  return require2("../package.json").version;
@@ -157,10 +166,23 @@ var SERVER_VERSION = (() => {
157
166
  return "0.0.0";
158
167
  }
159
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
+ };
160
176
 
161
177
  export {
162
178
  registerTools,
163
179
  SERVER_NAME,
164
- SERVER_VERSION
180
+ SERVER_TITLE,
181
+ SERVER_WEBSITE,
182
+ SERVER_DOCS,
183
+ SERVER_DESCRIPTION,
184
+ SERVER_INSTRUCTIONS,
185
+ SERVER_VERSION,
186
+ SERVER_INFO
165
187
  };
166
- //# sourceMappingURL=chunk-GARNEWIZ.js.map
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,9 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ SERVER_DESCRIPTION,
4
+ SERVER_DOCS,
5
+ SERVER_INFO,
6
+ SERVER_INSTRUCTIONS,
3
7
  SERVER_NAME,
8
+ SERVER_TITLE,
4
9
  SERVER_VERSION,
10
+ SERVER_WEBSITE,
5
11
  registerTools
6
- } from "./chunk-GARNEWIZ.js";
12
+ } from "./chunk-SPJV6ABL.js";
7
13
 
8
14
  // src/http.ts
9
15
  import http from "http";
@@ -11,6 +17,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
12
18
  import { RechnungsApiClient } from "rechnungsapi-sdk";
13
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
+ };
14
31
  var CORS_HEADERS = {
15
32
  "access-control-allow-origin": "*",
16
33
  "access-control-allow-methods": "POST, OPTIONS",
@@ -22,21 +39,92 @@ function extractBearerToken(req) {
22
39
  const header = req.headers["authorization"];
23
40
  if (!header || Array.isArray(header)) return null;
24
41
  const value = header.trim();
25
- if (!value) return null;
42
+ if (!value || /^bearer$/i.test(value)) return null;
26
43
  const match = /^Bearer\s+(.+)$/i.exec(value);
27
44
  const token = (match ? match[1] : value).trim();
28
45
  return token || null;
29
46
  }
30
- async function readJsonBody(req) {
31
- const chunks = [];
32
- for await (const chunk of req) chunks.push(chunk);
33
- const raw = Buffer.concat(chunks).toString("utf8");
34
- return raw ? JSON.parse(raw) : void 0;
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
+ });
79
+ }
80
+ function routeOf(req) {
81
+ const path = (req.url ?? "/").split("?")[0];
82
+ return path.replace(/\/+$/, "") || "/";
35
83
  }
36
84
  function sendJson(res, status, body, extraHeaders = {}) {
37
85
  res.writeHead(status, { "content-type": "application/json", ...extraHeaders });
38
86
  res.end(JSON.stringify(body));
39
87
  }
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 &amp; 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 &lt;token&gt;</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
+ }
40
128
  function describeRpc(body) {
41
129
  const messages = Array.isArray(body) ? body : [body];
42
130
  const methods = messages.map((m) => m && typeof m === "object" && "method" in m ? String(m.method) : null).filter((m) => m !== null);
@@ -60,7 +148,7 @@ async function handleMcpRequest(req, res, log) {
60
148
  baseUrl: process.env.RECHNUNGSAPI_BASE_URL,
61
149
  v2BaseUrl: process.env.RECHNUNGSAPI_V2_BASE_URL
62
150
  });
63
- const mcpServer = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
151
+ const mcpServer = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS });
64
152
  registerTools(mcpServer, client);
65
153
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
66
154
  res.on("close", () => {
@@ -81,12 +169,17 @@ var server = http.createServer(async (req, res) => {
81
169
  `[http] ${req.method} ${req.url} -> ${res.statusCode} ${Date.now() - startedAt}ms auth=${auth} rpc=${log.rpc ?? "-"} proto=${proto} ua="${ua}"`
82
170
  );
83
171
  });
84
- if (req.url === "/health" && (req.method === "GET" || req.method === "HEAD")) {
172
+ const route = routeOf(req);
173
+ if (route === "/health" && (req.method === "GET" || req.method === "HEAD")) {
85
174
  sendJson(res, 200, { ok: true, name: SERVER_NAME, version: SERVER_VERSION });
86
175
  return;
87
176
  }
88
- if (req.url !== "/mcp") {
89
- sendJson(res, 404, { error: "Not found. POST to /mcp." });
177
+ if (route === "/" && (req.method === "GET" || req.method === "HEAD")) {
178
+ sendLanding(req, res);
179
+ return;
180
+ }
181
+ if (route !== "/mcp") {
182
+ sendJson(res, 404, { error: "Not found. POST to /mcp.", docs: SERVER_DOCS });
90
183
  return;
91
184
  }
92
185
  for (const [k, v] of Object.entries(CORS_HEADERS)) res.setHeader(k, v);
@@ -102,9 +195,21 @@ var server = http.createServer(async (req, res) => {
102
195
  try {
103
196
  await handleMcpRequest(req, res, log);
104
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
+ }
105
210
  console.error("Request failed:", error);
106
211
  if (!res.headersSent) {
107
- sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
212
+ sendJson(res, 500, { error: "Internal server error" });
108
213
  }
109
214
  }
110
215
  });
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\"\nimport { SERVER_NAME, SERVER_VERSION } from \"./version.js\"\n\nconst PORT = Number(process.env.PORT ?? 3939)\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 if (!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\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, extraHeaders: Record<string, string> = {}) {\n res.writeHead(status, { \"content-type\": \"application/json\", ...extraHeaders })\n res.end(JSON.stringify(body))\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({ name: SERVER_NAME, version: SERVER_VERSION })\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 if (req.url === \"/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 (req.url !== \"/mcp\") {\n sendJson(res, 404, { error: \"Not found. POST to /mcp.\" })\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 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(`${SERVER_NAME} ${SERVER_VERSION} HTTP server listening on port ${PORT}`)\n})\n"],"mappings":";;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,qCAAqC;AAC9C,SAAS,0BAA0B;AAInC,IAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,IAAI;AAE5C,IAAM,eAAuC;AAAA,EAC3C,+BAA+B;AAAA,EAC/B,gCAAgC;AAAA,EAChC,gCACE;AAAA,EACF,iCAAiC;AAAA,EACjC,0BAA0B;AAC5B;AAEA,SAAS,mBAAmB,KAA0C;AACpE,QAAM,SAAS,IAAI,QAAQ,eAAe;AAC1C,MAAI,CAAC,UAAU,MAAM,QAAQ,MAAM,EAAG,QAAO;AAC7C,QAAM,QAAQ,OAAO,KAAK;AAC1B,MAAI,CAAC,MAAO,QAAO;AAInB,QAAM,QAAQ,mBAAmB,KAAK,KAAK;AAC3C,QAAM,SAAS,QAAQ,MAAM,CAAC,IAAI,OAAO,KAAK;AAC9C,SAAO,SAAS;AAClB;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,eAAuC,CAAC,GAAG;AACpH,MAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,GAAG,aAAa,CAAC;AAC7E,MAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAGA,SAAS,YAAY,MAAuB;AAC1C,QAAM,WAAW,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI;AACnD,QAAM,UAAU,SACb,IAAI,CAAC,MAAO,KAAK,OAAO,MAAM,YAAY,YAAY,IAAI,OAAQ,EAA0B,MAAM,IAAI,IAAK,EAC3G,OAAO,CAAC,MAAmB,MAAM,IAAI;AACxC,SAAO,QAAQ,SAAS,QAAQ,KAAK,GAAG,IAAI;AAC9C;AAQA,eAAe,iBAAiB,KAA2B,KAA0B,KAAuB;AAC1G,QAAM,QAAQ,mBAAmB,GAAG;AACpC,MAAI,CAAC,OAAO;AACV;AAAA,MACE;AAAA,MACA;AAAA,MACA,EAAE,OAAO,iEAAiE;AAAA,MAC1E,EAAE,oBAAoB,iBAAiB,WAAW,IAAI;AAAA,IACxD;AACA;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,aAAa,GAAG;AACnC,MAAI,MAAM,YAAY,IAAI;AAE1B,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,aAAa,SAAS,eAAe,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,UAAU,cAAc,KAAK,KAAK,IAAI;AAC9C;AAEA,IAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACnD,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,MAAwB,CAAC;AAC/B,MAAI,GAAG,UAAU,MAAM;AACrB,UAAM,KAAK,IAAI,QAAQ,YAAY,KAAK;AACxC,UAAM,QAAQ,IAAI,QAAQ,sBAAsB,KAAK;AACrD,UAAM,OAAO,IAAI,QAAQ,eAAe,IAAI,QAAQ;AACpD,YAAQ;AAAA,MACN,UAAU,IAAI,MAAM,IAAI,IAAI,GAAG,OAAO,IAAI,UAAU,IAAI,KAAK,IAAI,IAAI,SAAS,WAAW,IAAI,QAAQ,IAAI,OAAO,GAAG,UAAU,KAAK,QAAQ,EAAE;AAAA,IAC9I;AAAA,EACF,CAAC;AAED,MAAI,IAAI,QAAQ,cAAc,IAAI,WAAW,SAAS,IAAI,WAAW,SAAS;AAC5E,aAAS,KAAK,KAAK,EAAE,IAAI,MAAM,MAAM,aAAa,SAAS,eAAe,CAAC;AAC3E;AAAA,EACF;AAEA,MAAI,IAAI,QAAQ,QAAQ;AACtB,aAAS,KAAK,KAAK,EAAE,OAAO,2BAA2B,CAAC;AACxD;AAAA,EACF;AAEA,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,YAAY,EAAG,KAAI,UAAU,GAAG,CAAC;AAErE,MAAI,IAAI,WAAW,WAAW;AAC5B,QAAI,UAAU,GAAG;AACjB,QAAI,IAAI;AACR;AAAA,EACF;AASA,MAAI,IAAI,WAAW,QAAQ;AACzB,aAAS,KAAK,KAAK,EAAE,OAAO,mEAAmE,GAAG,EAAE,OAAO,gBAAgB,CAAC;AAC5H;AAAA,EACF;AAEA,MAAI;AACF,UAAM,iBAAiB,KAAK,KAAK,GAAG;AAAA,EACtC,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,GAAG,WAAW,IAAI,cAAc,kCAAkC,IAAI,EAAE;AACtF,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 &amp; 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 &lt;token&gt;</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":";;;;;;;;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,qCAAqC;AAC9C,SAAS,0BAA0B;AAanC,IAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,IAAI;AAK5C,IAAM,cAAc,OAAO,QAAQ,IAAI,eAAe,EAAE;AACxD,IAAM,kBAAkB,OAAO,SAAS,WAAW,KAAK,cAAc,IAAI,cAAc,MAAM,OAAO;AAErG,IAAM,YAAN,cAAwB,MAAM;AAAA,EAC5B,YACW,QACT,SACS,SACT;AACA,UAAM,OAAO;AAJJ;AAEA;AAAA,EAGX;AAAA,EALW;AAAA,EAEA;AAIb;AAEA,IAAM,eAAuC;AAAA,EAC3C,+BAA+B;AAAA,EAC/B,gCAAgC;AAAA,EAChC,gCACE;AAAA,EACF,iCAAiC;AAAA,EACjC,0BAA0B;AAC5B;AAEA,SAAS,mBAAmB,KAA0C;AACpE,QAAM,SAAS,IAAI,QAAQ,eAAe;AAC1C,MAAI,CAAC,UAAU,MAAM,QAAQ,MAAM,EAAG,QAAO;AAC7C,QAAM,QAAQ,OAAO,KAAK;AAE1B,MAAI,CAAC,SAAS,YAAY,KAAK,KAAK,EAAG,QAAO;AAI9C,QAAM,QAAQ,mBAAmB,KAAK,KAAK;AAC3C,QAAM,SAAS,QAAQ,MAAM,CAAC,IAAI,OAAO,KAAK;AAC9C,SAAO,SAAS;AAClB;AAEA,SAAS,aAAa,KAA6C;AACjE,QAAM,WAAW,MAAM,IAAI,UAAU,KAAK,4BAA4B,iBAAiB,OAAO,IAAI,WAAW;AAC7G,QAAM,WAAW,OAAO,IAAI,QAAQ,gBAAgB,CAAC;AACrD,MAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,eAAgB,QAAO,QAAQ,OAAO,SAAS,CAAC;AAI5F,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAmB,CAAC;AAC1B,QAAI,OAAO;AACX,QAAI,WAAW;AACf,QAAI,GAAG,QAAQ,CAAC,UAAkB;AAChC,UAAI,SAAU;AACd,cAAQ,MAAM;AACd,UAAI,OAAO,gBAAgB;AACzB,mBAAW;AACX,eAAO,SAAS;AAChB,eAAO,SAAS,CAAC;AACjB;AAAA,MACF;AACA,aAAO,KAAK,KAAK;AAAA,IACnB,CAAC;AACD,QAAI,GAAG,OAAO,MAAM;AAClB,UAAI,SAAU;AACd,YAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AACjD,UAAI,CAAC,IAAK,QAAO,QAAQ,MAAS;AAClC,UAAI;AACF,gBAAQ,KAAK,MAAM,GAAG,CAAC;AAAA,MACzB,QAAQ;AACN,eAAO,IAAI,UAAU,KAAK,+CAA+C,MAAM,CAAC;AAAA,MAClF;AAAA,IACF,CAAC;AACD,QAAI,GAAG,SAAS,MAAM;AAEtB,QAAI,GAAG,SAAS,MAAM,OAAO,IAAI,MAAM,wDAAwD,CAAC,CAAC;AAAA,EACnG,CAAC;AACH;AAMA,SAAS,QAAQ,KAAmC;AAClD,QAAM,QAAQ,IAAI,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AAC1C,SAAO,KAAK,QAAQ,QAAQ,EAAE,KAAK;AACrC;AAEA,SAAS,SAAS,KAA0B,QAAgB,MAAe,eAAuC,CAAC,GAAG;AACpH,MAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,GAAG,aAAa,CAAC;AAC7E,MAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAIA,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAMZ,YAAY;AAAA;AAAA;AAAA;AAAA,MAIf,YAAY;AAAA,0CACwB,cAAc,KAAK,YAAY;AAAA,8EACK,YAAY;AAAA;AAAA,eAE3E,WAAW;AAAA,eACX,cAAc;AAAA,eACd,cAAc;AAAA;AAAA,6BAEA,cAAc;AAAA;AAAA;AAAA;AAK3C,SAAS,YAAY,KAA2B,KAA0B;AACxE,QAAM,SAAS,EAAE,iBAAiB,uBAAuB,0BAA0B,UAAU;AAC7F,OAAK,IAAI,QAAQ,UAAU,IAAI,SAAS,WAAW,GAAG;AACpD,QAAI,UAAU,KAAK;AAAA,MACjB,GAAG;AAAA,MACH,gBAAgB;AAAA,MAChB,2BAA2B;AAAA,IAC7B,CAAC;AACD,QAAI,IAAI,YAAY;AACpB;AAAA,EACF;AACA;AAAA,IACE;AAAA,IACA;AAAA,IACA,EAAE,MAAM,cAAc,aAAa,oBAAoB,SAAS,gBAAgB,KAAK,QAAQ,QAAQ,WAAW,MAAM,aAAa,SAAS,eAAe;AAAA,IAC3J;AAAA,EACF;AACF;AAGA,SAAS,YAAY,MAAuB;AAC1C,QAAM,WAAW,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI;AACnD,QAAM,UAAU,SACb,IAAI,CAAC,MAAO,KAAK,OAAO,MAAM,YAAY,YAAY,IAAI,OAAQ,EAA0B,MAAM,IAAI,IAAK,EAC3G,OAAO,CAAC,MAAmB,MAAM,IAAI;AACxC,SAAO,QAAQ,SAAS,QAAQ,KAAK,GAAG,IAAI;AAC9C;AAQA,eAAe,iBAAiB,KAA2B,KAA0B,KAAuB;AAC1G,QAAM,QAAQ,mBAAmB,GAAG;AACpC,MAAI,CAAC,OAAO;AACV;AAAA,MACE;AAAA,MACA;AAAA,MACA,EAAE,OAAO,iEAAiE;AAAA,MAC1E,EAAE,oBAAoB,iBAAiB,WAAW,IAAI;AAAA,IACxD;AACA;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,aAAa,GAAG;AACnC,MAAI,MAAM,YAAY,IAAI;AAE1B,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,aAAa,EAAE,cAAc,oBAAoB,CAAC;AAClF,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,UAAU,cAAc,KAAK,KAAK,IAAI;AAC9C;AAEA,IAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACnD,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,MAAwB,CAAC;AAC/B,MAAI,GAAG,UAAU,MAAM;AACrB,UAAM,KAAK,IAAI,QAAQ,YAAY,KAAK;AACxC,UAAM,QAAQ,IAAI,QAAQ,sBAAsB,KAAK;AACrD,UAAM,OAAO,IAAI,QAAQ,eAAe,IAAI,QAAQ;AACpD,YAAQ;AAAA,MACN,UAAU,IAAI,MAAM,IAAI,IAAI,GAAG,OAAO,IAAI,UAAU,IAAI,KAAK,IAAI,IAAI,SAAS,WAAW,IAAI,QAAQ,IAAI,OAAO,GAAG,UAAU,KAAK,QAAQ,EAAE;AAAA,IAC9I;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,QAAQ,GAAG;AAEzB,MAAI,UAAU,cAAc,IAAI,WAAW,SAAS,IAAI,WAAW,SAAS;AAC1E,aAAS,KAAK,KAAK,EAAE,IAAI,MAAM,MAAM,aAAa,SAAS,eAAe,CAAC;AAC3E;AAAA,EACF;AAEA,MAAI,UAAU,QAAQ,IAAI,WAAW,SAAS,IAAI,WAAW,SAAS;AACpE,gBAAY,KAAK,GAAG;AACpB;AAAA,EACF;AAEA,MAAI,UAAU,QAAQ;AACpB,aAAS,KAAK,KAAK,EAAE,OAAO,4BAA4B,MAAM,YAAY,CAAC;AAC3E;AAAA,EACF;AAEA,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,YAAY,EAAG,KAAI,UAAU,GAAG,CAAC;AAErE,MAAI,IAAI,WAAW,WAAW;AAC5B,QAAI,UAAU,GAAG;AACjB,QAAI,IAAI;AACR;AAAA,EACF;AASA,MAAI,IAAI,WAAW,QAAQ;AACzB,aAAS,KAAK,KAAK,EAAE,OAAO,mEAAmE,GAAG,EAAE,OAAO,gBAAgB,CAAC;AAC5H;AAAA,EACF;AAEA,MAAI;AACF,UAAM,iBAAiB,KAAK,KAAK,GAAG;AAAA,EACtC,SAAS,OAAO;AACd,QAAI,iBAAiB,WAAW;AAC9B,UAAI,CAAC,IAAI,aAAa;AACpB,cAAM,OACJ,MAAM,YAAY,SACd,EAAE,SAAS,OAAO,OAAO,EAAE,MAAM,MAAM,SAAS,SAAS,MAAM,QAAQ,GAAG,IAAI,KAAK,IACnF,EAAE,OAAO,MAAM,QAAQ;AAC7B,iBAAS,KAAK,MAAM,QAAQ,IAAI;AAAA,MAClC;AACA,UAAI,MAAM,WAAW,KAAK;AAIxB,cAAM,OAAO,WAAW,MAAM,IAAI,QAAQ,GAAG,GAAM;AACnD,aAAK,MAAM;AACX,YAAI,KAAK,SAAS,MAAM,aAAa,IAAI,CAAC;AAAA,MAC5C;AACA;AAAA,IACF;AACA,YAAQ,MAAM,mBAAmB,KAAK;AACtC,QAAI,CAAC,IAAI,aAAa;AACpB,eAAS,KAAK,KAAK,EAAE,OAAO,wBAAwB,CAAC;AAAA,IACvD;AAAA,EACF;AACF,CAAC;AAED,OAAO,OAAO,MAAM,MAAM;AACxB,UAAQ,IAAI,GAAG,WAAW,IAAI,cAAc,kCAAkC,IAAI,EAAE;AACtF,CAAC;","names":[]}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- SERVER_NAME,
4
- SERVER_VERSION,
3
+ SERVER_INFO,
4
+ SERVER_INSTRUCTIONS,
5
5
  registerTools
6
- } from "./chunk-GARNEWIZ.js";
6
+ } from "./chunk-SPJV6ABL.js";
7
7
 
8
8
  // src/index.ts
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -15,7 +15,7 @@ function loadConfigFromEnv() {
15
15
  const apiToken = process.env.RECHNUNGSAPI_TOKEN;
16
16
  if (!apiToken) {
17
17
  throw new Error(
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").`
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`
19
19
  );
20
20
  }
21
21
  return {
@@ -29,7 +29,7 @@ function loadConfigFromEnv() {
29
29
  async function main() {
30
30
  const config = loadConfigFromEnv();
31
31
  const client = new RechnungsApiClient(config);
32
- const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
32
+ const server = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS });
33
33
  registerTools(server, client);
34
34
  const transport = new StdioServerTransport();
35
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\"\nimport { SERVER_NAME, SERVER_VERSION } from \"./version.js\"\n\nasync function main() {\n const config = loadConfigFromEnv()\n const client = new RechnungsApiClient(config)\n\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION })\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 )\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,IAEF;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,SAAS,QAAQ,IAAI;AAAA,IACrB,WAAW,QAAQ,IAAI;AAAA,EACzB;AACF;;;ADZA,eAAe,OAAO;AACpB,QAAM,SAAS,kBAAkB;AACjC,QAAM,SAAS,IAAI,mBAAmB,MAAM;AAE5C,QAAM,SAAS,IAAI,UAAU,EAAE,MAAM,aAAa,SAAS,eAAe,CAAC;AAC3E,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":[]}
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.3.0",
4
- "description": "MCP (Model Context Protocol) server for RechnungsAPI — lets AI agents create, validate, and analyze ZUGFeRD and X-Invoice e-invoices.",
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": "Finbc GmbH <support@rechnungsapi.de>",
7
- "homepage": "https://rechnungsapi.de",
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": ["mcp", "model-context-protocol", "rechnungsapi", "zugferd", "xinvoice", "e-invoice"],
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.22.0",
30
- "rechnungsapi-sdk": "^0.1.0",
31
- "zod": "^3.23.8"
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": ">=18"
58
+ "node": ">=20"
41
59
  }
42
60
  }
@@ -1 +0,0 @@
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 .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","import { createRequire } from \"node:module\"\n\nconst require = createRequire(import.meta.url)\n\nexport const SERVER_NAME = \"rechnungsapi-mcp\"\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"],"mappings":"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","names":["require"]}