rechnungsapi-mcp 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,6 +85,25 @@ See [`deploy/nginx-mcp.conf`](deploy/nginx-mcp.conf) for a ready-to-use reverse
85
85
 
86
86
  No install, no `npx`, nothing running on their machine.
87
87
 
88
+ ### Troubleshooting a client that "can't reach" the server
89
+
90
+ Every request is logged, so start with the container logs while the client tries to connect:
91
+
92
+ ```bash
93
+ docker logs -f rechnungsapi-mcp
94
+ # [http] POST /mcp -> 200 12ms auth=yes rpc=initialize proto=- ua="..."
95
+ ```
96
+
97
+ | What you see | What it means |
98
+ |---|---|
99
+ | 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
+ | `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. |
102
+ | `GET /mcp -> 405` | Expected. This server doesn't offer the optional server-push SSE stream; compliant clients continue over `POST`. |
103
+ | `OPTIONS /mcp -> 204` | Expected — a browser-based client's CORS preflight. |
104
+
105
+ 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
+
88
107
  ## Tools
89
108
 
90
109
  | Tool | Description |
@@ -146,7 +146,21 @@ function registerTools(server, client) {
146
146
  );
147
147
  }
148
148
 
149
+ // src/version.ts
150
+ import { createRequire } from "module";
151
+ var require2 = createRequire(import.meta.url);
152
+ var SERVER_NAME = "rechnungsapi-mcp";
153
+ var SERVER_VERSION = (() => {
154
+ try {
155
+ return require2("../package.json").version;
156
+ } catch {
157
+ return "0.0.0";
158
+ }
159
+ })();
160
+
149
161
  export {
150
- registerTools
162
+ registerTools,
163
+ SERVER_NAME,
164
+ SERVER_VERSION
151
165
  };
152
- //# sourceMappingURL=chunk-MIISOIDM.js.map
166
+ //# sourceMappingURL=chunk-GARNEWIZ.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 .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":";;;AACA,SAA6B,yBAAyB;AACtD,SAAS,SAAS;AAElB,IAAM,oBAAoB,EACvB,IAAI,EACJ,SAAS,2GAA2G;AAEvH,IAAM,kBAAkB,EACrB,IAAI,EACJ,SAAS,EACT,SAAS,oEAAoE;AAGhF,eAAe,aAAa,IAA4B;AACtD,MAAI;AACF,UAAM,SAAS,MAAM,GAAG;AACxB,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAE,CAAC,EAAE;AAAA,EACvF,SAAS,OAAO;AACd,QAAI,iBAAiB,mBAAmB;AACtC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU,EAAE,QAAQ,MAAM,QAAQ,SAAS,MAAM,SAAS,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC;AAAA,UAClG;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,WAAO,EAAE,SAAS,MAAM,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,QAAQ,CAAC,EAAE;AAAA,EAC9E;AACF;AAEO,SAAS,cAAc,QAAmB,QAAkC;AACjF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aACE;AAAA,MACF,aAAa;AAAA,QACX,SAAS;AAAA,QACT,cAAc,EAAE,OAAO,EAAE,SAAS,yDAAyD;AAAA,QAC3F,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,OAAO,EAAE,SAAS,cAAc,UAAU,MACxC,aAAa,MAAM,OAAO,sBAAsB,SAAS,cAAc,EAAE,UAAU,CAAC,CAAC;AAAA,EACzF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,QACX,SAAS;AAAA,QACT,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,OAAO,EAAE,SAAS,UAAU,MAAM,aAAa,MAAM,OAAO,uBAAuB,SAAS,EAAE,UAAU,CAAC,CAAC;AAAA,EAC5G;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,QACX,cAAc,EAAE,OAAO,EAAE,SAAS,2BAA2B;AAAA,QAC7D,aAAa,EAAE,OAAO,EAAE,SAAS,wBAAwB;AAAA,MAC3D;AAAA,IACF;AAAA,IACA,OAAO,EAAE,cAAc,YAAY,MAAM,aAAa,MAAM,OAAO,iBAAiB,cAAc,WAAW,CAAC;AAAA,EAChH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,QACX,WAAW,EAAE,OAAO,EAAE,SAAS,4BAA4B;AAAA,MAC7D;AAAA,IACF;AAAA,IACA,OAAO,EAAE,UAAU,MAAM,aAAa,MAAM,OAAO,2BAA2B,SAAS,CAAC;AAAA,EAC1F;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,QACX,KAAK,EAAE,OAAO,EAAE,SAAS,uCAAuC;AAAA,MAClE;AAAA,IACF;AAAA,IACA,OAAO,EAAE,IAAI,MAAM,aAAa,MAAM,OAAO,oBAAoB,GAAG,CAAC;AAAA,EACvE;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,QACX,eAAe,EAAE,OAAO,EAAE,SAAS,4BAA4B;AAAA,QAC/D,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,kEAAkE;AAAA,MACpH;AAAA,IACF;AAAA,IACA,OAAO,EAAE,eAAe,eAAe,MACrC,aAAa,MAAM,OAAO,mBAAmB,eAAe,EAAE,eAAe,CAAC,CAAC;AAAA,EACnF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aACE;AAAA,MACF,aAAa;AAAA,QACX,WAAW,EAAE,OAAO,EAAE,SAAS,gDAAgD;AAAA,QAC/E,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,oCAAoC;AAAA,MACrF;AAAA,IACF;AAAA,IACA,OAAO,EAAE,WAAW,cAAc,MAChC,aAAa,MAAM,OAAO,oBAAoB,WAAW,iBAAiB,KAAK,CAAC;AAAA,EACpF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aACE;AAAA,MAEF,aAAa;AAAA,QACX,WAAW,EAAE,OAAO,EAAE,SAAS,gDAAgD;AAAA,QAC/E,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,oCAAoC;AAAA,MACrF;AAAA,IACF;AAAA,IACA,OAAO,EAAE,WAAW,cAAc,MAChC,aAAa,MAAM,OAAO,uBAAuB,WAAW,aAAa,CAAC;AAAA,EAC9E;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,QACX,OAAO,EAAE,OAAO,EAAE,SAAS,yDAAyD;AAAA,MACtF;AAAA,IACF;AAAA,IACA,OAAO,EAAE,MAAM,MAAM,aAAa,MAAM,OAAO,kBAAkB,KAAK,CAAC;AAAA,EACzE;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aACE;AAAA,MAEF,aAAa;AAAA,QACX,WAAW,EAAE,OAAO,EAAE,SAAS,gDAAgD;AAAA,QAC/E,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,mDAAmD;AAAA,MAC9F;AAAA,IACF;AAAA,IACA,OAAO,EAAE,WAAW,SAAS,MAAM,aAAa,MAAM,OAAO,qBAAqB,WAAW,QAAQ,CAAC;AAAA,EACxG;AACF;;;AC7KA,SAAS,qBAAqB;AAE9B,IAAMA,WAAU,cAAc,YAAY,GAAG;AAEtC,IAAM,cAAc;AAEpB,IAAM,kBAA0B,MAAM;AAC3C,MAAI;AACF,WAAQA,SAAQ,iBAAiB,EAA0B;AAAA,EAC7D,QAAQ;AACN,WAAO;AAAA,EACT;AACF,GAAG;","names":["require"]}
package/dist/http.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ SERVER_NAME,
4
+ SERVER_VERSION,
3
5
  registerTools
4
- } from "./chunk-MIISOIDM.js";
6
+ } from "./chunk-GARNEWIZ.js";
5
7
 
6
8
  // src/http.ts
7
9
  import http from "http";
@@ -9,11 +11,21 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
11
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
12
  import { RechnungsApiClient } from "rechnungsapi-sdk";
11
13
  var PORT = Number(process.env.PORT ?? 3939);
14
+ var CORS_HEADERS = {
15
+ "access-control-allow-origin": "*",
16
+ "access-control-allow-methods": "POST, OPTIONS",
17
+ "access-control-allow-headers": "Authorization, Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID",
18
+ "access-control-expose-headers": "Mcp-Session-Id, MCP-Protocol-Version",
19
+ "access-control-max-age": "86400"
20
+ };
12
21
  function extractBearerToken(req) {
13
22
  const header = req.headers["authorization"];
14
23
  if (!header || Array.isArray(header)) return null;
15
- const match = /^Bearer\s+(.+)$/i.exec(header);
16
- return match ? match[1] : null;
24
+ const value = header.trim();
25
+ if (!value) return null;
26
+ const match = /^Bearer\s+(.+)$/i.exec(value);
27
+ const token = (match ? match[1] : value).trim();
28
+ return token || null;
17
29
  }
18
30
  async function readJsonBody(req) {
19
31
  const chunks = [];
@@ -21,22 +33,34 @@ async function readJsonBody(req) {
21
33
  const raw = Buffer.concat(chunks).toString("utf8");
22
34
  return raw ? JSON.parse(raw) : void 0;
23
35
  }
24
- function sendJson(res, status, body) {
25
- res.writeHead(status, { "content-type": "application/json" });
36
+ function sendJson(res, status, body, extraHeaders = {}) {
37
+ res.writeHead(status, { "content-type": "application/json", ...extraHeaders });
26
38
  res.end(JSON.stringify(body));
27
39
  }
28
- async function handleMcpRequest(req, res) {
40
+ function describeRpc(body) {
41
+ const messages = Array.isArray(body) ? body : [body];
42
+ const methods = messages.map((m) => m && typeof m === "object" && "method" in m ? String(m.method) : null).filter((m) => m !== null);
43
+ return methods.length ? methods.join(",") : "-";
44
+ }
45
+ async function handleMcpRequest(req, res, log) {
29
46
  const token = extractBearerToken(req);
30
47
  if (!token) {
31
- sendJson(res, 401, { error: "Missing Authorization: Bearer <your-rechnungsapi-token> header" });
48
+ sendJson(
49
+ res,
50
+ 401,
51
+ { error: "Missing Authorization: Bearer <your-rechnungsapi-token> header" },
52
+ { "www-authenticate": `Bearer realm="${SERVER_NAME}"` }
53
+ );
32
54
  return;
33
55
  }
56
+ const body = await readJsonBody(req);
57
+ log.rpc = describeRpc(body);
34
58
  const client = new RechnungsApiClient({
35
59
  apiToken: token,
36
60
  baseUrl: process.env.RECHNUNGSAPI_BASE_URL,
37
61
  v2BaseUrl: process.env.RECHNUNGSAPI_V2_BASE_URL
38
62
  });
39
- const mcpServer = new McpServer({ name: "rechnungsapi-mcp", version: "0.1.0" });
63
+ const mcpServer = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
40
64
  registerTools(mcpServer, client);
41
65
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
42
66
  res.on("close", () => {
@@ -44,24 +68,39 @@ async function handleMcpRequest(req, res) {
44
68
  mcpServer.close();
45
69
  });
46
70
  await mcpServer.connect(transport);
47
- const body = await readJsonBody(req);
48
71
  await transport.handleRequest(req, res, body);
49
72
  }
50
73
  var server = http.createServer(async (req, res) => {
51
- if (req.method === "GET" && req.url === "/health") {
52
- sendJson(res, 200, { ok: true });
74
+ const startedAt = Date.now();
75
+ const log = {};
76
+ res.on("finish", () => {
77
+ const ua = req.headers["user-agent"] ?? "-";
78
+ const proto = req.headers["mcp-protocol-version"] ?? "-";
79
+ const auth = req.headers["authorization"] ? "yes" : "no";
80
+ console.log(
81
+ `[http] ${req.method} ${req.url} -> ${res.statusCode} ${Date.now() - startedAt}ms auth=${auth} rpc=${log.rpc ?? "-"} proto=${proto} ua="${ua}"`
82
+ );
83
+ });
84
+ if (req.url === "/health" && (req.method === "GET" || req.method === "HEAD")) {
85
+ sendJson(res, 200, { ok: true, name: SERVER_NAME, version: SERVER_VERSION });
53
86
  return;
54
87
  }
55
88
  if (req.url !== "/mcp") {
56
89
  sendJson(res, 404, { error: "Not found. POST to /mcp." });
57
90
  return;
58
91
  }
92
+ for (const [k, v] of Object.entries(CORS_HEADERS)) res.setHeader(k, v);
93
+ if (req.method === "OPTIONS") {
94
+ res.writeHead(204);
95
+ res.end();
96
+ return;
97
+ }
59
98
  if (req.method !== "POST") {
60
- sendJson(res, 405, { error: "Only POST is supported on /mcp (no server-initiated SSE stream)." });
99
+ sendJson(res, 405, { error: "Only POST is supported on /mcp (no server-initiated SSE stream)." }, { allow: "POST, OPTIONS" });
61
100
  return;
62
101
  }
63
102
  try {
64
- await handleMcpRequest(req, res);
103
+ await handleMcpRequest(req, res, log);
65
104
  } catch (error) {
66
105
  console.error("Request failed:", error);
67
106
  if (!res.headersSent) {
@@ -70,6 +109,6 @@ var server = http.createServer(async (req, res) => {
70
109
  }
71
110
  });
72
111
  server.listen(PORT, () => {
73
- console.log(`rechnungsapi-mcp HTTP server listening on port ${PORT}`);
112
+ console.log(`${SERVER_NAME} ${SERVER_VERSION} HTTP server listening on port ${PORT}`);
74
113
  });
75
114
  //# 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 { 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":"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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_NAME,
4
+ SERVER_VERSION,
3
5
  registerTools
4
- } from "./chunk-MIISOIDM.js";
6
+ } from "./chunk-GARNEWIZ.js";
5
7
 
6
8
  // src/index.ts
7
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -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({ name: "rechnungsapi-mcp", version: "0.1.0" });
32
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
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({ name: \"rechnungsapi-mcp\", version: \"0.1.0\" })\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;;;ADbA,eAAe,OAAO;AACpB,QAAM,SAAS,kBAAkB;AACjC,QAAM,SAAS,IAAI,mBAAmB,MAAM;AAE5C,QAAM,SAAS,IAAI,UAAU,EAAE,MAAM,oBAAoB,SAAS,QAAQ,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_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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechnungsapi-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "MCP (Model Context Protocol) server for RechnungsAPI — lets AI agents create, validate, and analyze ZUGFeRD and X-Invoice e-invoices.",
5
5
  "license": "MIT",
6
6
  "author": "Finbc GmbH <support@rechnungsapi.de>",
@@ -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":[]}